• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.taskbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.graphics.BlendMode;
26 import android.graphics.BlendModeColorFilter;
27 import android.graphics.Canvas;
28 import android.graphics.Paint;
29 import android.graphics.drawable.Drawable;
30 import android.util.AttributeSet;
31 import android.util.FloatProperty;
32 import android.util.IntProperty;
33 import android.view.LayoutInflater;
34 import android.view.ViewGroup;
35 import android.widget.FrameLayout;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.VisibleForTesting;
39 import androidx.core.graphics.ColorUtils;
40 
41 import com.android.app.animation.Interpolators;
42 import com.android.launcher3.Reorderable;
43 import com.android.launcher3.Utilities;
44 import com.android.launcher3.icons.IconNormalizer;
45 import com.android.launcher3.util.MultiTranslateDelegate;
46 import com.android.launcher3.util.Themes;
47 import com.android.systemui.shared.recents.model.Task;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * View used as overflow icon within task bar, when the list of recent/running apps overflows the
54  * available display bounds - if display is not wide enough to show all running apps in the taskbar,
55  * this icon is added to the taskbar as an entry point to open UI that surfaces all running apps.
56  * The icon contains icon representations of up to 4 more recent tasks in overflow, stacked on top
57  * each other in counter clockwise manner (icons of tasks partially overlapping with each other).
58  */
59 public class TaskbarOverflowView extends FrameLayout implements Reorderable {
60     private static final int ALPHA_TRANSPARENT = 0;
61     private static final int ALPHA_OPAQUE = 255;
62     private static final long ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND = 300L;
63     private static final long ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS = 500L;
64     private static final long ANIMATION_SET_DURATION = 1000L;
65     private static final long ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION = 500L;
66     private static final long ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION = 600L;
67     private static final long ITEM_ICON_SIZE_ANIMATION_DURATION = 500L;
68     private static final long ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION = 500L;
69     private static final long LEAVE_BEHIND_ANIMATIONS_DELAY = 500L;
70     private static final long LEAVE_BEHIND_OPACITY_ANIMATION_DURATION = 100L;
71     private static final long LEAVE_BEHIND_SIZE_ANIMATION_DURATION = 500L;
72     private static final float LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER = 0.83f;
73     private static final int MAX_ITEMS_IN_PREVIEW = 4;
74 
75     // The height divided by the width of the horizontal box containing two overlapping app icons.
76     // According to the spec, this ratio is constant for different sizes of taskbar app icons.
77     // Assuming the width of this box = taskbar app icon size - 2 paddings - 2 stroke widths, and
78     // the height = width * 0.61, which is also equal to the height of a single item in the preview.
79     private static final float TWO_ITEM_ICONS_BOX_ASPECT_RATIO = 0.61f;
80 
81     private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_CENTER_OFFSET =
82             new FloatProperty<>("itemIconCenterOffset") {
83                 @Override
84                 public Float get(TaskbarOverflowView view) {
85                     return view.mItemIconCenterOffset;
86                 }
87 
88                 @Override
89                 public void setValue(TaskbarOverflowView view, float value) {
90                     view.mItemIconCenterOffset = value;
91                     view.invalidate();
92                 }
93             };
94 
95     private static final IntProperty<TaskbarOverflowView> ITEM_ICON_COLOR_FILTER_OPACITY =
96             new IntProperty<>("itemIconColorFilterOpacity") {
97                 @Override
98                 public Integer get(TaskbarOverflowView view) {
99                     return view.mItemIconColorFilterOpacity;
100                 }
101 
102                 @Override
103                 public void setValue(TaskbarOverflowView view, int value) {
104                     view.mItemIconColorFilterOpacity = value;
105                     view.invalidate();
106                 }
107             };
108 
109     private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_SIZE =
110             new FloatProperty<>("itemIconSize") {
111                 @Override
112                 public Float get(TaskbarOverflowView view) {
113                     return view.mItemIconSize;
114                 }
115 
116                 @Override
117                 public void setValue(TaskbarOverflowView view, float value) {
118                     view.mItemIconSize = value;
119                     view.invalidate();
120                 }
121             };
122 
123     private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_STROKE_WIDTH =
124             new FloatProperty<>("itemIconStrokeWidth") {
125                 @Override
126                 public Float get(TaskbarOverflowView view) {
127                     return view.mItemIconStrokeWidth;
128                 }
129 
130                 @Override
131                 public void setValue(TaskbarOverflowView view, float value) {
132                     view.mItemIconStrokeWidth = value;
133                     view.invalidate();
134                 }
135             };
136 
137     private static final IntProperty<TaskbarOverflowView> LEAVE_BEHIND_OPACITY =
138             new IntProperty<>("leaveBehindOpacity") {
139                 @Override
140                 public Integer get(TaskbarOverflowView view) {
141                     return view.mLeaveBehindOpacity;
142                 }
143 
144                 @Override
145                 public void setValue(TaskbarOverflowView view, int value) {
146                     view.mLeaveBehindOpacity = value;
147                     view.invalidate();
148                 }
149             };
150 
151     private static final FloatProperty<TaskbarOverflowView> LEAVE_BEHIND_SIZE =
152             new FloatProperty<>("leaveBehindSize") {
153                 @Override
154                 public Float get(TaskbarOverflowView view) {
155                     return view.mLeaveBehindSize;
156                 }
157 
158                 @Override
159                 public void setValue(TaskbarOverflowView view, float value) {
160                     view.mLeaveBehindSize = value;
161                     view.invalidate();
162                 }
163             };
164 
165     private boolean mIsRtlLayout;
166     private final List<Task> mItems = new ArrayList<Task>();
167     private int mIconSize;
168     private Paint mItemBackgroundPaint;
169     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
170     private float mScaleForReorderBounce = 1f;
171     private int mItemBackgroundColor;
172     private int mLeaveBehindColor;
173 
174     // Active means the overflow icon has been pressed, which replaces the app icons with the
175     // leave-behind circle and shows the KQS UI.
176     private boolean mIsActive = false;
177     private ValueAnimator mStateTransitionAnimationWrapper;
178 
179     private float mItemIconCenterOffsetDefault;
180     private float mItemIconCenterOffset;  // [0..mItemIconCenterOffsetDefault]
181     private int mItemIconColorFilterOpacity;  // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
182     private float mItemIconSizeDefault;
183     private float mItemIconSizeScaledDown;
184     private float mItemIconSize;  // [mItemIconSizeScaledDown..mItemIconSizeDefault]
185     private float mItemIconStrokeWidthDefault;
186     private float mItemIconStrokeWidth;  // [0..mItemIconStrokeWidthDefault]
187     private int mLeaveBehindOpacity;  // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
188     private float mLeaveBehindSizeScaledDown;
189     private float mLeaveBehindSizeDefault;
190     private float mLeaveBehindSize;  // [mLeaveBehindSizeScaledDown..mLeaveBehindSizeDefault]
191 
TaskbarOverflowView(Context context, AttributeSet attrs)192     public TaskbarOverflowView(Context context, AttributeSet attrs) {
193         super(context, attrs);
194         init();
195     }
196 
TaskbarOverflowView(Context context)197     public TaskbarOverflowView(Context context) {
198         super(context);
199         init();
200     }
201 
202     /**
203      * Inflates the taskbar overflow button view.
204      * @param resId The resource to inflate the view from.
205      * @param group The parent view.
206      * @param iconSize The size of the overflow button icon.
207      * @param padding The internal padding of the overflow view.
208      * @return A taskbar overflow button.
209      */
inflateIcon(int resId, ViewGroup group, int iconSize, int padding)210     public static TaskbarOverflowView inflateIcon(int resId, ViewGroup group, int iconSize,
211             int padding) {
212         LayoutInflater inflater = LayoutInflater.from(group.getContext());
213         TaskbarOverflowView icon = (TaskbarOverflowView) inflater.inflate(resId, group, false);
214 
215         icon.mIconSize = iconSize;
216 
217         final float taskbarIconRadius =
218                 (iconSize - padding * 2f) * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f;
219 
220         icon.mLeaveBehindSizeDefault = taskbarIconRadius;  // 1/2 of taskbar app icon size
221         icon.mLeaveBehindSizeScaledDown =
222                 icon.mLeaveBehindSizeDefault * LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER;
223         icon.mLeaveBehindSize = icon.mLeaveBehindSizeScaledDown;
224 
225         icon.mItemIconStrokeWidthDefault =
226                 taskbarIconRadius / 10f;  // 1/20 of taskbar app icon size
227         icon.mItemIconStrokeWidth = icon.mItemIconStrokeWidthDefault;
228 
229         icon.mItemIconSizeDefault = 2f * taskbarIconRadius * TWO_ITEM_ICONS_BOX_ASPECT_RATIO;
230         icon.mItemIconSizeScaledDown = icon.mLeaveBehindSizeScaledDown;
231         icon.mItemIconSize = icon.mItemIconSizeDefault;
232 
233         icon.mItemIconCenterOffsetDefault = taskbarIconRadius
234                 - icon.mItemIconSizeDefault * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f
235                 - icon.mItemIconStrokeWidthDefault;
236         icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault;
237 
238         return icon;
239     }
240 
init()241     private void init() {
242         mIsRtlLayout = Utilities.isRtl(getResources());
243         mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
244         mItemBackgroundColor = getContext().getColor(
245                 com.android.internal.R.color.materialColorInverseOnSurface);
246         mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
247 
248         setWillNotDraw(false);
249     }
250 
251     @Override
onDraw(@onNull Canvas canvas)252     protected void onDraw(@NonNull Canvas canvas) {
253         super.onDraw(canvas);
254 
255         drawAppIcons(canvas);
256         drawLeaveBehindCircle(canvas);
257     }
258 
drawAppIcons(@onNull Canvas canvas)259     private void drawAppIcons(@NonNull Canvas canvas) {
260         mItemBackgroundPaint.setColor(mItemBackgroundColor);
261         float canvasCenterXY = mIconSize / 2f;
262         int adjustedItemIconSize = Math.round(mItemIconSize);
263         float itemIconRadius = adjustedItemIconSize / 2f;
264 
265         int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW);
266         for (int i = itemsToShow - 1; i >= 0; --i) {
267             Drawable icon = mItems.get(mItems.size() - i - 1).icon;
268             if (icon == null) {
269                 continue;
270             }
271 
272             float itemCenterX = getItemXOffset(mItemIconCenterOffset, mIsRtlLayout, i, itemsToShow);
273             float itemCenterY = getItemYOffset(mItemIconCenterOffset, i, itemsToShow);
274 
275             Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
276             iconCopy.setBounds(0, 0, adjustedItemIconSize, adjustedItemIconSize);
277             iconCopy.setColorFilter(new BlendModeColorFilter(
278                     ColorUtils.setAlphaComponent(mLeaveBehindColor, mItemIconColorFilterOpacity),
279                     BlendMode.SRC_ATOP));
280 
281             canvas.save();
282             canvas.translate(
283                     canvasCenterXY + itemCenterX - itemIconRadius,
284                     canvasCenterXY + itemCenterY - itemIconRadius);
285             canvas.drawCircle(itemIconRadius, itemIconRadius,
286                     itemIconRadius * IconNormalizer.ICON_VISIBLE_AREA_FACTOR + mItemIconStrokeWidth,
287                     mItemBackgroundPaint);
288             iconCopy.draw(canvas);
289             canvas.restore();
290         }
291     }
292 
drawLeaveBehindCircle(@onNull Canvas canvas)293     private void drawLeaveBehindCircle(@NonNull Canvas canvas) {
294         mItemBackgroundPaint.setColor(
295                 ColorUtils.setAlphaComponent(mLeaveBehindColor, mLeaveBehindOpacity));
296 
297         final float xyCenter = mIconSize / 2f;
298         canvas.drawCircle(xyCenter, xyCenter, mLeaveBehindSize / 2f, mItemBackgroundPaint);
299     }
300 
301     /**
302      * Clears the list of tasks tracked by the view.
303      */
clearItems()304     public void clearItems() {
305         mItems.clear();
306         invalidate();
307     }
308 
309     /**
310      * Update the view to represent a new list of recent tasks.
311      * @param items Items to be shown in the view.
312      */
setItems(List<Task> items)313     public void setItems(List<Task> items) {
314         mItems.clear();
315         mItems.addAll(items);
316         invalidate();
317     }
318 
319     @VisibleForTesting
getItemIds()320     public List<Integer> getItemIds() {
321         return mItems.stream().map(task -> task.key.id).toList();
322     }
323 
324     /**
325      * Called when a task is updated. If the task is contained within the view, it's cached value
326      * gets updated. If the task is shown within the icon, invalidates the view, so the task icon
327      * gets updated.
328      * @param task The updated task.
329      */
updateTaskIsShown(Task task)330     public void updateTaskIsShown(Task task) {
331         for (int i = 0; i < mItems.size(); ++i) {
332             if (mItems.get(i).key.id == task.key.id) {
333                 mItems.set(i, task);
334                 if (i >= mItems.size() - MAX_ITEMS_IN_PREVIEW) {
335                     invalidate();
336                 }
337                 break;
338             }
339         }
340     }
341 
342     /**
343      * Returns the view's state (whether it shows a set of app icons or a leave-behind circle).
344      */
getIsActive()345     public boolean getIsActive() {
346         return mIsActive;
347     }
348 
349     /**
350      * Updates the view's state to draw either a set of app icons or a leave-behind circle.
351      * @param isActive The next state of the view.
352      */
setIsActive(boolean isActive)353     public void setIsActive(boolean isActive) {
354         if (mIsActive == isActive) {
355             return;
356         }
357         mIsActive = isActive;
358 
359         if (mStateTransitionAnimationWrapper != null
360                 && mStateTransitionAnimationWrapper.isRunning()) {
361             mStateTransitionAnimationWrapper.reverse();
362             return;
363         }
364 
365         final AnimatorSet stateTransitionAnimation = getStateTransitionAnimation();
366         mStateTransitionAnimationWrapper = ValueAnimator.ofFloat(0, 1f);
367         mStateTransitionAnimationWrapper.setDuration(mIsActive
368                 ? ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND
369                 : ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS);
370         mStateTransitionAnimationWrapper.setInterpolator(
371                 mIsActive ? Interpolators.STANDARD : Interpolators.EMPHASIZED);
372         mStateTransitionAnimationWrapper.addListener(new AnimatorListenerAdapter() {
373             @Override
374             public void onAnimationEnd(Animator animation) {
375                 mStateTransitionAnimationWrapper = null;
376             }
377         });
378         mStateTransitionAnimationWrapper.addUpdateListener(
379                 new ValueAnimator.AnimatorUpdateListener() {
380                     @Override
381                     public void onAnimationUpdate(ValueAnimator animator) {
382                         stateTransitionAnimation.setCurrentPlayTime(
383                                 (long) (ANIMATION_SET_DURATION * animator.getAnimatedFraction()));
384                     }
385                 });
386         mStateTransitionAnimationWrapper.start();
387     }
388 
getStateTransitionAnimation()389     private AnimatorSet getStateTransitionAnimation() {
390         final AnimatorSet animation = new AnimatorSet();
391         animation.setInterpolator(Interpolators.LINEAR);
392         animation.playTogether(
393                 buildAnimator(ITEM_ICON_CENTER_OFFSET, 0f, mItemIconCenterOffsetDefault,
394                         ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION, 0L,
395                         ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION),
396                 buildAnimator(ITEM_ICON_COLOR_FILTER_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
397                         ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION, 0L,
398                         ANIMATION_SET_DURATION - ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION),
399                 buildAnimator(ITEM_ICON_SIZE, mItemIconSizeScaledDown, mItemIconSizeDefault,
400                         ITEM_ICON_SIZE_ANIMATION_DURATION, 0L,
401                         ITEM_ICON_SIZE_ANIMATION_DURATION),
402                 buildAnimator(ITEM_ICON_STROKE_WIDTH, 0f, mItemIconStrokeWidthDefault,
403                         ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION, 0L,
404                         ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION),
405                 buildAnimator(LEAVE_BEHIND_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
406                         LEAVE_BEHIND_OPACITY_ANIMATION_DURATION, LEAVE_BEHIND_ANIMATIONS_DELAY,
407                         ANIMATION_SET_DURATION - LEAVE_BEHIND_ANIMATIONS_DELAY
408                                 - LEAVE_BEHIND_OPACITY_ANIMATION_DURATION),
409                 buildAnimator(LEAVE_BEHIND_SIZE, mLeaveBehindSizeDefault,
410                         mLeaveBehindSizeScaledDown, LEAVE_BEHIND_SIZE_ANIMATION_DURATION,
411                         LEAVE_BEHIND_ANIMATIONS_DELAY, 0L)
412         );
413         return animation;
414     }
415 
buildAnimator(IntProperty<TaskbarOverflowView> property, int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons, long duration, long delayWhenAnimatingToLeaveBehind, long delayWhenAnimatingToAppIcons)416     private ObjectAnimator buildAnimator(IntProperty<TaskbarOverflowView> property,
417             int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons,
418             long duration, long delayWhenAnimatingToLeaveBehind,
419             long delayWhenAnimatingToAppIcons) {
420         final ObjectAnimator animator = ObjectAnimator.ofInt(this, property,
421                 mIsActive ? finalValueWhenAnimatingToLeaveBehind
422                         : finalValueWhenAnimatingToAppIcons);
423         applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
424                 delayWhenAnimatingToAppIcons);
425         return animator;
426     }
427 
buildAnimator(FloatProperty<TaskbarOverflowView> property, float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons, long duration, long delayWhenAnimatingToLeaveBehind, long delayWhenAnimatingToAppIcons)428     private ObjectAnimator buildAnimator(FloatProperty<TaskbarOverflowView> property,
429             float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons,
430             long duration, long delayWhenAnimatingToLeaveBehind,
431             long delayWhenAnimatingToAppIcons) {
432         final ObjectAnimator animator = ObjectAnimator.ofFloat(this, property,
433                 mIsActive ? finalValueWhenAnimatingToLeaveBehind
434                         : finalValueWhenAnimatingToAppIcons);
435         applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
436                 delayWhenAnimatingToAppIcons);
437         return animator;
438     }
439 
applyTiming(ObjectAnimator animator, long duration, long delayWhenAnimatingToLeaveBehind, long delayWhenAnimatingToAppIcons)440     private void applyTiming(ObjectAnimator animator, long duration,
441             long delayWhenAnimatingToLeaveBehind,
442             long delayWhenAnimatingToAppIcons) {
443         animator.setDuration(duration);
444         animator.setStartDelay(
445                 mIsActive ? delayWhenAnimatingToLeaveBehind : delayWhenAnimatingToAppIcons);
446     }
447 
448     @Override
getTranslateDelegate()449     public MultiTranslateDelegate getTranslateDelegate() {
450         return mTranslateDelegate;
451     }
452 
453     @Override
getReorderBounceScale()454     public float getReorderBounceScale() {
455         return mScaleForReorderBounce;
456     }
457 
458     @Override
setReorderBounceScale(float scale)459     public void setReorderBounceScale(float scale) {
460         mScaleForReorderBounce = scale;
461         super.setScaleX(scale);
462         super.setScaleY(scale);
463     }
464 
getItemXOffset(float baseOffset, boolean isRtl, int itemIndex, int itemCount)465     private float getItemXOffset(float baseOffset, boolean isRtl, int itemIndex, int itemCount) {
466         // Item with index 1 is on the left in all cases.
467         if (itemIndex == 1) {
468             return (isRtl ? 1 : -1) * baseOffset;
469         }
470 
471         // First item is centered if total number of items shown is 3, on the right otherwise.
472         if (itemIndex == 0) {
473             if (itemCount == 3) {
474                 return 0;
475             }
476             return (isRtl ? -1 : 1) * baseOffset;
477         }
478 
479         // Last item is on the right when there are more than 2 items (case which is already handled
480         // as `itemIndex == 1`).
481         if (itemIndex == itemCount - 1) {
482             return (isRtl ? -1 : 1) * baseOffset;
483         }
484 
485         return (isRtl ? 1 : -1) * baseOffset;
486     }
487 
getItemYOffset(float baseOffset, int itemIndex, int itemCount)488     private float getItemYOffset(float baseOffset, int itemIndex, int itemCount) {
489         // If icon contains two items, they are both centered vertically.
490         if (itemCount == 2) {
491             return 0;
492         }
493         // First half of items is on top, later half is on bottom.
494         return (itemIndex + 1 <= itemCount / 2 ? -1 : 1) * baseOffset;
495     }
496 }
497