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