Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.graphics.drawable;
     18 
     19 import com.android.internal.R;
     20 
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 import android.annotation.NonNull;
     25 import android.annotation.Nullable;
     26 import android.content.res.ColorStateList;
     27 import android.content.res.Resources;
     28 import android.content.res.Resources.Theme;
     29 import android.content.res.TypedArray;
     30 import android.graphics.Bitmap;
     31 import android.graphics.BitmapShader;
     32 import android.graphics.Canvas;
     33 import android.graphics.Color;
     34 import android.graphics.ColorFilter;
     35 import android.graphics.Matrix;
     36 import android.graphics.Outline;
     37 import android.graphics.Paint;
     38 import android.graphics.PixelFormat;
     39 import android.graphics.PorterDuff;
     40 import android.graphics.PorterDuffColorFilter;
     41 import android.graphics.Rect;
     42 import android.graphics.Shader;
     43 import android.util.AttributeSet;
     44 import android.util.DisplayMetrics;
     45 
     46 import java.io.IOException;
     47 import java.util.Arrays;
     48 
     49 /**
     50  * Drawable that shows a ripple effect in response to state changes. The
     51  * anchoring position of the ripple for a given state may be specified by
     52  * calling {@link #setHotspot(float, float)} with the corresponding state
     53  * attribute identifier.
     54  * <p>
     55  * A touch feedback drawable may contain multiple child layers, including a
     56  * special mask layer that is not drawn to the screen. A single layer may be set
     57  * as the mask by specifying its android:id value as {@link android.R.id#mask}.
     58  * <pre>
     59  * <code>&lt!-- A red ripple masked against an opaque rectangle. --/>
     60  * &ltripple android:color="#ffff0000">
     61  *   &ltitem android:id="@android:id/mask"
     62  *         android:drawable="@android:color/white" />
     63  * &lt/ripple></code>
     64  * </pre>
     65  * <p>
     66  * If a mask layer is set, the ripple effect will be masked against that layer
     67  * before it is drawn over the composite of the remaining child layers.
     68  * <p>
     69  * If no mask layer is set, the ripple effect is masked against the composite
     70  * of the child layers.
     71  * <pre>
     72  * <code>&lt!-- A green ripple drawn atop a black rectangle. --/>
     73  * &ltripple android:color="#ff00ff00">
     74  *   &ltitem android:drawable="@android:color/black" />
     75  * &lt/ripple>
     76  *
     77  * &lt!-- A blue ripple drawn atop a drawable resource. --/>
     78  * &ltripple android:color="#ff0000ff">
     79  *   &ltitem android:drawable="@drawable/my_drawable" />
     80  * &lt/ripple></code>
     81  * </pre>
     82  * <p>
     83  * If no child layers or mask is specified and the ripple is set as a View
     84  * background, the ripple will be drawn atop the first available parent
     85  * background within the View's hierarchy. In this case, the drawing region
     86  * may extend outside of the Drawable bounds.
     87  * <pre>
     88  * <code>&lt!-- An unbounded red ripple. --/>
     89  * &ltripple android:color="#ffff0000" /></code>
     90  * </pre>
     91  *
     92  * @attr ref android.R.styleable#RippleDrawable_color
     93  */
     94 public class RippleDrawable extends LayerDrawable {
     95     private static final int MASK_UNKNOWN = -1;
     96     private static final int MASK_NONE = 0;
     97     private static final int MASK_CONTENT = 1;
     98     private static final int MASK_EXPLICIT = 2;
     99 
    100     /**
    101      * Constant for automatically determining the maximum ripple radius.
    102      *
    103      * @see #setMaxRadius(int)
    104      * @hide
    105      */
    106     public static final int RADIUS_AUTO = -1;
    107 
    108     /** The maximum number of ripples supported. */
    109     private static final int MAX_RIPPLES = 10;
    110 
    111     private final Rect mTempRect = new Rect();
    112 
    113     /** Current ripple effect bounds, used to constrain ripple effects. */
    114     private final Rect mHotspotBounds = new Rect();
    115 
    116     /** Current drawing bounds, used to compute dirty region. */
    117     private final Rect mDrawingBounds = new Rect();
    118 
    119     /** Current dirty bounds, union of current and previous drawing bounds. */
    120     private final Rect mDirtyBounds = new Rect();
    121 
    122     /** Mirrors mLayerState with some extra information. */
    123     private RippleState mState;
    124 
    125     /** The masking layer, e.g. the layer with id R.id.mask. */
    126     private Drawable mMask;
    127 
    128     /** The current background. May be actively animating or pending entry. */
    129     private RippleBackground mBackground;
    130 
    131     private Bitmap mMaskBuffer;
    132     private BitmapShader mMaskShader;
    133     private Canvas mMaskCanvas;
    134     private Matrix mMaskMatrix;
    135     private PorterDuffColorFilter mMaskColorFilter;
    136     private boolean mHasValidMask;
    137 
    138     /** Whether we expect to draw a background when visible. */
    139     private boolean mBackgroundActive;
    140 
    141     /** The current ripple. May be actively animating or pending entry. */
    142     private Ripple mRipple;
    143 
    144     /** Whether we expect to draw a ripple when visible. */
    145     private boolean mRippleActive;
    146 
    147     // Hotspot coordinates that are awaiting activation.
    148     private float mPendingX;
    149     private float mPendingY;
    150     private boolean mHasPending;
    151 
    152     /**
    153      * Lazily-created array of actively animating ripples. Inactive ripples are
    154      * pruned during draw(). The locations of these will not change.
    155      */
    156     private Ripple[] mExitingRipples;
    157     private int mExitingRipplesCount = 0;
    158 
    159     /** Paint used to control appearance of ripples. */
    160     private Paint mRipplePaint;
    161 
    162     /** Target density of the display into which ripples are drawn. */
    163     private float mDensity = 1.0f;
    164 
    165     /** Whether bounds are being overridden. */
    166     private boolean mOverrideBounds;
    167 
    168     /**
    169      * Constructor used for drawable inflation.
    170      */
    171     RippleDrawable() {
    172         this(new RippleState(null, null, null), null);
    173     }
    174 
    175     /**
    176      * Creates a new ripple drawable with the specified ripple color and
    177      * optional content and mask drawables.
    178      *
    179      * @param color The ripple color
    180      * @param content The content drawable, may be {@code null}
    181      * @param mask The mask drawable, may be {@code null}
    182      */
    183     public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
    184             @Nullable Drawable mask) {
    185         this(new RippleState(null, null, null), null);
    186 
    187         if (color == null) {
    188             throw new IllegalArgumentException("RippleDrawable requires a non-null color");
    189         }
    190 
    191         if (content != null) {
    192             addLayer(content, null, 0, 0, 0, 0, 0);
    193         }
    194 
    195         if (mask != null) {
    196             addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
    197         }
    198 
    199         setColor(color);
    200         ensurePadding();
    201         initializeFromState();
    202     }
    203 
    204     @Override
    205     public void jumpToCurrentState() {
    206         super.jumpToCurrentState();
    207 
    208         if (mRipple != null) {
    209             mRipple.jump();
    210         }
    211 
    212         if (mBackground != null) {
    213             mBackground.jump();
    214         }
    215 
    216         cancelExitingRipples();
    217         invalidateSelf();
    218     }
    219 
    220     private boolean cancelExitingRipples() {
    221         boolean needsDraw = false;
    222 
    223         final int count = mExitingRipplesCount;
    224         final Ripple[] ripples = mExitingRipples;
    225         for (int i = 0; i < count; i++) {
    226             needsDraw |= ripples[i].isHardwareAnimating();
    227             ripples[i].cancel();
    228         }
    229 
    230         if (ripples != null) {
    231             Arrays.fill(ripples, 0, count, null);
    232         }
    233         mExitingRipplesCount = 0;
    234 
    235         return needsDraw;
    236     }
    237 
    238     @Override
    239     public void setAlpha(int alpha) {
    240         super.setAlpha(alpha);
    241 
    242         // TODO: Should we support this?
    243     }
    244 
    245     @Override
    246     public void setColorFilter(ColorFilter cf) {
    247         super.setColorFilter(cf);
    248 
    249         // TODO: Should we support this?
    250     }
    251 
    252     @Override
    253     public int getOpacity() {
    254         // Worst-case scenario.
    255         return PixelFormat.TRANSLUCENT;
    256     }
    257 
    258     @Override
    259     protected boolean onStateChange(int[] stateSet) {
    260         final boolean changed = super.onStateChange(stateSet);
    261 
    262         boolean enabled = false;
    263         boolean pressed = false;
    264         boolean focused = false;
    265 
    266         for (int state : stateSet) {
    267             if (state == R.attr.state_enabled) {
    268                 enabled = true;
    269             }
    270             if (state == R.attr.state_focused) {
    271                 focused = true;
    272             }
    273             if (state == R.attr.state_pressed) {
    274                 pressed = true;
    275             }
    276         }
    277 
    278         setRippleActive(enabled && pressed);
    279         setBackgroundActive(focused || (enabled && pressed), focused);
    280 
    281         return changed;
    282     }
    283 
    284     private void setRippleActive(boolean active) {
    285         if (mRippleActive != active) {
    286             mRippleActive = active;
    287             if (active) {
    288                 tryRippleEnter();
    289             } else {
    290                 tryRippleExit();
    291             }
    292         }
    293     }
    294 
    295     private void setBackgroundActive(boolean active, boolean focused) {
    296         if (mBackgroundActive != active) {
    297             mBackgroundActive = active;
    298             if (active) {
    299                 tryBackgroundEnter(focused);
    300             } else {
    301                 tryBackgroundExit();
    302             }
    303         }
    304     }
    305 
    306     @Override
    307     protected void onBoundsChange(Rect bounds) {
    308         super.onBoundsChange(bounds);
    309 
    310         if (!mOverrideBounds) {
    311             mHotspotBounds.set(bounds);
    312             onHotspotBoundsChanged();
    313         }
    314 
    315         invalidateSelf();
    316     }
    317 
    318     @Override
    319     public boolean setVisible(boolean visible, boolean restart) {
    320         final boolean changed = super.setVisible(visible, restart);
    321 
    322         if (!visible) {
    323             clearHotspots();
    324         } else if (changed) {
    325             // If we just became visible, ensure the background and ripple
    326             // visibilities are consistent with their internal states.
    327             if (mRippleActive) {
    328                 tryRippleEnter();
    329             }
    330 
    331             if (mBackgroundActive) {
    332                 tryBackgroundEnter(false);
    333             }
    334 
    335             // Skip animations, just show the correct final states.
    336             jumpToCurrentState();
    337         }
    338 
    339         return changed;
    340     }
    341 
    342     /**
    343      * @hide
    344      */
    345     @Override
    346     public boolean isProjected() {
    347         return getNumberOfLayers() == 0;
    348     }
    349 
    350     @Override
    351     public boolean isStateful() {
    352         return true;
    353     }
    354 
    355     public void setColor(ColorStateList color) {
    356         mState.mColor = color;
    357         invalidateSelf();
    358     }
    359 
    360     @Override
    361     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
    362             throws XmlPullParserException, IOException {
    363         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable);
    364         updateStateFromTypedArray(a);
    365         a.recycle();
    366 
    367         // Force padding default to STACK before inflating.
    368         setPaddingMode(PADDING_MODE_STACK);
    369 
    370         super.inflate(r, parser, attrs, theme);
    371 
    372         setTargetDensity(r.getDisplayMetrics());
    373         initializeFromState();
    374     }
    375 
    376     @Override
    377     public boolean setDrawableByLayerId(int id, Drawable drawable) {
    378         if (super.setDrawableByLayerId(id, drawable)) {
    379             if (id == R.id.mask) {
    380                 mMask = drawable;
    381             }
    382 
    383             return true;
    384         }
    385 
    386         return false;
    387     }
    388 
    389     /**
    390      * Specifies how layer padding should affect the bounds of subsequent
    391      * layers. The default and recommended value for RippleDrawable is
    392      * {@link #PADDING_MODE_STACK}.
    393      *
    394      * @param mode padding mode, one of:
    395      *            <ul>
    396      *            <li>{@link #PADDING_MODE_NEST} to nest each layer inside the
    397      *            padding of the previous layer
    398      *            <li>{@link #PADDING_MODE_STACK} to stack each layer directly
    399      *            atop the previous layer
    400      *            </ul>
    401      * @see #getPaddingMode()
    402      */
    403     @Override
    404     public void setPaddingMode(int mode) {
    405         super.setPaddingMode(mode);
    406     }
    407 
    408     /**
    409      * Initializes the constant state from the values in the typed array.
    410      */
    411     private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
    412         final RippleState state = mState;
    413 
    414         // Account for any configuration changes.
    415         state.mChangingConfigurations |= a.getChangingConfigurations();
    416 
    417         // Extract the theme attributes, if any.
    418         state.mTouchThemeAttrs = a.extractThemeAttrs();
    419 
    420         final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
    421         if (color != null) {
    422             mState.mColor = color;
    423         }
    424 
    425         verifyRequiredAttributes(a);
    426     }
    427 
    428     private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
    429         if (mState.mColor == null && (mState.mTouchThemeAttrs == null
    430                 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) {
    431             throw new XmlPullParserException(a.getPositionDescription() +
    432                     ": <ripple> requires a valid color attribute");
    433         }
    434     }
    435 
    436     /**
    437      * Set the density at which this drawable will be rendered.
    438      *
    439      * @param metrics The display metrics for this drawable.
    440      */
    441     private void setTargetDensity(DisplayMetrics metrics) {
    442         if (mDensity != metrics.density) {
    443             mDensity = metrics.density;
    444             invalidateSelf();
    445         }
    446     }
    447 
    448     @Override
    449     public void applyTheme(Theme t) {
    450         super.applyTheme(t);
    451 
    452         final RippleState state = mState;
    453         if (state == null || state.mTouchThemeAttrs == null) {
    454             return;
    455         }
    456 
    457         final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs,
    458                 R.styleable.RippleDrawable);
    459         try {
    460             updateStateFromTypedArray(a);
    461         } catch (XmlPullParserException e) {
    462             throw new RuntimeException(e);
    463         } finally {
    464             a.recycle();
    465         }
    466 
    467         initializeFromState();
    468     }
    469 
    470     @Override
    471     public boolean canApplyTheme() {
    472         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
    473     }
    474 
    475     @Override
    476     public void setHotspot(float x, float y) {
    477         if (mRipple == null || mBackground == null) {
    478             mPendingX = x;
    479             mPendingY = y;
    480             mHasPending = true;
    481         }
    482 
    483         if (mRipple != null) {
    484             mRipple.move(x, y);
    485         }
    486     }
    487 
    488     /**
    489      * Creates an active hotspot at the specified location.
    490      */
    491     private void tryBackgroundEnter(boolean focused) {
    492         if (mBackground == null) {
    493             mBackground = new RippleBackground(this, mHotspotBounds);
    494         }
    495 
    496         mBackground.setup(mState.mMaxRadius, mDensity);
    497         mBackground.enter(focused);
    498     }
    499 
    500     private void tryBackgroundExit() {
    501         if (mBackground != null) {
    502             // Don't null out the background, we need it to draw!
    503             mBackground.exit();
    504         }
    505     }
    506 
    507     /**
    508      * Attempts to start an enter animation for the active hotspot. Fails if
    509      * there are too many animating ripples.
    510      */
    511     private void tryRippleEnter() {
    512         if (mExitingRipplesCount >= MAX_RIPPLES) {
    513             // This should never happen unless the user is tapping like a maniac
    514             // or there is a bug that's preventing ripples from being removed.
    515             return;
    516         }
    517 
    518         if (mRipple == null) {
    519             final float x;
    520             final float y;
    521             if (mHasPending) {
    522                 mHasPending = false;
    523                 x = mPendingX;
    524                 y = mPendingY;
    525             } else {
    526                 x = mHotspotBounds.exactCenterX();
    527                 y = mHotspotBounds.exactCenterY();
    528             }
    529             mRipple = new Ripple(this, mHotspotBounds, x, y);
    530         }
    531 
    532         mRipple.setup(mState.mMaxRadius, mDensity);
    533         mRipple.enter();
    534     }
    535 
    536     /**
    537      * Attempts to start an exit animation for the active hotspot. Fails if
    538      * there is no active hotspot.
    539      */
    540     private void tryRippleExit() {
    541         if (mRipple != null) {
    542             if (mExitingRipples == null) {
    543                 mExitingRipples = new Ripple[MAX_RIPPLES];
    544             }
    545             mExitingRipples[mExitingRipplesCount++] = mRipple;
    546             mRipple.exit();
    547             mRipple = null;
    548         }
    549     }
    550 
    551     /**
    552      * Cancels and removes the active ripple, all exiting ripples, and the
    553      * background. Nothing will be drawn after this method is called.
    554      */
    555     private void clearHotspots() {
    556         if (mRipple != null) {
    557             mRipple.cancel();
    558             mRipple = null;
    559             mRippleActive = false;
    560         }
    561 
    562         if (mBackground != null) {
    563             mBackground.cancel();
    564             mBackground = null;
    565             mBackgroundActive = false;
    566         }
    567 
    568         cancelExitingRipples();
    569         invalidateSelf();
    570     }
    571 
    572     @Override
    573     public void setHotspotBounds(int left, int top, int right, int bottom) {
    574         mOverrideBounds = true;
    575         mHotspotBounds.set(left, top, right, bottom);
    576 
    577         onHotspotBoundsChanged();
    578     }
    579 
    580     /** @hide */
    581     @Override
    582     public void getHotspotBounds(Rect outRect) {
    583         outRect.set(mHotspotBounds);
    584     }
    585 
    586     /**
    587      * Notifies all the animating ripples that the hotspot bounds have changed.
    588      */
    589     private void onHotspotBoundsChanged() {
    590         final int count = mExitingRipplesCount;
    591         final Ripple[] ripples = mExitingRipples;
    592         for (int i = 0; i < count; i++) {
    593             ripples[i].onHotspotBoundsChanged();
    594         }
    595 
    596         if (mRipple != null) {
    597             mRipple.onHotspotBoundsChanged();
    598         }
    599 
    600         if (mBackground != null) {
    601             mBackground.onHotspotBoundsChanged();
    602         }
    603     }
    604 
    605     /**
    606      * Populates <code>outline</code> with the first available layer outline,
    607      * excluding the mask layer.
    608      *
    609      * @param outline Outline in which to place the first available layer outline
    610      */
    611     @Override
    612     public void getOutline(@NonNull Outline outline) {
    613         final LayerState state = mLayerState;
    614         final ChildDrawable[] children = state.mChildren;
    615         final int N = state.mNum;
    616         for (int i = 0; i < N; i++) {
    617             if (children[i].mId != R.id.mask) {
    618                 children[i].mDrawable.getOutline(outline);
    619                 if (!outline.isEmpty()) return;
    620             }
    621         }
    622     }
    623 
    624     /**
    625      * Optimized for drawing ripples with a mask layer and optional content.
    626      */
    627     @Override
    628     public void draw(@NonNull Canvas canvas) {
    629         // Clip to the dirty bounds, which will be the drawable bounds if we
    630         // have a mask or content and the ripple bounds if we're projecting.
    631         final Rect bounds = getDirtyBounds();
    632         final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
    633         canvas.clipRect(bounds);
    634 
    635         drawContent(canvas);
    636         drawBackgroundAndRipples(canvas);
    637 
    638         canvas.restoreToCount(saveCount);
    639     }
    640 
    641     @Override
    642     public void invalidateSelf() {
    643         super.invalidateSelf();
    644 
    645         // Force the mask to update on the next draw().
    646         mHasValidMask = false;
    647     }
    648 
    649     /**
    650      * @return whether we need to use a mask
    651      */
    652     private void updateMaskShaderIfNeeded() {
    653         if (mHasValidMask) {
    654             return;
    655         }
    656 
    657         final int maskType = getMaskType();
    658         if (maskType == MASK_UNKNOWN) {
    659             return;
    660         }
    661 
    662         mHasValidMask = true;
    663 
    664         final Rect bounds = getBounds();
    665         if (maskType == MASK_NONE || bounds.isEmpty()) {
    666             if (mMaskBuffer != null) {
    667                 mMaskBuffer.recycle();
    668                 mMaskBuffer = null;
    669                 mMaskShader = null;
    670                 mMaskCanvas = null;
    671             }
    672             mMaskMatrix = null;
    673             mMaskColorFilter = null;
    674             return;
    675         }
    676 
    677         // Ensure we have a correctly-sized buffer.
    678         if (mMaskBuffer == null
    679                 || mMaskBuffer.getWidth() != bounds.width()
    680                 || mMaskBuffer.getHeight() != bounds.height()) {
    681             if (mMaskBuffer != null) {
    682                 mMaskBuffer.recycle();
    683             }
    684 
    685             mMaskBuffer = Bitmap.createBitmap(
    686                     bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
    687             mMaskShader = new BitmapShader(mMaskBuffer,
    688                     Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    689             mMaskCanvas = new Canvas(mMaskBuffer);
    690         } else {
    691             mMaskBuffer.eraseColor(Color.TRANSPARENT);
    692         }
    693 
    694         if (mMaskMatrix == null) {
    695             mMaskMatrix = new Matrix();
    696         } else {
    697             mMaskMatrix.reset();
    698         }
    699 
    700         if (mMaskColorFilter == null) {
    701             mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
    702         }
    703 
    704         // Draw the appropriate mask.
    705         if (maskType == MASK_EXPLICIT) {
    706             drawMask(mMaskCanvas);
    707         } else if (maskType == MASK_CONTENT) {
    708             drawContent(mMaskCanvas);
    709         }
    710     }
    711 
    712     private int getMaskType() {
    713         if (mRipple == null && mExitingRipplesCount <= 0
    714                 && (mBackground == null || !mBackground.shouldDraw())) {
    715             // We might need a mask later.
    716             return MASK_UNKNOWN;
    717         }
    718 
    719         if (mMask != null) {
    720             if (mMask.getOpacity() == PixelFormat.OPAQUE) {
    721                 // Clipping handles opaque explicit masks.
    722                 return MASK_NONE;
    723             } else {
    724                 return MASK_EXPLICIT;
    725             }
    726         }
    727 
    728         // Check for non-opaque, non-mask content.
    729         final ChildDrawable[] array = mLayerState.mChildren;
    730         final int count = mLayerState.mNum;
    731         for (int i = 0; i < count; i++) {
    732             if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
    733                 return MASK_CONTENT;
    734             }
    735         }
    736 
    737         // Clipping handles opaque content.
    738         return MASK_NONE;
    739     }
    740 
    741     /**
    742      * Removes a ripple from the exiting ripple list.
    743      *
    744      * @param ripple the ripple to remove
    745      */
    746     void removeRipple(Ripple ripple) {
    747         // Ripple ripple ripple ripple. Ripple ripple.
    748         final Ripple[] ripples = mExitingRipples;
    749         final int count = mExitingRipplesCount;
    750         final int index = getRippleIndex(ripple);
    751         if (index >= 0) {
    752             System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
    753             ripples[count - 1] = null;
    754             mExitingRipplesCount--;
    755 
    756             invalidateSelf();
    757         }
    758     }
    759 
    760     private int getRippleIndex(Ripple ripple) {
    761         final Ripple[] ripples = mExitingRipples;
    762         final int count = mExitingRipplesCount;
    763         for (int i = 0; i < count; i++) {
    764             if (ripples[i] == ripple) {
    765                 return i;
    766             }
    767         }
    768         return -1;
    769     }
    770 
    771     private void drawContent(Canvas canvas) {
    772         // Draw everything except the mask.
    773         final ChildDrawable[] array = mLayerState.mChildren;
    774         final int count = mLayerState.mNum;
    775         for (int i = 0; i < count; i++) {
    776             if (array[i].mId != R.id.mask) {
    777                 array[i].mDrawable.draw(canvas);
    778             }
    779         }
    780     }
    781 
    782     private void drawBackgroundAndRipples(Canvas canvas) {
    783         final Ripple active = mRipple;
    784         final RippleBackground background = mBackground;
    785         final int count = mExitingRipplesCount;
    786         if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
    787             // Move along, nothing to draw here.
    788             return;
    789         }
    790 
    791         final float x = mHotspotBounds.exactCenterX();
    792         final float y = mHotspotBounds.exactCenterY();
    793         canvas.translate(x, y);
    794 
    795         updateMaskShaderIfNeeded();
    796 
    797         // Position the shader to account for canvas translation.
    798         if (mMaskShader != null) {
    799             mMaskMatrix.setTranslate(-x, -y);
    800             mMaskShader.setLocalMatrix(mMaskMatrix);
    801         }
    802 
    803         // Grab the color for the current state and cut the alpha channel in
    804         // half so that the ripple and background together yield full alpha.
    805         final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
    806         final int halfAlpha = (Color.alpha(color) / 2) << 24;
    807         final Paint p = getRipplePaint();
    808 
    809         if (mMaskColorFilter != null) {
    810             // The ripple timing depends on the paint's alpha value, so we need
    811             // to push just the alpha channel into the paint and let the filter
    812             // handle the full-alpha color.
    813             final int fullAlphaColor = color | (0xFF << 24);
    814             mMaskColorFilter.setColor(fullAlphaColor);
    815 
    816             p.setColor(halfAlpha);
    817             p.setColorFilter(mMaskColorFilter);
    818             p.setShader(mMaskShader);
    819         } else {
    820             final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
    821             p.setColor(halfAlphaColor);
    822             p.setColorFilter(null);
    823             p.setShader(null);
    824         }
    825 
    826         if (background != null && background.shouldDraw()) {
    827             background.draw(canvas, p);
    828         }
    829 
    830         if (count > 0) {
    831             final Ripple[] ripples = mExitingRipples;
    832             for (int i = 0; i < count; i++) {
    833                 ripples[i].draw(canvas, p);
    834             }
    835         }
    836 
    837         if (active != null) {
    838             active.draw(canvas, p);
    839         }
    840 
    841         canvas.translate(-x, -y);
    842     }
    843 
    844     private void drawMask(Canvas canvas) {
    845         mMask.draw(canvas);
    846     }
    847 
    848     private Paint getRipplePaint() {
    849         if (mRipplePaint == null) {
    850             mRipplePaint = new Paint();
    851             mRipplePaint.setAntiAlias(true);
    852             mRipplePaint.setStyle(Paint.Style.FILL);
    853         }
    854         return mRipplePaint;
    855     }
    856 
    857     @Override
    858     public Rect getDirtyBounds() {
    859         if (isProjected()) {
    860             final Rect drawingBounds = mDrawingBounds;
    861             final Rect dirtyBounds = mDirtyBounds;
    862             dirtyBounds.set(drawingBounds);
    863             drawingBounds.setEmpty();
    864 
    865             final int cX = (int) mHotspotBounds.exactCenterX();
    866             final int cY = (int) mHotspotBounds.exactCenterY();
    867             final Rect rippleBounds = mTempRect;
    868 
    869             final Ripple[] activeRipples = mExitingRipples;
    870             final int N = mExitingRipplesCount;
    871             for (int i = 0; i < N; i++) {
    872                 activeRipples[i].getBounds(rippleBounds);
    873                 rippleBounds.offset(cX, cY);
    874                 drawingBounds.union(rippleBounds);
    875             }
    876 
    877             final RippleBackground background = mBackground;
    878             if (background != null) {
    879                 background.getBounds(rippleBounds);
    880                 rippleBounds.offset(cX, cY);
    881                 drawingBounds.union(rippleBounds);
    882             }
    883 
    884             dirtyBounds.union(drawingBounds);
    885             dirtyBounds.union(super.getDirtyBounds());
    886             return dirtyBounds;
    887         } else {
    888             return getBounds();
    889         }
    890     }
    891 
    892     @Override
    893     public ConstantState getConstantState() {
    894         return mState;
    895     }
    896 
    897     @Override
    898     public Drawable mutate() {
    899         super.mutate();
    900 
    901         // LayerDrawable creates a new state using createConstantState, so
    902         // this should always be a safe cast.
    903         mState = (RippleState) mLayerState;
    904 
    905         // The locally cached drawable may have changed.
    906         mMask = findDrawableByLayerId(R.id.mask);
    907 
    908         return this;
    909     }
    910 
    911     @Override
    912     RippleState createConstantState(LayerState state, Resources res) {
    913         return new RippleState(state, this, res);
    914     }
    915 
    916     static class RippleState extends LayerState {
    917         int[] mTouchThemeAttrs;
    918         ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA);
    919         int mMaxRadius = RADIUS_AUTO;
    920 
    921         public RippleState(LayerState orig, RippleDrawable owner, Resources res) {
    922             super(orig, owner, res);
    923 
    924             if (orig != null && orig instanceof RippleState) {
    925                 final RippleState origs = (RippleState) orig;
    926                 mTouchThemeAttrs = origs.mTouchThemeAttrs;
    927                 mColor = origs.mColor;
    928                 mMaxRadius = origs.mMaxRadius;
    929             }
    930         }
    931 
    932         @Override
    933         public boolean canApplyTheme() {
    934             return mTouchThemeAttrs != null || super.canApplyTheme();
    935         }
    936 
    937         @Override
    938         public Drawable newDrawable() {
    939             return new RippleDrawable(this, null);
    940         }
    941 
    942         @Override
    943         public Drawable newDrawable(Resources res) {
    944             return new RippleDrawable(this, res);
    945         }
    946     }
    947 
    948     /**
    949      * Sets the maximum ripple radius in pixels. The default value of
    950      * {@link #RADIUS_AUTO} defines the radius as the distance from the center
    951      * of the drawable bounds (or hotspot bounds, if specified) to a corner.
    952      *
    953      * @param maxRadius the maximum ripple radius in pixels or
    954      *            {@link #RADIUS_AUTO} to automatically determine the maximum
    955      *            radius based on the bounds
    956      * @see #getMaxRadius()
    957      * @see #setHotspotBounds(int, int, int, int)
    958      * @hide
    959      */
    960     public void setMaxRadius(int maxRadius) {
    961         if (maxRadius != RADIUS_AUTO && maxRadius < 0) {
    962             throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0");
    963         }
    964 
    965         mState.mMaxRadius = maxRadius;
    966     }
    967 
    968     /**
    969      * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if
    970      *         the radius is determined automatically
    971      * @see #setMaxRadius(int)
    972      * @hide
    973      */
    974     public int getMaxRadius() {
    975         return mState.mMaxRadius;
    976     }
    977 
    978     private RippleDrawable(RippleState state, Resources res) {
    979         mState = new RippleState(state, this, res);
    980         mLayerState = mState;
    981 
    982         if (mState.mNum > 0) {
    983             ensurePadding();
    984         }
    985 
    986         if (res != null) {
    987             mDensity = res.getDisplayMetrics().density;
    988         }
    989 
    990         initializeFromState();
    991     }
    992 
    993     private void initializeFromState() {
    994         // Initialize from constant state.
    995         mMask = findDrawableByLayerId(R.id.mask);
    996     }
    997 }
    998