• 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.launcher3.anim.Interpolators.ACCEL_DEACCEL;
19 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
20 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorSet;
24 import android.animation.ArgbEvaluator;
25 import android.animation.Keyframe;
26 import android.animation.ObjectAnimator;
27 import android.animation.PropertyValuesHolder;
28 import android.animation.ValueAnimator;
29 import android.annotation.Nullable;
30 import android.content.Context;
31 import android.graphics.BlurMaskFilter;
32 import android.graphics.Canvas;
33 import android.graphics.Color;
34 import android.graphics.Matrix;
35 import android.graphics.Paint;
36 import android.graphics.Path;
37 import android.graphics.Rect;
38 import android.graphics.drawable.Drawable;
39 import android.os.Process;
40 import android.util.AttributeSet;
41 import android.util.FloatProperty;
42 import android.view.LayoutInflater;
43 import android.view.ViewGroup;
44 
45 import androidx.core.graphics.ColorUtils;
46 
47 import com.android.launcher3.CellLayout;
48 import com.android.launcher3.DeviceProfile;
49 import com.android.launcher3.Launcher;
50 import com.android.launcher3.LauncherSettings;
51 import com.android.launcher3.R;
52 import com.android.launcher3.anim.AnimatorListeners;
53 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
54 import com.android.launcher3.icons.BitmapInfo;
55 import com.android.launcher3.icons.GraphicsUtils;
56 import com.android.launcher3.icons.IconNormalizer;
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.ItemClickHandler;
61 import com.android.launcher3.touch.ItemLongClickListener;
62 import com.android.launcher3.util.SafeCloseable;
63 import com.android.launcher3.views.ActivityContext;
64 import com.android.launcher3.views.DoubleShadowBubbleTextView;
65 
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.List;
69 
70 /**
71  * A BubbleTextView with a ring around it's drawable
72  */
73 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
74 
75     private static final int RING_SHADOW_COLOR = 0x99000000;
76     private static final float RING_EFFECT_RATIO = 0.095f;
77 
78     private static final long ICON_CHANGE_ANIM_DURATION = 360;
79     private static final long ICON_CHANGE_ANIM_STAGGER = 50;
80 
81     boolean mIsDrawingDot = false;
82     private final DeviceProfile mDeviceProfile;
83     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
84     private final Path mRingPath = new Path();
85     private final int mNormalizedIconSize;
86     private final Path mShapePath;
87     private final Matrix mTmpMatrix = new Matrix();
88 
89     private final BlurMaskFilter mShadowFilter;
90 
91     private boolean mIsPinned = false;
92     private int mPlateColor;
93     boolean mDrawForDrag = false;
94 
95     // Used for the "slot-machine" education animation.
96     private List<Drawable> mSlotMachineIcons;
97     private Animator mSlotMachineAnim;
98     private float mSlotMachineIconTranslationY;
99 
100     private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
101             new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
102         @Override
103         public void setValue(PredictedAppIcon predictedAppIcon, float transY) {
104             predictedAppIcon.mSlotMachineIconTranslationY = transY;
105             predictedAppIcon.invalidate();
106         }
107 
108         @Override
109         public Float get(PredictedAppIcon predictedAppIcon) {
110             return predictedAppIcon.mSlotMachineIconTranslationY;
111         }
112     };
113 
PredictedAppIcon(Context context)114     public PredictedAppIcon(Context context) {
115         this(context, null, 0);
116     }
117 
PredictedAppIcon(Context context, AttributeSet attrs)118     public PredictedAppIcon(Context context, AttributeSet attrs) {
119         this(context, attrs, 0);
120     }
121 
PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)122     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
123         super(context, attrs, defStyle);
124         mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
125         mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
126         int shadowSize = context.getResources().getDimensionPixelSize(
127                 R.dimen.blur_size_thin_outline);
128         mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
129         mShapePath = GraphicsUtils.getShapePath(context, mNormalizedIconSize);
130     }
131 
132     @Override
onDraw(Canvas canvas)133     public void onDraw(Canvas canvas) {
134         int count = canvas.save();
135         boolean isSlotMachineAnimRunning = mSlotMachineAnim != null;
136         if (!mIsPinned) {
137             drawEffect(canvas);
138             if (isSlotMachineAnimRunning) {
139                 // Clip to to outside of the ring during the slot machine animation.
140                 canvas.clipPath(mRingPath);
141             }
142             canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
143             canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
144         }
145         if (isSlotMachineAnimRunning) {
146             drawSlotMachineIcons(canvas);
147         } else {
148             super.onDraw(canvas);
149         }
150         canvas.restoreToCount(count);
151     }
152 
drawSlotMachineIcons(Canvas canvas)153     private void drawSlotMachineIcons(Canvas canvas) {
154         canvas.translate((getWidth() - getIconSize()) / 2f,
155                 (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY);
156         for (Drawable icon : mSlotMachineIcons) {
157             icon.setBounds(0, 0, getIconSize(), getIconSize());
158             icon.draw(canvas);
159             canvas.translate(0, getSlotMachineIconPlusSpacingSize());
160         }
161     }
162 
getSlotMachineIconPlusSpacingSize()163     private float getSlotMachineIconPlusSpacingSize() {
164         return getIconSize() + getOutlineOffsetY();
165     }
166 
167     @Override
drawDotIfNecessary(Canvas canvas)168     protected void drawDotIfNecessary(Canvas canvas) {
169         mIsDrawingDot = true;
170         int count = canvas.save();
171         canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
172         canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
173         super.drawDotIfNecessary(canvas);
174         canvas.restoreToCount(count);
175         mIsDrawingDot = false;
176     }
177 
178     @Override
applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex)179     public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
180         // Create the slot machine animation first, since it uses the current icon to start.
181         Animator slotMachineAnim = animate
182                 ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false)
183                 : null;
184         super.applyFromWorkspaceItem(info, animate, staggerIndex);
185         int oldPlateColor = mPlateColor;
186         int newPlateColor = ColorUtils.setAlphaComponent(mDotParams.appColor, 200);
187         if (!animate) {
188             mPlateColor = newPlateColor;
189         }
190         if (mIsPinned) {
191             setContentDescription(info.contentDescription);
192         } else {
193             setContentDescription(
194                     getContext().getString(R.string.hotseat_prediction_content_description,
195                             info.contentDescription));
196         }
197 
198         if (animate) {
199             ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(),
200                     oldPlateColor, newPlateColor);
201             plateColorAnim.addUpdateListener(valueAnimator -> {
202                 mPlateColor = (int) valueAnimator.getAnimatedValue();
203                 invalidate();
204             });
205             AnimatorSet changeIconAnim = new AnimatorSet();
206             if (slotMachineAnim != null) {
207                 changeIconAnim.play(slotMachineAnim);
208             }
209             changeIconAnim.play(plateColorAnim);
210             changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
211             changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
212         }
213     }
214 
215     /**
216      * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
217      * and ending with the original icon.
218      */
createSlotMachineAnim(List<BitmapInfo> iconsToAnimate)219     public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) {
220         return createSlotMachineAnim(iconsToAnimate, true);
221     }
222 
223     /**
224      * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
225      * with the original icon, then cycling through the given icons, optionally ending back with
226      * the original icon.
227      * @param endWithOriginalIcon Whether we should land back on the icon we started with, rather
228      *                            than the last item in iconsToAnimate.
229      */
createSlotMachineAnim(List<BitmapInfo> iconsToAnimate, boolean endWithOriginalIcon)230     public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate,
231             boolean endWithOriginalIcon) {
232         if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) {
233             return null;
234         }
235         if (mSlotMachineAnim != null) {
236             mSlotMachineAnim.end();
237         }
238 
239         // Bookend the other animating icons with the original icon on both ends.
240         mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2);
241         mSlotMachineIcons.add(getIcon());
242         iconsToAnimate.stream()
243                 .map(iconInfo -> iconInfo.newIcon(mContext, FLAG_THEMED))
244                 .forEach(mSlotMachineIcons::add);
245         if (endWithOriginalIcon) {
246             mSlotMachineIcons.add(getIcon());
247         }
248 
249         float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1);
250         Keyframe[] keyframes = new Keyframe[] {
251                 Keyframe.ofFloat(0f, 0f),
252                 Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
253                 Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
254         };
255         keyframes[1].setInterpolator(ACCEL_DEACCEL);
256         keyframes[2].setInterpolator(ACCEL_DEACCEL);
257 
258         mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
259                 PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
260         mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
261             mSlotMachineIcons = null;
262             mSlotMachineAnim = null;
263             mSlotMachineIconTranslationY = 0;
264             invalidate();
265         }));
266         return mSlotMachineAnim;
267     }
268 
269     /**
270      * Removes prediction ring from app icon
271      */
pin(WorkspaceItemInfo info)272     public void pin(WorkspaceItemInfo info) {
273         if (mIsPinned) return;
274         mIsPinned = true;
275         applyFromWorkspaceItem(info);
276         setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
277         ((CellLayoutLayoutParams) getLayoutParams()).canReorder = true;
278         invalidate();
279     }
280 
281     /**
282      * prepares prediction icon for usage after bind
283      */
finishBinding(OnLongClickListener longClickListener)284     public void finishBinding(OnLongClickListener longClickListener) {
285         setOnLongClickListener(longClickListener);
286         ((CellLayoutLayoutParams) getLayoutParams()).canReorder = false;
287         setTextVisibility(false);
288         verifyHighRes();
289     }
290 
291     @Override
getIconBounds(Rect outBounds)292     public void getIconBounds(Rect outBounds) {
293         super.getIconBounds(outBounds);
294         if (!mIsPinned && !mIsDrawingDot) {
295             int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
296             outBounds.inset(predictionInset, predictionInset);
297         }
298     }
299 
isPinned()300     public boolean isPinned() {
301         return mIsPinned;
302     }
303 
getOutlineOffsetX()304     private int getOutlineOffsetX() {
305         return (getMeasuredWidth() - mNormalizedIconSize) / 2;
306     }
307 
getOutlineOffsetY()308     private int getOutlineOffsetY() {
309         if (mDisplay != DISPLAY_TASKBAR) {
310             return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
311         }
312         return (getMeasuredHeight() - mNormalizedIconSize) / 2;
313     }
314 
315     @Override
onSizeChanged(int w, int h, int oldw, int oldh)316     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
317         super.onSizeChanged(w, h, oldw, oldh);
318         updateRingPath();
319     }
320 
321     @Override
setTag(Object tag)322     public void setTag(Object tag) {
323         super.setTag(tag);
324         updateRingPath();
325     }
326 
updateRingPath()327     private void updateRingPath() {
328         boolean isBadged = false;
329         if (getTag() instanceof WorkspaceItemInfo) {
330             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
331             isBadged = !Process.myUserHandle().equals(info.user)
332                     || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
333                     || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
334         }
335 
336         mRingPath.reset();
337         mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
338 
339         mRingPath.addPath(mShapePath, mTmpMatrix);
340         if (isBadged) {
341             float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
342             float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
343             float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
344             float scale = badgeSize / mNormalizedIconSize;
345             mTmpMatrix.postTranslate(mNormalizedIconSize, mNormalizedIconSize);
346             mTmpMatrix.preScale(scale, scale);
347             mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
348             mRingPath.addPath(mShapePath, mTmpMatrix);
349         }
350     }
351 
drawEffect(Canvas canvas)352     private void drawEffect(Canvas canvas) {
353         // Don't draw ring effect if item is about to be dragged.
354         if (mDrawForDrag) {
355             return;
356         }
357         mIconRingPaint.setColor(RING_SHADOW_COLOR);
358         mIconRingPaint.setMaskFilter(mShadowFilter);
359         canvas.drawPath(mRingPath, mIconRingPaint);
360         mIconRingPaint.setColor(mPlateColor);
361         mIconRingPaint.setMaskFilter(null);
362         canvas.drawPath(mRingPath, mIconRingPaint);
363     }
364 
365     @Override
setIconDisabled(boolean isDisabled)366     public void setIconDisabled(boolean isDisabled) {
367         super.setIconDisabled(isDisabled);
368         mIconRingPaint.setColorFilter(isDisabled ? getDisabledColorFilter() : null);
369         invalidate();
370     }
371 
372     @Override
setItemInfo(ItemInfoWithIcon itemInfo)373     protected void setItemInfo(ItemInfoWithIcon itemInfo) {
374         super.setItemInfo(itemInfo);
375         setIconDisabled(itemInfo.isDisabled());
376     }
377 
378     @Override
getSourceVisualDragBounds(Rect bounds)379     public void getSourceVisualDragBounds(Rect bounds) {
380         super.getSourceVisualDragBounds(bounds);
381         if (!mIsPinned) {
382             int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
383             bounds.inset(internalSize, internalSize);
384         }
385     }
386 
387     @Override
prepareDrawDragView()388     public SafeCloseable prepareDrawDragView() {
389         mDrawForDrag = true;
390         invalidate();
391         SafeCloseable r = super.prepareDrawDragView();
392         return () -> {
393             r.close();
394             mDrawForDrag = false;
395         };
396     }
397 
398     /**
399      * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
400      */
createIcon(ViewGroup parent, WorkspaceItemInfo info)401     public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
402         PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
403                 .inflate(R.layout.predicted_app_icon, parent, false);
404         icon.applyFromWorkspaceItem(info);
405         icon.setOnClickListener(ItemClickHandler.INSTANCE);
406         icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
407         return icon;
408     }
409 
410     /**
411      * Draws Predicted Icon outline on cell layout
412      */
413     public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
414 
415         private final PredictedAppIcon mIcon;
416         private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
417 
PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)418         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
419             mDelegateCellX = cellX;
420             mDelegateCellY = cellY;
421             mIcon = icon;
422             mOutlinePaint.setStyle(Paint.Style.FILL);
423             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
424         }
425 
426         /**
427          * Draws predicted app icon outline under CellLayout
428          */
429         @Override
drawUnderItem(Canvas canvas)430         public void drawUnderItem(Canvas canvas) {
431             canvas.save();
432             canvas.translate(mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY());
433             canvas.drawPath(mIcon.mShapePath, mOutlinePaint);
434             canvas.restore();
435         }
436 
437         /**
438          * Draws PredictedAppIcon outline over CellLayout
439          */
440         @Override
drawOverItem(Canvas canvas)441         public void drawOverItem(Canvas canvas) {
442             // Does nothing
443         }
444     }
445 }
446