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 | |
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
-rw-r--r-- | core/java/android/view/RenderNodeAnimator.java | 140 | ||||
-rw-r--r-- | core/java/android/view/View.java | 35 | ||||
-rw-r--r-- | core/java/android/widget/Switch.java | 28 | ||||
-rw-r--r-- | core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java | 6 | ||||
-rw-r--r-- | core/jni/android_view_RenderNodeAnimator.cpp | 23 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/Ripple.java | 612 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java | 295 | ||||
-rw-r--r-- | libs/hwui/Animator.cpp | 26 | ||||
-rw-r--r-- | libs/hwui/Animator.h | 10 |
9 files changed, 765 insertions, 410 deletions
diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index f14e73f..c1a4fee 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -16,6 +16,7 @@ package android.view; +import android.animation.Animator; import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.CanvasProperty; @@ -28,12 +29,12 @@ import com.android.internal.view.animation.HasNativeInterpolator; import com.android.internal.view.animation.NativeInterpolatorFactory; import java.lang.ref.WeakReference; +import java.util.ArrayList; /** * @hide */ -public final class RenderNodeAnimator { - +public final class RenderNodeAnimator extends Animator { // Keep in sync with enum RenderProperty in Animator.h public static final int TRANSLATION_X = 0; public static final int TRANSLATION_Y = 1; @@ -50,6 +51,11 @@ public final class RenderNodeAnimator { // Keep in sync with enum PaintFields in Animator.h public static final int PAINT_STROKE_WIDTH = 0; + + /** + * Field for the Paint alpha channel, which should be specified as a value + * between 0 and 255. + */ public static final int PAINT_ALPHA = 1; // ViewPropertyAnimator uses a mask for its values, we need to remap them @@ -74,8 +80,11 @@ public final class RenderNodeAnimator { private VirtualRefBasePtr mNativePtr; private RenderNode mTarget; + private View mViewTarget; private TimeInterpolator mInterpolator; + private boolean mStarted = false; + private boolean mFinished = false; public int mapViewPropertyToRenderProperty(int viewProperty) { return sViewPropertyAnimatorMap.get(viewProperty); @@ -92,6 +101,14 @@ public final class RenderNodeAnimator { property.getNativeContainer(), finalValue)); } + /** + * Creates a new render node animator for a field on a Paint property. + * + * @param property The paint property to target + * @param paintField Paint field to animate, one of {@link #PAINT_ALPHA} or + * {@link #PAINT_STROKE_WIDTH} + * @param finalValue The target value for the property + */ public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) { init(nCreateCanvasPropertyPaintAnimator( new WeakReference<RenderNodeAnimator>(this), @@ -115,56 +132,139 @@ public final class RenderNodeAnimator { if (mInterpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class)) { ni = ((NativeInterpolatorFactory)mInterpolator).createNativeInterpolator(); } else { - int duration = nGetDuration(mNativePtr.get()); + long duration = nGetDuration(mNativePtr.get()); ni = FallbackLUTInterpolator.createNativeInterpolator(mInterpolator, duration); } nSetInterpolator(mNativePtr.get(), ni); } - private void start(RenderNode node) { + @Override + public void start() { + if (mTarget == null) { + throw new IllegalStateException("Missing target!"); + } + if (mStarted) { throw new IllegalStateException("Already started!"); } + mStarted = true; applyInterpolator(); - mTarget = node; mTarget.addAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationStart(this); + } + + if (mViewTarget != null) { + // Kick off a frame to start the process + mViewTarget.invalidateViewProperty(true, false); + } + } + + @Override + public void cancel() { + mTarget.removeAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationCancel(this); + } + } + + @Override + public void end() { + throw new UnsupportedOperationException(); + } + + @Override + public void pause() { + throw new UnsupportedOperationException(); + } + + @Override + public void resume() { + throw new UnsupportedOperationException(); } - public void start(View target) { - start(target.mRenderNode); - // Kick off a frame to start the process - target.invalidateViewProperty(true, false); + public void setTarget(View view) { + mViewTarget = view; + mTarget = view.mRenderNode; } - public void start(Canvas canvas) { + public void setTarget(Canvas canvas) { if (!(canvas instanceof GLES20RecordingCanvas)) { throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); } - GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; - start(recordingCanvas.mNode); + + final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; + setTarget(recordingCanvas.mNode); } - public void cancel() { - mTarget.removeAnimator(this); + public void setTarget(RenderNode node) { + mViewTarget = null; + mTarget = node; + } + + public RenderNode getTarget() { + return mTarget; } - public void setDuration(int duration) { + @Override + public void setStartDelay(long startDelay) { + checkMutable(); + nSetStartDelay(mNativePtr.get(), startDelay); + } + + @Override + public long getStartDelay() { + return nGetStartDelay(mNativePtr.get()); + } + + @Override + public RenderNodeAnimator setDuration(long duration) { checkMutable(); nSetDuration(mNativePtr.get(), duration); + return this; } + @Override + public long getDuration() { + return nGetDuration(mNativePtr.get()); + } + + @Override + public boolean isRunning() { + return mStarted && !mFinished; + } + + @Override public void setInterpolator(TimeInterpolator interpolator) { checkMutable(); mInterpolator = interpolator; } - long getNativeAnimator() { - return mNativePtr.get(); + @Override + public TimeInterpolator getInterpolator() { + return mInterpolator; } private void onFinished() { + mFinished = true; mTarget.removeAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationEnd(this); + } + } + + long getNativeAnimator() { + return mNativePtr.get(); } // Called by native @@ -181,7 +281,9 @@ public final class RenderNodeAnimator { long canvasProperty, float deltaValue); private static native long nCreateCanvasPropertyPaintAnimator(WeakReference<RenderNodeAnimator> weakThis, long canvasProperty, int paintField, float deltaValue); - private static native void nSetDuration(long nativePtr, int duration); - private static native int nGetDuration(long nativePtr); + private static native void nSetDuration(long nativePtr, long duration); + private static native long nGetDuration(long nativePtr); + private static native void nSetStartDelay(long nativePtr, long startDelay); + private static native long nGetStartDelay(long nativePtr); private static native void nSetInterpolator(long animPtr, long interpolatorPtr); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5141877..fb7d57d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -4774,8 +4774,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } - manageFocusHotspot(true, oldFocus); onFocusChanged(true, direction, previouslyFocusedRect); + manageFocusHotspot(true, oldFocus); refreshDrawableState(); } } @@ -6752,6 +6752,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the pressed state for this view and provides a touch coordinate for + * animation hinting. + * + * @param pressed Pass true to set the View's internal state to "pressed", + * or false to reverts the View's internal state from a + * previously set "pressed" state. + * @param x The x coordinate of the touch that caused the press + * @param y The y coordinate of the touch that caused the press + */ + private void setPressed(boolean pressed, float x, float y) { + if (pressed) { + setHotspot(R.attr.state_pressed, x, y); + } + + setPressed(pressed); + } + + /** * Sets the pressed state for this view. * * @see #isClickable() @@ -6769,6 +6787,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags &= ~PFLAG_PRESSED; } + if (!pressed) { + clearHotspot(R.attr.state_pressed); + } + if (needsRefresh) { refreshDrawableState(); } @@ -8993,7 +9015,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { - clearHotspot(R.attr.state_pressed); setPressed(false); } // A disabled view that is clickable still consumes the touch @@ -9026,8 +9047,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. - setHotspot(R.attr.state_pressed, x, y); - setPressed(true); + setPressed(true, x, y); } if (!mHasPerformedLongPress) { @@ -9061,8 +9081,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } removeTapCallback(); - } else { - clearHotspot(R.attr.state_pressed); } break; @@ -9175,7 +9193,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private void removeUnsetPressCallback() { if ((mPrivateFlags & PFLAG_PRESSED) != 0 && mUnsetPressedState != null) { - clearHotspot(R.attr.state_pressed); setPressed(false); removeCallbacks(mUnsetPressedState); } @@ -19234,8 +19251,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; - setHotspot(R.attr.state_pressed, x, y); - setPressed(true); + setPressed(true, x, y); checkForLongClick(ViewConfiguration.getTapTimeout()); } } @@ -19516,7 +19532,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private final class UnsetPressedState implements Runnable { @Override public void run() { - clearHotspot(R.attr.state_pressed); setPressed(false); } } diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index 438e164..74a3eec 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -666,6 +666,8 @@ public class Switch extends CompoundButton { case MotionEvent.ACTION_CANCEL: { if (mTouchMode == TOUCH_MODE_DRAGGING) { stopDrag(ev); + // Allow super class to handle pressed state, etc. + super.onTouchEvent(ev); return true; } mTouchMode = TOUCH_MODE_IDLE; @@ -801,7 +803,7 @@ public class Switch extends CompoundButton { } @Override - protected void onDraw(Canvas canvas) { + public void draw(Canvas c) { final Rect tempRect = mTempRect; final Drawable trackDrawable = mTrackDrawable; final Drawable thumbDrawable = mThumbDrawable; @@ -815,9 +817,6 @@ public class Switch extends CompoundButton { trackDrawable.getPadding(tempRect); final int switchInnerLeft = switchLeft + tempRect.left; - final int switchInnerTop = switchTop + tempRect.top; - final int switchInnerRight = switchRight - tempRect.right; - final int switchInnerBottom = switchBottom - tempRect.bottom; // Relies on mTempRect, MUST be called first! final int thumbPos = getThumbOffset(); @@ -833,8 +832,26 @@ public class Switch extends CompoundButton { background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom); } + // Draw the background. + super.draw(c); + } + + @Override + protected void onDraw(Canvas canvas) { super.onDraw(canvas); + final Rect tempRect = mTempRect; + final Drawable trackDrawable = mTrackDrawable; + final Drawable thumbDrawable = mThumbDrawable; + trackDrawable.getPadding(tempRect); + + final int switchTop = mSwitchTop; + final int switchBottom = mSwitchBottom; + final int switchInnerLeft = mSwitchLeft + tempRect.left; + final int switchInnerTop = switchTop + tempRect.top; + final int switchInnerRight = mSwitchRight - tempRect.right; + final int switchInnerBottom = switchBottom - tempRect.bottom; + if (mSplitTrack) { final Insets insets = thumbDrawable.getOpticalInsets(); thumbDrawable.copyBounds(tempRect); @@ -861,7 +878,8 @@ public class Switch extends CompoundButton { } mTextPaint.drawableState = drawableState; - final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2; + final Rect thumbBounds = thumbDrawable.getBounds(); + final int left = (thumbBounds.left + thumbBounds.right) / 2 - switchText.getWidth() / 2; final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; canvas.translate(left, top); switchText.draw(canvas); diff --git a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java index aec2b7e..1feb943 100644 --- a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java +++ b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java @@ -34,11 +34,11 @@ public class FallbackLUTInterpolator implements NativeInterpolatorFactory { * Used to cache the float[] LUT for use across multiple native * interpolator creation */ - public FallbackLUTInterpolator(TimeInterpolator interpolator, int duration) { + public FallbackLUTInterpolator(TimeInterpolator interpolator, long duration) { mLut = createLUT(interpolator, duration); } - private static float[] createLUT(TimeInterpolator interpolator, int duration) { + private static float[] createLUT(TimeInterpolator interpolator, long duration) { long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos(); int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS); int numAnimFrames = (int) Math.ceil(duration / animIntervalMs); @@ -59,7 +59,7 @@ public class FallbackLUTInterpolator implements NativeInterpolatorFactory { /** * Used to create a one-shot float[] LUT & native interpolator */ - public static long createNativeInterpolator(TimeInterpolator interpolator, int duration) { + public static long createNativeInterpolator(TimeInterpolator interpolator, long duration) { float[] lut = createLUT(interpolator, duration); return NativeInterpolatorFactoryHelper.createLutInterpolator(lut); } diff --git a/core/jni/android_view_RenderNodeAnimator.cpp b/core/jni/android_view_RenderNodeAnimator.cpp index ea2f96e..e19ce36 100644 --- a/core/jni/android_view_RenderNodeAnimator.cpp +++ b/core/jni/android_view_RenderNodeAnimator.cpp @@ -116,15 +116,26 @@ static jlong createCanvasPropertyPaintAnimator(JNIEnv* env, jobject clazz, return reinterpret_cast<jlong>( animator ); } -static void setDuration(JNIEnv* env, jobject clazz, jlong animatorPtr, jint duration) { +static void setDuration(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong duration) { LOG_ALWAYS_FATAL_IF(duration < 0, "Duration cannot be negative"); BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr); animator->setDuration(duration); } -static jint getDuration(JNIEnv* env, jobject clazz, jlong animatorPtr) { +static jlong getDuration(JNIEnv* env, jobject clazz, jlong animatorPtr) { BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr); - return static_cast<jint>(animator->duration()); + return static_cast<jlong>(animator->duration()); +} + +static void setStartDelay(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong startDelay) { + LOG_ALWAYS_FATAL_IF(startDelay < 0, "Start delay cannot be negative"); + BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr); + animator->setStartDelay(startDelay); +} + +static jlong getStartDelay(JNIEnv* env, jobject clazz, jlong animatorPtr) { + BaseRenderNodeAnimator* animator = reinterpret_cast<BaseRenderNodeAnimator*>(animatorPtr); + return static_cast<jlong>(animator->startDelay()); } static void setInterpolator(JNIEnv* env, jobject clazz, jlong animatorPtr, jlong interpolatorPtr) { @@ -146,8 +157,10 @@ static JNINativeMethod gMethods[] = { { "nCreateAnimator", "(Ljava/lang/ref/WeakReference;IF)J", (void*) createAnimator }, { "nCreateCanvasPropertyFloatAnimator", "(Ljava/lang/ref/WeakReference;JF)J", (void*) createCanvasPropertyFloatAnimator }, { "nCreateCanvasPropertyPaintAnimator", "(Ljava/lang/ref/WeakReference;JIF)J", (void*) createCanvasPropertyPaintAnimator }, - { "nSetDuration", "(JI)V", (void*) setDuration }, - { "nGetDuration", "(J)I", (void*) getDuration }, + { "nSetDuration", "(JJ)V", (void*) setDuration }, + { "nGetDuration", "(J)J", (void*) getDuration }, + { "nSetStartDelay", "(JJ)V", (void*) setStartDelay }, + { "nGetStartDelay", "(J)J", (void*) getStartDelay }, { "nSetInterpolator", "(JJ)V", (void*) setInterpolator }, #endif }; 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) { diff --git a/libs/hwui/Animator.cpp b/libs/hwui/Animator.cpp index 83eedfb..b80f7e9 100644 --- a/libs/hwui/Animator.cpp +++ b/libs/hwui/Animator.cpp @@ -37,7 +37,10 @@ BaseRenderNodeAnimator::BaseRenderNodeAnimator(float finalValue) , mInterpolator(0) , mPlayState(NEEDS_START) , mStartTime(0) - , mDuration(300){ + , mDelayUntil(0) + , mDuration(300) + , mStartDelay(0) { + } BaseRenderNodeAnimator::~BaseRenderNodeAnimator() { @@ -49,10 +52,6 @@ void BaseRenderNodeAnimator::setInterpolator(Interpolator* interpolator) { mInterpolator = interpolator; } -void BaseRenderNodeAnimator::setDuration(nsecs_t duration) { - mDuration = duration; -} - void BaseRenderNodeAnimator::setStartValue(float value) { LOG_ALWAYS_FATAL_IF(mPlayState != NEEDS_START, "Cannot set the start value after the animator has started!"); @@ -68,7 +67,24 @@ void BaseRenderNodeAnimator::setupStartValueIfNecessary(RenderNode* target, Tree } } +void BaseRenderNodeAnimator::setDuration(nsecs_t duration) { + mDuration = duration; +} + +void BaseRenderNodeAnimator::setStartDelay(nsecs_t startDelay) { + mStartDelay = startDelay; +} + bool BaseRenderNodeAnimator::animate(RenderNode* target, TreeInfo& info) { + if (mPlayState == PENDING && mStartDelay > 0 && mDelayUntil == 0) { + mDelayUntil = info.frameTimeMs + mStartDelay; + return false; + } + + if (mDelayUntil > info.frameTimeMs) { + return false; + } + if (mPlayState == PENDING) { mPlayState = RUNNING; mStartTime = info.frameTimeMs; diff --git a/libs/hwui/Animator.h b/libs/hwui/Animator.h index fe88cbf..7741617 100644 --- a/libs/hwui/Animator.h +++ b/libs/hwui/Animator.h @@ -44,6 +44,8 @@ public: ANDROID_API void setInterpolator(Interpolator* interpolator); ANDROID_API void setDuration(nsecs_t durationInMs); ANDROID_API nsecs_t duration() { return mDuration; } + ANDROID_API void setStartDelay(nsecs_t startDelayInMs); + ANDROID_API nsecs_t startDelay() { return mStartDelay; } ANDROID_API void setListener(AnimationListener* listener) { mListener = listener; } @@ -82,10 +84,12 @@ private: Interpolator* mInterpolator; PlayState mPlayState; - long mStartTime; - long mDuration; + nsecs_t mStartTime; + nsecs_t mDelayUntil; + nsecs_t mDuration; + nsecs_t mStartDelay; - sp<AnimationListener> mListener; + sp<AnimationListener> mListener; }; class RenderPropertyAnimator : public BaseRenderNodeAnimator { |