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