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