• 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.icons;
18 
19 import static com.android.launcher3.icons.BaseIconFactory.getBadgeSizeForIconSize;
20 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
21 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
22 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
23 
24 import android.animation.ObjectAnimator;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.ColorFilter;
29 import android.graphics.ColorMatrix;
30 import android.graphics.ColorMatrixColorFilter;
31 import android.graphics.Paint;
32 import android.graphics.PixelFormat;
33 import android.graphics.Rect;
34 import android.graphics.drawable.Drawable;
35 import android.util.FloatProperty;
36 import android.view.animation.AccelerateInterpolator;
37 import android.view.animation.DecelerateInterpolator;
38 import android.view.animation.Interpolator;
39 import android.view.animation.PathInterpolator;
40 
41 import androidx.annotation.Nullable;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.core.graphics.ColorUtils;
44 
45 import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
46 
47 public class FastBitmapDrawable extends Drawable implements Drawable.Callback {
48 
49     private static final Interpolator ACCEL = new AccelerateInterpolator();
50     private static final Interpolator DEACCEL = new DecelerateInterpolator();
51     private static final Interpolator HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR =
52             new PathInterpolator(0.05f, 0.7f, 0.1f, 1.0f);
53 
54     @VisibleForTesting protected static final float PRESSED_SCALE = 1.1f;
55     @VisibleForTesting protected static final float HOVERED_SCALE = 1.1f;
56     public static final int WHITE_SCRIM_ALPHA = 138;
57 
58     private static final float DISABLED_DESATURATION = 1f;
59     private static final float DISABLED_BRIGHTNESS = 0.5f;
60     protected static final int FULLY_OPAQUE = 255;
61 
62     public static final int CLICK_FEEDBACK_DURATION = 200;
63     public static final int HOVER_FEEDBACK_DURATION = 300;
64 
65     private static boolean sFlagHoverEnabled = false;
66 
67     protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
68     public final BitmapInfo mBitmapInfo;
69 
70     @Nullable private ColorFilter mColorFilter;
71 
72     @VisibleForTesting protected boolean mIsPressed;
73     @VisibleForTesting protected boolean mIsHovered;
74     protected boolean mIsDisabled;
75     protected float mDisabledAlpha = 1f;
76 
77     @DrawableCreationFlags int mCreationFlags = 0;
78 
79     // Animator and properties for the fast bitmap drawable's scale
80     @VisibleForTesting protected static final FloatProperty<FastBitmapDrawable> SCALE
81             = new FloatProperty<FastBitmapDrawable>("scale") {
82         @Override
83         public Float get(FastBitmapDrawable fastBitmapDrawable) {
84             return fastBitmapDrawable.mScale;
85         }
86 
87         @Override
88         public void setValue(FastBitmapDrawable fastBitmapDrawable, float value) {
89             fastBitmapDrawable.mScale = value;
90             fastBitmapDrawable.invalidateSelf();
91         }
92     };
93     @VisibleForTesting protected ObjectAnimator mScaleAnimation;
94     private float mScale = 1;
95     private int mAlpha = 255;
96 
97     private Drawable mBadge;
98 
99     private boolean mHoverScaleEnabledForDisplay = true;
100 
FastBitmapDrawable(Bitmap b, int iconColor)101     protected FastBitmapDrawable(Bitmap b, int iconColor) {
102         this(BitmapInfo.of(b, iconColor));
103     }
104 
FastBitmapDrawable(Bitmap b)105     public FastBitmapDrawable(Bitmap b) {
106         this(BitmapInfo.fromBitmap(b));
107     }
108 
FastBitmapDrawable(BitmapInfo info)109     public FastBitmapDrawable(BitmapInfo info) {
110         mBitmapInfo = info;
111         setFilterBitmap(true);
112     }
113 
114     /**
115      * Returns true if the drawable points to the same bitmap icon object
116      */
isSameInfo(BitmapInfo info)117     public boolean isSameInfo(BitmapInfo info) {
118         return mBitmapInfo == info;
119     }
120 
121     @Override
onBoundsChange(Rect bounds)122     protected void onBoundsChange(Rect bounds) {
123         super.onBoundsChange(bounds);
124         updateBadgeBounds(bounds);
125     }
126 
updateBadgeBounds(Rect bounds)127     private void updateBadgeBounds(Rect bounds) {
128         if (mBadge != null) {
129             setBadgeBounds(mBadge, bounds);
130         }
131     }
132 
133     @Override
draw(Canvas canvas)134     public final void draw(Canvas canvas) {
135         if (mScale != 1f) {
136             int count = canvas.save();
137             Rect bounds = getBounds();
138             canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
139             drawInternal(canvas, bounds);
140             if (mBadge != null) {
141                 mBadge.draw(canvas);
142             }
143             canvas.restoreToCount(count);
144         } else {
145             drawInternal(canvas, getBounds());
146             if (mBadge != null) {
147                 mBadge.draw(canvas);
148             }
149         }
150     }
151 
drawInternal(Canvas canvas, Rect bounds)152     protected void drawInternal(Canvas canvas, Rect bounds) {
153         canvas.drawBitmap(mBitmapInfo.icon, null, bounds, mPaint);
154     }
155 
156     /**
157      * Returns the primary icon color, slightly tinted white
158      */
getIconColor()159     public int getIconColor() {
160         int whiteScrim = setColorAlphaBound(Color.WHITE, WHITE_SCRIM_ALPHA);
161         return ColorUtils.compositeColors(whiteScrim, mBitmapInfo.color);
162     }
163 
164     /**
165      * Returns if this represents a themed icon
166      */
isThemed()167     public boolean isThemed() {
168         return false;
169     }
170 
171     /**
172      * Returns true if the drawable was created with theme, even if it doesn't
173      * support theming itself.
174      */
isCreatedForTheme()175     public boolean isCreatedForTheme() {
176         return isThemed() || (mCreationFlags & FLAG_THEMED) != 0;
177     }
178 
179     @Override
setColorFilter(ColorFilter cf)180     public void setColorFilter(ColorFilter cf) {
181         mColorFilter = cf;
182         updateFilter();
183     }
184 
185     @Override
getOpacity()186     public int getOpacity() {
187         return PixelFormat.TRANSLUCENT;
188     }
189 
190     @Override
setAlpha(int alpha)191     public void setAlpha(int alpha) {
192         if (mAlpha != alpha) {
193             mAlpha = alpha;
194             mPaint.setAlpha(alpha);
195             invalidateSelf();
196             if (mBadge != null) {
197                 mBadge.setAlpha(alpha);
198             }
199         }
200     }
201 
202     @Override
setFilterBitmap(boolean filterBitmap)203     public void setFilterBitmap(boolean filterBitmap) {
204         mPaint.setFilterBitmap(filterBitmap);
205         mPaint.setAntiAlias(filterBitmap);
206     }
207 
208     @Override
getAlpha()209     public int getAlpha() {
210         return mAlpha;
211     }
212 
resetScale()213     public void resetScale() {
214         if (mScaleAnimation != null) {
215             mScaleAnimation.cancel();
216             mScaleAnimation = null;
217         }
218         mScale = 1;
219         invalidateSelf();
220     }
221 
getAnimatedScale()222     public float getAnimatedScale() {
223         return mScaleAnimation == null ? 1 : mScale;
224     }
225 
226     @Override
getIntrinsicWidth()227     public int getIntrinsicWidth() {
228         return mBitmapInfo.icon.getWidth();
229     }
230 
231     @Override
getIntrinsicHeight()232     public int getIntrinsicHeight() {
233         return mBitmapInfo.icon.getHeight();
234     }
235 
236     @Override
getMinimumWidth()237     public int getMinimumWidth() {
238         return getBounds().width();
239     }
240 
241     @Override
getMinimumHeight()242     public int getMinimumHeight() {
243         return getBounds().height();
244     }
245 
246     @Override
isStateful()247     public boolean isStateful() {
248         return true;
249     }
250 
251     @Override
getColorFilter()252     public ColorFilter getColorFilter() {
253         return mPaint.getColorFilter();
254     }
255 
256     @Override
onStateChange(int[] state)257     protected boolean onStateChange(int[] state) {
258         boolean isPressed = false;
259         boolean isHovered = false;
260         for (int s : state) {
261             if (s == android.R.attr.state_pressed) {
262                 isPressed = true;
263                 break;
264             } else if (sFlagHoverEnabled
265                     && s == android.R.attr.state_hovered
266                     && mHoverScaleEnabledForDisplay) {
267                 isHovered = true;
268                 // Do not break on hovered state, as pressed state should take precedence.
269             }
270         }
271         if (mIsPressed != isPressed || mIsHovered != isHovered) {
272             if (mScaleAnimation != null) {
273                 mScaleAnimation.cancel();
274             }
275 
276             float endScale = isPressed ? PRESSED_SCALE : (isHovered ? HOVERED_SCALE : 1f);
277             if (mScale != endScale) {
278                 if (isVisible()) {
279                     Interpolator interpolator =
280                             isPressed != mIsPressed ? (isPressed ? ACCEL : DEACCEL)
281                                     : HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR;
282                     int duration =
283                             isPressed != mIsPressed ? CLICK_FEEDBACK_DURATION
284                                     : HOVER_FEEDBACK_DURATION;
285                     mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, endScale);
286                     mScaleAnimation.setDuration(duration);
287                     mScaleAnimation.setInterpolator(interpolator);
288                     mScaleAnimation.start();
289                 } else {
290                     mScale = endScale;
291                     invalidateSelf();
292                 }
293             }
294             mIsPressed = isPressed;
295             mIsHovered = isHovered;
296             return true;
297         }
298         return false;
299     }
300 
setIsDisabled(boolean isDisabled)301     public void setIsDisabled(boolean isDisabled) {
302         if (mIsDisabled != isDisabled) {
303             mIsDisabled = isDisabled;
304             if (mBadge instanceof FastBitmapDrawable fbd) {
305                 fbd.setIsDisabled(isDisabled);
306             }
307             updateFilter();
308         }
309     }
310 
isDisabled()311     protected boolean isDisabled() {
312         return mIsDisabled;
313     }
314 
setBadge(Drawable badge)315     public void setBadge(Drawable badge) {
316         if (mBadge != null) {
317             mBadge.setCallback(null);
318         }
319         mBadge = badge;
320         if (mBadge != null) {
321             mBadge.setCallback(this);
322         }
323         updateBadgeBounds(getBounds());
324         updateFilter();
325     }
326 
327     @VisibleForTesting
getBadge()328     public Drawable getBadge() {
329         return mBadge;
330     }
331 
332     /**
333      * Updates the paint to reflect the current brightness and saturation.
334      */
updateFilter()335     protected void updateFilter() {
336         mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter(mDisabledAlpha) : mColorFilter);
337         if (mBadge != null) {
338             mBadge.setColorFilter(getColorFilter());
339         }
340         invalidateSelf();
341     }
342 
newConstantState()343     protected FastBitmapConstantState newConstantState() {
344         return new FastBitmapConstantState(mBitmapInfo);
345     }
346 
347     @Override
getConstantState()348     public final ConstantState getConstantState() {
349         FastBitmapConstantState cs = newConstantState();
350         cs.mIsDisabled = mIsDisabled;
351         if (mBadge != null) {
352             cs.mBadgeConstantState = mBadge.getConstantState();
353         }
354         cs.mCreationFlags = mCreationFlags;
355         return cs;
356     }
357 
getDisabledColorFilter()358     public static ColorFilter getDisabledColorFilter() {
359         return getDisabledColorFilter(1);
360     }
361 
362     // Returns if the FastBitmapDrawable contains a badge.
hasBadge()363     public boolean hasBadge() {
364         return (mCreationFlags & FLAG_NO_BADGE) == 0;
365     }
366 
getDisabledColorFilter(float disabledAlpha)367     private static ColorFilter getDisabledColorFilter(float disabledAlpha) {
368         ColorMatrix tempBrightnessMatrix = new ColorMatrix();
369         ColorMatrix tempFilterMatrix = new ColorMatrix();
370 
371         tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
372         float scale = 1 - DISABLED_BRIGHTNESS;
373         int brightnessI =   (int) (255 * DISABLED_BRIGHTNESS);
374         float[] mat = tempBrightnessMatrix.getArray();
375         mat[0] = scale;
376         mat[6] = scale;
377         mat[12] = scale;
378         mat[4] = brightnessI;
379         mat[9] = brightnessI;
380         mat[14] = brightnessI;
381         mat[18] = disabledAlpha;
382         tempFilterMatrix.preConcat(tempBrightnessMatrix);
383         return new ColorMatrixColorFilter(tempFilterMatrix);
384     }
385 
getDisabledColor(int color)386     protected static final int getDisabledColor(int color) {
387         int component = (Color.red(color) + Color.green(color) + Color.blue(color)) / 3;
388         float scale = 1 - DISABLED_BRIGHTNESS;
389         int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
390         component = Math.min(Math.round(scale * component + brightnessI), FULLY_OPAQUE);
391         return Color.rgb(component, component, component);
392     }
393 
394     /**
395      * Sets the bounds for the badge drawable based on the main icon bounds
396      */
setBadgeBounds(Drawable badge, Rect iconBounds)397     public static void setBadgeBounds(Drawable badge, Rect iconBounds) {
398         int size = getBadgeSizeForIconSize(iconBounds.width());
399         badge.setBounds(iconBounds.right - size, iconBounds.bottom - size,
400                 iconBounds.right, iconBounds.bottom);
401     }
402 
403     @Override
invalidateDrawable(Drawable who)404     public void invalidateDrawable(Drawable who) {
405         if (who == mBadge) {
406             invalidateSelf();
407         }
408     }
409 
410     @Override
scheduleDrawable(Drawable who, Runnable what, long when)411     public void scheduleDrawable(Drawable who, Runnable what, long when) {
412         if (who == mBadge) {
413             scheduleSelf(what, when);
414         }
415     }
416 
417     @Override
unscheduleDrawable(Drawable who, Runnable what)418     public void unscheduleDrawable(Drawable who, Runnable what) {
419         unscheduleSelf(what);
420     }
421 
422     /**
423      * Sets whether hover state functionality is enabled.
424      */
setFlagHoverEnabled(boolean isFlagHoverEnabled)425     public static void setFlagHoverEnabled(boolean isFlagHoverEnabled) {
426         sFlagHoverEnabled = isFlagHoverEnabled;
427     }
428 
setHoverScaleEnabledForDisplay(boolean hoverScaleEnabledForDisplay)429     public void setHoverScaleEnabledForDisplay(boolean hoverScaleEnabledForDisplay) {
430         mHoverScaleEnabledForDisplay = hoverScaleEnabledForDisplay;
431     }
432 
433     public static class FastBitmapConstantState extends ConstantState {
434         protected final BitmapInfo mBitmapInfo;
435 
436         // These are initialized later so that subclasses don't need to
437         // pass everything in constructor
438         protected boolean mIsDisabled;
439         private ConstantState mBadgeConstantState;
440 
441         @DrawableCreationFlags int mCreationFlags = 0;
442 
FastBitmapConstantState(Bitmap bitmap, int color)443         public FastBitmapConstantState(Bitmap bitmap, int color) {
444             this(BitmapInfo.of(bitmap, color));
445         }
446 
FastBitmapConstantState(BitmapInfo info)447         public FastBitmapConstantState(BitmapInfo info) {
448             mBitmapInfo = info;
449         }
450 
createDrawable()451         protected FastBitmapDrawable createDrawable() {
452             return new FastBitmapDrawable(mBitmapInfo);
453         }
454 
455         @Override
newDrawable()456         public final FastBitmapDrawable newDrawable() {
457             FastBitmapDrawable drawable = createDrawable();
458             drawable.setIsDisabled(mIsDisabled);
459             if (mBadgeConstantState != null) {
460                 drawable.setBadge(mBadgeConstantState.newDrawable());
461             }
462             drawable.mCreationFlags = mCreationFlags;
463             return drawable;
464         }
465 
466         @Override
getChangingConfigurations()467         public int getChangingConfigurations() {
468             return 0;
469         }
470     }
471 }
472