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