summaryrefslogtreecommitdiffstats
path: root/graphics
diff options
context:
space:
mode:
authorAlan Viverette <alanv@google.com>2014-06-01 15:58:04 -0700
committerAlan Viverette <alanv@google.com>2014-06-01 15:58:04 -0700
commit4d2f2483f6d9e2eb25d843d676981f4ebc9c79e5 (patch)
tree54ee4e9f3f16654dc90c3ff3f6fd3963f23a759b /graphics
parent377801463ef75d7c14f3ef6a346d8ddccb2a4045 (diff)
downloadframeworks_base-4d2f2483f6d9e2eb25d843d676981f4ebc9c79e5.zip
frameworks_base-4d2f2483f6d9e2eb25d843d676981f4ebc9c79e5.tar.gz
frameworks_base-4d2f2483f6d9e2eb25d843d676981f4ebc9c79e5.tar.bz2
Fixes hotspot list transitions, update to use a single hotspot
Also updates background and button colors to match spec, removes dependency on bouncycastle Arrays. Vastly simplifies ripple drawing. Adds APIs for maximum ripple radius. Makes selectableItemBackground bounded by default and adds an unbounded version and theme attribute. BUG: 15315168 BUG: 15314684 BUG: 15314830 BUG: 15316768 BUG: 15333033 BUG: 15344050 Change-Id: Ib0619587ce78e43056b66571bae185e0f1613185
Diffstat (limited to 'graphics')
-rw-r--r--graphics/java/android/graphics/drawable/LayerDrawable.java2
-rw-r--r--graphics/java/android/graphics/drawable/Ripple.java192
-rw-r--r--graphics/java/android/graphics/drawable/RippleDrawable.java434
3 files changed, 353 insertions, 275 deletions
diff --git a/graphics/java/android/graphics/drawable/LayerDrawable.java b/graphics/java/android/graphics/drawable/LayerDrawable.java
index 2e47d3a..75cb0a0 100644
--- a/graphics/java/android/graphics/drawable/LayerDrawable.java
+++ b/graphics/java/android/graphics/drawable/LayerDrawable.java
@@ -304,7 +304,7 @@ public class LayerDrawable extends Drawable implements Drawable.Callback {
* @param right The right padding of the new layer.
* @param bottom The bottom padding of the new layer.
*/
- private void addLayer(Drawable layer, int[] themeAttrs, int id, int left, int top, int right,
+ void addLayer(Drawable layer, int[] themeAttrs, int id, int left, int top, int right,
int bottom) {
final LayerState st = mLayerState;
final int N = st.mChildren != null ? st.mChildren.length : 0;
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java
index 24e8de6..65b6814 100644
--- a/graphics/java/android/graphics/drawable/Ripple.java
+++ b/graphics/java/android/graphics/drawable/Ripple.java
@@ -25,9 +25,10 @@ import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
+import android.util.MathUtils;
import android.view.HardwareCanvas;
import android.view.RenderNodeAnimator;
-import android.view.animation.AccelerateInterpolator;
+import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
@@ -35,7 +36,7 @@ import java.util.ArrayList;
* Draws a Quantum Paper ripple.
*/
class Ripple {
- private static final TimeInterpolator INTERPOLATOR = new AccelerateInterpolator();
+ private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final float GLOBAL_SPEED = 1.0f;
private static final float WAVE_TOUCH_DOWN_ACCELERATION = 512.0f * GLOBAL_SPEED;
@@ -47,17 +48,23 @@ class Ripple {
private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
- private final Drawable mOwner;
+ private final RippleDrawable mOwner;
/** Bounds used for computing max radius. */
private final Rect mBounds;
/** Full-opacity color for drawing this ripple. */
- private final int mColor;
+ private int mColor;
/** Maximum ripple radius. */
private float mOuterRadius;
+ /** Screen density used to adjust pixel-based velocities. */
+ private float mDensity;
+
+ private float mStartingX;
+ private float mStartingY;
+
// Hardware rendering properties.
private CanvasProperty<Paint> mPropPaint;
private CanvasProperty<Float> mPropRadius;
@@ -84,7 +91,9 @@ class Ripple {
private float mX;
private float mY;
- private boolean mFinished;
+ // Values used to tween between the start and end positions.
+ private float mXGravity = 0;
+ private float mYGravity = 0;
/** Whether we should be drawing hardware animations. */
private boolean mHardwareAnimating;
@@ -95,16 +104,27 @@ class Ripple {
/**
* Creates a new ripple.
*/
- public Ripple(Drawable owner, Rect bounds, int color) {
+ public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
mOwner = owner;
mBounds = bounds;
+ mStartingX = startingX;
+ mStartingY = startingY;
+ }
+
+ public void setup(int maxRadius, int color, float density) {
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);
+ if (maxRadius != RippleDrawable.RADIUS_AUTO) {
+ mOuterRadius = maxRadius;
+ } else {
+ final float halfWidth = mBounds.width() / 2.0f;
+ final float halfHeight = mBounds.height() / 2.0f;
+ mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+ }
+
mOuterX = 0;
mOuterY = 0;
+ mDensity = density;
}
public void setRadius(float r) {
@@ -134,6 +154,24 @@ class Ripple {
return mOuterOpacity;
}
+ public void setXGravity(float x) {
+ mXGravity = x;
+ invalidateSelf();
+ }
+
+ public float getXGravity() {
+ return mXGravity;
+ }
+
+ public void setYGravity(float y) {
+ mYGravity = y;
+ invalidateSelf();
+ }
+
+ public float getYGravity() {
+ return mYGravity;
+ }
+
public void setX(float x) {
mX = x;
invalidateSelf();
@@ -153,13 +191,6 @@ class Ripple {
}
/**
- * Returns whether this ripple has finished exiting.
- */
- public boolean isFinished() {
- return mFinished;
- }
-
- /**
* Draws the ripple centered at (0,0) using the specified paint.
*/
public boolean draw(Canvas c, Paint p) {
@@ -204,28 +235,26 @@ class Ripple {
}
private boolean drawSoftware(Canvas c, Paint p) {
- final float radius = mRadius;
- final float opacity = mOpacity;
- final float outerOpacity = mOuterOpacity;
+ boolean hasContent = false;
// Cache the paint alpha so we can restore it later.
final int paintAlpha = p.getAlpha();
- final int alpha = (int) (255 * opacity + 0.5f);
- final int outerAlpha = (int) (255 * outerOpacity + 0.5f);
-
- boolean hasContent = false;
- if (outerAlpha > 0 && alpha > 0) {
- p.setAlpha(Math.min(alpha, outerAlpha));
+ final int outerAlpha = (int) (255 * mOuterOpacity + 0.5f);
+ if (outerAlpha > 0 && mOuterRadius > 0) {
+ p.setAlpha(outerAlpha);
p.setStyle(Style.FILL);
c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
hasContent = true;
}
- if (opacity > 0 && radius > 0) {
+ final int alpha = (int) (255 * mOpacity + 0.5f);
+ if (alpha > 0 && mRadius > 0) {
+ final float x = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mXGravity);
+ final float y = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mYGravity);
p.setAlpha(alpha);
p.setStyle(Style.FILL);
- c.drawCircle(mX, mY, radius, p);
+ c.drawCircle(x, y, mRadius, p);
hasContent = true;
}
@@ -249,31 +278,42 @@ class Ripple {
}
/**
- * Starts the enter animation at the specified absolute coordinates.
+ * Specifies the starting position relative to the drawable bounds. No-op if
+ * the ripple has already entered.
+ */
+ public void move(float x, float y) {
+ mStartingX = x;
+ mStartingY = y;
+ }
+
+ /**
+ * Starts the enter animation.
*/
- public void enter(float x, float y) {
- mX = x - mBounds.exactCenterX();
- mY = y - mBounds.exactCenterY();
+ public void enter() {
+ mX = mStartingX - mBounds.exactCenterX();
+ mY = mStartingY - mBounds.exactCenterY();
final int radiusDuration = (int)
- (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION) + 0.5);
+ (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 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);
+ radius.setInterpolator(LINEAR_INTERPOLATOR);
- final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "x", mOuterX);
+ final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
cX.setAutoCancel(true);
cX.setDuration(radiusDuration);
- final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "y", mOuterY);
+ final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
cY.setAutoCancel(true);
cY.setDuration(radiusDuration);
final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
outer.setAutoCancel(true);
outer.setDuration(outerDuration);
+ outer.setInterpolator(LINEAR_INTERPOLATOR);
mAnimRadius = radius;
mAnimOuterOpacity = outer;
@@ -295,6 +335,9 @@ class Ripple {
public void exit() {
cancelSoftwareAnimations();
+ mX = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mXGravity);
+ mY = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mYGravity);
+
final float remaining;
if (mAnimRadius != null && mAnimRadius.isRunning()) {
remaining = mOuterRadius - mRadius;
@@ -303,7 +346,7 @@ class Ripple {
}
final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
- + WAVE_TOUCH_DOWN_ACCELERATION)) + 0.5);
+ + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
// Determine at what time the inner and outer opacity intersect.
@@ -347,17 +390,20 @@ class Ripple {
final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mOuterRadius);
radius.setDuration(radiusDuration);
+ radius.setInterpolator(LINEAR_INTERPOLATOR);
final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX);
x.setDuration(radiusDuration);
+ x.setInterpolator(LINEAR_INTERPOLATOR);
final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY);
y.setDuration(radiusDuration);
+ y.setInterpolator(LINEAR_INTERPOLATOR);
final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
RenderNodeAnimator.PAINT_ALPHA, 0);
opacity.setDuration(opacityDuration);
- opacity.addListener(mAnimationListener);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
final RenderNodeAnimator outerOpacity;
if (outerInflection > 0) {
@@ -365,6 +411,7 @@ class Ripple {
outerOpacity = new RenderNodeAnimator(
mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
outerOpacity.setDuration(outerInflection);
+ outerOpacity.setInterpolator(LINEAR_INTERPOLATOR);
// Chain the outer opacity exit animation.
final int outerDuration = opacityDuration - outerInflection;
@@ -372,14 +419,20 @@ class Ripple {
final RenderNodeAnimator outerFadeOut = new RenderNodeAnimator(
mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
outerFadeOut.setDuration(outerDuration);
+ outerFadeOut.setInterpolator(LINEAR_INTERPOLATOR);
outerFadeOut.setStartDelay(outerInflection);
+ outerFadeOut.addListener(mAnimationListener);
mPendingAnimations.add(outerFadeOut);
+ } else {
+ outerOpacity.addListener(mAnimationListener);
}
} else {
outerOpacity = new RenderNodeAnimator(
mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ outerOpacity.setInterpolator(LINEAR_INTERPOLATOR);
outerOpacity.setDuration(opacityDuration);
+ outerOpacity.addListener(mAnimationListener);
}
mPendingAnimations.add(radius);
@@ -394,52 +447,67 @@ class Ripple {
}
private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
- float inflectionOpacity) {
+ int inflectionOpacity) {
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radius", mOuterRadius);
radius.setAutoCancel(true);
radius.setDuration(radiusDuration);
+ radius.setInterpolator(LINEAR_INTERPOLATOR);
final ObjectAnimator x = ObjectAnimator.ofFloat(this, "x", mOuterX);
x.setAutoCancel(true);
x.setDuration(radiusDuration);
+ x.setInterpolator(LINEAR_INTERPOLATOR);
final ObjectAnimator y = ObjectAnimator.ofFloat(this, "y", mOuterY);
y.setAutoCancel(true);
y.setDuration(radiusDuration);
+ y.setInterpolator(LINEAR_INTERPOLATOR);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "opacity", 0);
opacity.setAutoCancel(true);
opacity.setDuration(opacityDuration);
- opacity.addListener(mAnimationListener);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
final ObjectAnimator outerOpacity;
if (outerInflection > 0) {
// Outer opacity continues to increase for a bit.
- outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", inflectionOpacity);
+ outerOpacity = ObjectAnimator.ofFloat(this,
+ "outerOpacity", inflectionOpacity / 255.0f);
+ outerOpacity.setAutoCancel(true);
outerOpacity.setDuration(outerInflection);
+ outerOpacity.setInterpolator(LINEAR_INTERPOLATOR);
// Chain the outer opacity exit animation.
final int outerDuration = opacityDuration - outerInflection;
- outerOpacity.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- 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);
- }
- });
+ if (outerDuration > 0) {
+ outerOpacity.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final ObjectAnimator outerFadeOut = ObjectAnimator.ofFloat(Ripple.this,
+ "outerOpacity", 0);
+ outerFadeOut.setAutoCancel(true);
+ outerFadeOut.setDuration(outerDuration);
+ outerFadeOut.setInterpolator(LINEAR_INTERPOLATOR);
+ outerFadeOut.addListener(mAnimationListener);
+
+ mAnimOuterOpacity = outerFadeOut;
+
+ outerFadeOut.start();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ animation.removeListener(this);
+ }
+ });
+ } else {
+ outerOpacity.addListener(mAnimationListener);
+ }
} else {
outerOpacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
+ outerOpacity.setAutoCancel(true);
outerOpacity.setDuration(opacityDuration);
+ outerOpacity.addListener(mAnimationListener);
}
mAnimRadius = radius;
@@ -498,6 +566,11 @@ class Ripple {
runningAnimations.clear();
}
+ private void removeSelf() {
+ // The owner will invalidate itself.
+ mOwner.removeRipple(this);
+ }
+
private void invalidateSelf() {
mOwner.invalidateSelf();
}
@@ -505,12 +578,7 @@ class Ripple {
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mFinished = true;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mFinished = true;
+ removeSelf();
}
};
}
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index 1bd7cac..4e786f0 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -25,17 +25,14 @@ import android.graphics.Color;
import android.graphics.ColorFilter;
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;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
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;
@@ -71,10 +68,17 @@ import java.io.IOException;
public class RippleDrawable extends LayerDrawable {
private static final String LOG_TAG = RippleDrawable.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);
+ /**
+ * Constant for automatically determining the maximum ripple radius.
+ *
+ * @see #setMaxRadius(int)
+ * @hide
+ */
+ public static final int RADIUS_AUTO = -1;
+
/** The maximum number of ripples supported. */
private static final int MAX_RIPPLES = 10;
@@ -91,17 +95,8 @@ public class RippleDrawable extends LayerDrawable {
private final RippleState mState;
- /**
- * Lazily-created map of pending hotspot locations. These may be modified by
- * calls to {@link #setHotspot(float, float)}.
- */
- private SparseArray<PointF> mPendingHotspots;
-
- /**
- * Lazily-created map of active hotspot locations. These may be modified by
- * calls to {@link #setHotspot(float, float)}.
- */
- private SparseArray<Ripple> mActiveHotspots;
+ /** The current hotspot. May be actively animating or pending entry. */
+ private Ripple mHotspot;
/**
* Lazily-created array of actively animating ripples. Inactive ripples are
@@ -122,18 +117,46 @@ public class RippleDrawable extends LayerDrawable {
/** Whether bounds are being overridden. */
private boolean mOverrideBounds;
+ /** Whether the hotspot is currently active (e.g. focused or pressed). */
+ private boolean mActive;
+
RippleDrawable() {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new ripple drawable with the specified content and mask
+ * drawables.
+ *
+ * @param content The content drawable, may be {@code null}
+ * @param mask The mask drawable, may be {@code null}
+ */
+ public RippleDrawable(Drawable content, Drawable mask) {
this(new RippleState(null, null, null), null, null);
+
+ if (content != null) {
+ addLayer(content, null, 0, 0, 0, 0, 0);
+ }
+
+ if (mask != null) {
+ addLayer(content, null, android.R.id.mask, 0, 0, 0, 0);
+ }
+
+ ensurePadding();
}
@Override
public void setAlpha(int alpha) {
-
+ super.setAlpha(alpha);
+
+ // TODO: Should we support this?
}
@Override
public void setColorFilter(ColorFilter cf) {
-
+ super.setColorFilter(cf);
+
+ // TODO: Should we support this?
}
@Override
@@ -146,20 +169,18 @@ public class RippleDrawable extends LayerDrawable {
protected boolean onStateChange(int[] stateSet) {
super.onStateChange(stateSet);
- final boolean pressed = Arrays.contains(stateSet, R.attr.state_pressed);
- if (!pressed) {
- removeHotspot(R.attr.state_pressed);
- } else {
- activateHotspot(R.attr.state_pressed);
- }
-
- final boolean focused = Arrays.contains(stateSet, R.attr.state_focused);
- if (!focused) {
- removeHotspot(R.attr.state_focused);
- } else {
- activateHotspot(R.attr.state_focused);
+ boolean active = false;
+ final int N = stateSet.length;
+ for (int i = 0; i < N; i++) {
+ if (stateSet[i] == R.attr.state_focused
+ || stateSet[i] == R.attr.state_pressed) {
+ active = true;
+ break;
+ }
}
+ setActive(active);
+ // Update the paint color. Only applicable when animated in software.
if (mRipplePaint != null && mState.mTint != null) {
final ColorStateList stateList = mState.mTint;
final int newColor = stateList.getColorForState(stateSet, 0);
@@ -174,6 +195,18 @@ public class RippleDrawable extends LayerDrawable {
return false;
}
+ private void setActive(boolean active) {
+ if (mActive != active) {
+ mActive = active;
+
+ if (active) {
+ activateHotspot();
+ } else {
+ removeHotspot();
+ }
+ }
+ }
+
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
@@ -272,7 +305,7 @@ public class RippleDrawable extends LayerDrawable {
/**
* Initializes the constant state from the values in the typed array.
*/
- private void updateStateFromTypedArray(TypedArray a) {
+ private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
final RippleState state = mState;
// Extract the theme attributes, if any.
@@ -289,6 +322,12 @@ public class RippleDrawable extends LayerDrawable {
}
mState.mPinned = a.getBoolean(R.styleable.RippleDrawable_pinned, mState.mPinned);
+
+ // If we're not waiting on a theme, verify required attributes.
+ if (state.mTouchThemeAttrs == null && mState.mTint == null) {
+ throw new XmlPullParserException(a.getPositionDescription() +
+ ": <ripple> requires a valid tint attribute");
+ }
}
/**
@@ -314,8 +353,13 @@ public class RippleDrawable extends LayerDrawable {
final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
R.styleable.RippleDrawable);
- updateStateFromTypedArray(a);
- a.recycle();
+ try {
+ updateStateFromTypedArray(a);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } finally {
+ a.recycle();
+ }
}
@Override
@@ -330,36 +374,15 @@ public class RippleDrawable extends LayerDrawable {
y = mHotspotBounds.exactCenterY();
}
- // TODO: We should only have a single pending/active hotspot.
- final int id = R.attr.state_pressed;
- 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) {
- // 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;
- }
-
- if (mActiveHotspots == null) {
- mActiveHotspots = new SparseArray<Ripple>();
- mAnimatingRipples = new Ripple[MAX_RIPPLES];
- }
+ if (mHotspot == null) {
+ mHotspot = new Ripple(this, mHotspotBounds, x, y);
- 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;
+ if (mActive) {
+ activateHotspot();
+ }
+ } else {
+ mHotspot.move(x, y);
}
-
- // The hotspot needs to be made active.
- createActiveHotspot(id, x, y);
}
private boolean circleContains(Rect bounds, float x, float y) {
@@ -374,74 +397,44 @@ public class RippleDrawable extends LayerDrawable {
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);
- }
-
- if (p == null) {
- final PointF newPoint = new PointF();
- mPendingHotspots.put(id, newPoint);
- return newPoint;
- } else {
- return p;
- }
- }
-
/**
- * Moves a hotspot from pending to active.
+ * Creates an active hotspot at the specified location.
*/
- 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);
- }
+ private void activateHotspot() {
+ if (mAnimatingRipplesCount >= MAX_RIPPLES) {
+ // 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;
+ }
+
+ if (mHotspot == null) {
+ final float x = mHotspotBounds.exactCenterX();
+ final float y = mHotspotBounds.exactCenterY();
+ mHotspot = new Ripple(this, mHotspotBounds, x, 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);
+ mHotspot.setup(mState.mMaxRadius, color, mDensity);
+ mHotspot.enter();
if (mAnimatingRipples == null) {
mAnimatingRipples = new Ripple[MAX_RIPPLES];
}
- mAnimatingRipples[mAnimatingRipplesCount++] = newRipple;
-
- if (mActiveHotspots == null) {
- mActiveHotspots = new SparseArray<Ripple>();
- }
- mActiveHotspots.put(id, newRipple);
+ mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot;
}
- private void removeHotspot(int id) {
- if (mActiveHotspots == null) {
- return;
- }
-
- final Ripple ripple = mActiveHotspots.get(id);
- if (ripple != null) {
- ripple.exit();
-
- mActiveHotspots.remove(id);
+ private void removeHotspot() {
+ if (mHotspot != null) {
+ mHotspot.exit();
+ mHotspot = null;
}
}
private void clearHotspots() {
- if (mActiveHotspots != null) {
- mActiveHotspots.clear();
+ if (mHotspot != null) {
+ mHotspot.cancel();
+ mHotspot = null;
}
final int count = mAnimatingRipplesCount;
@@ -463,69 +456,96 @@ public class RippleDrawable extends LayerDrawable {
@Override
public void draw(Canvas canvas) {
- final int N = mLayerState.mNum;
- final Rect bounds = getBounds();
- final ChildDrawable[] array = mLayerState.mChildren;
- final boolean maskOnly = mState.mMask != null && N == 1;
-
- int restoreToCount = drawRippleLayer(canvas, maskOnly);
-
- 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) {
- canvas.saveLayer(bounds.left, bounds.top, bounds.right,
- bounds.bottom, getMaskingPaint(DST_IN));
- mState.mMask.draw(canvas);
- canvas.restoreToCount(restoreToCount);
- restoreToCount = -1;
+ final Rect bounds = isProjected() ? getDirtyBounds() : getBounds();
+
+ // Draw the content into a layer first.
+ final int contentLayer = drawContentLayer(canvas, bounds, SRC_OVER);
+
+ // Next, draw the ripples into a layer.
+ final int rippleLayer = drawRippleLayer(canvas, bounds, mState.mTintXfermode);
+
+ // If we have ripples, draw the masking layer.
+ if (rippleLayer >= 0) {
+ drawMaskingLayer(canvas, bounds, DST_IN);
+ }
+
+ // Composite the layers if needed.
+ if (contentLayer >= 0) {
+ canvas.restoreToCount(contentLayer);
+ } else if (rippleLayer >= 0) {
+ canvas.restoreToCount(rippleLayer);
+ }
+ }
+
+ /**
+ * Removes a ripple from the animating ripple list.
+ *
+ * @param ripple the ripple to remove
+ */
+ void removeRipple(Ripple ripple) {
+ // Ripple ripple ripple ripple. Ripple ripple.
+ final Ripple[] ripples = mAnimatingRipples;
+ final int count = mAnimatingRipplesCount;
+ final int index = getRippleIndex(ripple);
+ if (index >= 0) {
+ for (int i = index + 1; i < count; i++) {
+ ripples[i - 1] = ripples[i];
}
+ ripples[count - 1] = null;
+ mAnimatingRipplesCount--;
+ invalidateSelf();
+ }
+ }
- // If there's more content, we need an extra masking layer to merge
- // the ripples over the content.
- if (!maskOnly) {
- final PorterDuffXfermode xfermode = mState.getTintXfermodeInverse();
- final int count = canvas.saveLayer(bounds.left, bounds.top,
- bounds.right, bounds.bottom, getMaskingPaint(xfermode));
- if (restoreToCount < 0) {
- restoreToCount = count;
- }
+ private int getRippleIndex(Ripple ripple) {
+ final Ripple[] ripples = mAnimatingRipples;
+ final int count = mAnimatingRipplesCount;
+ for (int i = 0; i < count; i++) {
+ if (ripples[i] == ripple) {
+ return i;
}
}
+ return -1;
+ }
+
+ private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
+ final int count = mLayerState.mNum;
+ if (count == 0 || (mState.mMask != null && count == 1)) {
+ return -1;
+ }
+
+ final Paint maskingPaint = getMaskingPaint(mode);
+ final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
+ bounds.right, bounds.bottom, maskingPaint);
// Draw everything except the mask.
- for (int i = 0; i < N; i++) {
+ final ChildDrawable[] array = mLayerState.mChildren;
+ for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask) {
array[i].mDrawable.draw(canvas);
}
}
- // Composite the layers if needed.
- if (restoreToCount >= 0) {
- canvas.restoreToCount(restoreToCount);
- }
+ return restoreToCount;
}
- private int drawRippleLayer(Canvas canvas, boolean maskOnly) {
+ private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
final int count = mAnimatingRipplesCount;
if (count == 0) {
return -1;
}
- final Ripple[] ripples = mAnimatingRipples;
- final boolean projected = isProjected();
- 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.
- final int rippleColor;
+ final int rippleARGB;
if (mState.mTint != null) {
- rippleColor = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
+ rippleARGB = mState.mTint.getColorForState(getState(), Color.TRANSPARENT);
} else {
- rippleColor = Color.TRANSPARENT;
+ rippleARGB = Color.TRANSPARENT;
}
- final int rippleAlpha = Color.alpha(rippleColor);
+ final int rippleAlpha = Color.alpha(rippleARGB);
+ final int rippleColor = rippleARGB | (0xFF << 24);
if (mRipplePaint == null) {
mRipplePaint = new Paint();
mRipplePaint.setAntiAlias(true);
@@ -536,36 +556,20 @@ public class RippleDrawable extends LayerDrawable {
boolean drewRipples = false;
int restoreToCount = -1;
int restoreTranslate = -1;
- int animatingCount = 0;
// Draw ripples and update the animating ripples array.
+ final Ripple[] ripples = mAnimatingRipples;
for (int i = 0; i < count; i++) {
final Ripple ripple = ripples[i];
- // Mark and skip finished ripples.
- if (ripple.isFinished()) {
- ripples[i] = null;
- continue;
- }
-
// If we're masking the ripple layer, make sure we have a layer
// first. This will merge SRC_OVER (directly) onto the canvas.
if (restoreToCount < 0) {
- // If we're projecting or we only have a mask, we want to treat the
- // underlying canvas as our content and merge the ripple layer down
- // using the tint xfermode.
- final PorterDuffXfermode xfermode;
- if (projected || maskOnly) {
- xfermode = mState.getTintXfermode();
- } else {
- xfermode = SRC_OVER;
- }
-
- final Paint layerPaint = getMaskingPaint(xfermode);
- layerPaint.setAlpha(rippleAlpha);
- restoreToCount = canvas.saveLayer(layerBounds.left, layerBounds.top,
- layerBounds.right, layerBounds.bottom, layerPaint);
- layerPaint.setAlpha(255);
+ final Paint maskingPaint = getMaskingPaint(mode);
+ maskingPaint.setAlpha(rippleAlpha);
+ restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
+ bounds.right, bounds.bottom, maskingPaint);
+ maskingPaint.setAlpha(255);
restoreTranslate = canvas.save();
// Translate the canvas to the current hotspot bounds.
@@ -573,13 +577,8 @@ public class RippleDrawable extends LayerDrawable {
}
drewRipples |= ripple.draw(canvas, ripplePaint);
-
- ripples[animatingCount] = ripples[i];
- animatingCount++;
}
- mAnimatingRipplesCount = animatingCount;
-
// Always restore the translation.
if (restoreTranslate >= 0) {
canvas.restoreToCount(restoreTranslate);
@@ -594,6 +593,20 @@ public class RippleDrawable extends LayerDrawable {
return restoreToCount;
}
+ private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
+ final Drawable mask = mState.mMask;
+ if (mask == null) {
+ return -1;
+ }
+
+ final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
+ bounds.right, bounds.bottom, getMaskingPaint(mode));
+
+ mask.draw(canvas);
+
+ return restoreToCount;
+ }
+
private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
if (mMaskingPaint == null) {
mMaskingPaint = new Paint();
@@ -634,8 +647,8 @@ public class RippleDrawable extends LayerDrawable {
int[] mTouchThemeAttrs;
ColorStateList mTint = null;
PorterDuffXfermode mTintXfermode = SRC_ATOP;
- PorterDuffXfermode mTintXfermodeInverse = DST_ATOP;
Drawable mMask;
+ int mMaxRadius = RADIUS_AUTO;
boolean mPinned = false;
public RippleState(RippleState orig, RippleDrawable owner, Resources res) {
@@ -645,14 +658,12 @@ public class RippleDrawable extends LayerDrawable {
mTouchThemeAttrs = orig.mTouchThemeAttrs;
mTint = orig.mTint;
mTintXfermode = orig.mTintXfermode;
- mTintXfermodeInverse = orig.mTintXfermodeInverse;
+ mMaxRadius = orig.mMaxRadius;
mPinned = orig.mPinned;
}
}
public void setTintMode(Mode mode) {
- final Mode invertedMode = RippleState.invertPorterDuffMode(mode);
- mTintXfermodeInverse = new PorterDuffXfermode(invertedMode);
mTintXfermode = new PorterDuffXfermode(mode);
}
@@ -660,10 +671,6 @@ public class RippleDrawable extends LayerDrawable {
return mTintXfermode;
}
- public PorterDuffXfermode getTintXfermodeInverse() {
- return mTintXfermodeInverse;
- }
-
@Override
public boolean canApplyTheme() {
return mTouchThemeAttrs != null || super.canApplyTheme();
@@ -683,33 +690,36 @@ public class RippleDrawable extends LayerDrawable {
public Drawable newDrawable(Resources res, Theme theme) {
return new RippleDrawable(this, res, theme);
}
+ }
- /**
- * Inverts SRC and DST in PorterDuff blending modes.
- */
- private static Mode invertPorterDuffMode(Mode src) {
- switch (src) {
- case SRC_ATOP:
- return Mode.DST_ATOP;
- case SRC_IN:
- return Mode.DST_IN;
- case SRC_OUT:
- return Mode.DST_OUT;
- case SRC_OVER:
- return Mode.DST_OVER;
- case DST_ATOP:
- return Mode.SRC_ATOP;
- case DST_IN:
- return Mode.SRC_IN;
- case DST_OUT:
- return Mode.SRC_OUT;
- case DST_OVER:
- return Mode.SRC_OVER;
- default:
- // Everything else is agnostic to SRC versus DST.
- return src;
- }
+ /**
+ * Sets the maximum ripple radius in pixels. The default value of
+ * {@link #RADIUS_AUTO} defines the radius as the distance from the center
+ * of the drawable bounds (or hotspot bounds, if specified) to a corner.
+ *
+ * @param maxRadius the maximum ripple radius in pixels or
+ * {@link #RADIUS_AUTO} to automatically determine the maximum
+ * radius based on the bounds
+ * @see #getMaxRadius()
+ * @see #setHotspotBounds(int, int, int, int)
+ * @hide
+ */
+ public void setMaxRadius(int maxRadius) {
+ if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
+ throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
}
+
+ mState.mMaxRadius = maxRadius;
+ }
+
+ /**
+ * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
+ * the radius is determined automatically
+ * @see #setMaxRadius(int)
+ * @hide
+ */
+ public int getMaxRadius() {
+ return mState.mMaxRadius;
}
private RippleDrawable(RippleState state, Resources res, Theme theme) {