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