diff options
author | Alan Viverette <alanv@google.com> | 2015-03-10 01:51:57 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2015-03-10 01:51:59 +0000 |
commit | 53776a2b3c18f3eb2217e5e3af4dda187d0fee62 (patch) | |
tree | 5e62f0a12e0eb947b9b56f4f2f52339428d38f42 /graphics | |
parent | 216f3edcb9fb0089b1bd621f88a682d7a2ebf115 (diff) | |
parent | f872ee0057ed247aa93589347f1b53afc99517f8 (diff) | |
download | frameworks_base-53776a2b3c18f3eb2217e5e3af4dda187d0fee62.zip frameworks_base-53776a2b3c18f3eb2217e5e3af4dda187d0fee62.tar.gz frameworks_base-53776a2b3c18f3eb2217e5e3af4dda187d0fee62.tar.bz2 |
Merge "Refactor ripple components, simplify background animation"
Diffstat (limited to 'graphics')
5 files changed, 813 insertions, 991 deletions
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java deleted file mode 100644 index 138d73a..0000000 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * 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.Paint; -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<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); - - private final RippleDrawable mOwner; - - /** Bounds used for computing max radius. */ - private final Rect mBounds; - - /** 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<Paint> mPropPaint; - private CanvasProperty<Float> mPropRadius; - private CanvasProperty<Float> mPropX; - private CanvasProperty<Float> 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; - - private boolean mHasPendingHardwareExit; - private int mPendingRadiusDuration; - private int mPendingOpacityDuration; - - /** - * 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(float maxRadius, float density) { - if (maxRadius >= 0) { - 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 || mHasPendingHardwareExit)) { - hasContent = drawHardware((HardwareCanvas) c, p); - } else { - hasContent = drawSoftware(c, p); - } - - return hasContent; - } - - private boolean drawHardware(HardwareCanvas c, Paint p) { - if (mHasPendingHardwareExit) { - cancelHardwareAnimations(false); - startPendingHardwareExit(c, p); - } - - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - - return true; - } - - private boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - - final int paintAlpha = p.getAlpha(); - final int alpha = (int) (paintAlpha * 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); - c.drawCircle(x, y, radius, p); - p.setAlpha(paintAlpha); - 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() { - final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); - final float remaining; - if (mAnimRadius != null && mAnimRadius.isRunning()) { - remaining = mOuterRadius - radius; - } else { - remaining = mOuterRadius; - } - - cancel(); - - 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) { - createPendingHardwareExit(radiusDuration, opacityDuration); - } else { - exitSoftware(radiusDuration, opacityDuration); - } - } - - private void createPendingHardwareExit(int radiusDuration, int opacityDuration) { - mHasPendingHardwareExit = true; - mPendingRadiusDuration = radiusDuration; - mPendingOpacityDuration = opacityDuration; - - // The animation will start on the next draw(). - invalidateSelf(); - } - - private void startPendingHardwareExit(HardwareCanvas c, Paint p) { - mHasPendingHardwareExit = false; - - final int radiusDuration = mPendingRadiusDuration; - final int opacityDuration = mPendingOpacityDuration; - - 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(p); - paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f)); - 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); - radiusAnim.setTarget(c); - radiusAnim.start(); - - final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX); - xAnim.setDuration(radiusDuration); - xAnim.setInterpolator(DECEL_INTERPOLATOR); - xAnim.setTarget(c); - xAnim.start(); - - final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY); - yAnim.setDuration(radiusDuration); - yAnim.setInterpolator(DECEL_INTERPOLATOR); - yAnim.setTarget(c); - yAnim.start(); - - final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint, - RenderNodeAnimator.PAINT_ALPHA, 0); - opacityAnim.setDuration(opacityDuration); - opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - opacityAnim.addListener(mAnimationListener); - opacityAnim.setTarget(c); - opacityAnim.start(); - - mRunningAnimations.add(radiusAnim); - mRunningAnimations.add(opacityAnim); - mRunningAnimations.add(xAnim); - mRunningAnimations.add(yAnim); - - mHardwareAnimating = true; - - // Set up the software values to match the hardware end values. - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; - } - - /** - * 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(Paint original) { - if (mTempPaint == null) { - mTempPaint = new Paint(); - } - mTempPaint.set(original); - 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(false); - 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 jumpToEnd) { - final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; - final int N = runningAnimations.size(); - for (int i = 0; i < N; i++) { - if (jumpToEnd) { - runningAnimations.get(i).end(); - } else { - runningAnimations.get(i).cancel(); - } - } - runningAnimations.clear(); - - if (mHasPendingHardwareExit) { - // If we had a pending hardware exit, jump to the end state. - mHasPendingHardwareExit = false; - - if (jumpToEnd) { - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; - } - } - - 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); - } - } -} diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java index ef35289..6d1b1fe 100644 --- a/graphics/java/android/graphics/drawable/RippleBackground.java +++ b/graphics/java/android/graphics/drawable/RippleBackground.java @@ -17,432 +17,162 @@ package android.graphics.drawable; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; 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.Rect; -import android.util.MathUtils; +import android.util.FloatProperty; import android.view.HardwareCanvas; import android.view.RenderNodeAnimator; import android.view.animation.LinearInterpolator; -import java.util.ArrayList; - /** - * Draws a Material ripple. + * Draws a ripple background. */ -class RippleBackground { +class RippleBackground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final float GLOBAL_SPEED = 1.0f; - private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; - private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; - private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; - private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f; - private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; - - private static final int ENTER_DURATION = 667; - private static final int ENTER_DURATION_FAST = 100; - - // Hardware animators. - private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); - - private final RippleDrawable mOwner; - - /** Bounds used for computing max radius. */ - private final Rect mBounds; - - /** ARGB color for drawing this ripple. */ - private int mColor; - - /** Maximum ripple radius. */ - private float mOuterRadius; - - /** Screen density used to adjust pixel-based velocities. */ - private float mDensity; + private static final int OPACITY_ENTER_DURATION = 600; + private static final int OPACITY_ENTER_DURATION_FAST = 120; + private static final int OPACITY_EXIT_DURATION = 480; // Hardware rendering properties. - private CanvasProperty<Paint> mPropOuterPaint; - private CanvasProperty<Float> mPropOuterRadius; - private CanvasProperty<Float> mPropOuterX; - private CanvasProperty<Float> mPropOuterY; - - // Software animators. - private ObjectAnimator mAnimOuterOpacity; - - // Temporary paint used for creating canvas properties. - private Paint mTempPaint; + private CanvasProperty<Paint> mPropPaint; + private CanvasProperty<Float> mPropRadius; + private CanvasProperty<Float> mPropX; + private CanvasProperty<Float> mPropY; // Software rendering properties. - private float mOuterOpacity = 0; - private float mOuterX; - private float mOuterY; - - /** 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; - - private boolean mHasPendingHardwareExit; - private int mPendingOpacityDuration; - private int mPendingInflectionDuration; - private int mPendingInflectionOpacity; + private float mOpacity = 0; - /** - * Creates a new ripple. - */ public RippleBackground(RippleDrawable owner, Rect bounds) { - mOwner = owner; - mBounds = bounds; - } - - public void setup(float maxRadius, float density) { - if (maxRadius >= 0) { - 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; + super(owner, bounds); } - 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); - } - } - - @SuppressWarnings("unused") - public void setOuterOpacity(float a) { - mOuterOpacity = a; - invalidateSelf(); - } - - @SuppressWarnings("unused") - public float getOuterOpacity() { - return mOuterOpacity; - } - - /** - * Draws the ripple centered at (0,0) using the specified paint. - */ - public boolean draw(Canvas c, Paint p) { - mColor = p.getColor(); - - 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 || mHasPendingHardwareExit)) { - hasContent = drawHardware((HardwareCanvas) c, p); - } else { - hasContent = drawSoftware(c, p); - } - - return hasContent; + public boolean isVisible() { + return mOpacity > 0 || isHardwareAnimating(); } - public boolean shouldDraw() { - return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0); - } - - private boolean drawHardware(HardwareCanvas c, Paint p) { - if (mHasPendingHardwareExit) { - cancelHardwareAnimations(false); - startPendingHardwareExit(c, p); - } - - c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); - - return true; - } - - private boolean drawSoftware(Canvas c, Paint p) { + @Override + protected boolean drawSoftware(Canvas c, Paint p) { boolean hasContent = false; - final int paintAlpha = p.getAlpha(); - final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f); - final float radius = mOuterRadius; - if (alpha > 0 && radius > 0) { + final int origAlpha = p.getAlpha(); + final int alpha = (int) (origAlpha * mOpacity + 0.5f); + if (alpha > 0) { p.setAlpha(alpha); - c.drawCircle(mOuterX, mOuterY, radius, p); - p.setAlpha(paintAlpha); + c.drawCircle(0, 0, mTargetRadius, p); + p.setAlpha(origAlpha); 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); + @Override + protected boolean drawHardware(HardwareCanvas c) { + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + return true; } - /** - * Starts the enter animation. - */ - public void enter(boolean fast) { - cancel(); + @Override + protected Animator createSoftwareEnter(boolean fast) { + // Linear enter based on current opacity. + final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION; + final int duration = (int) ((1 - mOpacity) * maxDuration); - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); opacity.setAutoCancel(true); - opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION); + opacity.setDuration(duration); opacity.setInterpolator(LINEAR_INTERPOLATOR); - mAnimOuterOpacity = opacity; - - // Enter animations always run on the UI thread, since it's unlikely - // that anything interesting is happening until the user lifts their - // finger. - opacity.start(); - } - - /** - * Starts the exit animation. - */ - public void exit() { - cancel(); - - // Scale the outer max opacity and opacity velocity based - // on the size of the outer radius. - final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - final float outerSizeInfluence = MathUtils.constrain( - (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) - / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); - final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, - WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); - - // Determine at what time the inner and outer opacity intersect. - // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 - // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 - final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity) - / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); - final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity - + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); - - if (mCanUseHardware) { - createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity); - } else { - exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); - } - } - - private void createPendingHardwareExit( - int opacityDuration, int inflectionDuration, int inflectionOpacity) { - mHasPendingHardwareExit = true; - mPendingOpacityDuration = opacityDuration; - mPendingInflectionDuration = inflectionDuration; - mPendingInflectionOpacity = inflectionOpacity; - - // The animation will start on the next draw(). - invalidateSelf(); + return opacity; } - private void startPendingHardwareExit(HardwareCanvas c, Paint p) { - mHasPendingHardwareExit = false; + @Override + protected Animator createSoftwareExit() { + final AnimatorSet set = new AnimatorSet(); - final int opacityDuration = mPendingOpacityDuration; - final int inflectionDuration = mPendingInflectionDuration; - final int inflectionOpacity = mPendingInflectionOpacity; + // Linear exit after enter is completed. + final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0); + exit.setInterpolator(LINEAR_INTERPOLATOR); + exit.setDuration(OPACITY_EXIT_DURATION); + exit.setAutoCancel(true); - final Paint outerPaint = getTempPaint(p); - outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f)); - mPropOuterPaint = CanvasProperty.createPaint(outerPaint); - mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); - mPropOuterX = CanvasProperty.createFloat(mOuterX); - mPropOuterY = CanvasProperty.createFloat(mOuterY); + final AnimatorSet.Builder builder = set.play(exit); - final RenderNodeAnimator outerOpacityAnim; - if (inflectionDuration > 0) { - // Outer opacity continues to increase for a bit. - outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint, - RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); - outerOpacityAnim.setDuration(inflectionDuration); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + // Linear "fast" enter based on current opacity. + final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST); + if (fastEnterDuration > 0) { + final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1); + enter.setInterpolator(LINEAR_INTERPOLATOR); + enter.setDuration(fastEnterDuration); + enter.setAutoCancel(true); - // Chain the outer opacity exit animation. - final int outerDuration = opacityDuration - inflectionDuration; - if (outerDuration > 0) { - final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( - mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - outerFadeOutAnim.setDuration(outerDuration); - outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerFadeOutAnim.setStartDelay(inflectionDuration); - outerFadeOutAnim.setStartValue(inflectionOpacity); - outerFadeOutAnim.addListener(mAnimationListener); - outerFadeOutAnim.setTarget(c); - outerFadeOutAnim.start(); - - mRunningAnimations.add(outerFadeOutAnim); - } else { - outerOpacityAnim.addListener(mAnimationListener); - } - } else { - outerOpacityAnim = new RenderNodeAnimator( - mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerOpacityAnim.setDuration(opacityDuration); - outerOpacityAnim.addListener(mAnimationListener); + builder.after(enter); } - outerOpacityAnim.setTarget(c); - outerOpacityAnim.start(); - - mRunningAnimations.add(outerOpacityAnim); - - mHardwareAnimating = true; - - // Set up the software values to match the hardware end values. - mOuterOpacity = 0; + return set; } - /** - * Jump all animations to their end state. The caller is responsible for - * removing the ripple from the list of animating ripples. - */ - public void jump() { - endSoftwareAnimations(); - cancelHardwareAnimations(true); - } - - private void endSoftwareAnimations() { - if (mAnimOuterOpacity != null) { - mAnimOuterOpacity.end(); - mAnimOuterOpacity = null; - } - } - - private Paint getTempPaint(Paint original) { - if (mTempPaint == null) { - mTempPaint = new Paint(); - } - mTempPaint.set(original); - return mTempPaint; - } - - private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { - final ObjectAnimator outerOpacityAnim; - if (inflectionDuration > 0) { - // Outer opacity continues to increase for a bit. - outerOpacityAnim = ObjectAnimator.ofFloat(this, - "outerOpacity", inflectionOpacity / 255.0f); - outerOpacityAnim.setAutoCancel(true); - outerOpacityAnim.setDuration(inflectionDuration); - outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); - - // Chain the outer opacity exit animation. - final int outerDuration = opacityDuration - inflectionDuration; - if (outerDuration > 0) { - outerOpacityAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( - RippleBackground.this, "outerOpacity", 0); - outerFadeOutAnim.setAutoCancel(true); - outerFadeOutAnim.setDuration(outerDuration); - outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); - outerFadeOutAnim.addListener(mAnimationListener); + @Override + protected RenderNodeAnimatorSet createHardwareExit(Paint p) { + final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - mAnimOuterOpacity = outerFadeOutAnim; + final int targetAlpha = p.getAlpha(); + final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f); + p.setAlpha(currentAlpha); - outerFadeOutAnim.start(); - } + mPropPaint = CanvasProperty.createPaint(p); + mPropRadius = CanvasProperty.createFloat(mTargetRadius); + mPropX = CanvasProperty.createFloat(0); + mPropY = CanvasProperty.createFloat(0); - @Override - public void onAnimationCancel(Animator animation) { - animation.removeListener(this); - } - }); - } else { - outerOpacityAnim.addListener(mAnimationListener); - } - } else { - outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); - outerOpacityAnim.setAutoCancel(true); - outerOpacityAnim.setDuration(opacityDuration); - outerOpacityAnim.addListener(mAnimationListener); + // Linear "fast" enter based on current opacity. + final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST); + if (fastEnterDuration > 0) { + final RenderNodeAnimator enter = new RenderNodeAnimator( + mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha); + enter.setInterpolator(LINEAR_INTERPOLATOR); + enter.setDuration(fastEnterDuration); + set.add(enter); } - mAnimOuterOpacity = outerOpacityAnim; + // Linear exit after enter is completed. + final RenderNodeAnimator exit = new RenderNodeAnimator( + mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); + exit.setInterpolator(LINEAR_INTERPOLATOR); + exit.setDuration(OPACITY_EXIT_DURATION); + exit.setStartDelay(fastEnterDuration); + set.add(exit); - outerOpacityAnim.start(); + return set; } - /** - * Cancel all animations. The caller is responsible for removing - * the ripple from the list of animating ripples. - */ - public void cancel() { - cancelSoftwareAnimations(); - cancelHardwareAnimations(false); + @Override + protected void jumpValuesToExit() { + mOpacity = 0; } - private void cancelSoftwareAnimations() { - if (mAnimOuterOpacity != null) { - mAnimOuterOpacity.cancel(); - mAnimOuterOpacity = null; + private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> { + public BackgroundProperty(String name) { + super(name); } } - /** - * Cancels any running hardware animations. - */ - private void cancelHardwareAnimations(boolean jumpToEnd) { - final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; - final int N = runningAnimations.size(); - for (int i = 0; i < N; i++) { - if (jumpToEnd) { - runningAnimations.get(i).end(); - } else { - runningAnimations.get(i).cancel(); - } - } - runningAnimations.clear(); - - if (mHasPendingHardwareExit) { - // If we had a pending hardware exit, jump to the end state. - mHasPendingHardwareExit = false; - - if (jumpToEnd) { - mOuterOpacity = 0; - } + private static final BackgroundProperty OPACITY = new BackgroundProperty("opacity") { + @Override + public void setValue(RippleBackground object, float value) { + object.mOpacity = value; + object.invalidateSelf(); } - mHardwareAnimating = false; - } - - private void invalidateSelf() { - mOwner.invalidateSelf(); - } - - private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { - mHardwareAnimating = false; + public Float get(RippleBackground object) { + return object.mOpacity; } }; } diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java new file mode 100644 index 0000000..fd3e06c --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleComponent.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2015 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.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.HardwareCanvas; +import android.view.RenderNodeAnimator; + +import java.util.ArrayList; + +/** + * Abstract class that handles hardware/software hand-off and lifecycle for + * animated ripple foreground and background components. + */ +abstract class RippleComponent { + private final RippleDrawable mOwner; + + /** Bounds used for computing max radius. May be modified by the owner. */ + protected final Rect mBounds; + + /** Whether we can use hardware acceleration for the exit animation. */ + private boolean mHasHardwareCanvas; + + private boolean mHasPendingHardwareAnimator; + private RenderNodeAnimatorSet mHardwareAnimator; + + private Animator mSoftwareAnimator; + + /** Whether we have an explicit maximum radius. */ + private boolean mHasMaxRadius; + + /** How big this ripple should be when fully entered. */ + protected float mTargetRadius; + + /** Screen density used to adjust pixel-based constants. */ + protected float mDensity; + + public RippleComponent(RippleDrawable owner, Rect bounds) { + mOwner = owner; + mBounds = bounds; + } + + public final void setup(float maxRadius, float density) { + if (maxRadius >= 0) { + mHasMaxRadius = true; + mTargetRadius = maxRadius; + } else { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mTargetRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + + mDensity = density; + + onSetup(); + onTargetRadiusChanged(mTargetRadius); + } + + /** + * Starts a ripple enter animation. + * + * @param fast whether the ripple should enter quickly + */ + public final void enter(boolean fast) { + cancel(); + + mSoftwareAnimator = createSoftwareEnter(fast); + mSoftwareAnimator.start(); + } + + /** + * Starts a ripple exit animation. + */ + public final void exit() { + cancel(); + + if (mHasHardwareCanvas) { + // We don't have access to a canvas here, but we expect one on the + // next frame. We'll start the render thread animation then. + mHasPendingHardwareAnimator = true; + + // Request another frame. + invalidateSelf(); + } else { + mSoftwareAnimator = createSoftwareExit(); + mSoftwareAnimator.start(); + } + } + + /** + * Cancels all animations. Software animation values are left in the + * current state, while hardware animation values jump to the end state. + */ + public void cancel() { + cancelSoftwareAnimations(); + endHardwareAnimations(); + } + + /** + * Ends all animations, jumping values to the end state. + */ + public void end() { + endSoftwareAnimations(); + endHardwareAnimations(); + } + + /** + * Draws the ripple to the canvas, inheriting the paint's color and alpha + * properties. + * + * @param c the canvas to which the ripple should be drawn + * @param p the paint used to draw the ripple + * @return {@code true} if something was drawn, {@code false} otherwise + */ + public boolean draw(Canvas c, Paint p) { + final boolean hasHardwareCanvas = c.isHardwareAccelerated() + && c instanceof HardwareCanvas; + if (mHasHardwareCanvas != hasHardwareCanvas) { + mHasHardwareCanvas = hasHardwareCanvas; + + if (!hasHardwareCanvas) { + // We've switched from hardware to non-hardware mode. Panic. + endHardwareAnimations(); + } + } + + if (hasHardwareCanvas) { + final HardwareCanvas hw = (HardwareCanvas) c; + startPendingAnimation(hw, p); + + if (mHardwareAnimator != null) { + return drawHardware(hw); + } + } + + return drawSoftware(c, p); + } + + /** + * Populates {@code bounds} with the maximum drawing bounds of the ripple + * relative to its center. The resulting bounds should be translated into + * parent drawable coordinates before use. + * + * @param bounds the rect to populate with drawing bounds + */ + public void getBounds(Rect bounds) { + final int r = (int) Math.ceil(mTargetRadius); + bounds.set(-r, -r, r, r); + } + + /** + * Starts the pending hardware animation, if available. + * + * @param hw hardware canvas on which the animation should draw + * @param p paint whose properties the hardware canvas should use + */ + private void startPendingAnimation(HardwareCanvas hw, Paint p) { + if (mHasPendingHardwareAnimator) { + mHasPendingHardwareAnimator = false; + + mHardwareAnimator = createHardwareExit(new Paint(p)); + mHardwareAnimator.start(hw); + + // Preemptively jump the software values to the end state now that + // the hardware exit has read whatever values it needs. + jumpValuesToExit(); + } + } + + /** + * Cancels any current software animations, leaving the values in their + * current state. + */ + private void cancelSoftwareAnimations() { + if (mSoftwareAnimator != null) { + mSoftwareAnimator.cancel(); + } + } + + /** + * Ends any current software animations, jumping the values to their end + * state. + */ + private void endSoftwareAnimations() { + if (mSoftwareAnimator != null) { + mSoftwareAnimator.end(); + } + } + + /** + * Ends any pending or current hardware animations. + * <p> + * Hardware animations can't synchronize values back to the software + * thread, so there is no "cancel" equivalent. + */ + private void endHardwareAnimations() { + if (mHardwareAnimator != null) { + mHardwareAnimator.end(); + mHardwareAnimator = null; + } + + if (mHasPendingHardwareAnimator) { + mHasPendingHardwareAnimator = false; + } + } + + protected final void invalidateSelf() { + mOwner.invalidateSelf(); + } + + protected final boolean isHardwareAnimating() { + return mHardwareAnimator != null && mHardwareAnimator.isRunning() + || mHasPendingHardwareAnimator; + } + + protected final void onHotspotBoundsChanged() { + if (!mHasMaxRadius) { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth + + halfHeight * halfHeight); + + onTargetRadiusChanged(targetRadius); + } + } + + /** + * Called when the target radius changes. + * + * @param targetRadius the new target radius + */ + protected void onTargetRadiusChanged(float targetRadius) { + // Stub. + } + + /** + * Called during ripple setup, which occurs before the first enter + * animation. + */ + protected void onSetup() { + // Stub. + } + + protected abstract Animator createSoftwareEnter(boolean fast); + + protected abstract Animator createSoftwareExit(); + + protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p); + + protected abstract boolean drawHardware(HardwareCanvas c); + + protected abstract boolean drawSoftware(Canvas c, Paint p); + + /** + * Called when the hardware exit is cancelled. Jumps software values to end + * state to ensure that software and hardware values are synchronized. + */ + protected abstract void jumpValuesToExit(); + + public static class RenderNodeAnimatorSet { + private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>(); + + public void add(RenderNodeAnimator anim) { + mAnimators.add(anim); + } + + public void clear() { + mAnimators.clear(); + } + + public void start(HardwareCanvas target) { + if (target == null) { + throw new IllegalArgumentException("Hardware canvas must be non-null"); + } + + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.setTarget(target); + anim.start(); + } + } + + public void cancel() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.cancel(); + } + } + + public void end() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + anim.end(); + } + } + + public boolean isRunning() { + final ArrayList<RenderNodeAnimator> animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final RenderNodeAnimator anim = animators.get(i); + if (anim.isRunning()) { + return true; + } + } + return false; + } + } +} diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index b028eeb..bc8c7d2 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -137,7 +137,7 @@ public class RippleDrawable extends LayerDrawable { private boolean mBackgroundActive; /** The current ripple. May be actively animating or pending entry. */ - private Ripple mRipple; + private RippleForeground mRipple; /** Whether we expect to draw a ripple when visible. */ private boolean mRippleActive; @@ -151,7 +151,7 @@ public class RippleDrawable extends LayerDrawable { * Lazily-created array of actively animating ripples. Inactive ripples are * pruned during draw(). The locations of these will not change. */ - private Ripple[] mExitingRipples; + private RippleForeground[] mExitingRipples; private int mExitingRipplesCount = 0; /** Paint used to control appearance of ripples. */ @@ -204,11 +204,11 @@ public class RippleDrawable extends LayerDrawable { super.jumpToCurrentState(); if (mRipple != null) { - mRipple.jump(); + mRipple.end(); } if (mBackground != null) { - mBackground.jump(); + mBackground.end(); } cancelExitingRipples(); @@ -219,10 +219,13 @@ public class RippleDrawable extends LayerDrawable { boolean needsDraw = false; final int count = mExitingRipplesCount; - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { + // If the ripple is animating on the hardware thread, we'll need to + // draw an additional frame after canceling to restore the software + // drawing path. needsDraw |= ripples[i].isHardwareAnimating(); - ripples[i].cancel(); + ripples[i].end(); } if (ripples != null) { @@ -264,11 +267,9 @@ public class RippleDrawable extends LayerDrawable { for (int state : stateSet) { if (state == R.attr.state_enabled) { enabled = true; - } - if (state == R.attr.state_focused) { + } else if (state == R.attr.state_focused) { focused = true; - } - if (state == R.attr.state_pressed) { + } else if (state == R.attr.state_pressed) { pressed = true; } } @@ -563,11 +564,11 @@ public class RippleDrawable extends LayerDrawable { x = mHotspotBounds.exactCenterX(); y = mHotspotBounds.exactCenterY(); } - mRipple = new Ripple(this, mHotspotBounds, x, y); + mRipple = new RippleForeground(this, mHotspotBounds, x, y); } mRipple.setup(mState.mMaxRadius, mDensity); - mRipple.enter(); + mRipple.enter(false); } /** @@ -577,7 +578,7 @@ public class RippleDrawable extends LayerDrawable { private void tryRippleExit() { if (mRipple != null) { if (mExitingRipples == null) { - mExitingRipples = new Ripple[MAX_RIPPLES]; + mExitingRipples = new RippleForeground[MAX_RIPPLES]; } mExitingRipples[mExitingRipplesCount++] = mRipple; mRipple.exit(); @@ -591,13 +592,13 @@ public class RippleDrawable extends LayerDrawable { */ private void clearHotspots() { if (mRipple != null) { - mRipple.cancel(); + mRipple.end(); mRipple = null; mRippleActive = false; } if (mBackground != null) { - mBackground.cancel(); + mBackground.end(); mBackground = null; mBackgroundActive = false; } @@ -624,7 +625,7 @@ public class RippleDrawable extends LayerDrawable { */ private void onHotspotBoundsChanged() { final int count = mExitingRipplesCount; - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].onHotspotBoundsChanged(); } @@ -662,6 +663,8 @@ public class RippleDrawable extends LayerDrawable { */ @Override public void draw(@NonNull Canvas canvas) { + pruneRipples(); + // Clip to the dirty bounds, which will be the drawable bounds if we // have a mask or content and the ripple bounds if we're projecting. final Rect bounds = getDirtyBounds(); @@ -682,6 +685,26 @@ public class RippleDrawable extends LayerDrawable { mHasValidMask = false; } + private void pruneRipples() { + int remaining = 0; + + // Move remaining entries into pruned spaces. + final RippleForeground[] ripples = mExitingRipples; + final int count = mExitingRipplesCount; + for (int i = 0; i < count; i++) { + if (!ripples[i].hasFinishedExit()) { + ripples[remaining++] = ripples[i]; + } + } + + // Null out the remaining entries. + for (int i = remaining; i < count; i++) { + ripples[i] = null; + } + + mExitingRipplesCount = remaining; + } + /** * @return whether we need to use a mask */ @@ -747,7 +770,7 @@ public class RippleDrawable extends LayerDrawable { private int getMaskType() { if (mRipple == null && mExitingRipplesCount <= 0 - && (mBackground == null || !mBackground.shouldDraw())) { + && (mBackground == null || !mBackground.isVisible())) { // We might need a mask later. return MASK_UNKNOWN; } @@ -774,36 +797,6 @@ public class RippleDrawable extends LayerDrawable { return MASK_NONE; } - /** - * Removes a ripple from the exiting ripple list. - * - * @param ripple the ripple to remove - */ - void removeRipple(Ripple ripple) { - // Ripple ripple ripple ripple. Ripple ripple. - final Ripple[] ripples = mExitingRipples; - final int count = mExitingRipplesCount; - final int index = getRippleIndex(ripple); - if (index >= 0) { - System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); - ripples[count - 1] = null; - mExitingRipplesCount--; - - invalidateSelf(); - } - } - - private int getRippleIndex(Ripple ripple) { - final Ripple[] ripples = mExitingRipples; - final int count = mExitingRipplesCount; - for (int i = 0; i < count; i++) { - if (ripples[i] == ripple) { - return i; - } - } - return -1; - } - private void drawContent(Canvas canvas) { // Draw everything except the mask. final ChildDrawable[] array = mLayerState.mChildren; @@ -816,10 +809,10 @@ public class RippleDrawable extends LayerDrawable { } private void drawBackgroundAndRipples(Canvas canvas) { - final Ripple active = mRipple; + final RippleForeground active = mRipple; final RippleBackground background = mBackground; final int count = mExitingRipplesCount; - if (active == null && count <= 0 && (background == null || !background.shouldDraw())) { + if (active == null && count <= 0 && (background == null || !background.isVisible())) { // Move along, nothing to draw here. return; } @@ -859,12 +852,12 @@ public class RippleDrawable extends LayerDrawable { p.setShader(null); } - if (background != null && background.shouldDraw()) { + if (background != null && background.isVisible()) { background.draw(canvas, p); } if (count > 0) { - final Ripple[] ripples = mExitingRipples; + final RippleForeground[] ripples = mExitingRipples; for (int i = 0; i < count; i++) { ripples[i].draw(canvas, p); } @@ -902,7 +895,7 @@ public class RippleDrawable extends LayerDrawable { final int cY = (int) mHotspotBounds.exactCenterY(); final Rect rippleBounds = mTempRect; - final Ripple[] activeRipples = mExitingRipples; + final RippleForeground[] activeRipples = mExitingRipples; final int N = mExitingRipplesCount; for (int i = 0; i < N; i++) { activeRipples[i].getBounds(rippleBounds); diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java new file mode 100644 index 0000000..2023f04 --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleForeground.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2015 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.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Canvas; +import android.graphics.CanvasProperty; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.FloatProperty; +import android.util.MathUtils; +import android.view.HardwareCanvas; +import android.view.RenderNodeAnimator; +import android.view.animation.LinearInterpolator; + +/** + * Draws a ripple foreground. + */ +class RippleForeground extends RippleComponent { + private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( + 400f, 1.4f, 0); + + // Pixel-based accelerations and velocities. + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; + private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; + + private static final int RIPPLE_ENTER_DELAY = 80; + private static final int OPACITY_ENTER_DURATION_FAST = 120; + + private float mStartingX; + private float mStartingY; + private float mClampedStartingX; + private float mClampedStartingY; + + // Hardware rendering properties. + private CanvasProperty<Paint> mPropPaint; + private CanvasProperty<Float> mPropRadius; + private CanvasProperty<Float> mPropX; + private CanvasProperty<Float> mPropY; + + // 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 this ripple has finished its exit animation. */ + private boolean mHasFinishedExit; + + public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY) { + super(owner, bounds); + + mStartingX = startingX; + mStartingY = startingY; + } + + @Override + public void onSetup() { + mOuterX = 0; + mOuterY = 0; + } + + @Override + protected void onTargetRadiusChanged(float targetRadius) { + clampStartingPosition(); + } + + @Override + protected boolean drawSoftware(Canvas c, Paint p) { + boolean hasContent = false; + + final int origAlpha = p.getAlpha(); + final int alpha = (int) (origAlpha * mOpacity + 0.5f); + final float radius = MathUtils.lerp(0, mTargetRadius, 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); + c.drawCircle(x, y, radius, p); + p.setAlpha(origAlpha); + hasContent = true; + } + + return hasContent; + } + + @Override + protected boolean drawHardware(HardwareCanvas c) { + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + return true; + } + + /** + * 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) mTargetRadius + 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(); + } + + /** + * @return {@code true} if this ripple has finished its exit animation + */ + public boolean hasFinishedExit() { + return mHasFinishedExit; + } + + @Override + protected Animator createSoftwareEnter(boolean fast) { + final int duration = (int) + (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); + + final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); + tweenAll.setAutoCancel(true); + tweenAll.setDuration(duration); + tweenAll.setInterpolator(LINEAR_INTERPOLATOR); + tweenAll.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); + opacity.setAutoCancel(true); + opacity.setDuration(OPACITY_ENTER_DURATION_FAST); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + + final AnimatorSet set = new AnimatorSet(); + set.play(tweenAll).with(opacity); + + return set; + } + + private int getRadiusExitDuration() { + final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); + final float remaining = mTargetRadius - radius; + return (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION + + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); + } + + private int getOpacityExitDuration() { + return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); + } + + @Override + protected Animator createSoftwareExit() { + final int radiusDuration = getRadiusExitDuration(); + final int opacityDuration = getOpacityExitDuration(); + + final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); + tweenAll.setAutoCancel(true); + tweenAll.setDuration(radiusDuration); + tweenAll.setInterpolator(DECELERATE_INTERPOLATOR); + + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); + opacity.setAutoCancel(true); + opacity.setDuration(opacityDuration); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + + final AnimatorSet set = new AnimatorSet(); + set.play(tweenAll).with(opacity); + set.addListener(mAnimationListener); + + return set; + } + + @Override + protected RenderNodeAnimatorSet createHardwareExit(Paint p) { + final int radiusDuration = getRadiusExitDuration(); + final int opacityDuration = getOpacityExitDuration(); + + 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, mTargetRadius, mTweenRadius); + p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); + + mPropPaint = CanvasProperty.createPaint(p); + mPropRadius = CanvasProperty.createFloat(startRadius); + mPropX = CanvasProperty.createFloat(startX); + mPropY = CanvasProperty.createFloat(startY); + + final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); + radius.setDuration(radiusDuration); + radius.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX); + x.setDuration(radiusDuration); + x.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY); + y.setDuration(radiusDuration); + y.setInterpolator(DECELERATE_INTERPOLATOR); + + final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, + RenderNodeAnimator.PAINT_ALPHA, 0); + opacity.setDuration(opacityDuration); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + opacity.addListener(mAnimationListener); + + final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); + set.add(radius); + set.add(opacity); + set.add(x); + set.add(y); + + return set; + } + + @Override + protected void jumpValuesToExit() { + mOpacity = 0; + mTweenX = 1; + mTweenY = 1; + mTweenRadius = 1; + } + + /** + * Clamps the starting position to fit within the ripple bounds. + */ + 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 = mTargetRadius; + if (dX * dX + dY * dY > r * r) { + // Point is outside the circle, clamp to the perimeter. + 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; + } + } + + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mHasFinishedExit = true; + } + }; + + /** + * Interpolator with a smooth log deceleration. + */ + private static final class LogDecelerateInterpolator implements TimeInterpolator { + private final float mBase; + private final float mDrift; + private final float mTimeScale; + private final float mOutputScale; + + public LogDecelerateInterpolator(float base, float timeScale, float drift) { + mBase = base; + mDrift = drift; + mTimeScale = 1f / timeScale; + + mOutputScale = 1f / computeLog(1f); + } + + private float computeLog(float t) { + return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); + } + + @Override + public float getInterpolation(float t) { + return computeLog(t) * mOutputScale; + } + } + + /** + * Property for animating radius, center X, and center Y between their + * initial and target values. + */ + private static final FloatProperty<RippleForeground> TWEEN_ALL = + new FloatProperty<RippleForeground>("tweenAll") { + @Override + public void setValue(RippleForeground object, float value) { + object.mTweenRadius = value; + object.mTweenX = value; + object.mTweenY = value; + object.invalidateSelf(); + } + + @Override + public Float get(RippleForeground object) { + return object.mTweenRadius; + } + }; + + /** + * Property for animating opacity between 0 and its target value. + */ + private static final FloatProperty<RippleForeground> OPACITY = + new FloatProperty<RippleForeground>("opacity") { + @Override + public void setValue(RippleForeground object, float value) { + object.mOpacity = value; + object.invalidateSelf(); + } + + @Override + public Float get(RippleForeground object) { + return object.mOpacity; + } + }; +} |