diff options
author | Alan Viverette <alanv@google.com> | 2014-05-16 13:28:33 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2014-05-16 13:28:33 -0700 |
commit | ad2f8e334f3ef22d3e412b0660a2e1f996f94116 (patch) | |
tree | 0f23be1a722743ed216713a00304c234a782aca3 /graphics | |
parent | 891e65c5ceff6a7859afab34f75f149b3aefa0c6 (diff) | |
download | frameworks_base-ad2f8e334f3ef22d3e412b0660a2e1f996f94116.zip frameworks_base-ad2f8e334f3ef22d3e412b0660a2e1f996f94116.tar.gz frameworks_base-ad2f8e334f3ef22d3e412b0660a2e1f996f94116.tar.bz2 |
Update ripple behavior, use render thread animation
Change-Id: Ib6bc1e08b05d29606f452961963d58b8fc866746
Diffstat (limited to 'graphics')
-rw-r--r-- | graphics/java/android/graphics/drawable/Ripple.java | 612 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java | 295 |
2 files changed, 547 insertions, 360 deletions
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java index 218a057..24e8de6 100644 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -17,227 +17,220 @@ package android.graphics.drawable; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; 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.Paint.Style; import android.graphics.Rect; -import android.view.animation.DecelerateInterpolator; +import android.view.HardwareCanvas; +import android.view.RenderNodeAnimator; +import android.view.animation.AccelerateInterpolator; + +import java.util.ArrayList; /** * Draws a Quantum Paper ripple. */ class Ripple { - private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(); - - /** Starting radius for a ripple. */ - private static final int STARTING_RADIUS_DP = 16; - - /** Radius when finger is outside view bounds. */ - private static final int OUTSIDE_RADIUS_DP = 16; - - /** Radius when finger is inside view bounds. */ - private static final int INSIDE_RADIUS_DP = 96; - - /** Margin when constraining outside touches (fraction of outer radius). */ - private static final float OUTSIDE_MARGIN = 0.8f; - - /** Resistance factor when constraining outside touches. */ - private static final float OUTSIDE_RESISTANCE = 0.7f; - - /** Minimum alpha value during a pulse animation. */ - private static final float PULSE_MIN_ALPHA = 0.5f; - - /** Duration for animating the trailing edge of the ripple. */ - private static final int EXIT_DURATION = 600; + private static final TimeInterpolator INTERPOLATOR = new AccelerateInterpolator(); - /** Duration for animating the leading edge of the ripple. */ - private static final int ENTER_DURATION = 400; + private static final float GLOBAL_SPEED = 1.0f; + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 512.0f * GLOBAL_SPEED; + private static final float WAVE_TOUCH_UP_ACCELERATION = 1024.0f * GLOBAL_SPEED; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 1.6f / GLOBAL_SPEED; + private static final float WAVE_OUTER_OPACITY_VELOCITY = 1.2f * GLOBAL_SPEED; - /** Duration for animating the ripple alpha in and out. */ - private static final int FADE_DURATION = 50; - - /** Minimum elapsed time between start of enter and exit animations. */ - private static final int EXIT_MIN_DELAY = 200; - - /** Duration for animating between inside and outside touch. */ - private static final int OUTSIDE_DURATION = 300; - - /** Duration for animating pulses. */ - private static final int PULSE_DURATION = 400; - - /** Interval between pulses while inside and fully entered. */ - private static final int PULSE_INTERVAL = 400; - - /** Delay before pulses start. */ - private static final int PULSE_DELAY = 500; + // Hardware animators. + private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>(); + private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>(); private final Drawable mOwner; - /** Bounds used for computing max radius and containment. */ + /** Bounds used for computing max radius. */ private final Rect mBounds; - /** Configured maximum ripple radius when the center is outside the bounds. */ - private final int mMaxOutsideRadius; - - /** Configured maximum ripple radius. */ - private final int mMaxInsideRadius; - - private ObjectAnimator mOuter; - private ObjectAnimator mInner; - private ObjectAnimator mAlpha; + /** Full-opacity color for drawing this ripple. */ + private final int mColor; /** Maximum ripple radius. */ - private int mMaxRadius; - private float mOuterRadius; - private float mInnerRadius; - private float mAlphaMultiplier; - /** Center x-coordinate. */ + // Hardware rendering properties. + private CanvasProperty<Paint> mPropPaint; + private CanvasProperty<Float> mPropRadius; + private CanvasProperty<Float> mPropX; + private CanvasProperty<Float> mPropY; + private CanvasProperty<Paint> mPropOuterPaint; + private CanvasProperty<Float> mPropOuterRadius; + private CanvasProperty<Float> mPropOuterX; + private CanvasProperty<Float> mPropOuterY; + + // Software animators. + private ObjectAnimator mAnimRadius; + private ObjectAnimator mAnimOpacity; + private ObjectAnimator mAnimOuterOpacity; + private ObjectAnimator mAnimX; + private ObjectAnimator mAnimY; + + // Software rendering properties. + private float mOuterOpacity = 0; + private float mOpacity = 1; + private float mRadius = 0; + private float mOuterX; + private float mOuterY; private float mX; - - /** Center y-coordinate. */ private float mY; - /** Whether the center is within the parent bounds. */ - private boolean mInsideBounds; + private boolean mFinished; - /** Whether to pulse this ripple. */ - private boolean mPulseEnabled; + /** Whether we should be drawing hardware animations. */ + private boolean mHardwareAnimating; - /** Temporary hack since we can't check finished state of animator. */ - private boolean mExitFinished; - - /** Whether this ripple has ever moved. */ - private boolean mHasMoved; + /** Whether we can use hardware acceleration for the exit animation. */ + private boolean mCanUseHardware; /** * Creates a new ripple. */ - public Ripple(Drawable owner, Rect bounds, float density, boolean pulseEnabled) { + public Ripple(Drawable owner, Rect bounds, int color) { mOwner = owner; mBounds = bounds; - mPulseEnabled = pulseEnabled; + mColor = color | 0xFF000000; + + final float halfWidth = bounds.width() / 2.0f; + final float halfHeight = bounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + mOuterX = 0; + mOuterY = 0; + } + + public void setRadius(float r) { + mRadius = r; + invalidateSelf(); + } - mOuterRadius = (int) (density * STARTING_RADIUS_DP + 0.5f); - mMaxOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f); - mMaxInsideRadius = (int) (density * INSIDE_RADIUS_DP + 0.5f); - mMaxRadius = Math.min(mMaxInsideRadius, Math.max(bounds.width(), bounds.height())); + public float getRadius() { + return mRadius; } - public void setOuterRadius(float r) { - mOuterRadius = r; + public void setOpacity(float a) { + mOpacity = a; invalidateSelf(); } - public float getOuterRadius() { - return mOuterRadius; + public float getOpacity() { + return mOpacity; + } + + public void setOuterOpacity(float a) { + mOuterOpacity = a; + invalidateSelf(); } - public void setInnerRadius(float r) { - mInnerRadius = r; + public float getOuterOpacity() { + return mOuterOpacity; + } + + public void setX(float x) { + mX = x; invalidateSelf(); } - public float getInnerRadius() { - return mInnerRadius; + public float getX() { + return mX; } - public void setAlphaMultiplier(float a) { - mAlphaMultiplier = a; + public void setY(float y) { + mY = y; invalidateSelf(); } - public float getAlphaMultiplier() { - return mAlphaMultiplier; + public float getY() { + return mY; } /** * Returns whether this ripple has finished exiting. */ public boolean isFinished() { - return mExitFinished; + return mFinished; } /** - * Called when the bounds change. + * Draws the ripple centered at (0,0) using the specified paint. */ - public void onBoundsChanged() { - mMaxRadius = Math.min(mMaxInsideRadius, Math.max(mBounds.width(), mBounds.height())); + 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(); + } + mCanUseHardware = canUseHardware; - updateInsideBounds(); - } + final boolean hasContent; + if (canUseHardware && mHardwareAnimating) { + hasContent = drawHardware((HardwareCanvas) c); + } else { + hasContent = drawSoftware(c, p); + } - private void updateInsideBounds() { - final boolean insideBounds = mBounds.contains((int) (mX + 0.5f), (int) (mY + 0.5f)); - if (mInsideBounds != insideBounds || !mHasMoved) { - mInsideBounds = insideBounds; - mHasMoved = true; + return hasContent; + } - if (insideBounds) { - enter(); - } else { - outside(); + private boolean drawHardware(HardwareCanvas c) { + // If we have any pending hardware animations, cancel any running + // animations and start those now. + final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations; + final int N = pendingAnimations == null ? 0 : pendingAnimations.size(); + if (N > 0) { + cancelHardwareAnimations(); + + for (int i = 0; i < N; i++) { + pendingAnimations.get(i).setTarget(c); + pendingAnimations.get(i).start(); } + + mRunningAnimations.addAll(pendingAnimations); + pendingAnimations.clear(); } + + c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + + return true; } - /** - * Draws the ripple using the specified paint. - */ - public boolean draw(Canvas c, Paint p) { - final Rect bounds = mBounds; - final float outerRadius = mOuterRadius; - final float innerRadius = mInnerRadius; - final float alphaMultiplier = mAlphaMultiplier; + private boolean drawSoftware(Canvas c, Paint p) { + final float radius = mRadius; + final float opacity = mOpacity; + final float outerOpacity = mOuterOpacity; // Cache the paint alpha so we can restore it later. final int paintAlpha = p.getAlpha(); - final int alpha = (int) (paintAlpha * alphaMultiplier + 0.5f); - - // Apply resistance effect when outside bounds. - final float x; - final float y; - if (mInsideBounds) { - x = mX; - y = mY; - } else { - // TODO: We need to do this outside of draw() so that our dirty - // bounds accurately reflect resistance. - x = looseConstrain(mX, bounds.left, bounds.right, - mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); - y = looseConstrain(mY, bounds.top, bounds.bottom, - mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); - } + final int alpha = (int) (255 * opacity + 0.5f); + final int outerAlpha = (int) (255 * outerOpacity + 0.5f); - final boolean hasContent; - if (alphaMultiplier <= 0 || innerRadius >= outerRadius) { - // Nothing to draw. - hasContent = false; - } else if (innerRadius > 0) { - // Draw a ring. - final float strokeWidth = outerRadius - innerRadius; - final float strokeRadius = innerRadius + strokeWidth / 2.0f; - p.setAlpha(alpha); - p.setStyle(Style.STROKE); - p.setStrokeWidth(strokeWidth); - c.drawCircle(x, y, strokeRadius, p); + boolean hasContent = false; + + if (outerAlpha > 0 && alpha > 0) { + p.setAlpha(Math.min(alpha, outerAlpha)); + p.setStyle(Style.FILL); + c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); hasContent = true; - } else if (outerRadius > 0) { - // Draw a circle. + } + + if (opacity > 0 && radius > 0) { p.setAlpha(alpha); p.setStyle(Style.FILL); - c.drawCircle(x, y, outerRadius, p); + c.drawCircle(mX, mY, radius, p); hasContent = true; - } else { - hasContent = false; } p.setAlpha(paintAlpha); + return hasContent; } @@ -245,156 +238,279 @@ class Ripple { * Returns the maximum bounds for this ripple. */ public void getBounds(Rect bounds) { + final int outerX = (int) mOuterX; + final int outerY = (int) mOuterY; + final int r = (int) mOuterRadius; + bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); + final int x = (int) mX; final int y = (int) mY; - final int maxRadius = mMaxRadius; - bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius); + bounds.union(x - r, y - r, x + r, y + r); } /** - * Updates the center coordinates. + * Starts the enter animation at the specified absolute coordinates. */ - public void move(float x, float y) { - mX = x; - mY = y; + public void enter(float x, float y) { + mX = x - mBounds.exactCenterX(); + mY = y - mBounds.exactCenterY(); - updateInsideBounds(); - invalidateSelf(); + final int radiusDuration = (int) + (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION) + 0.5); + final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY); + + final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", 0, mOuterRadius); + radius.setAutoCancel(true); + radius.setDuration(radiusDuration); + + final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "x", mOuterX); + cX.setAutoCancel(true); + cX.setDuration(radiusDuration); + + final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "y", mOuterY); + cY.setAutoCancel(true); + cY.setDuration(radiusDuration); + + final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); + outer.setAutoCancel(true); + outer.setDuration(outerDuration); + + mAnimRadius = radius; + mAnimOuterOpacity = outer; + 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(); + outer.start(); + cX.start(); + cY.start(); } /** - * Starts the exit animation. If {@link #enter()} was called recently, the - * animation may be postponed. + * Starts the exit animation. */ public void exit() { - mExitFinished = false; - - final ObjectAnimator inner = ObjectAnimator.ofFloat(this, "innerRadius", 0, mMaxRadius); - inner.setAutoCancel(true); - inner.setDuration(EXIT_DURATION); - inner.setInterpolator(INTERPOLATOR); - inner.addListener(mAnimationListener); - - if (mOuter != null && mOuter.isStarted()) { - // If we haven't been running the enter animation for long enough, - // delay the exit animator. - final int elapsed = (int) (mOuter.getAnimatedFraction() * mOuter.getDuration()); - final int delay = Math.max(0, EXIT_MIN_DELAY - elapsed); - inner.setStartDelay(delay); + cancelSoftwareAnimations(); + + final float remaining; + if (mAnimRadius != null && mAnimRadius.isRunning()) { + remaining = mOuterRadius - mRadius; + } else { + remaining = mOuterRadius; } - inner.start(); + final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION + + WAVE_TOUCH_DOWN_ACCELERATION)) + 0.5); + final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 0); - alpha.setAutoCancel(true); - alpha.setDuration(EXIT_DURATION); - alpha.start(); + // 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 outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity) + / (WAVE_OPACITY_DECAY_VELOCITY + WAVE_OUTER_OPACITY_VELOCITY) + 0.5f)); + final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection + * WAVE_OUTER_OPACITY_VELOCITY / 1000) + 0.5f); - mInner = inner; - mAlpha = alpha; + if (mCanUseHardware) { + exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity); + } else { + exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity); + } } - /** - * Cancel all animations. - */ - public void cancel() { - if (mInner != null) { - mInner.cancel(); + private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection, + int inflectionOpacity) { + mPendingAnimations.clear(); + + final Paint outerPaint = new Paint(); + outerPaint.setAntiAlias(true); + outerPaint.setColor(mColor); + outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f)); + outerPaint.setStyle(Style.FILL); + mPropOuterPaint = CanvasProperty.createPaint(outerPaint); + mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); + mPropOuterX = CanvasProperty.createFloat(mOuterX); + mPropOuterY = CanvasProperty.createFloat(mOuterY); + + final Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(mColor); + paint.setAlpha((int) (255 * mOpacity + 0.5f)); + paint.setStyle(Style.FILL); + mPropPaint = CanvasProperty.createPaint(paint); + mPropRadius = CanvasProperty.createFloat(mRadius); + mPropX = CanvasProperty.createFloat(mX); + mPropY = CanvasProperty.createFloat(mY); + + final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mOuterRadius); + radius.setDuration(radiusDuration); + + final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX); + x.setDuration(radiusDuration); + + final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY); + y.setDuration(radiusDuration); + + final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, + RenderNodeAnimator.PAINT_ALPHA, 0); + opacity.setDuration(opacityDuration); + opacity.addListener(mAnimationListener); + + final RenderNodeAnimator outerOpacity; + if (outerInflection > 0) { + // Outer opacity continues to increase for a bit. + outerOpacity = new RenderNodeAnimator( + mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); + outerOpacity.setDuration(outerInflection); + + // Chain the outer opacity exit animation. + final int outerDuration = opacityDuration - outerInflection; + if (outerDuration > 0) { + final RenderNodeAnimator outerFadeOut = new RenderNodeAnimator( + mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); + outerFadeOut.setDuration(outerDuration); + outerFadeOut.setStartDelay(outerInflection); + + mPendingAnimations.add(outerFadeOut); + } + } else { + outerOpacity = new RenderNodeAnimator( + mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); + outerOpacity.setDuration(opacityDuration); } - if (mOuter != null) { - mOuter.cancel(); - } + mPendingAnimations.add(radius); + mPendingAnimations.add(opacity); + mPendingAnimations.add(outerOpacity); + mPendingAnimations.add(x); + mPendingAnimations.add(y); - if (mAlpha != null) { - mAlpha.cancel(); - } - } + mHardwareAnimating = true; - private void invalidateSelf() { - mOwner.invalidateSelf(); + invalidateSelf(); } - /** - * Starts the enter animation. - */ - private void enter() { - final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerRadius", mMaxRadius); - outer.setAutoCancel(true); - outer.setDuration(ENTER_DURATION); - outer.setInterpolator(INTERPOLATOR); - outer.start(); - - final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1); - if (mPulseEnabled) { - alpha.addListener(new AnimatorListenerAdapter() { + private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection, + float inflectionOpacity) { + final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", mOuterRadius); + radius.setAutoCancel(true); + radius.setDuration(radiusDuration); + + final ObjectAnimator x = ObjectAnimator.ofFloat(this, "x", mOuterX); + x.setAutoCancel(true); + x.setDuration(radiusDuration); + + final ObjectAnimator y = ObjectAnimator.ofFloat(this, "y", mOuterY); + y.setAutoCancel(true); + y.setDuration(radiusDuration); + + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "opacity", 0); + opacity.setAutoCancel(true); + opacity.setDuration(opacityDuration); + opacity.addListener(mAnimationListener); + + final ObjectAnimator outerOpacity; + if (outerInflection > 0) { + // Outer opacity continues to increase for a bit. + outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", inflectionOpacity); + outerOpacity.setDuration(outerInflection); + + // Chain the outer opacity exit animation. + final int outerDuration = opacityDuration - outerInflection; + outerOpacity.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - final ObjectAnimator pulse = ObjectAnimator.ofFloat( - this, "alphaMultiplier", 1, PULSE_MIN_ALPHA); - pulse.setAutoCancel(true); - pulse.setDuration(PULSE_DURATION + PULSE_INTERVAL); - pulse.setRepeatCount(ObjectAnimator.INFINITE); - pulse.setRepeatMode(ObjectAnimator.REVERSE); - pulse.setStartDelay(PULSE_DELAY); - pulse.start(); - - mAlpha = pulse; + final ObjectAnimator outerFadeOut = ObjectAnimator.ofFloat(Ripple.this, + "outerOpacity", 0); + outerFadeOut.setDuration(outerDuration); + + mAnimOuterOpacity = outerFadeOut; + + outerFadeOut.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + animation.removeListener(this); } }); + } else { + outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0); + outerOpacity.setDuration(opacityDuration); } - alpha.setAutoCancel(true); - alpha.setDuration(FADE_DURATION); - alpha.start(); - mOuter = outer; - mAlpha = alpha; + mAnimRadius = radius; + mAnimOpacity = opacity; + mAnimOuterOpacity = outerOpacity; + mAnimX = opacity; + mAnimY = opacity; + + radius.start(); + opacity.start(); + outerOpacity.start(); + x.start(); + y.start(); } /** - * Starts the outside transition animation. + * Cancel all animations. */ - private void outside() { - final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerRadius", mMaxOutsideRadius); - outer.setAutoCancel(true); - outer.setDuration(OUTSIDE_DURATION); - outer.setInterpolator(INTERPOLATOR); - outer.start(); + public void cancel() { + cancelSoftwareAnimations(); + cancelHardwareAnimations(); + } - final ObjectAnimator alpha = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1); - alpha.setAutoCancel(true); - alpha.setDuration(FADE_DURATION); - alpha.start(); + private void cancelSoftwareAnimations() { + if (mAnimRadius != null) { + mAnimRadius.cancel(); + } - mOuter = outer; - mAlpha = alpha; + if (mAnimOpacity != null) { + mAnimOpacity.cancel(); + } + + if (mAnimOuterOpacity != null) { + mAnimOuterOpacity.cancel(); + } + + if (mAnimX != null) { + mAnimX.cancel(); + } + + if (mAnimY != null) { + mAnimY.cancel(); + } } /** - * Constrains a value within a specified asymptotic margin outside a minimum - * and maximum. + * Cancels any running hardware animations. */ - private static float looseConstrain(float value, float min, float max, float margin, - float factor) { - // TODO: Can we use actual spring physics here? - if (value < min) { - return min - Math.min(margin, (float) Math.pow(min - value, factor)); - } else if (value > max) { - return max + Math.min(margin, (float) Math.pow(value - max, factor)); - } else { - return value; + private void cancelHardwareAnimations() { + final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; + final int N = runningAnimations == null ? 0 : runningAnimations.size(); + for (int i = 0; i < N; i++) { + runningAnimations.get(i).cancel(); } + + runningAnimations.clear(); + } + + private void invalidateSelf() { + mOwner.invalidateSelf(); } - private final AnimatorListener mAnimationListener = new AnimatorListenerAdapter() { + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - if (animation == mInner) { - mExitFinished = true; - mOuterRadius = 0; - mInnerRadius = 0; - mAlphaMultiplier = 1; - } + mFinished = true; + } + + @Override + public void onAnimationCancel(Animator animation) { + mFinished = true; } }; } diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java index 8128b5f..a55a4b2 100644 --- a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java +++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java @@ -24,6 +24,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; @@ -33,6 +34,7 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.R; +import com.android.org.bouncycastle.util.Arrays; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -40,11 +42,36 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; /** - * Documentation pending. + * Drawable that shows a ripple effect in response to state changes. The + * anchoring position of the ripple for a given state may be specified by + * calling {@link #setHotspot(int, float, float)} with the corresponding state + * attribute identifier. + * <p> + * A touch feedback drawable may contain multiple child layers, including a + * special mask layer that is not drawn to the screen. A single layer may be set + * as the mask by specifying its android:id value as {@link android.R.id#mask}. + * <p> + * If a mask layer is set, the ripple effect will be masked against that layer + * before it is blended onto the composite of the remaining child layers. + * <p> + * If no mask layer is set, the ripple effect is simply blended onto the + * composite of the child layers using the specified + * {@link android.R.styleable#TouchFeedbackDrawable_tintMode}. + * <p> + * If no child layers or mask is specified and the ripple is set as a View + * background, the ripple will be blended onto the first available parent + * background within the View's hierarchy using the specified + * {@link android.R.styleable#TouchFeedbackDrawable_tintMode}. In this case, the + * drawing region may extend outside of the Drawable bounds. + * + * @attr ref android.R.styleable#DrawableStates_state_focused + * @attr ref android.R.styleable#DrawableStates_state_pressed */ public class TouchFeedbackDrawable extends LayerDrawable { private static final String LOG_TAG = TouchFeedbackDrawable.class.getSimpleName(); private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); + private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP); + private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER); /** The maximum number of ripples supported. */ @@ -63,10 +90,22 @@ public class TouchFeedbackDrawable extends LayerDrawable { private final TouchFeedbackState mState; - /** Lazily-created map of touch hotspot IDs to ripples. */ - private SparseArray<Ripple> mRipples; + /** + * Lazily-created map of pending hotspot locations. These may be modified by + * calls to {@link #setHotspot(int, float, float)}. + */ + private SparseArray<PointF> mPendingHotspots; + + /** + * Lazily-created map of active hotspot locations. These may be modified by + * calls to {@link #setHotspot(int, float, float)}. + */ + private SparseArray<Ripple> mActiveHotspots; - /** Lazily-created array of actively animating ripples. */ + /** + * Lazily-created array of actively animating ripples. Inactive ripples are + * pruned during draw(). The locations of these will not change. + */ private Ripple[] mAnimatingRipples; private int mAnimatingRipplesCount = 0; @@ -96,24 +135,18 @@ public class TouchFeedbackDrawable extends LayerDrawable { protected boolean onStateChange(int[] stateSet) { super.onStateChange(stateSet); - // TODO: Implicitly tie states to ripple IDs. For now, just clear - // focused and pressed if they aren't in the state set. - boolean hasFocused = false; - boolean hasPressed = false; - for (int i = 0; i < stateSet.length; i++) { - if (stateSet[i] == R.attr.state_pressed) { - hasPressed = true; - } else if (stateSet[i] == R.attr.state_focused) { - hasFocused = true; - } - } - - if (!hasPressed) { + final boolean pressed = Arrays.contains(stateSet, R.attr.state_pressed); + if (!pressed) { removeHotspot(R.attr.state_pressed); + } else { + activateHotspot(R.attr.state_pressed); } - if (!hasFocused) { + final boolean focused = Arrays.contains(stateSet, R.attr.state_focused); + if (!focused) { removeHotspot(R.attr.state_focused); + } else { + activateHotspot(R.attr.state_focused); } if (mRipplePaint != null && mState.mTint != null) { @@ -138,19 +171,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { mHotspotBounds.set(bounds); } - onHotspotBoundsChange(); - } - - private void onHotspotBoundsChange() { - final int x = mHotspotBounds.centerX(); - final int y = mHotspotBounds.centerY(); - final int N = mAnimatingRipplesCount; - for (int i = 0; i < N; i++) { - if (mState.mPinned) { - mAnimatingRipples[i].move(x, y); - } - mAnimatingRipples[i].onBoundsChanged(); - } + invalidateSelf(); } @Override @@ -172,7 +193,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { @Override public boolean isStateful() { - return super.isStateful() || mState.mTint != null && mState.mTint.isStateful(); + return true; } /** @@ -213,7 +234,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes( r, theme, attrs, R.styleable.TouchFeedbackDrawable); - inflateStateFromTypedArray(a); + updateStateFromTypedArray(a); a.recycle(); super.inflate(r, parser, attrs, theme); @@ -245,25 +266,23 @@ public class TouchFeedbackDrawable extends LayerDrawable { /** * Initializes the constant state from the values in the typed array. */ - private void inflateStateFromTypedArray(TypedArray a) { + private void updateStateFromTypedArray(TypedArray a) { final TouchFeedbackState state = mState; // Extract the theme attributes, if any. - final int[] themeAttrs = a.extractThemeAttrs(); - state.mTouchThemeAttrs = themeAttrs; + state.mTouchThemeAttrs = a.extractThemeAttrs(); - if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tint] == 0) { - mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint); + final ColorStateList tint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint); + if (tint != null) { + mState.mTint = tint; } - if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tintMode] == 0) { - mState.setTintMode(Drawable.parseTintMode( - a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1), Mode.SRC_ATOP)); + final int tintMode = a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1); + if (tintMode != -1) { + mState.setTintMode(Drawable.parseTintMode(tintMode, Mode.SRC_ATOP)); } - if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_pinned] == 0) { - mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false); - } + mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, mState.mPinned); } /** @@ -283,38 +302,14 @@ public class TouchFeedbackDrawable extends LayerDrawable { super.applyTheme(t); final TouchFeedbackState state = mState; - if (state == null) { - throw new RuntimeException( - "Can't apply theme to <touch-feedback> with no constant state"); - } - - final int[] themeAttrs = state.mTouchThemeAttrs; - if (themeAttrs != null) { - final TypedArray a = t.resolveAttributes( - themeAttrs, R.styleable.TouchFeedbackDrawable); - updateStateFromTypedArray(a); - a.recycle(); - } - } - - /** - * Updates the constant state from the values in the typed array. - */ - private void updateStateFromTypedArray(TypedArray a) { - final TouchFeedbackState state = mState; - - if (a.hasValue(R.styleable.TouchFeedbackDrawable_tint)) { - state.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint); - } - - if (a.hasValue(R.styleable.TouchFeedbackDrawable_tintMode)) { - mState.setTintMode(Drawable.parseTintMode( - a.getInt(R.styleable.TouchFeedbackDrawable_tintMode, -1), Mode.SRC_ATOP)); + if (state == null || state.mTouchThemeAttrs == null) { + return; } - if (a.hasValue(R.styleable.TouchFeedbackDrawable_pinned)) { - mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false); - } + final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, + R.styleable.TouchFeedbackDrawable); + updateStateFromTypedArray(a); + a.recycle(); } @Override @@ -329,59 +324,123 @@ public class TouchFeedbackDrawable extends LayerDrawable { @Override public void setHotspot(int id, float x, float y) { - if (mRipples == null) { - mRipples = new SparseArray<Ripple>(); - mAnimatingRipples = new Ripple[MAX_RIPPLES]; + if (mState.mPinned && !circleContains(mHotspotBounds, x, y)) { + x = mHotspotBounds.exactCenterX(); + y = mHotspotBounds.exactCenterY(); + } + + final int[] stateSet = getState(); + if (!Arrays.contains(stateSet, id)) { + // The hotspot is not active, so just modify the pending location. + getOrCreatePendingHotspot(id).set(x, y); + return; } if (mAnimatingRipplesCount >= MAX_RIPPLES) { - Log.e(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); + // This should never happen unless the user is tapping like a maniac + // or there is a bug that's preventing ripples from being removed. + Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); return; } - final Ripple ripple = mRipples.get(id); - if (ripple == null) { - final Rect bounds = mHotspotBounds; - if (mState.mPinned) { - x = bounds.exactCenterX(); - y = bounds.exactCenterY(); - } + if (mActiveHotspots == null) { + mActiveHotspots = new SparseArray<Ripple>(); + mAnimatingRipples = new Ripple[MAX_RIPPLES]; + } + + final Ripple ripple = mActiveHotspots.get(id); + if (ripple != null) { + // The hotspot is active, but we can't move it because it's probably + // busy animating the center position. + return; + } + + // The hotspot needs to be made active. + createActiveHotspot(id, x, y); + } + + private boolean circleContains(Rect bounds, float x, float y) { + final float pX = bounds.exactCenterX() - x; + final float pY = bounds.exactCenterY() - y; + final double pointRadius = Math.sqrt(pX * pX + pY * pY); + + final float bX = bounds.width() / 2.0f; + final float bY = bounds.height() / 2.0f; + final double boundsRadius = Math.sqrt(bX * bX + bY * bY); + + return pointRadius < boundsRadius; + } + + private PointF getOrCreatePendingHotspot(int id) { + final PointF p; + if (mPendingHotspots == null) { + mPendingHotspots = new SparseArray<>(2); + p = null; + } else { + p = mPendingHotspots.get(id); + } - // TODO: Clean this up in the API. - final boolean pulse = (id != R.attr.state_focused); - final Ripple newRipple = new Ripple(this, bounds, mDensity, pulse); - newRipple.move(x, y); - - mAnimatingRipples[mAnimatingRipplesCount++] = newRipple; - mRipples.put(id, newRipple); - } else if (mState.mPinned) { - final Rect bounds = mHotspotBounds; - x = bounds.exactCenterX(); - y = bounds.exactCenterY(); - ripple.move(x, y); + if (p == null) { + final PointF newPoint = new PointF(); + mPendingHotspots.put(id, newPoint); + return newPoint; } else { - ripple.move(x, y); + return p; } } + /** + * Moves a hotspot from pending to active. + */ + private void activateHotspot(int id) { + final SparseArray<PointF> pendingHotspots = mPendingHotspots; + if (pendingHotspots != null) { + final int index = pendingHotspots.indexOfKey(id); + if (index >= 0) { + final PointF hotspot = pendingHotspots.valueAt(index); + pendingHotspots.removeAt(index); + createActiveHotspot(id, hotspot.x, hotspot.y); + } + } + } + + /** + * Creates an active hotspot at the specified location. + */ + private void createActiveHotspot(int id, float x, float y) { + final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); + final Ripple newRipple = new Ripple(this, mHotspotBounds, color); + newRipple.enter(x, y); + + if (mAnimatingRipples == null) { + mAnimatingRipples = new Ripple[MAX_RIPPLES]; + } + mAnimatingRipples[mAnimatingRipplesCount++] = newRipple; + + if (mActiveHotspots == null) { + mActiveHotspots = new SparseArray<Ripple>(); + } + mActiveHotspots.put(id, newRipple); + } + @Override public void removeHotspot(int id) { - if (mRipples == null) { + if (mActiveHotspots == null) { return; } - final Ripple ripple = mRipples.get(id); + final Ripple ripple = mActiveHotspots.get(id); if (ripple != null) { ripple.exit(); - mRipples.remove(id); + mActiveHotspots.remove(id); } } @Override public void clearHotspots() { - if (mRipples != null) { - mRipples.clear(); + if (mActiveHotspots != null) { + mActiveHotspots.clear(); } final int count = mAnimatingRipplesCount; @@ -402,7 +461,6 @@ public class TouchFeedbackDrawable extends LayerDrawable { public void setHotspotBounds(int left, int top, int right, int bottom) { mOverrideBounds = true; mHotspotBounds.set(left, top, right, bottom); - onHotspotBoundsChange(); } @Override @@ -412,9 +470,9 @@ public class TouchFeedbackDrawable extends LayerDrawable { final ChildDrawable[] array = mLayerState.mChildren; final boolean maskOnly = mState.mMask != null && N == 1; - int restoreToCount = drawRippleLayer(canvas, bounds, maskOnly); + int restoreToCount = drawRippleLayer(canvas, maskOnly); - if (restoreToCount >= 0) { + if (restoreToCount >= 0) { // We have a ripple layer that contains ripples. If we also have an // explicit mask drawable, apply it now using DST_IN blending. if (mState.mMask != null) { @@ -450,7 +508,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { } } - private int drawRippleLayer(Canvas canvas, Rect bounds, boolean maskOnly) { + private int drawRippleLayer(Canvas canvas, boolean maskOnly) { final int count = mAnimatingRipplesCount; if (count == 0) { return -1; @@ -458,7 +516,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { final Ripple[] ripples = mAnimatingRipples; final boolean projected = isProjected(); - final Rect layerBounds = projected ? getDirtyBounds() : bounds; + final Rect layerBounds = projected ? getDirtyBounds() : getBounds(); // Separate the ripple color and alpha channel. The alpha will be // applied when we merge the ripples down to the canvas. @@ -479,6 +537,7 @@ public class TouchFeedbackDrawable extends LayerDrawable { boolean drewRipples = false; int restoreToCount = -1; + int restoreTranslate = -1; int animatingCount = 0; // Draw ripples and update the animating ripples array. @@ -509,6 +568,10 @@ public class TouchFeedbackDrawable extends LayerDrawable { restoreToCount = canvas.saveLayer(layerBounds.left, layerBounds.top, layerBounds.right, layerBounds.bottom, layerPaint); layerPaint.setAlpha(255); + + restoreTranslate = canvas.save(); + // Translate the canvas to the current hotspot bounds. + canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); } drewRipples |= ripple.draw(canvas, ripplePaint); @@ -519,6 +582,11 @@ public class TouchFeedbackDrawable extends LayerDrawable { mAnimatingRipplesCount = animatingCount; + // Always restore the translation. + if (restoreTranslate >= 0) { + canvas.restoreToCount(restoreTranslate); + } + // If we created a layer with no content, merge it immediately. if (restoreToCount >= 0 && !drewRipples) { canvas.restoreToCount(restoreToCount); @@ -543,11 +611,14 @@ public class TouchFeedbackDrawable extends LayerDrawable { dirtyBounds.set(drawingBounds); drawingBounds.setEmpty(); + final int cX = (int) mHotspotBounds.exactCenterX(); + final int cY = (int) mHotspotBounds.exactCenterY(); final Rect rippleBounds = mTempRect; final Ripple[] activeRipples = mAnimatingRipples; final int N = mAnimatingRipplesCount; for (int i = 0; i < N; i++) { activeRipples[i].getBounds(rippleBounds); + rippleBounds.offset(cX, cY); drawingBounds.union(rippleBounds); } @@ -563,11 +634,11 @@ public class TouchFeedbackDrawable extends LayerDrawable { static class TouchFeedbackState extends LayerState { int[] mTouchThemeAttrs; - ColorStateList mTint; - PorterDuffXfermode mTintXfermode; - PorterDuffXfermode mTintXfermodeInverse; + ColorStateList mTint = null; + PorterDuffXfermode mTintXfermode = SRC_ATOP; + PorterDuffXfermode mTintXfermodeInverse = DST_ATOP; Drawable mMask; - boolean mPinned; + boolean mPinned = false; public TouchFeedbackState( TouchFeedbackState orig, TouchFeedbackDrawable owner, Resources res) { |