• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 com.android.launcher3;
18 
19 import android.animation.ObjectAnimator;
20 import android.animation.TimeInterpolator;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.ColorFilter;
25 import android.graphics.ColorMatrix;
26 import android.graphics.ColorMatrixColorFilter;
27 import android.graphics.Paint;
28 import android.graphics.PixelFormat;
29 import android.graphics.PorterDuff;
30 import android.graphics.PorterDuffColorFilter;
31 import android.graphics.drawable.Drawable;
32 import android.util.Property;
33 import android.util.SparseArray;
34 
35 import com.android.launcher3.graphics.IconPalette;
36 
37 public class FastBitmapDrawable extends Drawable {
38 
39     private static final float PRESSED_BRIGHTNESS = 100f / 255f;
40     private static final float DISABLED_DESATURATION = 1f;
41     private static final float DISABLED_BRIGHTNESS = 0.5f;
42 
43     public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
44 
45         @Override
46         public float getInterpolation(float input) {
47             if (input < 0.05f) {
48                 return input / 0.05f;
49             } else if (input < 0.3f){
50                 return 1;
51             } else {
52                 return (1 - input) / 0.7f;
53             }
54         }
55     };
56     public static final int CLICK_FEEDBACK_DURATION = 2000;
57 
58     // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
59     // reduce the value space to a smaller value V, which reduces the number of cached
60     // ColorMatrixColorFilters that we need to keep to V^2
61     private static final int REDUCED_FILTER_VALUE_SPACE = 48;
62 
63     // A cache of ColorFilters for optimizing brightness and saturation animations
64     private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
65 
66     // Temporary matrices used for calculation
67     private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
68     private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
69 
70     protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
71     private final Bitmap mBitmap;
72 
73     private boolean mIsPressed;
74     private boolean mIsDisabled;
75 
76     private IconPalette mIconPalette;
77 
78     private static final Property<FastBitmapDrawable, Float> BRIGHTNESS
79             = new Property<FastBitmapDrawable, Float>(Float.TYPE, "brightness") {
80         @Override
81         public Float get(FastBitmapDrawable fastBitmapDrawable) {
82             return fastBitmapDrawable.getBrightness();
83         }
84 
85         @Override
86         public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
87             fastBitmapDrawable.setBrightness(value);
88         }
89     };
90 
91     // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
92     // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
93     private int mDesaturation = 0;
94     private int mBrightness = 0;
95     private int mAlpha = 255;
96     private int mPrevUpdateKey = Integer.MAX_VALUE;
97 
98     // Animators for the fast bitmap drawable's brightness
99     private ObjectAnimator mBrightnessAnimator;
100 
FastBitmapDrawable(Bitmap b)101     public FastBitmapDrawable(Bitmap b) {
102         mBitmap = b;
103         setFilterBitmap(true);
104     }
105 
106     @Override
draw(Canvas canvas)107     public void draw(Canvas canvas) {
108         canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
109     }
110 
getIconPalette()111     public IconPalette getIconPalette() {
112         if (mIconPalette == null) {
113             mIconPalette = IconPalette.fromDominantColor(Utilities
114                     .findDominantColorByHue(mBitmap, 20), true /* desaturateBackground */);
115         }
116         return mIconPalette;
117     }
118 
119     @Override
setColorFilter(ColorFilter cf)120     public void setColorFilter(ColorFilter cf) {
121         // No op
122     }
123 
124     @Override
getOpacity()125     public int getOpacity() {
126         return PixelFormat.TRANSLUCENT;
127     }
128 
129     @Override
setAlpha(int alpha)130     public void setAlpha(int alpha) {
131         mAlpha = alpha;
132         mPaint.setAlpha(alpha);
133     }
134 
135     @Override
setFilterBitmap(boolean filterBitmap)136     public void setFilterBitmap(boolean filterBitmap) {
137         mPaint.setFilterBitmap(filterBitmap);
138         mPaint.setAntiAlias(filterBitmap);
139     }
140 
getAlpha()141     public int getAlpha() {
142         return mAlpha;
143     }
144 
145     @Override
getIntrinsicWidth()146     public int getIntrinsicWidth() {
147         return mBitmap.getWidth();
148     }
149 
150     @Override
getIntrinsicHeight()151     public int getIntrinsicHeight() {
152         return mBitmap.getHeight();
153     }
154 
155     @Override
getMinimumWidth()156     public int getMinimumWidth() {
157         return getBounds().width();
158     }
159 
160     @Override
getMinimumHeight()161     public int getMinimumHeight() {
162         return getBounds().height();
163     }
164 
getBitmap()165     public Bitmap getBitmap() {
166         return mBitmap;
167     }
168 
169     @Override
isStateful()170     public boolean isStateful() {
171         return true;
172     }
173 
174     @Override
getColorFilter()175     public ColorFilter getColorFilter() {
176         return mPaint.getColorFilter();
177     }
178 
179     @Override
onStateChange(int[] state)180     protected boolean onStateChange(int[] state) {
181         boolean isPressed = false;
182         for (int s : state) {
183             if (s == android.R.attr.state_pressed) {
184                 isPressed = true;
185                 break;
186             }
187         }
188         if (mIsPressed != isPressed) {
189             mIsPressed = isPressed;
190 
191             if (mBrightnessAnimator != null) {
192                 mBrightnessAnimator.cancel();
193             }
194 
195             if (mIsPressed) {
196                 // Animate when going to pressed state
197                 mBrightnessAnimator = ObjectAnimator.ofFloat(
198                         this, BRIGHTNESS, getExpectedBrightness());
199                 mBrightnessAnimator.setDuration(CLICK_FEEDBACK_DURATION);
200                 mBrightnessAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR);
201                 mBrightnessAnimator.start();
202             } else {
203                 setBrightness(getExpectedBrightness());
204             }
205             return true;
206         }
207         return false;
208     }
209 
invalidateDesaturationAndBrightness()210     private void invalidateDesaturationAndBrightness() {
211         setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0);
212         setBrightness(getExpectedBrightness());
213     }
214 
getExpectedBrightness()215     private float getExpectedBrightness() {
216         return mIsDisabled ? DISABLED_BRIGHTNESS :
217                 (mIsPressed ? PRESSED_BRIGHTNESS : 0);
218     }
219 
setIsDisabled(boolean isDisabled)220     public void setIsDisabled(boolean isDisabled) {
221         if (mIsDisabled != isDisabled) {
222             mIsDisabled = isDisabled;
223             invalidateDesaturationAndBrightness();
224         }
225     }
226 
227     /**
228      * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
229      */
setDesaturation(float desaturation)230     private void setDesaturation(float desaturation) {
231         int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
232         if (mDesaturation != newDesaturation) {
233             mDesaturation = newDesaturation;
234             updateFilter();
235         }
236     }
237 
getDesaturation()238     public float getDesaturation() {
239         return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
240     }
241 
242     /**
243      * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
244      */
setBrightness(float brightness)245     private void setBrightness(float brightness) {
246         int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
247         if (mBrightness != newBrightness) {
248             mBrightness = newBrightness;
249             updateFilter();
250         }
251     }
252 
getBrightness()253     private float getBrightness() {
254         return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
255     }
256 
257     /**
258      * Updates the paint to reflect the current brightness and saturation.
259      */
updateFilter()260     private void updateFilter() {
261         boolean usePorterDuffFilter = false;
262         int key = -1;
263         if (mDesaturation > 0) {
264             key = (mDesaturation << 16) | mBrightness;
265         } else if (mBrightness > 0) {
266             // Compose a key with a fully saturated icon if we are just animating brightness
267             key = (1 << 16) | mBrightness;
268 
269             // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
270             // icons, so just use a PorterDuff filter when we aren't animating saturation
271             usePorterDuffFilter = true;
272         }
273 
274         // Debounce multiple updates on the same frame
275         if (key == mPrevUpdateKey) {
276             return;
277         }
278         mPrevUpdateKey = key;
279 
280         if (key != -1) {
281             ColorFilter filter = sCachedFilter.get(key);
282             if (filter == null) {
283                 float brightnessF = getBrightness();
284                 int brightnessI = (int) (255 * brightnessF);
285                 if (usePorterDuffFilter) {
286                     filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
287                             PorterDuff.Mode.SRC_ATOP);
288                 } else {
289                     float saturationF = 1f - getDesaturation();
290                     sTempFilterMatrix.setSaturation(saturationF);
291                     if (mBrightness > 0) {
292                         // Brightness: C-new = C-old*(1-amount) + amount
293                         float scale = 1f - brightnessF;
294                         float[] mat = sTempBrightnessMatrix.getArray();
295                         mat[0] = scale;
296                         mat[6] = scale;
297                         mat[12] = scale;
298                         mat[4] = brightnessI;
299                         mat[9] = brightnessI;
300                         mat[14] = brightnessI;
301                         sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
302                     }
303                     filter = new ColorMatrixColorFilter(sTempFilterMatrix);
304                 }
305                 sCachedFilter.append(key, filter);
306             }
307             mPaint.setColorFilter(filter);
308         } else {
309             mPaint.setColorFilter(null);
310         }
311         invalidateSelf();
312     }
313 }
314