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