/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.graphics.drawable; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; import android.util.MathUtils; import android.view.HardwareCanvas; import android.view.RenderNodeAnimator; import android.view.animation.LinearInterpolator; import java.util.ArrayList; /** * Draws a Material ripple. */ class Ripple { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator(); private static final float GLOBAL_SPEED = 1.0f; private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED; private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED; private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; private static final long RIPPLE_ENTER_DELAY = 80; // Hardware animators. private final ArrayList mRunningAnimations = new ArrayList(); private final ArrayList mPendingAnimations = new ArrayList(); private final RippleDrawable mOwner; /** Bounds used for computing max radius. */ private final Rect mBounds; /** Full-opacity color for drawing this ripple. */ private int mColorOpaque; /** Maximum ripple radius. */ private float mOuterRadius; /** Screen density used to adjust pixel-based velocities. */ private float mDensity; private float mStartingX; private float mStartingY; private float mClampedStartingX; private float mClampedStartingY; // Hardware rendering properties. private CanvasProperty mPropPaint; private CanvasProperty mPropRadius; private CanvasProperty mPropX; private CanvasProperty mPropY; // Software animators. private ObjectAnimator mAnimRadius; private ObjectAnimator mAnimOpacity; private ObjectAnimator mAnimX; private ObjectAnimator mAnimY; // Temporary paint used for creating canvas properties. private Paint mTempPaint; // Software rendering properties. private float mOpacity = 1; private float mOuterX; private float mOuterY; // Values used to tween between the start and end positions. private float mTweenRadius = 0; private float mTweenX = 0; private float mTweenY = 0; /** Whether we should be drawing hardware animations. */ private boolean mHardwareAnimating; /** Whether we can use hardware acceleration for the exit animation. */ private boolean mCanUseHardware; /** Whether we have an explicit maximum radius. */ private boolean mHasMaxRadius; /** Whether we were canceled externally and should avoid self-removal. */ private boolean mCanceled; /** * Creates a new ripple. */ public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) { mOwner = owner; mBounds = bounds; mStartingX = startingX; mStartingY = startingY; } public void setup(int maxRadius, int color, float density) { mColorOpaque = color | 0xFF000000; if (maxRadius != RippleDrawable.RADIUS_AUTO) { mHasMaxRadius = true; mOuterRadius = maxRadius; } else { final float halfWidth = mBounds.width() / 2.0f; final float halfHeight = mBounds.height() / 2.0f; mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); } mOuterX = 0; mOuterY = 0; mDensity = density; clampStartingPosition(); } public boolean isHardwareAnimating() { return mHardwareAnimating; } private void clampStartingPosition() { final float cX = mBounds.exactCenterX(); final float cY = mBounds.exactCenterY(); final float dX = mStartingX - cX; final float dY = mStartingY - cY; final float r = mOuterRadius; if (dX * dX + dY * dY > r * r) { // Point is outside the circle, clamp to the circumference. final double angle = Math.atan2(dY, dX); mClampedStartingX = cX + (float) (Math.cos(angle) * r); mClampedStartingY = cY + (float) (Math.sin(angle) * r); } else { mClampedStartingX = mStartingX; mClampedStartingY = mStartingY; } } public void onHotspotBoundsChanged() { if (!mHasMaxRadius) { final float halfWidth = mBounds.width() / 2.0f; final float halfHeight = mBounds.height() / 2.0f; mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); clampStartingPosition(); } } public void setOpacity(float a) { mOpacity = a; invalidateSelf(); } public float getOpacity() { return mOpacity; } @SuppressWarnings("unused") public void setRadiusGravity(float r) { mTweenRadius = r; invalidateSelf(); } @SuppressWarnings("unused") public float getRadiusGravity() { return mTweenRadius; } @SuppressWarnings("unused") public void setXGravity(float x) { mTweenX = x; invalidateSelf(); } @SuppressWarnings("unused") public float getXGravity() { return mTweenX; } @SuppressWarnings("unused") public void setYGravity(float y) { mTweenY = y; invalidateSelf(); } @SuppressWarnings("unused") public float getYGravity() { return mTweenY; } /** * Draws the ripple centered at (0,0) using the specified paint. */ public boolean draw(Canvas c, Paint p) { final boolean canUseHardware = c.isHardwareAccelerated(); if (mCanUseHardware != canUseHardware && mCanUseHardware) { // We've switched from hardware to non-hardware mode. Panic. cancelHardwareAnimations(true); } mCanUseHardware = canUseHardware; final boolean hasContent; if (canUseHardware && mHardwareAnimating) { hasContent = drawHardware((HardwareCanvas) c); } else { hasContent = drawSoftware(c, p); } return hasContent; } private boolean drawHardware(HardwareCanvas c) { // If we have any pending hardware animations, cancel any running // animations and start those now. final ArrayList pendingAnimations = mPendingAnimations; final int N = pendingAnimations.size(); if (N > 0) { cancelHardwareAnimations(false); // We canceled old animations, but we're about to run new ones. mHardwareAnimating = true; for (int i = 0; i < N; i++) { pendingAnimations.get(i).setTarget(c); pendingAnimations.get(i).start(); } mRunningAnimations.addAll(pendingAnimations); pendingAnimations.clear(); } c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); return true; } private boolean drawSoftware(Canvas c, Paint p) { boolean hasContent = false; p.setColor(mColorOpaque); final int alpha = (int) (255 * mOpacity + 0.5f); final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); if (alpha > 0 && radius > 0) { final float x = MathUtils.lerp( mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); final float y = MathUtils.lerp( mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); p.setAlpha(alpha); p.setStyle(Style.FILL); c.drawCircle(x, y, radius, p); hasContent = true; } return hasContent; } /** * Returns the maximum bounds of the ripple relative to the ripple center. */ public void getBounds(Rect bounds) { final int outerX = (int) mOuterX; final int outerY = (int) mOuterY; final int r = (int) mOuterRadius + 1; bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); } /** * Specifies the starting position relative to the drawable bounds. No-op if * the ripple has already entered. */ public void move(float x, float y) { mStartingX = x; mStartingY = y; clampStartingPosition(); } /** * Starts the enter animation. */ public void enter() { cancel(); final int radiusDuration = (int) (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1); radius.setAutoCancel(true); radius.setDuration(radiusDuration); radius.setInterpolator(LINEAR_INTERPOLATOR); radius.setStartDelay(RIPPLE_ENTER_DELAY); final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); cX.setAutoCancel(true); cX.setDuration(radiusDuration); cX.setInterpolator(LINEAR_INTERPOLATOR); cX.setStartDelay(RIPPLE_ENTER_DELAY); final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); cY.setAutoCancel(true); cY.setDuration(radiusDuration); cY.setInterpolator(LINEAR_INTERPOLATOR); cY.setStartDelay(RIPPLE_ENTER_DELAY); mAnimRadius = radius; mAnimX = cX; mAnimY = cY; // Enter animations always run on the UI thread, since it's unlikely // that anything interesting is happening until the user lifts their // finger. radius.start(); cX.start(); cY.start(); } /** * Starts the exit animation. */ public void exit() { cancel(); final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); final float remaining; if (mAnimRadius != null && mAnimRadius.isRunning()) { remaining = mOuterRadius - radius; } else { remaining = mOuterRadius; } final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); if (mCanUseHardware) { exitHardware(radiusDuration, opacityDuration); } else { exitSoftware(radiusDuration, opacityDuration); } } private void exitHardware(int radiusDuration, int opacityDuration) { mPendingAnimations.clear(); final float startX = MathUtils.lerp( mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); final float startY = MathUtils.lerp( mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); final Paint paint = getTempPaint(); paint.setAntiAlias(true); paint.setColor(mColorOpaque); paint.setAlpha((int) (255 * mOpacity + 0.5f)); paint.setStyle(Style.FILL); mPropPaint = CanvasProperty.createPaint(paint); mPropRadius = CanvasProperty.createFloat(startRadius); mPropX = CanvasProperty.createFloat(startX); mPropY = CanvasProperty.createFloat(startY); final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius); radiusAnim.setDuration(radiusDuration); radiusAnim.setInterpolator(DECEL_INTERPOLATOR); final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX); xAnim.setDuration(radiusDuration); xAnim.setInterpolator(DECEL_INTERPOLATOR); final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY); yAnim.setDuration(radiusDuration); yAnim.setInterpolator(DECEL_INTERPOLATOR); final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); opacityAnim.setDuration(opacityDuration); opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); opacityAnim.addListener(mAnimationListener); mPendingAnimations.add(radiusAnim); mPendingAnimations.add(opacityAnim); mPendingAnimations.add(xAnim); mPendingAnimations.add(yAnim); mHardwareAnimating = true; // Set up the software values to match the hardware end values. mOpacity = 0; mTweenX = 1; mTweenY = 1; mTweenRadius = 1; invalidateSelf(); } /** * Jump all animations to their end state. The caller is responsible for * removing the ripple from the list of animating ripples. */ public void jump() { mCanceled = true; endSoftwareAnimations(); cancelHardwareAnimations(true); mCanceled = false; } private void endSoftwareAnimations() { if (mAnimRadius != null) { mAnimRadius.end(); mAnimRadius = null; } if (mAnimOpacity != null) { mAnimOpacity.end(); mAnimOpacity = null; } if (mAnimX != null) { mAnimX.end(); mAnimX = null; } if (mAnimY != null) { mAnimY.end(); mAnimY = null; } } private Paint getTempPaint() { if (mTempPaint == null) { mTempPaint = new Paint(); } return mTempPaint; } private void exitSoftware(int radiusDuration, int opacityDuration) { final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1); radiusAnim.setAutoCancel(true); radiusAnim.setDuration(radiusDuration); radiusAnim.setInterpolator(DECEL_INTERPOLATOR); final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); xAnim.setAutoCancel(true); xAnim.setDuration(radiusDuration); xAnim.setInterpolator(DECEL_INTERPOLATOR); final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); yAnim.setAutoCancel(true); yAnim.setDuration(radiusDuration); yAnim.setInterpolator(DECEL_INTERPOLATOR); final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0); opacityAnim.setAutoCancel(true); opacityAnim.setDuration(opacityDuration); opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); opacityAnim.addListener(mAnimationListener); mAnimRadius = radiusAnim; mAnimOpacity = opacityAnim; mAnimX = xAnim; mAnimY = yAnim; radiusAnim.start(); opacityAnim.start(); xAnim.start(); yAnim.start(); } /** * Cancels all animations. The caller is responsible for removing * the ripple from the list of animating ripples. */ public void cancel() { mCanceled = true; cancelSoftwareAnimations(); cancelHardwareAnimations(true); mCanceled = false; } private void cancelSoftwareAnimations() { if (mAnimRadius != null) { mAnimRadius.cancel(); mAnimRadius = null; } if (mAnimOpacity != null) { mAnimOpacity.cancel(); mAnimOpacity = null; } if (mAnimX != null) { mAnimX.cancel(); mAnimX = null; } if (mAnimY != null) { mAnimY.cancel(); mAnimY = null; } } /** * Cancels any running hardware animations. */ private void cancelHardwareAnimations(boolean cancelPending) { final ArrayList runningAnimations = mRunningAnimations; final int N = runningAnimations.size(); for (int i = 0; i < N; i++) { runningAnimations.get(i).cancel(); } runningAnimations.clear(); if (cancelPending && !mPendingAnimations.isEmpty()) { mPendingAnimations.clear(); } mHardwareAnimating = false; } private void removeSelf() { // The owner will invalidate itself. if (!mCanceled) { mOwner.removeRipple(this); } } private void invalidateSelf() { mOwner.invalidateSelf(); } private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { removeSelf(); } }; /** * Interpolator with a smooth log deceleration */ private static final class LogInterpolator implements TimeInterpolator { @Override public float getInterpolation(float input) { return 1 - (float) Math.pow(400, -input * 1.4); } } }