FileDocCategorySizeDatePackage
RippleDrawable.javaAPI DocAndroid 5.1 API30328Thu Mar 12 22:22:30 GMT 2015android.graphics.drawable

RippleDrawable

public class RippleDrawable extends LayerDrawable
Drawable that shows a ripple effect in response to state changes. The anchoring position of the ripple for a given state may be specified by calling {@link #setHotspot(float, float)} with the corresponding state attribute identifier.

A touch feedback drawable may contain multiple child layers, including a special mask layer that is not drawn to the screen. A single layer may be set as the mask by specifying its android:id value as {@link android.R.id#mask}.

<!-- A red ripple masked against an opaque rectangle. --/>
<ripple android:color="#ffff0000">
<item android:id="@android:id/mask"
android:drawable="@android:color/white" />
</ripple>

If a mask layer is set, the ripple effect will be masked against that layer before it is drawn over the composite of the remaining child layers.

If no mask layer is set, the ripple effect is masked against the composite of the child layers.

<!-- A green ripple drawn atop a black rectangle. --/>
<ripple android:color="#ff00ff00">
<item android:drawable="@android:color/black" />
</ripple>

<!-- A blue ripple drawn atop a drawable resource. --/>
<ripple android:color="#ff0000ff">
<item android:drawable="@drawable/my_drawable" />
</ripple>

If no child layers or mask is specified and the ripple is set as a View background, the ripple will be drawn atop the first available parent background within the View's hierarchy. In this case, the drawing region may extend outside of the Drawable bounds.

<!-- An unbounded red ripple. --/>
<ripple android:color="#ffff0000" />
attr
ref android.R.styleable#RippleDrawable_color

Fields Summary
private static final int
MASK_UNKNOWN
private static final int
MASK_NONE
private static final int
MASK_CONTENT
private static final int
MASK_EXPLICIT
public static final int
RADIUS_AUTO
Constant for automatically determining the maximum ripple radius.
private static final int
MAX_RIPPLES
The maximum number of ripples supported.
private final android.graphics.Rect
mTempRect
private final android.graphics.Rect
mHotspotBounds
Current ripple effect bounds, used to constrain ripple effects.
private final android.graphics.Rect
mDrawingBounds
Current drawing bounds, used to compute dirty region.
private final android.graphics.Rect
mDirtyBounds
Current dirty bounds, union of current and previous drawing bounds.
private RippleState
mState
Mirrors mLayerState with some extra information.
private Drawable
mMask
The masking layer, e.g. the layer with id R.id.mask.
private RippleBackground
mBackground
The current background. May be actively animating or pending entry.
private android.graphics.Bitmap
mMaskBuffer
private android.graphics.BitmapShader
mMaskShader
private android.graphics.Canvas
mMaskCanvas
private android.graphics.Matrix
mMaskMatrix
private android.graphics.PorterDuffColorFilter
mMaskColorFilter
private boolean
mHasValidMask
private boolean
mBackgroundActive
Whether we expect to draw a background when visible.
private Ripple
mRipple
The current ripple. May be actively animating or pending entry.
private boolean
mRippleActive
Whether we expect to draw a ripple when visible.
private float
mPendingX
private float
mPendingY
private boolean
mHasPending
private Ripple[]
mExitingRipples
Lazily-created array of actively animating ripples. Inactive ripples are pruned during draw(). The locations of these will not change.
private int
mExitingRipplesCount
private android.graphics.Paint
mRipplePaint
Paint used to control appearance of ripples.
private float
mDensity
Target density of the display into which ripples are drawn.
private boolean
mOverrideBounds
Whether bounds are being overridden.
Constructors Summary
RippleDrawable()
Constructor used for drawable inflation.


              
     
        this(new RippleState(null, null, null), null);
    
public RippleDrawable(android.content.res.ColorStateList color, Drawable content, Drawable mask)
Creates a new ripple drawable with the specified ripple color and optional content and mask drawables.

param
color The ripple color
param
content The content drawable, may be {@code null}
param
mask The mask drawable, may be {@code null}

        this(new RippleState(null, null, null), null);

        if (color == null) {
            throw new IllegalArgumentException("RippleDrawable requires a non-null color");
        }

        if (content != null) {
            addLayer(content, null, 0, 0, 0, 0, 0);
        }

        if (mask != null) {
            addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
        }

        setColor(color);
        ensurePadding();
        initializeFromState();
    
private RippleDrawable(RippleState state, android.content.res.Resources res)

        mState = new RippleState(state, this, res);
        mLayerState = mState;

        if (mState.mNum > 0) {
            ensurePadding();
        }

        if (res != null) {
            mDensity = res.getDisplayMetrics().density;
        }

        initializeFromState();
    
Methods Summary
public voidapplyTheme(android.content.res.Resources.Theme t)

        super.applyTheme(t);

        final RippleState state = mState;
        if (state == null || state.mTouchThemeAttrs == null) {
            return;
        }

        final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
                R.styleable.RippleDrawable);
        try {
            updateStateFromTypedArray(a);
        } catch (XmlPullParserException e) {
            throw new RuntimeException(e);
        } finally {
            a.recycle();
        }

        initializeFromState();
    
public booleancanApplyTheme()

        return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
    
private booleancancelExitingRipples()

        boolean needsDraw = false;

        final int count = mExitingRipplesCount;
        final Ripple[] ripples = mExitingRipples;
        for (int i = 0; i < count; i++) {
            needsDraw |= ripples[i].isHardwareAnimating();
            ripples[i].cancel();
        }

        if (ripples != null) {
            Arrays.fill(ripples, 0, count, null);
        }
        mExitingRipplesCount = 0;

        return needsDraw;
    
private voidclearHotspots()
Cancels and removes the active ripple, all exiting ripples, and the background. Nothing will be drawn after this method is called.

        if (mRipple != null) {
            mRipple.cancel();
            mRipple = null;
            mRippleActive = false;
        }

        if (mBackground != null) {
            mBackground.cancel();
            mBackground = null;
            mBackgroundActive = false;
        }

        cancelExitingRipples();
        invalidateSelf();
    
android.graphics.drawable.RippleDrawable$RippleStatecreateConstantState(LayerState state, android.content.res.Resources res)

        return new RippleState(state, this, res);
    
public voiddraw(android.graphics.Canvas canvas)
Optimized for drawing ripples with a mask layer and optional content.

        // 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.
        final Rect bounds = getDirtyBounds();
        final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(bounds);

        drawContent(canvas);
        drawBackgroundAndRipples(canvas);

        canvas.restoreToCount(saveCount);
    
private voiddrawBackgroundAndRipples(android.graphics.Canvas canvas)

        final Ripple active = mRipple;
        final RippleBackground background = mBackground;
        final int count = mExitingRipplesCount;
        if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
            // Move along, nothing to draw here.
            return;
        }

        final float x = mHotspotBounds.exactCenterX();
        final float y = mHotspotBounds.exactCenterY();
        canvas.translate(x, y);

        updateMaskShaderIfNeeded();

        // 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
        // half so that the ripple and background together yield full alpha.
        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
        final int halfAlpha = (Color.alpha(color) / 2) << 24;
        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);
        }

        if (background != null && background.shouldDraw()) {
            background.draw(canvas, p);
        }

        if (count > 0) {
            final Ripple[] ripples = mExitingRipples;
            for (int i = 0; i < count; i++) {
                ripples[i].draw(canvas, p);
            }
        }

        if (active != null) {
            active.draw(canvas, p);
        }

        canvas.translate(-x, -y);
    
private voiddrawContent(android.graphics.Canvas canvas)

        // Draw everything except the mask.
        final ChildDrawable[] array = mLayerState.mChildren;
        final int count = mLayerState.mNum;
        for (int i = 0; i < count; i++) {
            if (array[i].mId != R.id.mask) {
                array[i].mDrawable.draw(canvas);
            }
        }
    
private voiddrawMask(android.graphics.Canvas canvas)

        mMask.draw(canvas);
    
public ConstantStategetConstantState()

        return mState;
    
public android.graphics.RectgetDirtyBounds()

        if (isProjected()) {
            final Rect drawingBounds = mDrawingBounds;
            final Rect dirtyBounds = mDirtyBounds;
            dirtyBounds.set(drawingBounds);
            drawingBounds.setEmpty();

            final int cX = (int) mHotspotBounds.exactCenterX();
            final int cY = (int) mHotspotBounds.exactCenterY();
            final Rect rippleBounds = mTempRect;

            final Ripple[] activeRipples = mExitingRipples;
            final int N = mExitingRipplesCount;
            for (int i = 0; i < N; i++) {
                activeRipples[i].getBounds(rippleBounds);
                rippleBounds.offset(cX, cY);
                drawingBounds.union(rippleBounds);
            }

            final RippleBackground background = mBackground;
            if (background != null) {
                background.getBounds(rippleBounds);
                rippleBounds.offset(cX, cY);
                drawingBounds.union(rippleBounds);
            }

            dirtyBounds.union(drawingBounds);
            dirtyBounds.union(super.getDirtyBounds());
            return dirtyBounds;
        } else {
            return getBounds();
        }
    
public voidgetHotspotBounds(android.graphics.Rect outRect)

hide

        outRect.set(mHotspotBounds);
    
private intgetMaskType()

        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;
    
public intgetMaxRadius()

return
the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if the radius is determined automatically
see
#setMaxRadius(int)
hide

        return mState.mMaxRadius;
    
public intgetOpacity()

        // Worst-case scenario.
        return PixelFormat.TRANSLUCENT;
    
public voidgetOutline(android.graphics.Outline outline)
Populates outline with the first available layer outline, excluding the mask layer.

param
outline Outline in which to place the first available layer outline

        final LayerState state = mLayerState;
        final ChildDrawable[] children = state.mChildren;
        final int N = state.mNum;
        for (int i = 0; i < N; i++) {
            if (children[i].mId != R.id.mask) {
                children[i].mDrawable.getOutline(outline);
                if (!outline.isEmpty()) return;
            }
        }
    
private intgetRippleIndex(Ripple ripple)

        final Ripple[] ripples = mExitingRipples;
        final int count = mExitingRipplesCount;
        for (int i = 0; i < count; i++) {
            if (ripples[i] == ripple) {
                return i;
            }
        }
        return -1;
    
private android.graphics.PaintgetRipplePaint()

        if (mRipplePaint == null) {
            mRipplePaint = new Paint();
            mRipplePaint.setAntiAlias(true);
            mRipplePaint.setStyle(Paint.Style.FILL);
        }
        return mRipplePaint;
    
public voidinflate(android.content.res.Resources r, org.xmlpull.v1.XmlPullParser parser, android.util.AttributeSet attrs, android.content.res.Resources.Theme theme)

        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
        updateStateFromTypedArray(a);
        a.recycle();

        // Force padding default to STACK before inflating.
        setPaddingMode(PADDING_MODE_STACK);

        super.inflate(r, parser, attrs, theme);

        setTargetDensity(r.getDisplayMetrics());
        initializeFromState();
    
private voidinitializeFromState()

        // Initialize from constant state.
        mMask = findDrawableByLayerId(R.id.mask);
    
public voidinvalidateSelf()

        super.invalidateSelf();

        // Force the mask to update on the next draw().
        mHasValidMask = false;
    
public booleanisProjected()

hide

        return getNumberOfLayers() == 0;
    
public booleanisStateful()

        return true;
    
public voidjumpToCurrentState()

        super.jumpToCurrentState();

        if (mRipple != null) {
            mRipple.jump();
        }

        if (mBackground != null) {
            mBackground.jump();
        }

        cancelExitingRipples();
        invalidateSelf();
    
public Drawablemutate()

        super.mutate();

        // LayerDrawable creates a new state using createConstantState, so
        // this should always be a safe cast.
        mState = (RippleState) mLayerState;

        // The locally cached drawable may have changed.
        mMask = findDrawableByLayerId(R.id.mask);

        return this;
    
protected voidonBoundsChange(android.graphics.Rect bounds)

        super.onBoundsChange(bounds);

        if (!mOverrideBounds) {
            mHotspotBounds.set(bounds);
            onHotspotBoundsChanged();
        }

        invalidateSelf();
    
private voidonHotspotBoundsChanged()
Notifies all the animating ripples that the hotspot bounds have changed.

        final int count = mExitingRipplesCount;
        final Ripple[] ripples = mExitingRipples;
        for (int i = 0; i < count; i++) {
            ripples[i].onHotspotBoundsChanged();
        }

        if (mRipple != null) {
            mRipple.onHotspotBoundsChanged();
        }

        if (mBackground != null) {
            mBackground.onHotspotBoundsChanged();
        }
    
protected booleanonStateChange(int[] stateSet)

        final boolean changed = super.onStateChange(stateSet);

        boolean enabled = false;
        boolean pressed = false;
        boolean focused = false;

        for (int state : stateSet) {
            if (state == R.attr.state_enabled) {
                enabled = true;
            }
            if (state == R.attr.state_focused) {
                focused = true;
            }
            if (state == R.attr.state_pressed) {
                pressed = true;
            }
        }

        setRippleActive(enabled && pressed);
        setBackgroundActive(focused || (enabled && pressed), focused);

        return changed;
    
voidremoveRipple(Ripple ripple)
Removes a ripple from the exiting ripple list.

param
ripple the ripple to remove

        // Ripple ripple ripple ripple. Ripple ripple.
        final Ripple[] ripples = mExitingRipples;
        final int count = mExitingRipplesCount;
        final int index = getRippleIndex(ripple);
        if (index >= 0) {
            System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
            ripples[count - 1] = null;
            mExitingRipplesCount--;

            invalidateSelf();
        }
    
public voidsetAlpha(int alpha)

        super.setAlpha(alpha);

        // TODO: Should we support this?
    
private voidsetBackgroundActive(boolean active, boolean focused)

        if (mBackgroundActive != active) {
            mBackgroundActive = active;
            if (active) {
                tryBackgroundEnter(focused);
            } else {
                tryBackgroundExit();
            }
        }
    
public voidsetColor(android.content.res.ColorStateList color)

        mState.mColor = color;
        invalidateSelf();
    
public voidsetColorFilter(android.graphics.ColorFilter cf)

        super.setColorFilter(cf);

        // TODO: Should we support this?
    
public booleansetDrawableByLayerId(int id, Drawable drawable)

        if (super.setDrawableByLayerId(id, drawable)) {
            if (id == R.id.mask) {
                mMask = drawable;
            }

            return true;
        }

        return false;
    
public voidsetHotspot(float x, float y)

        if (mRipple == null || mBackground == null) {
            mPendingX = x;
            mPendingY = y;
            mHasPending = true;
        }

        if (mRipple != null) {
            mRipple.move(x, y);
        }
    
public voidsetHotspotBounds(int left, int top, int right, int bottom)

        mOverrideBounds = true;
        mHotspotBounds.set(left, top, right, bottom);

        onHotspotBoundsChanged();
    
public voidsetMaxRadius(int maxRadius)
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

        if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
            throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
        }

        mState.mMaxRadius = maxRadius;
    
public voidsetPaddingMode(int mode)
Specifies how layer padding should affect the bounds of subsequent layers. The default and recommended value for RippleDrawable is {@link #PADDING_MODE_STACK}.

param
mode padding mode, one of:
  • {@link #PADDING_MODE_NEST} to nest each layer inside the padding of the previous layer
  • {@link #PADDING_MODE_STACK} to stack each layer directly atop the previous layer
see
#getPaddingMode()

        super.setPaddingMode(mode);
    
private voidsetRippleActive(boolean active)

        if (mRippleActive != active) {
            mRippleActive = active;
            if (active) {
                tryRippleEnter();
            } else {
                tryRippleExit();
            }
        }
    
private voidsetTargetDensity(android.util.DisplayMetrics metrics)
Set the density at which this drawable will be rendered.

param
metrics The display metrics for this drawable.

        if (mDensity != metrics.density) {
            mDensity = metrics.density;
            invalidateSelf();
        }
    
public booleansetVisible(boolean visible, boolean restart)

        final boolean changed = super.setVisible(visible, restart);

        if (!visible) {
            clearHotspots();
        } else if (changed) {
            // If we just became visible, ensure the background and ripple
            // visibilities are consistent with their internal states.
            if (mRippleActive) {
                tryRippleEnter();
            }

            if (mBackgroundActive) {
                tryBackgroundEnter(false);
            }

            // Skip animations, just show the correct final states.
            jumpToCurrentState();
        }

        return changed;
    
private voidtryBackgroundEnter(boolean focused)
Creates an active hotspot at the specified location.

        if (mBackground == null) {
            mBackground = new RippleBackground(this, mHotspotBounds);
        }

        mBackground.setup(mState.mMaxRadius, mDensity);
        mBackground.enter(focused);
    
private voidtryBackgroundExit()

        if (mBackground != null) {
            // Don't null out the background, we need it to draw!
            mBackground.exit();
        }
    
private voidtryRippleEnter()
Attempts to start an enter animation for the active hotspot. Fails if there are too many animating ripples.

        if (mExitingRipplesCount >= 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.
            return;
        }

        if (mRipple == null) {
            final float x;
            final float y;
            if (mHasPending) {
                mHasPending = false;
                x = mPendingX;
                y = mPendingY;
            } else {
                x = mHotspotBounds.exactCenterX();
                y = mHotspotBounds.exactCenterY();
            }
            mRipple = new Ripple(this, mHotspotBounds, x, y);
        }

        mRipple.setup(mState.mMaxRadius, mDensity);
        mRipple.enter();
    
private voidtryRippleExit()
Attempts to start an exit animation for the active hotspot. Fails if there is no active hotspot.

        if (mRipple != null) {
            if (mExitingRipples == null) {
                mExitingRipples = new Ripple[MAX_RIPPLES];
            }
            mExitingRipples[mExitingRipplesCount++] = mRipple;
            mRipple.exit();
            mRipple = null;
        }
    
private voidupdateMaskShaderIfNeeded()

return
whether we need to use a mask

        if (mHasValidMask) {
            return;
        }

        final int maskType = getMaskType();
        if (maskType == MASK_UNKNOWN) {
            return;
        }

        mHasValidMask = true;

        final Rect bounds = getBounds();
        if (maskType == MASK_NONE || bounds.isEmpty()) {
            if (mMaskBuffer != null) {
                mMaskBuffer.recycle();
                mMaskBuffer = null;
                mMaskShader = null;
                mMaskCanvas = null;
            }
            mMaskMatrix = null;
            mMaskColorFilter = null;
            return;
        }

        // Ensure we have a correctly-sized buffer.
        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 voidupdateStateFromTypedArray(android.content.res.TypedArray a)
Initializes the constant state from the values in the typed array.

        final RippleState state = mState;

        // Account for any configuration changes.
        state.mChangingConfigurations |= a.getChangingConfigurations();

        // Extract the theme attributes, if any.
        state.mTouchThemeAttrs = a.extractThemeAttrs();

        final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
        if (color != null) {
            mState.mColor = color;
        }

        verifyRequiredAttributes(a);
    
private voidverifyRequiredAttributes(android.content.res.TypedArray a)

        if (mState.mColor == null && (mState.mTouchThemeAttrs == null
                || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
            throw new XmlPullParserException(a.getPositionDescription() +
                    ": <ripple> requires a valid color attribute");
        }