Make ripples silky smooth
* Updates press state ripple to match UX spec
* Makes it ungodly silky smooth LIKE BUTTAH
* Update hover & focus states to be closer to UX spec,
still needs a final pass.
Bug: 63635160
Test: Clicked on a bunch of stuff
Change-Id: I162ab9d8d669002f2ae511f93b5d9fe67f99c533
(cherry picked from commit 0c453ccb87e0b5a4f4b318df01700c9a9a0da545)
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java
index 9515040..c4a7160 100644
--- a/core/java/android/view/RenderNodeAnimator.java
+++ b/core/java/android/view/RenderNodeAnimator.java
@@ -19,7 +19,6 @@
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
-import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.util.SparseIntArray;
@@ -281,12 +280,9 @@
setTarget(mViewTarget.mRenderNode);
}
- public void setTarget(Canvas canvas) {
- if (!(canvas instanceof DisplayListCanvas)) {
- throw new IllegalArgumentException("Not a GLES20RecordingCanvas");
- }
- final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas;
- setTarget(recordingCanvas.mNode);
+ /** Sets the animation target to the owning view of the DisplayListCanvas */
+ public void setTarget(DisplayListCanvas canvas) {
+ setTarget(canvas.mNode);
}
private void setTarget(RenderNode node) {
diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java
index 3bf4f90..dea194e 100644
--- a/graphics/java/android/graphics/drawable/RippleBackground.java
+++ b/graphics/java/android/graphics/drawable/RippleBackground.java
@@ -36,138 +36,69 @@
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- 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;
+ private static final int OPACITY_DURATION = 80;
- // Hardware rendering properties.
- private CanvasProperty<Paint> mPropPaint;
- private CanvasProperty<Float> mPropRadius;
- private CanvasProperty<Float> mPropX;
- private CanvasProperty<Float> mPropY;
+ private ObjectAnimator mAnimator;
- // Software rendering properties.
private float mOpacity = 0;
/** Whether this ripple is bounded. */
private boolean mIsBounded;
- public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded,
- boolean forceSoftware) {
- super(owner, bounds, forceSoftware);
+ private boolean mFocused = false;
+ private boolean mHovered = false;
+
+ public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded) {
+ super(owner, bounds);
mIsBounded = isBounded;
}
public boolean isVisible() {
- return mOpacity > 0 || isHardwareAnimating();
+ return mOpacity > 0;
}
- @Override
- protected boolean drawSoftware(Canvas c, Paint p) {
- boolean hasContent = false;
-
+ public void draw(Canvas c, Paint p) {
final int origAlpha = p.getAlpha();
- final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+ final int alpha = Math.min((int) (origAlpha * mOpacity + 0.5f), 255);
if (alpha > 0) {
p.setAlpha(alpha);
c.drawCircle(0, 0, mTargetRadius, p);
p.setAlpha(origAlpha);
- hasContent = true;
}
-
- return hasContent;
}
- @Override
- protected boolean drawHardware(DisplayListCanvas c) {
- c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
- return true;
- }
-
- @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, OPACITY, 1);
- opacity.setAutoCancel(true);
- opacity.setDuration(duration);
- opacity.setInterpolator(LINEAR_INTERPOLATOR);
-
- return opacity;
- }
-
- @Override
- protected Animator createSoftwareExit() {
- final AnimatorSet set = new AnimatorSet();
-
- // Linear exit after enter is completed.
- final ObjectAnimator exit = ObjectAnimator.ofFloat(this, OPACITY, 0);
- exit.setInterpolator(LINEAR_INTERPOLATOR);
- exit.setDuration(OPACITY_EXIT_DURATION);
- exit.setAutoCancel(true);
-
- final AnimatorSet.Builder builder = set.play(exit);
-
- // Linear "fast" enter based on current opacity.
- final int fastEnterDuration = mIsBounded ?
- (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
- if (fastEnterDuration > 0) {
- final ObjectAnimator enter = ObjectAnimator.ofFloat(this, OPACITY, 1);
- enter.setInterpolator(LINEAR_INTERPOLATOR);
- enter.setDuration(fastEnterDuration);
- enter.setAutoCancel(true);
-
- builder.after(enter);
+ public void setState(boolean focused, boolean hovered, boolean animateChanged) {
+ if (mHovered != hovered || mFocused != focused) {
+ mHovered = hovered;
+ mFocused = focused;
+ onStateChanged(animateChanged);
}
-
- return set;
}
- @Override
- protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
- final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
-
- final int targetAlpha = p.getAlpha();
- final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
- p.setAlpha(currentAlpha);
-
- mPropPaint = CanvasProperty.createPaint(p);
- mPropRadius = CanvasProperty.createFloat(mTargetRadius);
- mPropX = CanvasProperty.createFloat(0);
- mPropY = CanvasProperty.createFloat(0);
-
- final int fastEnterDuration = mIsBounded ?
- (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
-
- // 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);
- if (fastEnterDuration > 0) {
- exit.setStartDelay(fastEnterDuration);
- exit.setStartValue(targetAlpha);
+ private void onStateChanged(boolean animateChanged) {
+ float newOpacity = 0.0f;
+ if (mHovered) newOpacity += 1.0f;
+ if (mFocused) newOpacity += 1.0f;
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ mAnimator = null;
}
- set.add(exit);
-
- // Linear "fast" enter based on current opacity.
- if (fastEnterDuration > 0) {
- final RenderNodeAnimator enter = new RenderNodeAnimator(
- mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha);
- enter.setInterpolator(LINEAR_INTERPOLATOR);
- enter.setDuration(fastEnterDuration);
- set.add(enter);
+ if (animateChanged) {
+ mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
+ mAnimator.setDuration(OPACITY_DURATION);
+ mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
+ mAnimator.start();
+ } else {
+ mOpacity = newOpacity;
}
-
- return set;
}
- @Override
- protected void jumpValuesToExit() {
- mOpacity = 0;
+ public void jumpToFinal() {
+ if (mAnimator != null) {
+ mAnimator.end();
+ mAnimator = null;
+ }
}
private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java
index e83513c..0e38826 100644
--- a/graphics/java/android/graphics/drawable/RippleComponent.java
+++ b/graphics/java/android/graphics/drawable/RippleComponent.java
@@ -27,23 +27,14 @@
import java.util.ArrayList;
/**
- * Abstract class that handles hardware/software hand-off and lifecycle for
- * animated ripple foreground and background components.
+ * Abstract class that handles size & positioning common to the ripple & focus states.
*/
abstract class RippleComponent {
- private final RippleDrawable mOwner;
+ protected 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 mHasDisplayListCanvas;
-
- private boolean mHasPendingHardwareAnimator;
- private RenderNodeAnimatorSet mHardwareAnimator;
-
- private Animator mSoftwareAnimator;
-
/** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius;
@@ -53,16 +44,9 @@
/** Screen density used to adjust pixel-based constants. */
protected float mDensityScale;
- /**
- * If set, force all ripple animations to not run on RenderThread, even if it would be
- * available.
- */
- private final boolean mForceSoftware;
-
- public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
+ public RippleComponent(RippleDrawable owner, Rect bounds) {
mOwner = owner;
mBounds = bounds;
- mForceSoftware = forceSoftware;
}
public void onBoundsChange() {
@@ -92,89 +76,6 @@
}
/**
- * Starts a ripple enter animation.
- *
- * @param fast whether the ripple should enter quickly
- */
- public final void enter(boolean fast) {
- cancel();
-
- mSoftwareAnimator = createSoftwareEnter(fast);
-
- if (mSoftwareAnimator != null) {
- mSoftwareAnimator.start();
- }
- }
-
- /**
- * Starts a ripple exit animation.
- */
- public final void exit() {
- cancel();
-
- if (mHasDisplayListCanvas) {
- // 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 hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
- && c instanceof DisplayListCanvas;
- if (mHasDisplayListCanvas != hasDisplayListCanvas) {
- mHasDisplayListCanvas = hasDisplayListCanvas;
-
- if (!hasDisplayListCanvas) {
- // We've switched from hardware to non-hardware mode. Panic.
- endHardwareAnimations();
- }
- }
-
- if (hasDisplayListCanvas) {
- final DisplayListCanvas hw = (DisplayListCanvas) 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.
@@ -186,77 +87,10 @@
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(DisplayListCanvas 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();
- mSoftwareAnimator = null;
- }
- }
-
- /**
- * Ends any current software animations, jumping the values to their end
- * state.
- */
- private void endSoftwareAnimations() {
- if (mSoftwareAnimator != null) {
- mSoftwareAnimator.end();
- mSoftwareAnimator = null;
- }
- }
-
- /**
- * 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;
-
- // Manually jump values to their exited state. Normally we'd do that
- // later when starting the hardware exit, but we're aborting early.
- jumpValuesToExit();
- }
- }
-
protected final void invalidateSelf() {
mOwner.invalidateSelf(false);
}
- protected final boolean isHardwareAnimating() {
- return mHardwareAnimator != null && mHardwareAnimator.isRunning()
- || mHasPendingHardwareAnimator;
- }
-
protected final void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
final float halfWidth = mBounds.width() / 2.0f;
@@ -276,76 +110,4 @@
protected void onTargetRadiusChanged(float targetRadius) {
// Stub.
}
-
- protected abstract Animator createSoftwareEnter(boolean fast);
-
- protected abstract Animator createSoftwareExit();
-
- protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
-
- protected abstract boolean drawHardware(DisplayListCanvas 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(DisplayListCanvas 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 1727eca..8b185f2 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -16,11 +16,6 @@
package android.graphics.drawable;
-import com.android.internal.R;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ActivityInfo.Config;
@@ -42,6 +37,11 @@
import android.graphics.Shader;
import android.util.AttributeSet;
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.IOException;
import java.util.Arrays;
@@ -135,9 +135,6 @@
private PorterDuffColorFilter mMaskColorFilter;
private boolean mHasValidMask;
- /** Whether we expect to draw a background when visible. */
- private boolean mBackgroundActive;
-
/** The current ripple. May be actively animating or pending entry. */
private RippleForeground mRipple;
@@ -217,7 +214,7 @@
}
if (mBackground != null) {
- mBackground.end();
+ mBackground.jumpToFinal();
}
cancelExitingRipples();
@@ -266,9 +263,9 @@
}
}
- setRippleActive(focused || (enabled && pressed));
+ setRippleActive(enabled && pressed);
- setBackgroundActive(hovered, hovered);
+ setBackgroundActive(hovered, focused);
return changed;
}
@@ -283,14 +280,13 @@
}
}
- private void setBackgroundActive(boolean active, boolean focused) {
- if (mBackgroundActive != active) {
- mBackgroundActive = active;
- if (active) {
- tryBackgroundEnter(focused);
- } else {
- tryBackgroundExit();
- }
+ private void setBackgroundActive(boolean hovered, boolean focused) {
+ if (mBackground == null && (hovered || focused)) {
+ mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
+ mBackground.setup(mState.mMaxRadius, mDensity);
+ }
+ if (mBackground != null) {
+ mBackground.setState(focused, hovered, true);
}
}
@@ -327,10 +323,6 @@
tryRippleEnter();
}
- if (mBackgroundActive) {
- tryBackgroundEnter(false);
- }
-
// Skip animations, just show the correct final states.
jumpToCurrentState();
}
@@ -546,26 +538,6 @@
}
/**
- * Creates an active hotspot at the specified location.
- */
- private void tryBackgroundEnter(boolean focused) {
- if (mBackground == null) {
- final boolean isBounded = isBounded();
- mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
- }
-
- mBackground.setup(mState.mMaxRadius, mDensity);
- mBackground.enter(focused);
- }
-
- private void tryBackgroundExit() {
- if (mBackground != null) {
- // Don't null out the background, we need it to draw!
- mBackground.exit();
- }
- }
-
- /**
* Attempts to start an enter animation for the active hotspot. Fails if
* there are too many animating ripples.
*/
@@ -593,7 +565,7 @@
}
mRipple.setup(mState.mMaxRadius, mDensity);
- mRipple.enter(false);
+ mRipple.enter();
}
/**
@@ -623,9 +595,7 @@
}
if (mBackground != null) {
- mBackground.end();
- mBackground = null;
- mBackgroundActive = false;
+ mBackground.setState(false, false, false);
}
cancelExitingRipples();
@@ -858,38 +828,8 @@
final float y = mHotspotBounds.exactCenterY();
canvas.translate(x, y);
- updateMaskShaderIfNeeded();
-
- // Position the shader to account for canvas translation.
- if (mMaskShader != null) {
- final Rect bounds = getBounds();
- mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
- mMaskShader.setLocalMatrix(mMaskMatrix);
- }
-
- // Grab the color for the current state and cut the alpha channel in
- // half so that the ripple and background together yield full alpha.
- final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
- final int halfAlpha = (Color.alpha(color) / 2) << 24;
final Paint p = getRipplePaint();
- if (mMaskColorFilter != null) {
- // The ripple timing depends on the paint's alpha value, so we need
- // to push just the alpha channel into the paint and let the filter
- // handle the full-alpha color.
- final int fullAlphaColor = color | (0xFF << 24);
- mMaskColorFilter.setColor(fullAlphaColor);
-
- p.setColor(halfAlpha);
- p.setColorFilter(mMaskColorFilter);
- p.setShader(mMaskShader);
- } else {
- final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
- p.setColor(halfAlphaColor);
- p.setColorFilter(null);
- p.setShader(null);
- }
-
if (background != null && background.isVisible()) {
background.draw(canvas, p);
}
@@ -912,13 +852,49 @@
mMask.draw(canvas);
}
- private Paint getRipplePaint() {
+ Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
mRipplePaint.setStyle(Paint.Style.FILL);
}
- return mRipplePaint;
+
+ final float x = mHotspotBounds.exactCenterX();
+ final float y = mHotspotBounds.exactCenterY();
+
+ updateMaskShaderIfNeeded();
+
+ // Position the shader to account for canvas translation.
+ if (mMaskShader != null) {
+ final Rect bounds = getBounds();
+ mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
+ mMaskShader.setLocalMatrix(mMaskMatrix);
+ }
+
+ // Grab the color for the current state and cut the alpha channel in
+ // half so that the ripple and background together yield full alpha.
+ final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
+ final int halfAlpha = (Color.alpha(color) / 2) << 24;
+ final Paint p = mRipplePaint;
+
+ if (mMaskColorFilter != null) {
+ // The ripple timing depends on the paint's alpha value, so we need
+ // to push just the alpha channel into the paint and let the filter
+ // handle the full-alpha color.
+ final int fullAlphaColor = color | (0xFF << 24);
+ mMaskColorFilter.setColor(fullAlphaColor);
+
+ p.setColor(halfAlpha);
+ p.setColorFilter(mMaskColorFilter);
+ p.setShader(mMaskShader);
+ } else {
+ final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
+ p.setColor(halfAlphaColor);
+ p.setColorFilter(null);
+ p.setShader(null);
+ }
+
+ return p;
}
@Override
diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java
index a675eaf..0b5020c 100644
--- a/graphics/java/android/graphics/drawable/RippleForeground.java
+++ b/graphics/java/android/graphics/drawable/RippleForeground.java
@@ -18,7 +18,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
@@ -29,8 +28,11 @@
import android.util.MathUtils;
import android.view.DisplayListCanvas;
import android.view.RenderNodeAnimator;
+import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
+import java.util.ArrayList;
+
/**
* Draws a ripple foreground.
*/
@@ -40,7 +42,7 @@
400f, 1.4f, 0);
// Pixel-based accelerations and velocities.
- private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
+ private static final float WAVE_TOUCH_DOWN_ACCELERATION = 2048;
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
// Bounded ripple animation properties.
@@ -49,8 +51,9 @@
private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
private static final float MAX_BOUNDED_RADIUS = 350;
- private static final int RIPPLE_ENTER_DELAY = 80;
- private static final int OPACITY_ENTER_DURATION_FAST = 120;
+ private static final int OPACITY_ENTER_DURATION = 75;
+ private static final int OPACITY_EXIT_DURATION = 150;
+ private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150;
// Parent-relative values for starting position.
private float mStartingX;
@@ -72,7 +75,7 @@
private float mBoundedRadius = 0;
// Software rendering properties.
- private float mOpacity = 1;
+ private float mOpacity = 0;
// Values used to tween between the start and end positions.
private float mTweenRadius = 0;
@@ -82,6 +85,22 @@
/** Whether this ripple has finished its exit animation. */
private boolean mHasFinishedExit;
+ /** Whether we can use hardware acceleration for the exit animation. */
+ private boolean mUsingProperties;
+
+ private long mEnterStartedAtMillis;
+
+ private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>();
+ private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>();
+
+ private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>();
+
+ /**
+ * If set, force all ripple animations to not run on RenderThread, even if it would be
+ * available.
+ */
+ private final boolean mForceSoftware;
+
/**
* If we have a bound, don't start from 0. Start from 60% of the max out of width and height.
*/
@@ -89,8 +108,9 @@
public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
boolean isBounded, boolean forceSoftware) {
- super(owner, bounds, forceSoftware);
+ super(owner, bounds);
+ mForceSoftware = forceSoftware;
mStartingX = startingX;
mStartingY = startingY;
@@ -109,10 +129,7 @@
clampStartingPosition();
}
- @Override
- protected boolean drawSoftware(Canvas c, Paint p) {
- boolean hasContent = false;
-
+ private void drawSoftware(Canvas c, Paint p) {
final int origAlpha = p.getAlpha();
final int alpha = (int) (origAlpha * mOpacity + 0.5f);
final float radius = getCurrentRadius();
@@ -122,16 +139,51 @@
p.setAlpha(alpha);
c.drawCircle(x, y, radius, p);
p.setAlpha(origAlpha);
- hasContent = true;
}
-
- return hasContent;
}
- @Override
- protected boolean drawHardware(DisplayListCanvas c) {
- c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
- return true;
+ private void startPending(DisplayListCanvas c) {
+ if (!mPendingHwAnimators.isEmpty()) {
+ for (int i = 0; i < mPendingHwAnimators.size(); i++) {
+ RenderNodeAnimator animator = mPendingHwAnimators.get(i);
+ animator.setTarget(c);
+ animator.start();
+ mRunningHwAnimators.add(animator);
+ }
+ mPendingHwAnimators.clear();
+ }
+ }
+
+ private void pruneHwFinished() {
+ if (!mRunningHwAnimators.isEmpty()) {
+ for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) {
+ if (!mRunningHwAnimators.get(i).isRunning()) {
+ mRunningHwAnimators.remove(i);
+ }
+ }
+ }
+ }
+
+ private void pruneSwFinished() {
+ if (!mRunningSwAnimators.isEmpty()) {
+ for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) {
+ if (!mRunningSwAnimators.get(i).isRunning()) {
+ mRunningSwAnimators.remove(i);
+ }
+ }
+ }
+ }
+
+ private void drawHardware(DisplayListCanvas c, Paint p) {
+ startPending(c);
+ pruneHwFinished();
+ if (mPropPaint != null) {
+ mUsingProperties = true;
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+ } else {
+ mUsingProperties = false;
+ drawSoftware(c, p);
+ }
}
/**
@@ -162,31 +214,115 @@
return mHasFinishedExit;
}
- @Override
- protected Animator createSoftwareEnter(boolean fast) {
+ private long computeFadeOutDelay() {
+ long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis;
+ if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) {
+ return OPACITY_HOLD_DURATION - timeSinceEnter;
+ }
+ return 0;
+ }
+
+ private void startSoftwareEnter() {
+ for (int i = 0; i < mRunningSwAnimators.size(); i++) {
+ mRunningSwAnimators.get(i).cancel();
+ }
+ mRunningSwAnimators.clear();
+
final int duration = getRadiusDuration();
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
- tweenRadius.setAutoCancel(true);
tweenRadius.setDuration(duration);
tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
- tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
+ tweenRadius.start();
+ mRunningSwAnimators.add(tweenRadius);
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
- tweenOrigin.setAutoCancel(true);
tweenOrigin.setDuration(duration);
tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
- tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
+ tweenOrigin.start();
+ mRunningSwAnimators.add(tweenOrigin);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
- opacity.setAutoCancel(true);
- opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
+ opacity.setDuration(OPACITY_ENTER_DURATION);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.start();
+ mRunningSwAnimators.add(opacity);
+ }
- final AnimatorSet set = new AnimatorSet();
- set.play(tweenOrigin).with(tweenRadius).with(opacity);
+ private void startSoftwareExit() {
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
+ opacity.setDuration(OPACITY_EXIT_DURATION);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.addListener(mAnimationListener);
+ opacity.setStartDelay(computeFadeOutDelay());
+ opacity.start();
+ mRunningSwAnimators.add(opacity);
+ }
- return set;
+ private void startHardwareEnter() {
+ if (mForceSoftware) { return; }
+ mPropX = CanvasProperty.createFloat(getCurrentX());
+ mPropY = CanvasProperty.createFloat(getCurrentY());
+ mPropRadius = CanvasProperty.createFloat(getCurrentRadius());
+ final Paint paint = mOwner.getRipplePaint();
+ mPropPaint = CanvasProperty.createPaint(paint);
+
+ final int radiusDuration = getRadiusDuration();
+
+ final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
+ radius.setDuration(radiusDuration);
+ radius.setInterpolator(DECELERATE_INTERPOLATOR);
+ mPendingHwAnimators.add(radius);
+
+ final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
+ x.setDuration(radiusDuration);
+ x.setInterpolator(DECELERATE_INTERPOLATOR);
+ mPendingHwAnimators.add(x);
+
+ final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
+ y.setDuration(radiusDuration);
+ y.setInterpolator(DECELERATE_INTERPOLATOR);
+ mPendingHwAnimators.add(y);
+
+ final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+ RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha());
+ opacity.setDuration(OPACITY_ENTER_DURATION);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.setStartValue(0);
+ mPendingHwAnimators.add(opacity);
+
+ invalidateSelf();
+ }
+
+ private void startHardwareExit() {
+ // Only run a hardware exit if we had a hardware enter to continue from
+ if (mForceSoftware || mPropPaint == null) return;
+
+ final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacity.setDuration(OPACITY_EXIT_DURATION);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.addListener(mAnimationListener);
+ opacity.setStartDelay(computeFadeOutDelay());
+ mPendingHwAnimators.add(opacity);
+ invalidateSelf();
+ }
+
+ /**
+ * Starts a ripple enter animation.
+ */
+ public final void enter() {
+ mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis();
+ startSoftwareEnter();
+ startHardwareEnter();
+ }
+
+ /**
+ * Starts a ripple exit animation.
+ */
+ public final void exit() {
+ startSoftwareExit();
+ startHardwareExit();
}
private float getCurrentX() {
@@ -207,96 +343,23 @@
return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius);
}
- private int getOpacityExitDuration() {
- return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
- }
+ /**
+ * 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
+ */
+ public void draw(Canvas c, Paint p) {
+ final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas;
- @Override
- protected Animator createSoftwareExit() {
- final int radiusDuration;
- final int originDuration;
- final int opacityDuration;
-
- radiusDuration = getRadiusDuration();
- originDuration = radiusDuration;
- opacityDuration = getOpacityExitDuration();
-
- final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
- tweenRadius.setAutoCancel(true);
- tweenRadius.setDuration(radiusDuration);
- tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
-
- final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
- tweenOrigin.setAutoCancel(true);
- tweenOrigin.setDuration(originDuration);
- tweenOrigin.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(tweenOrigin).with(tweenRadius).with(opacity);
- set.addListener(mAnimationListener);
-
- return set;
- }
-
- @Override
- protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
- final int radiusDuration;
- final int originDuration;
- final int opacityDuration;
-
- radiusDuration = getRadiusDuration();
- originDuration = radiusDuration;
- opacityDuration = getOpacityExitDuration();
-
- final float startX = getCurrentX();
- final float startY = getCurrentY();
- final float startRadius = getCurrentRadius();
-
- 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, mTargetX);
- x.setDuration(originDuration);
- x.setInterpolator(DECELERATE_INTERPOLATOR);
-
- final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
- y.setDuration(originDuration);
- 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;
+ pruneSwFinished();
+ if (hasDisplayListCanvas) {
+ final DisplayListCanvas hw = (DisplayListCanvas) c;
+ drawHardware(hw, p);
+ } else {
+ drawSoftware(c, p);
+ }
}
/**
@@ -319,10 +382,39 @@
}
}
+ /**
+ * Ends all animations, jumping values to the end state.
+ */
+ public void end() {
+ for (int i = 0; i < mRunningSwAnimators.size(); i++) {
+ mRunningSwAnimators.get(i).end();
+ }
+ mRunningSwAnimators.clear();
+ for (int i = 0; i < mRunningHwAnimators.size(); i++) {
+ mRunningHwAnimators.get(i).end();
+ }
+ mRunningHwAnimators.clear();
+ }
+
+ private void onAnimationPropertyChanged() {
+ if (!mUsingProperties) {
+ invalidateSelf();
+ }
+ }
+
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
mHasFinishedExit = true;
+ pruneHwFinished();
+ pruneSwFinished();
+
+ if (mRunningHwAnimators.isEmpty()) {
+ mPropPaint = null;
+ mPropRadius = null;
+ mPropX = null;
+ mPropY = null;
+ }
}
};
@@ -361,7 +453,7 @@
@Override
public void setValue(RippleForeground object, float value) {
object.mTweenRadius = value;
- object.invalidateSelf();
+ object.onAnimationPropertyChanged();
}
@Override
@@ -375,18 +467,18 @@
*/
private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
new FloatProperty<RippleForeground>("tweenOrigin") {
- @Override
- public void setValue(RippleForeground object, float value) {
- object.mTweenX = value;
- object.mTweenY = value;
- object.invalidateSelf();
- }
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mTweenX = value;
+ object.mTweenY = value;
+ object.onAnimationPropertyChanged();
+ }
- @Override
- public Float get(RippleForeground object) {
- return object.mTweenX;
- }
- };
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mTweenX;
+ }
+ };
/**
* Property for animating opacity between 0 and its target value.
@@ -396,7 +488,7 @@
@Override
public void setValue(RippleForeground object, float value) {
object.mOpacity = value;
- object.invalidateSelf();
+ object.onAnimationPropertyChanged();
}
@Override