Avoid extra saveLayer calls in RippleDrawable, fix docs

Also fixes opacity returned from InsetDrawable to accurately reflect
the transparent inset area and updates button to correctly use tint.

BUG: 18226391
Change-Id: Ia9a88d9d663990a6829d2f251c7f59ea2a79d816
This commit is contained in:
Alan Viverette
2014-12-04 14:10:16 -08:00
parent 3a0d878ab5
commit 6dfa60f33c
5 changed files with 254 additions and 196 deletions

View File

@@ -21,9 +21,10 @@
android:insetTop="@dimen/button_inset_vertical_material" android:insetTop="@dimen/button_inset_vertical_material"
android:insetRight="@dimen/button_inset_horizontal_material" android:insetRight="@dimen/button_inset_horizontal_material"
android:insetBottom="@dimen/button_inset_vertical_material"> android:insetBottom="@dimen/button_inset_vertical_material">
<shape android:shape="rectangle"> <shape android:shape="rectangle"
android:tint="?attr/colorButtonNormal">
<corners android:radius="@dimen/control_corner_material" /> <corners android:radius="@dimen/control_corner_material" />
<solid android:color="?attr/colorButtonNormal" /> <solid android:color="@color/white" />
<padding android:left="@dimen/button_padding_horizontal_material" <padding android:left="@dimen/button_padding_horizontal_material"
android:top="@dimen/button_padding_vertical_material" android:top="@dimen/button_padding_vertical_material"
android:right="@dimen/button_padding_horizontal_material" android:right="@dimen/button_padding_horizontal_material"

View File

@@ -30,6 +30,7 @@ import android.graphics.Canvas;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.Insets; import android.graphics.Insets;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuff.Mode;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -317,7 +318,13 @@ public class InsetDrawable extends Drawable implements Drawable.Callback {
@Override @Override
public int getOpacity() { public int getOpacity() {
return mState.mDrawable.getOpacity(); final InsetState state = mState;
final int opacity = state.mDrawable.getOpacity();
if (opacity == PixelFormat.OPAQUE && (state.mInsetLeft > 0 || state.mInsetTop > 0
|| state.mInsetRight > 0 || state.mInsetBottom > 0)) {
return PixelFormat.TRANSLUCENT;
}
return opacity;
} }
@Override @Override

View File

@@ -22,11 +22,8 @@ import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator; import android.animation.TimeInterpolator;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.CanvasProperty; import android.graphics.CanvasProperty;
import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Xfermode;
import android.util.MathUtils; import android.util.MathUtils;
import android.view.HardwareCanvas; import android.view.HardwareCanvas;
import android.view.RenderNodeAnimator; import android.view.RenderNodeAnimator;
@@ -51,19 +48,12 @@ class Ripple {
// Hardware animators. // Hardware animators.
private final ArrayList<RenderNodeAnimator> mRunningAnimations = private final ArrayList<RenderNodeAnimator> mRunningAnimations =
new ArrayList<RenderNodeAnimator>(); new ArrayList<RenderNodeAnimator>();
private final ArrayList<RenderNodeAnimator> mPendingAnimations =
new ArrayList<RenderNodeAnimator>();
private final RippleDrawable mOwner; private final RippleDrawable mOwner;
/** Bounds used for computing max radius. */ /** Bounds used for computing max radius. */
private final Rect mBounds; private final Rect mBounds;
/** ARGB color for drawing this ripple. */
private int mColor;
private Xfermode mXfermode;
/** Maximum ripple radius. */ /** Maximum ripple radius. */
private float mOuterRadius; private float mOuterRadius;
@@ -112,6 +102,10 @@ class Ripple {
/** Whether we were canceled externally and should avoid self-removal. */ /** Whether we were canceled externally and should avoid self-removal. */
private boolean mCanceled; private boolean mCanceled;
private boolean mHasPendingHardwareExit;
private int mPendingRadiusDuration;
private int mPendingOpacityDuration;
/** /**
* Creates a new ripple. * Creates a new ripple.
*/ */
@@ -217,10 +211,6 @@ class Ripple {
* Draws the ripple centered at (0,0) using the specified paint. * Draws the ripple centered at (0,0) using the specified paint.
*/ */
public boolean draw(Canvas c, Paint p) { public boolean draw(Canvas c, Paint p) {
// Store the color and xfermode, we might need them later.
mColor = p.getColor();
mXfermode = p.getXfermode();
final boolean canUseHardware = c.isHardwareAccelerated(); final boolean canUseHardware = c.isHardwareAccelerated();
if (mCanUseHardware != canUseHardware && mCanUseHardware) { if (mCanUseHardware != canUseHardware && mCanUseHardware) {
// We've switched from hardware to non-hardware mode. Panic. // We've switched from hardware to non-hardware mode. Panic.
@@ -229,8 +219,8 @@ class Ripple {
mCanUseHardware = canUseHardware; mCanUseHardware = canUseHardware;
final boolean hasContent; final boolean hasContent;
if (canUseHardware && mHardwareAnimating) { if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
hasContent = drawHardware((HardwareCanvas) c); hasContent = drawHardware((HardwareCanvas) c, p);
} else { } else {
hasContent = drawSoftware(c, p); hasContent = drawSoftware(c, p);
} }
@@ -238,24 +228,10 @@ class Ripple {
return hasContent; return hasContent;
} }
private boolean drawHardware(HardwareCanvas c) { private boolean drawHardware(HardwareCanvas c, Paint p) {
// If we have any pending hardware animations, cancel any running if (mHasPendingHardwareExit) {
// animations and start those now.
final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
final int N = pendingAnimations.size();
if (N > 0) {
cancelHardwareAnimations(false); cancelHardwareAnimations(false);
startPendingHardwareExit(c, p);
// We canceled old animations, but we're about to run new ones.
mHardwareAnimating = true;
for (int i = 0; i < N; i++) {
pendingAnimations.get(i).setTarget(c);
pendingAnimations.get(i).start();
}
mRunningAnimations.addAll(pendingAnimations);
pendingAnimations.clear();
} }
c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
@@ -347,8 +323,6 @@ class Ripple {
* Starts the exit animation. * Starts the exit animation.
*/ */
public void exit() { public void exit() {
cancel();
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
final float remaining; final float remaining;
if (mAnimRadius != null && mAnimRadius.isRunning()) { if (mAnimRadius != null && mAnimRadius.isRunning()) {
@@ -357,19 +331,33 @@ class Ripple {
remaining = mOuterRadius; remaining = mOuterRadius;
} }
cancel();
final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
+ WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
if (mCanUseHardware) { if (mCanUseHardware) {
exitHardware(radiusDuration, opacityDuration); createPendingHardwareExit(radiusDuration, opacityDuration);
} else { } else {
exitSoftware(radiusDuration, opacityDuration); exitSoftware(radiusDuration, opacityDuration);
} }
} }
private void exitHardware(int radiusDuration, int opacityDuration) { private void createPendingHardwareExit(int radiusDuration, int opacityDuration) {
mPendingAnimations.clear(); mHasPendingHardwareExit = true;
mPendingRadiusDuration = radiusDuration;
mPendingOpacityDuration = opacityDuration;
// The animation will start on the next draw().
invalidateSelf();
}
private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
mHasPendingHardwareExit = false;
final int radiusDuration = mPendingRadiusDuration;
final int opacityDuration = mPendingOpacityDuration;
final float startX = MathUtils.lerp( final float startX = MathUtils.lerp(
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
@@ -377,12 +365,8 @@ class Ripple {
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
final Paint paint = getTempPaint(); final Paint paint = getTempPaint(p);
paint.setAntiAlias(true); paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f));
paint.setColor(mColor);
paint.setXfermode(mXfermode);
paint.setAlpha((int) (Color.alpha(mColor) * mOpacity + 0.5f));
paint.setStyle(Style.FILL);
mPropPaint = CanvasProperty.createPaint(paint); mPropPaint = CanvasProperty.createPaint(paint);
mPropRadius = CanvasProperty.createFloat(startRadius); mPropRadius = CanvasProperty.createFloat(startRadius);
mPropX = CanvasProperty.createFloat(startX); mPropX = CanvasProperty.createFloat(startX);
@@ -391,25 +375,33 @@ class Ripple {
final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius); final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
radiusAnim.setDuration(radiusDuration); radiusAnim.setDuration(radiusDuration);
radiusAnim.setInterpolator(DECEL_INTERPOLATOR); radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
radiusAnim.setTarget(c);
radiusAnim.start();
final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX); final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
xAnim.setDuration(radiusDuration); xAnim.setDuration(radiusDuration);
xAnim.setInterpolator(DECEL_INTERPOLATOR); xAnim.setInterpolator(DECEL_INTERPOLATOR);
xAnim.setTarget(c);
xAnim.start();
final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY); final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
yAnim.setDuration(radiusDuration); yAnim.setDuration(radiusDuration);
yAnim.setInterpolator(DECEL_INTERPOLATOR); yAnim.setInterpolator(DECEL_INTERPOLATOR);
yAnim.setTarget(c);
yAnim.start();
final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint, final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
RenderNodeAnimator.PAINT_ALPHA, 0); RenderNodeAnimator.PAINT_ALPHA, 0);
opacityAnim.setDuration(opacityDuration); opacityAnim.setDuration(opacityDuration);
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
opacityAnim.addListener(mAnimationListener); opacityAnim.addListener(mAnimationListener);
opacityAnim.setTarget(c);
opacityAnim.start();
mPendingAnimations.add(radiusAnim); mRunningAnimations.add(radiusAnim);
mPendingAnimations.add(opacityAnim); mRunningAnimations.add(opacityAnim);
mPendingAnimations.add(xAnim); mRunningAnimations.add(xAnim);
mPendingAnimations.add(yAnim); mRunningAnimations.add(yAnim);
mHardwareAnimating = true; mHardwareAnimating = true;
@@ -418,8 +410,6 @@ class Ripple {
mTweenX = 1; mTweenX = 1;
mTweenY = 1; mTweenY = 1;
mTweenRadius = 1; mTweenRadius = 1;
invalidateSelf();
} }
/** /**
@@ -455,10 +445,11 @@ class Ripple {
} }
} }
private Paint getTempPaint() { private Paint getTempPaint(Paint original) {
if (mTempPaint == null) { if (mTempPaint == null) {
mTempPaint = new Paint(); mTempPaint = new Paint();
} }
mTempPaint.set(original);
return mTempPaint; return mTempPaint;
} }
@@ -539,10 +530,7 @@ class Ripple {
} }
runningAnimations.clear(); runningAnimations.clear();
if (cancelPending && !mPendingAnimations.isEmpty()) { mHasPendingHardwareExit = false;
mPendingAnimations.clear();
}
mHardwareAnimating = false; mHardwareAnimating = false;
} }

View File

@@ -24,9 +24,7 @@ import android.graphics.Canvas;
import android.graphics.CanvasProperty; import android.graphics.CanvasProperty;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Xfermode;
import android.util.MathUtils; import android.util.MathUtils;
import android.view.HardwareCanvas; import android.view.HardwareCanvas;
import android.view.RenderNodeAnimator; import android.view.RenderNodeAnimator;
@@ -53,8 +51,6 @@ class RippleBackground {
// Hardware animators. // Hardware animators.
private final ArrayList<RenderNodeAnimator> mRunningAnimations = private final ArrayList<RenderNodeAnimator> mRunningAnimations =
new ArrayList<RenderNodeAnimator>(); new ArrayList<RenderNodeAnimator>();
private final ArrayList<RenderNodeAnimator> mPendingAnimations =
new ArrayList<RenderNodeAnimator>();
private final RippleDrawable mOwner; private final RippleDrawable mOwner;
@@ -64,8 +60,6 @@ class RippleBackground {
/** ARGB color for drawing this ripple. */ /** ARGB color for drawing this ripple. */
private int mColor; private int mColor;
private Xfermode mXfermode;
/** Maximum ripple radius. */ /** Maximum ripple radius. */
private float mOuterRadius; private float mOuterRadius;
@@ -98,6 +92,11 @@ class RippleBackground {
/** Whether we have an explicit maximum radius. */ /** Whether we have an explicit maximum radius. */
private boolean mHasMaxRadius; private boolean mHasMaxRadius;
private boolean mHasPendingHardwareExit;
private int mPendingOpacityDuration;
private int mPendingInflectionDuration;
private int mPendingInflectionOpacity;
/** /**
* Creates a new ripple. * Creates a new ripple.
*/ */
@@ -144,9 +143,7 @@ class RippleBackground {
* Draws the ripple centered at (0,0) using the specified paint. * Draws the ripple centered at (0,0) using the specified paint.
*/ */
public boolean draw(Canvas c, Paint p) { public boolean draw(Canvas c, Paint p) {
// Store the color and xfermode, we might need them later.
mColor = p.getColor(); mColor = p.getColor();
mXfermode = p.getXfermode();
final boolean canUseHardware = c.isHardwareAccelerated(); final boolean canUseHardware = c.isHardwareAccelerated();
if (mCanUseHardware != canUseHardware && mCanUseHardware) { if (mCanUseHardware != canUseHardware && mCanUseHardware) {
@@ -156,8 +153,8 @@ class RippleBackground {
mCanUseHardware = canUseHardware; mCanUseHardware = canUseHardware;
final boolean hasContent; final boolean hasContent;
if (canUseHardware && mHardwareAnimating) { if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
hasContent = drawHardware((HardwareCanvas) c); hasContent = drawHardware((HardwareCanvas) c, p);
} else { } else {
hasContent = drawSoftware(c, p); hasContent = drawSoftware(c, p);
} }
@@ -169,24 +166,10 @@ class RippleBackground {
return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0); return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0);
} }
private boolean drawHardware(HardwareCanvas c) { private boolean drawHardware(HardwareCanvas c, Paint p) {
// If we have any pending hardware animations, cancel any running if (mHasPendingHardwareExit) {
// animations and start those now.
final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
final int N = pendingAnimations.size();
if (N > 0) {
cancelHardwareAnimations(false); cancelHardwareAnimations(false);
startPendingHardwareExit(c, p);
// We canceled old animations, but we're about to run new ones.
mHardwareAnimating = true;
for (int i = 0; i < N; i++) {
pendingAnimations.get(i).setTarget(c);
pendingAnimations.get(i).start();
}
mRunningAnimations.addAll(pendingAnimations);
pendingAnimations.clear();
} }
c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
@@ -263,21 +246,32 @@ class RippleBackground {
+ inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
if (mCanUseHardware) { if (mCanUseHardware) {
exitHardware(opacityDuration, inflectionDuration, inflectionOpacity); createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity);
} else { } else {
exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
} }
} }
private void exitHardware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { private void createPendingHardwareExit(
mPendingAnimations.clear(); int opacityDuration, int inflectionDuration, int inflectionOpacity) {
mHasPendingHardwareExit = true;
mPendingOpacityDuration = opacityDuration;
mPendingInflectionDuration = inflectionDuration;
mPendingInflectionOpacity = inflectionOpacity;
final Paint outerPaint = getTempPaint(); // The animation will start on the next draw().
outerPaint.setAntiAlias(true); invalidateSelf();
outerPaint.setXfermode(mXfermode); }
outerPaint.setColor(mColor);
outerPaint.setAlpha((int) (Color.alpha(mColor) * mOuterOpacity + 0.5f)); private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
outerPaint.setStyle(Style.FILL); mHasPendingHardwareExit = false;
final int opacityDuration = mPendingOpacityDuration;
final int inflectionDuration = mPendingInflectionDuration;
final int inflectionOpacity = mPendingInflectionOpacity;
final Paint outerPaint = getTempPaint(p);
outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f));
mPropOuterPaint = CanvasProperty.createPaint(outerPaint); mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
mPropOuterX = CanvasProperty.createFloat(mOuterX); mPropOuterX = CanvasProperty.createFloat(mOuterX);
@@ -301,8 +295,10 @@ class RippleBackground {
outerFadeOutAnim.setStartDelay(inflectionDuration); outerFadeOutAnim.setStartDelay(inflectionDuration);
outerFadeOutAnim.setStartValue(inflectionOpacity); outerFadeOutAnim.setStartValue(inflectionOpacity);
outerFadeOutAnim.addListener(mAnimationListener); outerFadeOutAnim.addListener(mAnimationListener);
outerFadeOutAnim.setTarget(c);
outerFadeOutAnim.start();
mPendingAnimations.add(outerFadeOutAnim); mRunningAnimations.add(outerFadeOutAnim);
} else { } else {
outerOpacityAnim.addListener(mAnimationListener); outerOpacityAnim.addListener(mAnimationListener);
} }
@@ -314,14 +310,15 @@ class RippleBackground {
outerOpacityAnim.addListener(mAnimationListener); outerOpacityAnim.addListener(mAnimationListener);
} }
mPendingAnimations.add(outerOpacityAnim); outerOpacityAnim.setTarget(c);
outerOpacityAnim.start();
mRunningAnimations.add(outerOpacityAnim);
mHardwareAnimating = true; mHardwareAnimating = true;
// Set up the software values to match the hardware end values. // Set up the software values to match the hardware end values.
mOuterOpacity = 0; mOuterOpacity = 0;
invalidateSelf();
} }
/** /**
@@ -340,10 +337,11 @@ class RippleBackground {
} }
} }
private Paint getTempPaint() { private Paint getTempPaint(Paint original) {
if (mTempPaint == null) { if (mTempPaint == null) {
mTempPaint = new Paint(); mTempPaint = new Paint();
} }
mTempPaint.set(original);
return mTempPaint; return mTempPaint;
} }
@@ -422,10 +420,7 @@ class RippleBackground {
} }
runningAnimations.clear(); runningAnimations.clear();
if (cancelPending && !mPendingAnimations.isEmpty()) { mHasPendingHardwareExit = false;
mPendingAnimations.clear();
}
mHardwareAnimating = false; mHardwareAnimating = false;
} }

View File

@@ -27,15 +27,19 @@ import android.content.res.ColorStateList;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@@ -56,7 +60,7 @@ import java.util.Arrays;
* &ltripple android:color="#ffff0000"> * &ltripple android:color="#ffff0000">
* &ltitem android:id="@android:id/mask" * &ltitem android:id="@android:id/mask"
* android:drawable="@android:color/white" /> * android:drawable="@android:color/white" />
* &ltripple /></code> * &lt/ripple></code>
* </pre> * </pre>
* <p> * <p>
* If a mask layer is set, the ripple effect will be masked against that layer * If a mask layer is set, the ripple effect will be masked against that layer
@@ -65,15 +69,15 @@ import java.util.Arrays;
* If no mask layer is set, the ripple effect is masked against the composite * If no mask layer is set, the ripple effect is masked against the composite
* of the child layers. * of the child layers.
* <pre> * <pre>
* <code>&lt!-- A blue ripple drawn atop a black rectangle. --/> * <code>&lt!-- A green ripple drawn atop a black rectangle. --/>
* &ltripple android:color="#ff00ff00"> * &ltripple android:color="#ff00ff00">
* &ltitem android:drawable="@android:color/black" /> * &ltitem android:drawable="@android:color/black" />
* &ltripple /> * &lt/ripple>
* *
* &lt!-- A red ripple drawn atop a drawable resource. --/> * &lt!-- A blue ripple drawn atop a drawable resource. --/>
* &ltripple android:color="#ff00ff00"> * &ltripple android:color="#ff0000ff">
* &ltitem android:drawable="@drawable/my_drawable" /> * &ltitem android:drawable="@drawable/my_drawable" />
* &ltripple /></code> * &lt/ripple></code>
* </pre> * </pre>
* <p> * <p>
* If no child layers or mask is specified and the ripple is set as a View * If no child layers or mask is specified and the ripple is set as a View
@@ -81,16 +85,17 @@ import java.util.Arrays;
* background within the View's hierarchy. In this case, the drawing region * background within the View's hierarchy. In this case, the drawing region
* may extend outside of the Drawable bounds. * may extend outside of the Drawable bounds.
* <pre> * <pre>
* <code>&lt!-- An unbounded green ripple. --/> * <code>&lt!-- An unbounded red ripple. --/>
* &ltripple android:color="#ff0000ff" /></code> * &ltripple android:color="#ffff0000" /></code>
* </pre> * </pre>
* *
* @attr ref android.R.styleable#RippleDrawable_color * @attr ref android.R.styleable#RippleDrawable_color
*/ */
public class RippleDrawable extends LayerDrawable { public class RippleDrawable extends LayerDrawable {
private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); private static final int MASK_UNKNOWN = -1;
private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); private static final int MASK_NONE = 0;
private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER); private static final int MASK_CONTENT = 1;
private static final int MASK_EXPLICIT = 2;
/** /**
* Constant for automatically determining the maximum ripple radius. * Constant for automatically determining the maximum ripple radius.
@@ -123,6 +128,13 @@ public class RippleDrawable extends LayerDrawable {
/** The current background. May be actively animating or pending entry. */ /** The current background. May be actively animating or pending entry. */
private RippleBackground mBackground; private RippleBackground mBackground;
private Bitmap mMaskBuffer;
private BitmapShader mMaskShader;
private Canvas mMaskCanvas;
private Matrix mMaskMatrix;
private PorterDuffColorFilter mMaskColorFilter;
private boolean mHasValidMask;
/** Whether we expect to draw a background when visible. */ /** Whether we expect to draw a background when visible. */
private boolean mBackgroundActive; private boolean mBackgroundActive;
@@ -147,9 +159,6 @@ public class RippleDrawable extends LayerDrawable {
/** Paint used to control appearance of ripples. */ /** Paint used to control appearance of ripples. */
private Paint mRipplePaint; private Paint mRipplePaint;
/** Paint used to control reveal layer masking. */
private Paint mMaskingPaint;
/** Target density of the display into which ripples are drawn. */ /** Target density of the display into which ripples are drawn. */
private float mDensity = 1.0f; private float mDensity = 1.0f;
@@ -615,39 +624,118 @@ public class RippleDrawable extends LayerDrawable {
*/ */
@Override @Override
public void draw(@NonNull Canvas canvas) { public void draw(@NonNull Canvas canvas) {
final boolean hasMask = mMask != null;
final boolean hasRipples = mRipple != null || mExitingRipplesCount > 0
|| (mBackground != null && mBackground.shouldDraw());
// Clip to the dirty bounds, which will be the drawable bounds if we // Clip to the dirty bounds, which will be the drawable bounds if we
// have a mask or content and the ripple bounds if we're projecting. // have a mask or content and the ripple bounds if we're projecting.
final Rect bounds = getDirtyBounds(); final Rect bounds = getDirtyBounds();
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(bounds); canvas.clipRect(bounds);
// If we have content, draw it first. If we have ripples and no mask, drawContent(canvas);
// we'll draw it into a SRC_OVER layer so that we can mask ripples drawBackgroundAndRipples(canvas);
// against it using SRC_IN.
final boolean hasContentLayer = drawContent(canvas, bounds, hasRipples, hasMask);
// Next, try to draw the ripples. If we have a non-opaque mask, we'll
// draw the ripples into a SRC_OVER layer, draw the mask into a DST_IN
// layer, and blend.
if (hasRipples) {
final boolean hasNonOpaqueMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE;
final boolean hasRippleLayer = drawBackgroundAndRipples(canvas, bounds,
hasNonOpaqueMask, hasContentLayer);
// If drawing ripples created a layer, we have a non-opaque mask
// that needs to be blended on top of the ripples with DST_IN.
if (hasRippleLayer) {
drawMaskingLayer(canvas, bounds, DST_IN);
}
}
canvas.restoreToCount(saveCount); canvas.restoreToCount(saveCount);
} }
@Override
public void invalidateSelf() {
super.invalidateSelf();
// Force the mask to update on the next draw().
mHasValidMask = false;
}
/**
* @return whether we need to use a mask
*/
private void updateMaskShaderIfNeeded() {
if (mHasValidMask) {
return;
}
final int maskType = getMaskType();
if (maskType == MASK_UNKNOWN) {
return;
}
mHasValidMask = true;
if (maskType == MASK_NONE) {
if (mMaskBuffer != null) {
mMaskBuffer.recycle();
mMaskBuffer = null;
mMaskShader = null;
mMaskCanvas = null;
}
mMaskMatrix = null;
mMaskColorFilter = null;
return;
}
// Ensure we have a correctly-sized buffer.
final Rect bounds = getBounds();
if (mMaskBuffer == null
|| mMaskBuffer.getWidth() != bounds.width()
|| mMaskBuffer.getHeight() != bounds.height()) {
if (mMaskBuffer != null) {
mMaskBuffer.recycle();
}
mMaskBuffer = Bitmap.createBitmap(
bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
mMaskShader = new BitmapShader(mMaskBuffer,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mMaskCanvas = new Canvas(mMaskBuffer);
} else {
mMaskBuffer.eraseColor(Color.TRANSPARENT);
}
if (mMaskMatrix == null) {
mMaskMatrix = new Matrix();
} else {
mMaskMatrix.reset();
}
if (mMaskColorFilter == null) {
mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
}
// Draw the appropriate mask.
if (maskType == MASK_EXPLICIT) {
drawMask(mMaskCanvas);
} else if (maskType == MASK_CONTENT) {
drawContent(mMaskCanvas);
}
}
private int getMaskType() {
if (mRipple == null && mExitingRipplesCount <= 0
&& (mBackground == null || !mBackground.shouldDraw())) {
// We might need a mask later.
return MASK_UNKNOWN;
}
if (mMask != null) {
if (mMask.getOpacity() == PixelFormat.OPAQUE) {
// Clipping handles opaque explicit masks.
return MASK_NONE;
} else {
return MASK_EXPLICIT;
}
}
// Check for non-opaque, non-mask content.
final ChildDrawable[] array = mLayerState.mChildren;
final int count = mLayerState.mNum;
for (int i = 0; i < count; i++) {
if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
return MASK_CONTENT;
}
}
// Clipping handles opaque content.
return MASK_NONE;
}
/** /**
* Removes a ripple from the exiting ripple list. * Removes a ripple from the exiting ripple list.
* *
@@ -678,65 +766,65 @@ public class RippleDrawable extends LayerDrawable {
return -1; return -1;
} }
private boolean drawContent(Canvas canvas, Rect bounds, boolean hasRipples, boolean hasMask) { private void drawContent(Canvas canvas) {
// Draw everything except the mask.
final ChildDrawable[] array = mLayerState.mChildren; final ChildDrawable[] array = mLayerState.mChildren;
final int count = mLayerState.mNum; final int count = mLayerState.mNum;
boolean needsLayer = false;
if (hasRipples && !hasMask) {
// If we only have opaque content, we don't really need a layer
// because the ripples will be clipped to the drawable bounds.
for (int i = 0; i < count; i++) {
if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
needsLayer = true;
break;
}
}
}
if (needsLayer) {
canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom,
getMaskingPaint(SRC_OVER));
}
// Draw everything except the mask.
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask) { if (array[i].mId != R.id.mask) {
array[i].mDrawable.draw(canvas); array[i].mDrawable.draw(canvas);
} }
} }
return needsLayer;
} }
private boolean drawBackgroundAndRipples( private void drawBackgroundAndRipples(Canvas canvas) {
Canvas canvas, Rect bounds, boolean hasNonOpaqueMask, boolean hasContentLayer) { final Ripple active = mRipple;
if (hasNonOpaqueMask) { final RippleBackground background = mBackground;
final Paint p = getMaskingPaint(SRC_OVER); final int count = mExitingRipplesCount;
canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, p); if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
// Move along, nothing to draw here.
return;
} }
final PorterDuffXfermode mode = hasContentLayer ? SRC_ATOP : SRC_OVER;
final float x = mHotspotBounds.exactCenterX(); final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY(); final float y = mHotspotBounds.exactCenterY();
canvas.translate(x, y); canvas.translate(x, y);
final Paint p = getRipplePaint(); updateMaskShaderIfNeeded();
p.setXfermode(mode);
// Position the shader to account for canvas translation.
if (mMaskShader != null) {
mMaskMatrix.setTranslate(-x, -y);
mMaskShader.setLocalMatrix(mMaskMatrix);
}
// Grab the color for the current state and cut the alpha channel in // Grab the color for the current state and cut the alpha channel in
// half so that the ripple and background together yield full alpha. // half so that the ripple and background together yield full alpha.
final int color = mState.mColor.getColorForState(getState(), Color.BLACK); final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
final int alpha = (Color.alpha(color) / 2) << 24; final int halfAlpha = (Color.alpha(color) / 2) << 24;
p.setColor(color & 0xFFFFFF | alpha); final Paint p = getRipplePaint();
if (mMaskColorFilter != null) {
// The ripple timing depends on the paint's alpha value, so we need
// to push just the alpha channel into the paint and let the filter
// handle the full-alpha color.
final int fullAlphaColor = color | (0xFF << 24);
mMaskColorFilter.setColor(fullAlphaColor);
p.setColor(halfAlpha);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
p.setColor(halfAlphaColor);
p.setColorFilter(null);
p.setShader(null);
}
final RippleBackground background = mBackground;
if (background != null && background.shouldDraw()) { if (background != null && background.shouldDraw()) {
background.draw(canvas, p); background.draw(canvas, p);
} }
final int count = mExitingRipplesCount;
if (count > 0) { if (count > 0) {
final Ripple[] ripples = mExitingRipples; final Ripple[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -744,27 +832,15 @@ public class RippleDrawable extends LayerDrawable {
} }
} }
final Ripple active = mRipple;
if (active != null) { if (active != null) {
active.draw(canvas, p); active.draw(canvas, p);
} }
canvas.translate(-x, -y); canvas.translate(-x, -y);
// Returns true if a layer was created.
return hasNonOpaqueMask;
} }
private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { private void drawMask(Canvas canvas) {
final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
bounds.right, bounds.bottom, getMaskingPaint(mode));
// Ensure that DST_IN blends using the entire layer.
canvas.drawColor(Color.TRANSPARENT);
mMask.draw(canvas); mMask.draw(canvas);
return restoreToCount;
} }
private Paint getRipplePaint() { private Paint getRipplePaint() {
@@ -776,15 +852,6 @@ public class RippleDrawable extends LayerDrawable {
return mRipplePaint; return mRipplePaint;
} }
private Paint getMaskingPaint(PorterDuffXfermode xfermode) {
if (mMaskingPaint == null) {
mMaskingPaint = new Paint();
}
mMaskingPaint.setXfermode(xfermode);
mMaskingPaint.setAlpha(0xFF);
return mMaskingPaint;
}
@Override @Override
public Rect getDirtyBounds() { public Rect getDirtyBounds() {
if (isProjected()) { if (isProjected()) {