summaryrefslogtreecommitdiffstats
path: root/graphics
diff options
context:
space:
mode:
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) {