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.AnimatorSet;
20 import android.animation.ObjectAnimator;
21 import android.animation.TimeInterpolator;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.ColorFilter;
26 import android.graphics.ColorMatrix;
27 import android.graphics.ColorMatrixColorFilter;
28 import android.graphics.Paint;
29 import android.graphics.PixelFormat;
30 import android.graphics.PorterDuff;
31 import android.graphics.PorterDuffColorFilter;
32 import android.graphics.drawable.Drawable;
33 import android.util.SparseArray;
34 import android.view.animation.DecelerateInterpolator;
35 
36 public class FastBitmapDrawable extends Drawable {
37 
38     /**
39      * The possible states that a FastBitmapDrawable can be in.
40      */
41     public enum State {
42 
43         NORMAL                      (0f, 0f, 1f, new DecelerateInterpolator()),
44         PRESSED                     (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR),
45         FAST_SCROLL_HIGHLIGHTED     (0f, 0f, 1.15f, new DecelerateInterpolator()),
46         FAST_SCROLL_UNHIGHLIGHTED   (0f, 0f, 1f, new DecelerateInterpolator()),
47         DISABLED                    (1f, 0.5f, 1f, new DecelerateInterpolator());
48 
49         public final float desaturation;
50         public final float brightness;
51         /**
52          * Used specifically by the view drawing this FastBitmapDrawable.
53          */
54         public final float viewScale;
55         public final TimeInterpolator interpolator;
56 
State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator)57         State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) {
58             this.desaturation = desaturation;
59             this.brightness = brightness;
60             this.viewScale = viewScale;
61             this.interpolator = interpolator;
62         }
63     }
64 
65     public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
66 
67         @Override
68         public float getInterpolation(float input) {
69             if (input < 0.05f) {
70                 return input / 0.05f;
71             } else if (input < 0.3f){
72                 return 1;
73             } else {
74                 return (1 - input) / 0.7f;
75             }
76         }
77     };
78     public static final int CLICK_FEEDBACK_DURATION = 2000;
79     public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225;
80     public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150;
81     public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225;
82     public static final int FAST_SCROLL_INACTIVE_DURATION = 275;
83 
84     // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
85     // reduce the value space to a smaller value V, which reduces the number of cached
86     // ColorMatrixColorFilters that we need to keep to V^2
87     private static final int REDUCED_FILTER_VALUE_SPACE = 48;
88 
89     // A cache of ColorFilters for optimizing brightness and saturation animations
90     private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
91 
92     // Temporary matrices used for calculation
93     private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
94     private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
95 
96     private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
97     private final Bitmap mBitmap;
98     private State mState = State.NORMAL;
99 
100     // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
101     // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
102     private int mDesaturation = 0;
103     private int mBrightness = 0;
104     private int mAlpha = 255;
105     private int mPrevUpdateKey = Integer.MAX_VALUE;
106 
107     // Animators for the fast bitmap drawable's properties
108     private AnimatorSet mPropertyAnimator;
109 
FastBitmapDrawable(Bitmap b)110     public FastBitmapDrawable(Bitmap b) {
111         mBitmap = b;
112         setBounds(0, 0, b.getWidth(), b.getHeight());
113     }
114 
115     @Override
draw(Canvas canvas)116     public void draw(Canvas canvas) {
117         canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
118     }
119 
120     @Override
setColorFilter(ColorFilter cf)121     public void setColorFilter(ColorFilter cf) {
122         // No op
123     }
124 
125     @Override
getOpacity()126     public int getOpacity() {
127         return PixelFormat.TRANSLUCENT;
128     }
129 
130     @Override
setAlpha(int alpha)131     public void setAlpha(int alpha) {
132         mAlpha = alpha;
133         mPaint.setAlpha(alpha);
134     }
135 
136     @Override
setFilterBitmap(boolean filterBitmap)137     public void setFilterBitmap(boolean filterBitmap) {
138         mPaint.setFilterBitmap(filterBitmap);
139         mPaint.setAntiAlias(filterBitmap);
140     }
141 
getAlpha()142     public int getAlpha() {
143         return mAlpha;
144     }
145 
146     @Override
getIntrinsicWidth()147     public int getIntrinsicWidth() {
148         return mBitmap.getWidth();
149     }
150 
151     @Override
getIntrinsicHeight()152     public int getIntrinsicHeight() {
153         return mBitmap.getHeight();
154     }
155 
156     @Override
getMinimumWidth()157     public int getMinimumWidth() {
158         return getBounds().width();
159     }
160 
161     @Override
getMinimumHeight()162     public int getMinimumHeight() {
163         return getBounds().height();
164     }
165 
getBitmap()166     public Bitmap getBitmap() {
167         return mBitmap;
168     }
169 
170     /**
171      * Animates this drawable to a new state.
172      *
173      * @return whether the state has changed.
174      */
animateState(State newState)175     public boolean animateState(State newState) {
176         State prevState = mState;
177         if (mState != newState) {
178             mState = newState;
179 
180             mPropertyAnimator = cancelAnimator(mPropertyAnimator);
181             mPropertyAnimator = new AnimatorSet();
182             mPropertyAnimator.playTogether(
183                     ObjectAnimator
184                             .ofFloat(this, "desaturation", newState.desaturation),
185                     ObjectAnimator
186                             .ofFloat(this, "brightness", newState.brightness));
187             mPropertyAnimator.setInterpolator(newState.interpolator);
188             mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState));
189             mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState));
190             mPropertyAnimator.start();
191             return true;
192         }
193         return false;
194     }
195 
196     /**
197      * Immediately sets this drawable to a new state.
198      *
199      * @return whether the state has changed.
200      */
setState(State newState)201     public boolean setState(State newState) {
202         if (mState != newState) {
203             mState = newState;
204 
205             mPropertyAnimator = cancelAnimator(mPropertyAnimator);
206 
207             setDesaturation(newState.desaturation);
208             setBrightness(newState.brightness);
209             return true;
210         }
211         return false;
212     }
213 
214     /**
215      * Returns the current state.
216      */
getCurrentState()217     public State getCurrentState() {
218         return mState;
219     }
220 
221     /**
222      * Returns the duration for the state change animation.
223      */
getDurationForStateChange(State fromState, State toState)224     public static int getDurationForStateChange(State fromState, State toState) {
225         switch (toState) {
226             case NORMAL:
227                 switch (fromState) {
228                     case PRESSED:
229                         return 0;
230                     case FAST_SCROLL_HIGHLIGHTED:
231                     case FAST_SCROLL_UNHIGHLIGHTED:
232                         return FAST_SCROLL_INACTIVE_DURATION;
233                 }
234             case PRESSED:
235                 return CLICK_FEEDBACK_DURATION;
236             case FAST_SCROLL_HIGHLIGHTED:
237                 return FAST_SCROLL_HIGHLIGHT_DURATION;
238             case FAST_SCROLL_UNHIGHLIGHTED:
239                 switch (fromState) {
240                     case NORMAL:
241                         // When animating from normal state, take a little longer
242                         return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION;
243                     default:
244                         return FAST_SCROLL_UNHIGHLIGHT_DURATION;
245                 }
246         }
247         return 0;
248     }
249 
250     /**
251      * Returns the start delay when animating between certain fast scroll states.
252      */
getStartDelayForStateChange(State fromState, State toState)253     public static int getStartDelayForStateChange(State fromState, State toState) {
254         switch (toState) {
255             case FAST_SCROLL_UNHIGHLIGHTED:
256                 switch (fromState) {
257                     case NORMAL:
258                         return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4;
259                 }
260         }
261         return 0;
262     }
263 
264     /**
265      * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
266      */
setDesaturation(float desaturation)267     public 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     public 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     public 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     private 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 
cancelAnimator(AnimatorSet animator)351     private AnimatorSet cancelAnimator(AnimatorSet animator) {
352         if (animator != null) {
353             animator.removeAllListeners();
354             animator.cancel();
355         }
356         return null;
357     }
358 }
359