• 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 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         mAlpha = alpha;
146         mPaint.setAlpha(alpha);
147     }
148 
149     @Override
setFilterBitmap(boolean filterBitmap)150     public void setFilterBitmap(boolean filterBitmap) {
151         mPaint.setFilterBitmap(filterBitmap);
152         mPaint.setAntiAlias(filterBitmap);
153     }
154 
getAlpha()155     public int getAlpha() {
156         return mAlpha;
157     }
158 
setScale(float scale)159     public void setScale(float scale) {
160         if (mScaleAnimation != null) {
161             mScaleAnimation.cancel();
162             mScaleAnimation = null;
163         }
164         mScale = scale;
165         invalidateSelf();
166     }
167 
getAnimatedScale()168     public float getAnimatedScale() {
169         return mScaleAnimation == null ? 1 : mScale;
170     }
171 
getScale()172     public float getScale() {
173         return mScale;
174     }
175 
176     @Override
getIntrinsicWidth()177     public int getIntrinsicWidth() {
178         return mBitmap.getWidth();
179     }
180 
181     @Override
getIntrinsicHeight()182     public int getIntrinsicHeight() {
183         return mBitmap.getHeight();
184     }
185 
186     @Override
getMinimumWidth()187     public int getMinimumWidth() {
188         return getBounds().width();
189     }
190 
191     @Override
getMinimumHeight()192     public int getMinimumHeight() {
193         return getBounds().height();
194     }
195 
196     @Override
isStateful()197     public boolean isStateful() {
198         return true;
199     }
200 
201     @Override
getColorFilter()202     public ColorFilter getColorFilter() {
203         return mPaint.getColorFilter();
204     }
205 
206     @Override
onStateChange(int[] state)207     protected boolean onStateChange(int[] state) {
208         boolean isPressed = false;
209         for (int s : state) {
210             if (s == android.R.attr.state_pressed) {
211                 isPressed = true;
212                 break;
213             }
214         }
215         if (mIsPressed != isPressed) {
216             mIsPressed = isPressed;
217 
218             if (mScaleAnimation != null) {
219                 mScaleAnimation.cancel();
220                 mScaleAnimation = null;
221             }
222 
223             if (mIsPressed) {
224                 // Animate when going to pressed state
225                 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE);
226                 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
227                 mScaleAnimation.setInterpolator(ACCEL);
228                 mScaleAnimation.start();
229             } else {
230                 if (isVisible()) {
231                     mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f);
232                     mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
233                     mScaleAnimation.setInterpolator(DEACCEL);
234                     mScaleAnimation.start();
235                 } else {
236                     mScale = 1f;
237                     invalidateSelf();
238                 }
239             }
240             return true;
241         }
242         return false;
243     }
244 
invalidateDesaturationAndBrightness()245     private void invalidateDesaturationAndBrightness() {
246         setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0);
247         setBrightness(mIsDisabled ? DISABLED_BRIGHTNESS : 0);
248     }
249 
setIsDisabled(boolean isDisabled)250     public void setIsDisabled(boolean isDisabled) {
251         if (mIsDisabled != isDisabled) {
252             mIsDisabled = isDisabled;
253             invalidateDesaturationAndBrightness();
254         }
255     }
256 
isDisabled()257     protected boolean isDisabled() {
258         return mIsDisabled;
259     }
260 
261     /**
262      * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
263      */
setDesaturation(float desaturation)264     private void setDesaturation(float desaturation) {
265         int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
266         if (mDesaturation != newDesaturation) {
267             mDesaturation = newDesaturation;
268             updateFilter();
269         }
270     }
271 
getDesaturation()272     public float getDesaturation() {
273         return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
274     }
275 
276     /**
277      * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
278      */
setBrightness(float brightness)279     private void setBrightness(float brightness) {
280         int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
281         if (mBrightness != newBrightness) {
282             mBrightness = newBrightness;
283             updateFilter();
284         }
285     }
286 
getBrightness()287     private float getBrightness() {
288         return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
289     }
290 
291     /**
292      * Updates the paint to reflect the current brightness and saturation.
293      */
updateFilter()294     protected void updateFilter() {
295         boolean usePorterDuffFilter = false;
296         int key = -1;
297         if (mDesaturation > 0) {
298             key = (mDesaturation << 16) | mBrightness;
299         } else if (mBrightness > 0) {
300             // Compose a key with a fully saturated icon if we are just animating brightness
301             key = (1 << 16) | mBrightness;
302 
303             // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
304             // icons, so just use a PorterDuff filter when we aren't animating saturation
305             usePorterDuffFilter = true;
306         }
307 
308         // Debounce multiple updates on the same frame
309         if (key == mPrevUpdateKey) {
310             return;
311         }
312         mPrevUpdateKey = key;
313 
314         if (key != -1) {
315             ColorFilter filter = sCachedFilter.get(key);
316             if (filter == null) {
317                 float brightnessF = getBrightness();
318                 int brightnessI = (int) (255 * brightnessF);
319                 if (usePorterDuffFilter) {
320                     filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
321                             PorterDuff.Mode.SRC_ATOP);
322                 } else {
323                     float saturationF = 1f - getDesaturation();
324                     sTempFilterMatrix.setSaturation(saturationF);
325                     if (mBrightness > 0) {
326                         // Brightness: C-new = C-old*(1-amount) + amount
327                         float scale = 1f - brightnessF;
328                         float[] mat = sTempBrightnessMatrix.getArray();
329                         mat[0] = scale;
330                         mat[6] = scale;
331                         mat[12] = scale;
332                         mat[4] = brightnessI;
333                         mat[9] = brightnessI;
334                         mat[14] = brightnessI;
335                         sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
336                     }
337                     filter = new ColorMatrixColorFilter(sTempFilterMatrix);
338                 }
339                 sCachedFilter.append(key, filter);
340             }
341             mPaint.setColorFilter(filter);
342         } else {
343             mPaint.setColorFilter(null);
344         }
345         invalidateSelf();
346     }
347 
348     @Override
getConstantState()349     public ConstantState getConstantState() {
350         return new MyConstantState(mBitmap, mIconColor, mIsDisabled);
351     }
352 
353     protected static class MyConstantState extends ConstantState {
354         protected final Bitmap mBitmap;
355         protected final int mIconColor;
356         protected final boolean mIsDisabled;
357 
MyConstantState(Bitmap bitmap, int color, boolean isDisabled)358         public MyConstantState(Bitmap bitmap, int color, boolean isDisabled) {
359             mBitmap = bitmap;
360             mIconColor = color;
361             mIsDisabled = isDisabled;
362         }
363 
364         @Override
newDrawable()365         public Drawable newDrawable() {
366             return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
367         }
368 
369         @Override
getChangingConfigurations()370         public int getChangingConfigurations() {
371             return 0;
372         }
373     }
374 }
375