summaryrefslogtreecommitdiffstats
path: root/graphics/java
diff options
context:
space:
mode:
authorAlan Viverette <alanv@google.com>2014-03-26 16:43:07 -0700
committerAlan Viverette <alanv@google.com>2014-03-26 16:43:07 -0700
commit47bf0d95ef6c9ac68773567d503749c874a07f2f (patch)
treebd976e8d6770d5b5d3c0923a494672545190a45f /graphics/java
parent460572b22fe8fe5880ad099090b38765e2f8a2f6 (diff)
downloadframeworks_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')
-rw-r--r--graphics/java/android/graphics/drawable/Drawable.java2
-rw-r--r--graphics/java/android/graphics/drawable/LayerDrawable.java2
-rw-r--r--graphics/java/android/graphics/drawable/RevealDrawable.java317
-rw-r--r--graphics/java/android/graphics/drawable/Ripple.java257
-rw-r--r--graphics/java/android/graphics/drawable/TouchFeedbackDrawable.java309
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>&lt;reveal&gt;</code> element.
- * Each Drawable in the transition is defined in a nested <code>&lt;item&gt;</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);
}
}