diff options
author | Alan Viverette <alanv@google.com> | 2014-03-26 16:43:07 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2014-03-26 16:43:07 -0700 |
commit | 47bf0d95ef6c9ac68773567d503749c874a07f2f (patch) | |
tree | bd976e8d6770d5b5d3c0923a494672545190a45f /graphics/java | |
parent | 460572b22fe8fe5880ad099090b38765e2f8a2f6 (diff) | |
download | frameworks_base-47bf0d95ef6c9ac68773567d503749c874a07f2f.zip frameworks_base-47bf0d95ef6c9ac68773567d503749c874a07f2f.tar.gz frameworks_base-47bf0d95ef6c9ac68773567d503749c874a07f2f.tar.bz2 |
Separate ripple animation logic, remove RevealDrawable
Change-Id: I9d0370cea288e6caf518209b5bc94a66a0f9176f
Diffstat (limited to 'graphics/java')
5 files changed, 267 insertions, 620 deletions
diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index de2b68f..5840381 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -1054,8 +1054,6 @@ public abstract class Drawable { drawable = new LayerDrawable(); } else if (name.equals("transition")) { drawable = new TransitionDrawable(); - } else if (name.equals("reveal")) { - drawable = new RevealDrawable(); } else if (name.equals("touch-feedback")) { drawable = new TouchFeedbackDrawable(); } else if (name.equals("color")) { diff --git a/graphics/java/android/graphics/drawable/LayerDrawable.java b/graphics/java/android/graphics/drawable/LayerDrawable.java index 3d48cda..3c3c841 100644 --- a/graphics/java/android/graphics/drawable/LayerDrawable.java +++ b/graphics/java/android/graphics/drawable/LayerDrawable.java @@ -839,7 +839,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback { /** * Ensures the child padding caches are large enough. */ - private void ensurePadding() { + void ensurePadding() { final int N = mLayerState.mNum; if (mPaddingL != null && mPaddingL.length >= N) { return; diff --git a/graphics/java/android/graphics/drawable/RevealDrawable.java b/graphics/java/android/graphics/drawable/RevealDrawable.java deleted file mode 100644 index 2f96fe4..0000000 --- a/graphics/java/android/graphics/drawable/RevealDrawable.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.graphics.drawable; - -import android.content.res.Resources; -import android.content.res.Resources.Theme; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.SparseArray; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; - -/** - * An extension of LayerDrawable that is intended to react to touch hotspots - * and reveal the second layer atop the first. - * <p> - * It can be defined in an XML file with the <code><reveal></code> element. - * Each Drawable in the transition is defined in a nested <code><item></code>. - * For more information, see the guide to <a href="{@docRoot} - * guide/topics/resources/drawable-resource.html">Drawable Resources</a>. - * - * @attr ref android.R.styleable#LayerDrawableItem_left - * @attr ref android.R.styleable#LayerDrawableItem_top - * @attr ref android.R.styleable#LayerDrawableItem_right - * @attr ref android.R.styleable#LayerDrawableItem_bottom - * @attr ref android.R.styleable#LayerDrawableItem_drawable - * @attr ref android.R.styleable#LayerDrawableItem_id - */ -public class RevealDrawable extends LayerDrawable { - private final Rect mTempRect = new Rect(); - - /** Lazily-created map of touch hotspot IDs to ripples. */ - private SparseArray<Ripple> mTouchedRipples; - - /** Lazily-created list of actively animating ripples. */ - private ArrayList<Ripple> mActiveRipples; - - /** Lazily-created runnable for scheduling invalidation. */ - private Runnable mAnimationRunnable; - - /** Whether the animation runnable has been posted. */ - private boolean mAnimating; - - /** Target density, used to scale density-independent pixels. */ - private float mDensity = 1.0f; - - /** Paint used to control appearance of ripples. */ - private Paint mRipplePaint; - - /** Paint used to control reveal layer masking. */ - private Paint mMaskingPaint; - - /** - * Create a new reveal drawable with the specified list of layers. At least - * two layers are required for this drawable to work properly. - */ - public RevealDrawable(Drawable[] layers) { - this(new RevealState(null, null, null), layers); - } - - /** - * Create a new reveal drawable with no layers. To work correctly, at least - * two layers must be added to this drawable. - * - * @see #RevealDrawable(Drawable[]) - */ - RevealDrawable() { - this(new RevealState(null, null, null), (Resources) null, null); - } - - private RevealDrawable(RevealState state, Resources res) { - super(state, res, null); - } - - private RevealDrawable(RevealState state, Resources res, Theme theme) { - super(state, res, theme); - } - - private RevealDrawable(RevealState state, Drawable[] layers) { - super(layers, state); - } - - @Override - public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) - throws XmlPullParserException, IOException { - super.inflate(r, parser, attrs, theme); - - setTargetDensity(r.getDisplayMetrics()); - setPaddingMode(PADDING_MODE_STACK); - } - - @Override - LayerState createConstantState(LayerState state, Resources res) { - return new RevealState((RevealState) state, this, res); - } - - /** - * Set the density at which this drawable will be rendered. - * - * @param metrics The display metrics for this drawable. - */ - private void setTargetDensity(DisplayMetrics metrics) { - if (mDensity != metrics.density) { - mDensity = metrics.density; - invalidateSelf(); - } - } - - /** - * @hide until hotspot APIs are finalized - */ - @Override - public boolean supportsHotspots() { - return true; - } - - /** - * @hide until hotspot APIs are finalized - */ - @Override - public void setHotspot(int id, float x, float y) { - if (mTouchedRipples == null) { - mTouchedRipples = new SparseArray<Ripple>(); - mActiveRipples = new ArrayList<Ripple>(); - } - - final Ripple ripple = mTouchedRipples.get(id); - if (ripple == null) { - final Rect padding = mTempRect; - getPadding(padding); - - final Ripple newRipple = new Ripple(getBounds(), padding, x, y, mDensity); - newRipple.enter(); - - mActiveRipples.add(newRipple); - mTouchedRipples.put(id, newRipple); - } else { - ripple.move(x, y); - } - - scheduleAnimation(); - } - - /** - * @hide until hotspot APIs are finalized - */ - @Override - public void removeHotspot(int id) { - if (mTouchedRipples == null) { - return; - } - - final Ripple ripple = mTouchedRipples.get(id); - if (ripple != null) { - ripple.exit(); - - mTouchedRipples.remove(id); - scheduleAnimation(); - } - } - - /** - * @hide until hotspot APIs are finalized - */ - @Override - public void clearHotspots() { - if (mTouchedRipples == null) { - return; - } - - final int n = mTouchedRipples.size(); - for (int i = 0; i < n; i++) { - final Ripple ripple = mTouchedRipples.valueAt(i); - ripple.exit(); - } - - if (n > 0) { - mTouchedRipples.clear(); - scheduleAnimation(); - } - } - - /** - * Schedules the next animation, if necessary. - */ - private void scheduleAnimation() { - if (mActiveRipples == null || mActiveRipples.isEmpty()) { - mAnimating = false; - } else if (!mAnimating) { - mAnimating = true; - - if (mAnimationRunnable == null) { - mAnimationRunnable = new Runnable() { - @Override - public void run() { - mAnimating = false; - scheduleAnimation(); - invalidateSelf(); - } - }; - } - - scheduleSelf(mAnimationRunnable, SystemClock.uptimeMillis() + 1000 / 60); - } - } - - @Override - public void draw(Canvas canvas) { - final int layerCount = getNumberOfLayers(); - if (layerCount == 0) { - return; - } - - getDrawable(0).draw(canvas); - - final Rect bounds = getBounds(); - final ArrayList<Ripple> activeRipples = mActiveRipples; - if (layerCount == 1 || bounds.isEmpty() || activeRipples == null - || activeRipples.isEmpty()) { - // Nothing to reveal, we're done here. - return; - } - - if (mRipplePaint == null) { - mRipplePaint = new Paint(); - mRipplePaint.setAntiAlias(true); - } - - // Draw ripple mask into a buffer that merges using SRC_OVER. - boolean needsMask = false; - int layerSaveCount = -1; - int n = activeRipples.size(); - for (int i = 0; i < n; i++) { - final Ripple ripple = activeRipples.get(i); - if (!ripple.active()) { - activeRipples.remove(i); - i--; - n--; - } else { - if (layerSaveCount < 0) { - layerSaveCount = canvas.saveLayer( - bounds.left, bounds.top, bounds.right, bounds.bottom, null, 0); - // Ripples must be clipped to bounds, otherwise SRC_IN will - // miss them and we'll get artifacts. - canvas.clipRect(bounds); - } - - needsMask |= ripple.draw(canvas, mRipplePaint); - } - } - - // If a layer was saved, it contains the ripple mask. Draw the reveal - // into another layer and composite using SRC_IN, then composite onto - // the original canvas. - if (layerSaveCount >= 0) { - if (needsMask) { - if (mMaskingPaint == null) { - mMaskingPaint = new Paint(); - mMaskingPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); - } - - // TODO: When Drawable.setXfermode() is supported by all drawables, - // we won't need an extra layer. - canvas.saveLayer( - bounds.left, bounds.top, bounds.right, bounds.bottom, mMaskingPaint, 0); - getDrawable(1).draw(canvas); - } - - canvas.restoreToCount(layerSaveCount); - } - } - - private static class RevealState extends LayerState { - public RevealState(RevealState orig, RevealDrawable owner, Resources res) { - super(orig, owner, res); - } - - @Override - public Drawable newDrawable() { - return newDrawable(null); - } - - @Override - public Drawable newDrawable(Resources res) { - return new RevealDrawable(this, res); - } - - @Override - public Drawable newDrawable(Resources res, Theme theme) { - return new RevealDrawable(this, res, theme); - } - } -} diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java index 618afb8..03dd841 100644 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -43,34 +43,16 @@ class Ripple { /** Resistance factor when constraining outside touches. */ private static final float OUTSIDE_RESISTANCE = 0.7f; - /** Duration for animating the trailing edge of the ripple. */ - private static final int EXIT_DURATION = 600; - - /** Duration for animating the leading edge of the ripple. */ - private static final int ENTER_DURATION = 400; - - /** 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; - /** Minimum alpha value during a pulse animation. */ private static final int PULSE_MIN_ALPHA = 128; - /** Delay before pulses start. */ - private static final int PULSE_DELAY = 500; - private final Rect mBounds; private final Rect mPadding; - private final int mMinRadius; - private final int mOutsideRadius; + + private RippleAnimator mAnimator; + + private int mMinRadius; + private int mOutsideRadius; /** Center x-coordinate. */ private float mX; @@ -80,15 +62,18 @@ class Ripple { /** Whether the center is within the parent bounds. */ private boolean mInside; + + /** Enter state. A value in [0...1] or -1 if not set. */ + private float mEnterState = -1; - /** When the ripple started appearing. */ - private long mEnterTime = -1; + /** Exit state. A value in [0...1] or -1 if not set. */ + private float mExitState = -1; - /** When the ripple started vanishing. */ - private long mExitTime = -1; + /** Outside state. A value in [0...1] or -1 if not set. */ + private float mOutsideState = -1; - /** When the ripple last transitioned between inside and outside touch. */ - private long mOutsideTime = -1; + /** Pulse state. A value in [0...1] or -1 if not set. */ + private float mPulseState = -1; /** * Creates a new ripple with the specified parent bounds, padding, initial @@ -105,6 +90,14 @@ class Ripple { mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f); mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f); } + + public void setMinRadius(int minRadius) { + mMinRadius = minRadius; + } + + public void setOutsideRadius(int outsideRadius) { + mOutsideRadius = outsideRadius; + } /** * Updates the center coordinates. @@ -115,49 +108,18 @@ class Ripple { final boolean inside = mBounds.contains((int) x, (int) y); if (mInside != inside) { - mOutsideTime = AnimationUtils.currentAnimationTimeMillis(); + if (mAnimator != null) { + mAnimator.outside(); + } mInside = inside; } } - /** - * Starts the enter animation. - */ - public void enter() { - mEnterTime = AnimationUtils.currentAnimationTimeMillis(); - } - - /** - * Starts the exit animation. If {@link #enter()} was called recently, the - * animation may be postponed. - */ - public void exit() { - final long minTime = mEnterTime + EXIT_MIN_DELAY; - mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis()); - } - - /** - * Returns whether this ripple is currently animating. - */ - public boolean active() { - final long currentTime = AnimationUtils.currentAnimationTimeMillis(); - return mEnterTime >= 0 && mEnterTime <= currentTime - && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION); - } - - /** - * Constrains a value within a specified asymptotic margin outside a minimum - * and maximum. - */ - private static float looseConstrain(float value, float min, float max, float margin, - float factor) { - 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; + public RippleAnimator animate() { + if (mAnimator == null) { + mAnimator = new RippleAnimator(this); } + return mAnimator; } public boolean draw(Canvas c, Paint p) { @@ -167,17 +129,10 @@ class Ripple { final float dY = Math.max(mY, bounds.bottom - mY); final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); - // Track three states: - // - Enter: touch begins, affects outer radius - // - Outside: touch moves outside bounds, affects maximum outer radius - // - Exit: touch ends, affects inner radius - final long currentTime = AnimationUtils.currentAnimationTimeMillis(); - final float enterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation( - MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1)); - final float outsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation( - MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1)); - final float exitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation( - MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1)); + final float enterState = mEnterState; + final float exitState = mExitState; + final float outsideState = mOutsideState; + final float pulseState = mPulseState; final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState); final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius, mInside ? outsideState : 1 - outsideState); @@ -189,35 +144,28 @@ class Ripple { outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); // Compute maximum alpha, taking pulse into account when active. - final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY); final int maxAlpha; - if (pulseTime < 0) { + if (pulseState < 0 || pulseState >= 1) { maxAlpha = 255; } else { - final float pulseState = (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) - / (float) PULSE_DURATION; - if (pulseState >= 1) { - maxAlpha = 255; + final float pulseAlpha; + if (pulseState > 0.5) { + // Pulsing in to max alpha. + pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2); } else { - final float pulseAlpha; - if (pulseState > 0.5) { - // Pulsing in to max alpha. - pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2); - } else { - // Pulsing out to min alpha. - pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f); - } + // Pulsing out to min alpha. + pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f); + } - if (exitState > 0) { - // Animating exit, interpolate pulse with exit state. - maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f); - } else if (mInside) { - // No animation, no need to interpolate. - maxAlpha = (int) (pulseAlpha + 0.5f); - } else { - // Animating inside, interpolate pulse with inside state. - maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f); - } + if (exitState > 0) { + // Animating exit, interpolate pulse with exit state. + maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f); + } else if (mInside) { + // No animation, no need to interpolate. + maxAlpha = (int) (pulseAlpha + 0.5f); + } else { + // Animating inside, interpolate pulse with inside state. + maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f); } } @@ -260,4 +208,109 @@ class Ripple { final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius); } + + /** + * Constrains a value within a specified asymptotic margin outside a minimum + * and maximum. + */ + private static float looseConstrain(float value, float min, float max, float margin, + float factor) { + 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; + } + } + + public static class RippleAnimator { + /** Duration for animating the trailing edge of the ripple. */ + private static final int EXIT_DURATION = 600; + + /** Duration for animating the leading edge of the ripple. */ + private static final int ENTER_DURATION = 400; + + /** 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; + + /** The target ripple being animated. */ + private final Ripple mTarget; + + /** When the ripple started appearing. */ + private long mEnterTime = -1; + + /** When the ripple started vanishing. */ + private long mExitTime = -1; + + /** When the ripple last transitioned between inside and outside touch. */ + private long mOutsideTime = -1; + + public RippleAnimator(Ripple target) { + mTarget = target; + } + + /** + * Starts the enter animation. + */ + public void enter() { + mEnterTime = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Starts the exit animation. If {@link #enter()} was called recently, the + * animation may be postponed. + */ + public void exit() { + final long minTime = mEnterTime + EXIT_MIN_DELAY; + mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis()); + } + + /** + * Starts the outside transition animation. + */ + public void outside() { + mOutsideTime = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Returns whether this ripple is currently animating. + */ + public boolean isRunning() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + return mEnterTime >= 0 && mEnterTime <= currentTime + && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION); + } + + public void update() { + // Track three states: + // - Enter: touch begins, affects outer radius + // - Outside: touch moves outside bounds, affects maximum outer radius + // - Exit: touch ends, affects inner radius + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1)); + mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1)); + mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation( + MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1)); + + // Pulse is a little more complicated. + final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY); + mTarget.mPulseState = pulseTime < 0 ? -1 + : (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION; + } + } } diff --git a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java index 47a9374..b66d86d 100644 --- a/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java +++ b/graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java @@ -25,6 +25,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff.Mode; +import android.graphics.drawable.Ripple.RippleAnimator; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.os.SystemClock; @@ -43,7 +44,13 @@ import java.util.ArrayList; /** * Documentation pending. */ -public class TouchFeedbackDrawable extends DrawableWrapper { +public class TouchFeedbackDrawable extends LayerDrawable { + private static final PorterDuffXfermode DST_ATOP = new PorterDuffXfermode(Mode.DST_ATOP); + private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); + + /** The maximum number of ripples supported. */ + private static final int MAX_RIPPLES = 10; + private final Rect mTempRect = new Rect(); private final Rect mPaddingRect = new Rect(); @@ -58,8 +65,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper { /** Lazily-created map of touch hotspot IDs to ripples. */ private SparseArray<Ripple> mTouchedRipples; - /** Lazily-created list of actively animating ripples. */ - private ArrayList<Ripple> mActiveRipples; + /** Lazily-created array of actively animating ripples. */ + private Ripple[] mActiveRipples; + private int mActiveRipplesCount = 0; /** Lazily-created runnable for scheduling invalidation. */ private Runnable mAnimationRunnable; @@ -76,43 +84,14 @@ public class TouchFeedbackDrawable extends DrawableWrapper { /** Whether the animation runnable has been posted. */ private boolean mAnimating; - /** The drawable to use as the mask. */ - private Drawable mMask; - TouchFeedbackDrawable() { - this(new TouchFeedbackState(null), null, null); - } - - private void setConstantState(TouchFeedbackState wrapperState, Resources res) { - super.setConstantState(wrapperState, res); - - // Load a new mask drawable from the constant state. - if (wrapperState == null || wrapperState.mMaskState == null) { - mMask = null; - } else if (res != null) { - mMask = wrapperState.mMaskState.newDrawable(res); - } else { - mMask = wrapperState.mMaskState.newDrawable(); - } - - if (res != null) { - mDensity = res.getDisplayMetrics().density; - } + this(new TouchFeedbackState(null, null, null), null, null); } @Override public int getOpacity() { - return mActiveRipples != null && !mActiveRipples.isEmpty() ? - PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT; - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - - if (mMask != null) { - mMask.setBounds(bounds); - } + // Worst-case scenario. + return PixelFormat.TRANSLUCENT; } @Override @@ -138,7 +117,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { */ @Override public boolean isProjected() { - return mState.mProjected; + return getNumberOfLayers() == 0; } @Override @@ -149,59 +128,25 @@ public class TouchFeedbackDrawable extends DrawableWrapper { @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { - super.inflate(r, parser, attrs, theme); - final TypedArray a = obtainAttributes( r, theme, attrs, R.styleable.TouchFeedbackDrawable); - inflateStateFromTypedArray(r, a); + inflateStateFromTypedArray(a); a.recycle(); - - inflateChildElements(r, parser, attrs, theme); - - setTargetDensity(r.getDisplayMetrics()); - } - - private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, - Theme theme) throws XmlPullParserException, IOException { - int type; - while ((type = parser.next()) == XmlPullParser.TEXT) { - // Find the next non-text element. - } - - if (type == XmlPullParser.START_TAG) { - final Drawable dr = Drawable.createFromXmlInner(r, parser, attrs); - setDrawable(dr, r); - } - } - /** - * Sets the wrapped drawable and update the constant state. - * - * @param drawable - * @param res - */ - void setMaskDrawable(Drawable drawable, Resources res) { - mMask = drawable; - - if (drawable != null) { - // Nobody cares if the mask has a callback. - drawable.setCallback(null); + super.inflate(r, parser, attrs, theme); - mState.mMaskState = drawable.getConstantState(); - } else { - mState.mMaskState = null; - } + setTargetDensity(r.getDisplayMetrics()); } /** * Initializes the constant state from the values in the typed array. */ - private void inflateStateFromTypedArray(Resources r, TypedArray a) { + private void inflateStateFromTypedArray(TypedArray a) { final TouchFeedbackState state = mState; // Extract the theme attributes, if any. final int[] themeAttrs = a.extractThemeAttrs(); - state.mThemeAttrs = themeAttrs; + state.mTouchThemeAttrs = themeAttrs; if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_tint] == 0) { mState.mTint = a.getColorStateList(R.styleable.TouchFeedbackDrawable_tint); @@ -219,34 +164,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper { if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_pinned] == 0) { mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false); } - - Drawable mask = mMask; - if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_mask] == 0) { - mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask); - } - - Drawable dr = super.getDrawable(); - if (themeAttrs == null || themeAttrs[R.styleable.TouchFeedbackDrawable_drawable] == 0) { - final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0); - if (drawableRes != 0) { - dr = r.getDrawable(drawableRes); - } - } - - // If neither a mask not a bottom layer was specified, assume we're - // projecting onto a parent surface. - mState.mProjected = mask == null && dr == null; - - if (dr != null) { - setDrawable(dr, r); - } else { - // For now at least, we MUST have a wrapped drawable. - setDrawable(new ColorDrawable(Color.TRANSPARENT), r); - } - - if (mask != null) { - setMaskDrawable(mask, r); - } } /** @@ -271,7 +188,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { "Can't apply theme to <touch-feedback> with no constant state"); } - final int[] themeAttrs = state.mThemeAttrs; + final int[] themeAttrs = state.mTouchThemeAttrs; if (themeAttrs != null) { final TypedArray a = t.resolveAttributes( themeAttrs, R.styleable.TouchFeedbackDrawable, 0, 0); @@ -298,39 +215,11 @@ public class TouchFeedbackDrawable extends DrawableWrapper { if (a.hasValue(R.styleable.TouchFeedbackDrawable_pinned)) { mState.mPinned = a.getBoolean(R.styleable.TouchFeedbackDrawable_pinned, false); } - - Drawable mask = mMask; - if (a.hasValue(R.styleable.TouchFeedbackDrawable_mask)) { - mask = a.getDrawable(R.styleable.TouchFeedbackDrawable_mask); - } - - Drawable dr = super.getDrawable(); - if (a.hasValue(R.styleable.TouchFeedbackDrawable_drawable)) { - final int drawableRes = a.getResourceId(R.styleable.TouchFeedbackDrawable_drawable, 0); - if (drawableRes != 0) { - dr = a.getResources().getDrawable(drawableRes); - } - } - - // If neither a mask not a bottom layer was specified, assume we're - // projecting onto a parent surface. - mState.mProjected = mask == null && dr == null; - - if (dr != null) { - setDrawable(dr, a.getResources()); - } else { - // For now at least, we MUST have a wrapped drawable. - setDrawable(new ColorDrawable(Color.TRANSPARENT), a.getResources()); - } - - if (mask != null) { - setMaskDrawable(mask, a.getResources()); - } } @Override public boolean canApplyTheme() { - return mState != null && mState.mThemeAttrs != null; + return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; } /** @@ -351,7 +240,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { public void setHotspot(int id, float x, float y) { if (mTouchedRipples == null) { mTouchedRipples = new SparseArray<Ripple>(); - mActiveRipples = new ArrayList<Ripple>(); + mActiveRipples = new Ripple[MAX_RIPPLES]; } final Ripple ripple = mTouchedRipples.get(id); @@ -366,9 +255,9 @@ public class TouchFeedbackDrawable extends DrawableWrapper { } final Ripple newRipple = new Ripple(bounds, padding, x, y, mDensity); - newRipple.enter(); + newRipple.animate().enter(); - mActiveRipples.add(newRipple); + mActiveRipples[mActiveRipplesCount++] = newRipple; mTouchedRipples.put(id, newRipple); } else if (!mState.mPinned) { ripple.move(x, y); @@ -388,7 +277,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { final Ripple ripple = mTouchedRipples.get(id); if (ripple != null) { - ripple.exit(); + ripple.animate().exit(); mTouchedRipples.remove(id); scheduleAnimation(); @@ -406,8 +295,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { final int n = mTouchedRipples.size(); for (int i = 0; i < n; i++) { - final Ripple ripple = mTouchedRipples.valueAt(i); - ripple.exit(); + mTouchedRipples.valueAt(i).animate().exit(); } if (n > 0) { @@ -420,7 +308,7 @@ public class TouchFeedbackDrawable extends DrawableWrapper { * Schedules the next animation, if necessary. */ private void scheduleAnimation() { - if (mActiveRipples == null || mActiveRipples.isEmpty()) { + if (mActiveRipplesCount == 0) { mAnimating = false; } else if (!mAnimating) { mAnimating = true; @@ -442,53 +330,68 @@ public class TouchFeedbackDrawable extends DrawableWrapper { @Override public void draw(Canvas canvas) { - // The lower layer always draws normally. - super.draw(canvas); - - if (mActiveRipples == null || mActiveRipples.size() == 0) { - // No ripples to draw. - return; - } - - final ArrayList<Ripple> activeRipples = mActiveRipples; - final Drawable mask = mMask == null && !mState.mProjected ? getDrawable() : null; - final Rect bounds = mask == null ? null : mask.getBounds(); + final boolean projected = getNumberOfLayers() == 0; + final Ripple[] activeRipples = mActiveRipples; + final int ripplesCount = mActiveRipplesCount; + final Rect bounds = getBounds(); - // Draw ripples into a layer that merges using SRC_IN. - boolean hasRipples = false; + // Draw ripples. + boolean drewRipples = false; int rippleRestoreCount = -1; - int n = activeRipples.size(); - for (int i = 0; i < n; i++) { - final Ripple ripple = activeRipples.get(i); - if (!ripple.active()) { - // TODO: Mark and sweep is more efficient. - activeRipples.remove(i); - i--; - n--; + int activeRipplesCount = 0; + for (int i = 0; i < ripplesCount; i++) { + final Ripple ripple = activeRipples[i]; + final RippleAnimator animator = ripple.animate(); + animator.update(); + if (!animator.isRunning()) { + activeRipples[i] = null; } else { - // If we're masking the ripple layer, make sure we have a layer first. - if (mask != null && rippleRestoreCount < 0) { + // If we're masking the ripple layer, make sure we have a layer + // first. This will merge SRC_OVER (directly) onto the canvas. + if (!projected && rippleRestoreCount < 0) { rippleRestoreCount = canvas.saveLayer(bounds.left, bounds.top, - bounds.right, bounds.bottom, getMaskingPaint(SRC_ATOP), 0); + bounds.right, bounds.bottom, null, 0); canvas.clipRect(bounds); } - hasRipples |= ripple.draw(canvas, getRipplePaint()); + drewRipples |= ripple.draw(canvas, getRipplePaint()); + + activeRipples[activeRipplesCount] = activeRipples[i]; + activeRipplesCount++; + } + } + mActiveRipplesCount = activeRipplesCount; + + // TODO: Use the masking layer first, if there is one. + + // If we have ripples and content, we need a masking layer. This will + // merge DST_ATOP onto (effectively under) the ripple layer. + if (drewRipples && !projected && rippleRestoreCount >= 0) { + canvas.saveLayer(bounds.left, bounds.top, + bounds.right, bounds.bottom, getMaskingPaint(DST_ATOP), 0); + } + + Drawable mask = null; + final ChildDrawable[] array = mLayerState.mChildren; + final int N = mLayerState.mNum; + for (int i = 0; i < N; i++) { + if (array[i].mId != R.id.mask) { + array[i].mDrawable.draw(canvas); + } else { + mask = array[i].mDrawable; } } // If we have ripples, mask them. - if (mask != null && hasRipples) { + if (mask != null && drewRipples) { + // TODO: This will also mask the lower layer, which is bad. canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, getMaskingPaint(DST_IN), 0); mask.draw(canvas); } - // Composite the layers if needed: - // 1. Mask DST_IN - // 2. Ripples SRC_ATOP - // 3. Lower n/a - if (rippleRestoreCount > 0) { + // Composite the layers if needed. + if (rippleRestoreCount >= 0) { canvas.restoreToCount(rippleRestoreCount); } } @@ -503,9 +406,6 @@ public class TouchFeedbackDrawable extends DrawableWrapper { } return mRipplePaint; } - - private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); - private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); private Paint getMaskingPaint(PorterDuffXfermode mode) { if (mMaskingPaint == null) { @@ -521,15 +421,12 @@ public class TouchFeedbackDrawable extends DrawableWrapper { final Rect drawingBounds = mDrawingBounds; dirtyBounds.set(drawingBounds); drawingBounds.setEmpty(); - final Rect rippleBounds = mTempRect; - final ArrayList<Ripple> activeRipples = mActiveRipples; - if (activeRipples != null) { - final int N = activeRipples.size(); - for (int i = 0; i < N; i++) { - activeRipples.get(i).getBounds(rippleBounds); - drawingBounds.union(rippleBounds); - } + final Ripple[] activeRipples = mActiveRipples; + final int N = mActiveRipplesCount; + for (int i = 0; i < N; i++) { + activeRipples[i].getBounds(rippleBounds); + drawingBounds.union(rippleBounds); } dirtyBounds.union(drawingBounds); @@ -539,34 +436,30 @@ public class TouchFeedbackDrawable extends DrawableWrapper { @Override public ConstantState getConstantState() { - // TODO: Can we just rely on super.getConstantState()? return mState; } - static class TouchFeedbackState extends WrapperState { - int[] mThemeAttrs; - ConstantState mMaskState; + static class TouchFeedbackState extends LayerState { + int[] mTouchThemeAttrs; ColorStateList mTint; Mode mTintMode; boolean mPinned; - boolean mProjected; - public TouchFeedbackState(TouchFeedbackState orig) { - super(orig); + public TouchFeedbackState( + TouchFeedbackState orig, TouchFeedbackDrawable owner, Resources res) { + super(orig, owner, res); if (orig != null) { - mThemeAttrs = orig.mThemeAttrs; + mTouchThemeAttrs = orig.mTouchThemeAttrs; mTint = orig.mTint; mTintMode = orig.mTintMode; - mMaskState = orig.mMaskState; mPinned = orig.mPinned; - mProjected = orig.mProjected; } } @Override public boolean canApplyTheme() { - return mThemeAttrs != null; + return mTouchThemeAttrs != null || super.canApplyTheme(); } @Override @@ -586,13 +479,33 @@ public class TouchFeedbackDrawable extends DrawableWrapper { } private TouchFeedbackDrawable(TouchFeedbackState state, Resources res, Theme theme) { - if (theme != null && state.canApplyTheme()) { - mState = new TouchFeedbackState(state); - applyTheme(theme); + boolean needsTheme = false; + + final TouchFeedbackState ns; + if (theme != null && state != null && state.canApplyTheme()) { + ns = new TouchFeedbackState(state, this, res); + needsTheme = true; + } else if (state == null) { + ns = new TouchFeedbackState(null, this, res); } else { - mState = state; + ns = state; + } + + if (res != null) { + mDensity = res.getDisplayMetrics().density; + } + + mState = ns; + mLayerState = ns; + + if (ns.mNum > 0) { + ensurePadding(); + } + + if (needsTheme) { + applyTheme(theme); } - setConstantState(state, res); + setPaddingMode(PADDING_MODE_STACK); } } |