• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 package com.android.launcher3.uioverrides;
17 
18 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
19 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
20 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.animation.ArgbEvaluator;
26 import android.animation.Keyframe;
27 import android.animation.ObjectAnimator;
28 import android.animation.PropertyValuesHolder;
29 import android.content.Context;
30 import android.graphics.BlurMaskFilter;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.Matrix;
34 import android.graphics.Paint;
35 import android.graphics.Path;
36 import android.graphics.Rect;
37 import android.graphics.drawable.Drawable;
38 import android.util.AttributeSet;
39 import android.util.FloatProperty;
40 import android.util.Log;
41 import android.util.Property;
42 import android.view.LayoutInflater;
43 import android.view.ViewGroup;
44 
45 import androidx.core.graphics.ColorUtils;
46 
47 import com.android.launcher3.DeviceProfile;
48 import com.android.launcher3.Flags;
49 import com.android.launcher3.Launcher;
50 import com.android.launcher3.R;
51 import com.android.launcher3.anim.AnimatedFloat;
52 import com.android.launcher3.anim.AnimatorListeners;
53 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
54 import com.android.launcher3.celllayout.DelegatedCellDrawing;
55 import com.android.launcher3.graphics.ThemeManager;
56 import com.android.launcher3.icons.FastBitmapDrawable;
57 import com.android.launcher3.icons.LauncherIcons;
58 import com.android.launcher3.model.data.ItemInfoWithIcon;
59 import com.android.launcher3.model.data.WorkspaceItemInfo;
60 import com.android.launcher3.touch.ItemLongClickListener;
61 import com.android.launcher3.util.SafeCloseable;
62 import com.android.launcher3.views.ActivityContext;
63 import com.android.launcher3.views.DoubleShadowBubbleTextView;
64 
65 /**
66  * A BubbleTextView with a ring around it's drawable
67  */
68 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
69 
70     private static final float RING_SCALE_START_VALUE = 0.75f;
71     private static final int RING_SHADOW_COLOR = 0x99000000;
72     private static final float RING_EFFECT_RATIO = Flags.enableLauncherIconShapes() ? 0.1f : 0.095f;
73     private static final long ICON_CHANGE_ANIM_DURATION = 360;
74     private static final long ICON_CHANGE_ANIM_STAGGER = 50;
75 
76     private static final Property<PredictedAppIcon, Float> RING_SCALE_PROPERTY =
77             new Property<>(Float.TYPE, "ringScale") {
78                 @Override
79                 public Float get(PredictedAppIcon icon) {
80                     return icon.mRingScale;
81                 }
82 
83                 @Override
84                 public void set(PredictedAppIcon icon, Float value) {
85                     icon.mRingScale = value;
86                     icon.invalidate();
87                 }
88             };
89 
90     boolean mIsDrawingDot = false;
91     private final DeviceProfile mDeviceProfile;
92     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
93     private final Path mRingPath = new Path();
94     private final int mNormalizedIconSize;
95     private final Path mShapePath;
96     private final Matrix mTmpMatrix = new Matrix();
97 
98     private final BlurMaskFilter mShadowFilter;
99 
100     private boolean mIsPinned = false;
101     private final AnimColorHolder mPlateColor = new AnimColorHolder();
102     boolean mDrawForDrag = false;
103 
104     // Used for the "slot-machine" animation when prediction changes.
105     private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize());
106     private Drawable mSlotMachineIcon;
107     private float mSlotMachineIconTranslationY;
108 
109     // Used to animate the "ring" around predicted icons
110     private float mRingScale = 1f;
111     private boolean mForceHideRing = false;
112     private Animator mRingScaleAnim;
113 
114     private int mWidth;
115 
116     private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
117             new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
118         @Override
119         public void setValue(PredictedAppIcon predictedAppIcon, float transY) {
120             predictedAppIcon.mSlotMachineIconTranslationY = transY;
121             predictedAppIcon.invalidate();
122         }
123 
124         @Override
125         public Float get(PredictedAppIcon predictedAppIcon) {
126             return predictedAppIcon.mSlotMachineIconTranslationY;
127         }
128     };
129 
PredictedAppIcon(Context context)130     public PredictedAppIcon(Context context) {
131         this(context, null, 0);
132     }
133 
PredictedAppIcon(Context context, AttributeSet attrs)134     public PredictedAppIcon(Context context, AttributeSet attrs) {
135         this(context, attrs, 0);
136     }
137 
PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)138     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
139         super(context, attrs, defStyle);
140         mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
141         mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR);
142         int shadowSize = context.getResources().getDimensionPixelSize(
143                 R.dimen.blur_size_thin_outline);
144         mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
145         mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize);
146     }
147 
148     @Override
onDraw(Canvas canvas)149     public void onDraw(Canvas canvas) {
150         int count = canvas.save();
151         boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
152         if (!mIsPinned) {
153             drawRingEffect(canvas);
154             if (isSlotMachineAnimRunning) {
155                 // Clip to to outside of the ring during the slot machine animation.
156                 canvas.clipPath(mRingPath);
157             }
158             canvas.scale(1 - 2f * RING_EFFECT_RATIO, 1 - 2f * RING_EFFECT_RATIO,
159                     getWidth() * .5f, getHeight() * .5f);
160             if (isSlotMachineAnimRunning) {
161                 canvas.translate(0, mSlotMachineIconTranslationY);
162                 mSlotMachineIcon.setBounds(mSlotIconBound);
163                 mSlotMachineIcon.draw(canvas);
164                 canvas.translate(0, getSlotMachineIconPlusSpacingSize());
165             }
166         }
167         super.onDraw(canvas);
168         canvas.restoreToCount(count);
169     }
170 
getSlotMachineIconPlusSpacingSize()171     private float getSlotMachineIconPlusSpacingSize() {
172         return getIconSize() + getOutlineOffsetY();
173     }
174 
175     @Override
drawDotIfNecessary(Canvas canvas)176     protected void drawDotIfNecessary(Canvas canvas) {
177         mIsDrawingDot = true;
178         int count = canvas.save();
179         canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
180         canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
181         super.drawDotIfNecessary(canvas);
182         canvas.restoreToCount(count);
183         mIsDrawingDot = false;
184     }
185 
186     /**
187      * Returns whether the newInfo differs from the current getTag().
188      */
shouldAnimateIconChange(WorkspaceItemInfo newInfo)189     private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
190         boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo
191                 && oldInfo.getTargetComponent() != null
192                 && newInfo.getTargetComponent() != null
193                 && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
194         return changedIcons && isShown();
195     }
196 
197     @Override
applyIconAndLabel(ItemInfoWithIcon info)198     public void applyIconAndLabel(ItemInfoWithIcon info) {
199         super.applyIconAndLabel(info);
200         if (getIcon().isThemed()) {
201             mPlateColor.endColor = getResources().getColor(android.R.color.system_accent1_300);
202         } else {
203             float[] hctPlateColor = new float[3];
204             ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
205             mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
206         }
207         mPlateColor.onUpdate();
208     }
209 
210     /**
211      * Tries to apply the icon with animation and returns true if the icon was indeed animated
212      */
applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex)213     public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) {
214         boolean animate = shouldAnimateIconChange(info);
215         Drawable oldIcon = getIcon();
216         int oldPlateColor = mPlateColor.currentColor;
217         applyFromWorkspaceItem(info);
218 
219         setContentDescription(
220                 mIsPinned ? info.contentDescription :
221                         getContext().getString(R.string.hotseat_prediction_content_description,
222                                 info.contentDescription));
223 
224         if (!animate) {
225             mPlateColor.startColor = mPlateColor.endColor;
226             mPlateColor.progress.value = 1;
227             mPlateColor.onUpdate();
228         } else {
229             mPlateColor.startColor = oldPlateColor;
230             mPlateColor.progress.value = 0;
231             mPlateColor.onUpdate();
232 
233             AnimatorSet changeIconAnim = new AnimatorSet();
234 
235             ObjectAnimator plateColorAnim =
236                     ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1);
237             plateColorAnim.setAutoCancel(true);
238             changeIconAnim.play(plateColorAnim);
239 
240             if (!mIsPinned && oldIcon != null) {
241                 // Play the slot machine icon
242                 mSlotMachineIcon = oldIcon;
243 
244                 float finalTrans = -getSlotMachineIconPlusSpacingSize();
245                 Keyframe[] keyframes = new Keyframe[] {
246                         Keyframe.ofFloat(0f, 0f),
247                         Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
248                         Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
249                 };
250                 keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
251                 keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
252 
253                 ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
254                         PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
255                 slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
256                     mSlotMachineIcon = null;
257                     mSlotMachineIconTranslationY = 0;
258                     invalidate();
259                 }));
260                 slotMachineAnim.setAutoCancel(true);
261                 changeIconAnim.play(slotMachineAnim);
262             }
263 
264             changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
265             changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
266         }
267         return animate;
268     }
269 
270     /**
271      * Removes prediction ring from app icon
272      */
pin(WorkspaceItemInfo info)273     public void pin(WorkspaceItemInfo info) {
274         if (mIsPinned) return;
275         mIsPinned = true;
276         applyFromWorkspaceItem(info);
277         setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
278         ((CellLayoutLayoutParams) getLayoutParams()).canReorder = true;
279         invalidate();
280     }
281 
282     /**
283      * prepares prediction icon for usage after bind
284      */
finishBinding(OnLongClickListener longClickListener)285     public void finishBinding(OnLongClickListener longClickListener) {
286         setOnLongClickListener(longClickListener);
287         ((CellLayoutLayoutParams) getLayoutParams()).canReorder = false;
288         setTextVisibility(false);
289         verifyHighRes();
290     }
291 
292     @Override
getIconBounds(Rect outBounds)293     public void getIconBounds(Rect outBounds) {
294         super.getIconBounds(outBounds);
295         if (!mIsPinned && !mIsDrawingDot) {
296             int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
297             outBounds.inset(predictionInset, predictionInset);
298         }
299     }
300 
isPinned()301     public boolean isPinned() {
302         return mIsPinned;
303     }
304 
getOutlineOffsetX()305     private int getOutlineOffsetX() {
306         int measuredWidth = getMeasuredWidth();
307         if (mDisplay != DISPLAY_TASKBAR) {
308             Log.d("b/387844520", "getOutlineOffsetX: measured width = " + measuredWidth
309                     + ", mNormalizedIconSize = " + mNormalizedIconSize
310                     + ", last updated width = " + mWidth);
311         }
312         return (mWidth - mNormalizedIconSize) / 2;
313     }
314 
getOutlineOffsetY()315     private int getOutlineOffsetY() {
316         if (mDisplay != DISPLAY_TASKBAR) {
317             return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
318         }
319         return (getMeasuredHeight() - mNormalizedIconSize) / 2;
320     }
321 
322     @Override
onSizeChanged(int w, int h, int oldw, int oldh)323     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
324         super.onSizeChanged(w, h, oldw, oldh);
325         mWidth = w;
326         mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
327         if (mDisplay != DISPLAY_TASKBAR) {
328             Log.d("b/387844520", "calling updateRingPath from onSizeChanged");
329         }
330         updateRingPath();
331     }
332 
333     @Override
setTag(Object tag)334     public void setTag(Object tag) {
335         super.setTag(tag);
336         updateRingPath();
337     }
338 
updateRingPath()339     private void updateRingPath() {
340         mRingPath.reset();
341         mTmpMatrix.reset();
342         mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
343         mRingPath.addPath(mShapePath, mTmpMatrix);
344 
345         FastBitmapDrawable icon = getIcon();
346         if (icon != null && icon.getBadge() != null) {
347             float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
348             float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
349             float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
350             float scale = badgeSize / mNormalizedIconSize;
351             mTmpMatrix.postTranslate(mNormalizedIconSize, mNormalizedIconSize);
352             mTmpMatrix.preScale(scale, scale);
353             mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
354             mRingPath.addPath(mShapePath, mTmpMatrix);
355         }
356         invalidate();
357     }
358 
359     @Override
setForceHideRing(boolean forceHideRing)360     public void setForceHideRing(boolean forceHideRing) {
361         if (mForceHideRing == forceHideRing) {
362             return;
363         }
364         mForceHideRing = forceHideRing;
365 
366         if (forceHideRing) {
367             invalidate();
368         } else {
369             animateRingScale(RING_SCALE_START_VALUE, 1);
370         }
371     }
372 
cancelRingScaleAnim()373     private void cancelRingScaleAnim() {
374         if (mRingScaleAnim != null) {
375             mRingScaleAnim.cancel();
376         }
377     }
378 
animateRingScale(float... ringScale)379     private void animateRingScale(float... ringScale) {
380         cancelRingScaleAnim();
381         mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale);
382         mRingScaleAnim.addListener(new AnimatorListenerAdapter() {
383             @Override
384             public void onAnimationEnd(Animator animation) {
385                 mRingScaleAnim = null;
386             }
387         });
388         mRingScaleAnim.start();
389     }
390 
drawRingEffect(Canvas canvas)391     private void drawRingEffect(Canvas canvas) {
392         // Don't draw ring effect if item is about to be dragged or if the icon is not visible.
393         if (mDrawForDrag || !mIsIconVisible || mForceHideRing) {
394             return;
395         }
396         mIconRingPaint.setColor(RING_SHADOW_COLOR);
397         mIconRingPaint.setMaskFilter(mShadowFilter);
398         int count = canvas.save();
399         if (Flags.enableLauncherIconShapes()) {
400             // Scale canvas properly to for ring to be inner stroke and not exceed bounds.
401             // Since STROKE draws half on either side of Path, scale canvas down by 1x stroke ratio.
402             canvas.scale(
403                     mRingScale * (1f - RING_EFFECT_RATIO),
404                     mRingScale * (1f - RING_EFFECT_RATIO),
405                     getWidth() / 2f,
406                     getHeight() / 2f);
407         } else if (Float.compare(1, mRingScale) != 0) {
408             canvas.scale(mRingScale, mRingScale, getWidth() / 2f, getHeight() / 2f);
409         }
410         // Draw ring shadow around canvas.
411         canvas.drawPath(mRingPath, mIconRingPaint);
412         mIconRingPaint.setColor(mPlateColor.currentColor);
413         if (Flags.enableLauncherIconShapes()) {
414             mIconRingPaint.setStrokeWidth(getWidth() * RING_EFFECT_RATIO);
415             // Using FILL_AND_STROKE as there is still some gap to fill,
416             // between inner curve of ring / outer curve of icon.
417             mIconRingPaint.setStyle(Paint.Style.FILL_AND_STROKE);
418         }
419         mIconRingPaint.setMaskFilter(null);
420         // Draw ring around canvas.
421         canvas.drawPath(mRingPath, mIconRingPaint);
422         canvas.restoreToCount(count);
423     }
424 
425     @Override
setIconDisabled(boolean isDisabled)426     public void setIconDisabled(boolean isDisabled) {
427         super.setIconDisabled(isDisabled);
428         mIconRingPaint.setColorFilter(isDisabled ? getDisabledColorFilter() : null);
429         invalidate();
430     }
431 
432     @Override
setItemInfo(ItemInfoWithIcon itemInfo)433     protected void setItemInfo(ItemInfoWithIcon itemInfo) {
434         super.setItemInfo(itemInfo);
435         setIconDisabled(itemInfo.isDisabled());
436     }
437 
438     @Override
getSourceVisualDragBounds(Rect bounds)439     public void getSourceVisualDragBounds(Rect bounds) {
440         super.getSourceVisualDragBounds(bounds);
441         if (!mIsPinned) {
442             int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
443             bounds.inset(internalSize, internalSize);
444         }
445     }
446 
447     @Override
prepareDrawDragView()448     public SafeCloseable prepareDrawDragView() {
449         mDrawForDrag = true;
450         invalidate();
451         SafeCloseable r = super.prepareDrawDragView();
452         return () -> {
453             r.close();
454             mDrawForDrag = false;
455         };
456     }
457 
458     /**
459      * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
460      */
createIcon(ViewGroup parent, WorkspaceItemInfo info)461     public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
462         PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
463                 .inflate(R.layout.predicted_app_icon, parent, false);
464         icon.applyFromWorkspaceItem(info);
465         Launcher launcher = Launcher.getLauncher(parent.getContext());
466         icon.setOnClickListener(launcher.getItemOnClickListener());
467         icon.setOnFocusChangeListener(launcher.getFocusHandler());
468         return icon;
469     }
470 
471     private class AnimColorHolder {
472 
473         public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1);
474         public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
475         public Integer startColor = 0;
476         public Integer endColor = 0;
477 
478         public int currentColor = 0;
479 
onUpdate()480         private void onUpdate() {
481             currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor);
482             invalidate();
483         }
484     }
485 
486     /**
487      * Draws Predicted Icon outline on cell layout
488      */
489     public static class PredictedIconOutlineDrawing extends DelegatedCellDrawing {
490 
491         private final PredictedAppIcon mIcon;
492         private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
493 
PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)494         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
495             mDelegateCellX = cellX;
496             mDelegateCellY = cellY;
497             mIcon = icon;
498             mOutlinePaint.setStyle(Paint.Style.FILL);
499             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
500         }
501 
502         /**
503          * Draws predicted app icon outline under CellLayout
504          */
505         @Override
drawUnderItem(Canvas canvas)506         public void drawUnderItem(Canvas canvas) {
507             canvas.save();
508             canvas.translate(mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY());
509             canvas.drawPath(mIcon.mShapePath, mOutlinePaint);
510             canvas.restore();
511         }
512 
513         /**
514          * Draws PredictedAppIcon outline over CellLayout
515          */
516         @Override
drawOverItem(Canvas canvas)517         public void drawOverItem(Canvas canvas) {
518             // Does nothing
519         }
520     }
521 }
522