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; 18 19 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; 20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.ObjectAnimator; 25 import android.content.Context; 26 import android.content.res.ColorStateList; 27 import android.content.res.TypedArray; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.graphics.drawable.ColorDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.text.TextUtils.TruncateAt; 36 import android.util.AttributeSet; 37 import android.util.Property; 38 import android.util.TypedValue; 39 import android.view.KeyEvent; 40 import android.view.MotionEvent; 41 import android.view.View; 42 import android.view.ViewDebug; 43 import android.widget.TextView; 44 45 import androidx.annotation.Nullable; 46 import androidx.annotation.UiThread; 47 48 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; 49 import com.android.launcher3.dot.DotInfo; 50 import com.android.launcher3.dragndrop.DraggableView; 51 import com.android.launcher3.folder.FolderIcon; 52 import com.android.launcher3.graphics.IconPalette; 53 import com.android.launcher3.graphics.IconShape; 54 import com.android.launcher3.graphics.PreloadIconDrawable; 55 import com.android.launcher3.icons.DotRenderer; 56 import com.android.launcher3.icons.FastBitmapDrawable; 57 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 58 import com.android.launcher3.icons.PlaceHolderIconDrawable; 59 import com.android.launcher3.icons.cache.HandlerRunnable; 60 import com.android.launcher3.model.data.AppInfo; 61 import com.android.launcher3.model.data.ItemInfo; 62 import com.android.launcher3.model.data.ItemInfoWithIcon; 63 import com.android.launcher3.model.data.PackageItemInfo; 64 import com.android.launcher3.model.data.SearchActionItemInfo; 65 import com.android.launcher3.model.data.WorkspaceItemInfo; 66 import com.android.launcher3.util.SafeCloseable; 67 import com.android.launcher3.views.ActivityContext; 68 import com.android.launcher3.views.IconLabelDotView; 69 70 import java.text.NumberFormat; 71 72 /** 73 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 74 * because we want to make the bubble taller than the text and TextView's clip is 75 * too aggressive. 76 */ 77 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 78 IconLabelDotView, DraggableView, Reorderable { 79 80 private static final int DISPLAY_WORKSPACE = 0; 81 private static final int DISPLAY_ALL_APPS = 1; 82 private static final int DISPLAY_FOLDER = 2; 83 protected static final int DISPLAY_TASKBAR = 5; 84 private static final int DISPLAY_SEARCH_RESULT = 6; 85 private static final int DISPLAY_SEARCH_RESULT_SMALL = 7; 86 87 private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; 88 private static final float HIGHLIGHT_SCALE = 1.16f; 89 90 private final PointF mTranslationForReorderBounce = new PointF(0, 0); 91 private final PointF mTranslationForReorderPreview = new PointF(0, 0); 92 93 private float mScaleForReorderBounce = 1f; 94 95 private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY 96 = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") { 97 @Override 98 public Float get(BubbleTextView bubbleTextView) { 99 return bubbleTextView.mDotParams.scale; 100 } 101 102 @Override 103 public void set(BubbleTextView bubbleTextView, Float value) { 104 bubbleTextView.mDotParams.scale = value; 105 bubbleTextView.invalidate(); 106 } 107 }; 108 109 public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY 110 = new Property<BubbleTextView, Float>(Float.class, "textAlpha") { 111 @Override 112 public Float get(BubbleTextView bubbleTextView) { 113 return bubbleTextView.mTextAlpha; 114 } 115 116 @Override 117 public void set(BubbleTextView bubbleTextView, Float alpha) { 118 bubbleTextView.setTextAlpha(alpha); 119 } 120 }; 121 122 private final ActivityContext mActivity; 123 private FastBitmapDrawable mIcon; 124 private boolean mCenterVertically; 125 126 protected final int mDisplay; 127 128 private final CheckLongPressHelper mLongPressHelper; 129 130 private final boolean mLayoutHorizontal; 131 private final int mIconSize; 132 133 @ViewDebug.ExportedProperty(category = "launcher") 134 private boolean mIsIconVisible = true; 135 @ViewDebug.ExportedProperty(category = "launcher") 136 private int mTextColor; 137 @ViewDebug.ExportedProperty(category = "launcher") 138 private float mTextAlpha = 1; 139 140 @ViewDebug.ExportedProperty(category = "launcher") 141 private DotInfo mDotInfo; 142 private DotRenderer mDotRenderer; 143 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 144 protected DotRenderer.DrawParams mDotParams; 145 private Animator mDotScaleAnim; 146 private boolean mForceHideDot; 147 148 @ViewDebug.ExportedProperty(category = "launcher") 149 private boolean mStayPressed; 150 @ViewDebug.ExportedProperty(category = "launcher") 151 private boolean mIgnorePressedStateChange; 152 @ViewDebug.ExportedProperty(category = "launcher") 153 private boolean mDisableRelayout = false; 154 155 private HandlerRunnable mIconLoadRequest; 156 157 private boolean mEnableIconUpdateAnimation = false; 158 BubbleTextView(Context context)159 public BubbleTextView(Context context) { 160 this(context, null, 0); 161 } 162 BubbleTextView(Context context, AttributeSet attrs)163 public BubbleTextView(Context context, AttributeSet attrs) { 164 this(context, attrs, 0); 165 } 166 BubbleTextView(Context context, AttributeSet attrs, int defStyle)167 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 168 super(context, attrs, defStyle); 169 mActivity = ActivityContext.lookupContext(context); 170 171 TypedArray a = context.obtainStyledAttributes(attrs, 172 R.styleable.BubbleTextView, defStyle, 0); 173 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 174 DeviceProfile grid = mActivity.getDeviceProfile(); 175 176 mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 177 final int defaultIconSize; 178 if (mDisplay == DISPLAY_WORKSPACE) { 179 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); 180 setCompoundDrawablePadding(grid.iconDrawablePaddingPx); 181 defaultIconSize = grid.iconSizePx; 182 setCenterVertically(grid.isScalableGrid); 183 } else if (mDisplay == DISPLAY_ALL_APPS) { 184 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); 185 setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx); 186 defaultIconSize = grid.allAppsIconSizePx; 187 } else if (mDisplay == DISPLAY_FOLDER) { 188 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx); 189 setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx); 190 defaultIconSize = grid.folderChildIconSizePx; 191 } else if (mDisplay == DISPLAY_SEARCH_RESULT) { 192 defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size); 193 } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) { 194 defaultIconSize = getResources().getDimensionPixelSize( 195 R.dimen.search_row_small_icon_size); 196 } else if (mDisplay == DISPLAY_TASKBAR) { 197 defaultIconSize = grid.iconSizePx; 198 } else { 199 // widget_selection or shortcut_popup 200 defaultIconSize = grid.iconSizePx; 201 } 202 203 mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); 204 205 mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, 206 defaultIconSize); 207 a.recycle(); 208 209 mLongPressHelper = new CheckLongPressHelper(this); 210 211 mDotParams = new DotRenderer.DrawParams(); 212 213 setEllipsize(TruncateAt.END); 214 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 215 setTextAlpha(1f); 216 } 217 218 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)219 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 220 // Disable marques when not focused to that, so that updating text does not cause relayout. 221 setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END); 222 super.onFocusChanged(focused, direction, previouslyFocusedRect); 223 } 224 225 /** 226 * Resets the view so it can be recycled. 227 */ reset()228 public void reset() { 229 mDotInfo = null; 230 mDotParams.color = Color.TRANSPARENT; 231 cancelDotScaleAnim(); 232 mDotParams.scale = 0f; 233 mForceHideDot = false; 234 setBackground(null); 235 } 236 cancelDotScaleAnim()237 private void cancelDotScaleAnim() { 238 if (mDotScaleAnim != null) { 239 mDotScaleAnim.cancel(); 240 } 241 } 242 animateDotScale(float... dotScales)243 private void animateDotScale(float... dotScales) { 244 cancelDotScaleAnim(); 245 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 246 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 247 @Override 248 public void onAnimationEnd(Animator animation) { 249 mDotScaleAnim = null; 250 } 251 }); 252 mDotScaleAnim.start(); 253 } 254 255 @UiThread applyFromWorkspaceItem(WorkspaceItemInfo info)256 public void applyFromWorkspaceItem(WorkspaceItemInfo info) { 257 applyFromWorkspaceItem(info, false); 258 } 259 260 @Override setAccessibilityDelegate(AccessibilityDelegate delegate)261 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 262 if (delegate instanceof LauncherAccessibilityDelegate) { 263 super.setAccessibilityDelegate(delegate); 264 } else { 265 // NO-OP 266 // Workaround for b/129745295 where RecyclerView is setting our Accessibility 267 // delegate incorrectly. There are no cases when we shouldn't be using the 268 // LauncherAccessibilityDelegate for BubbleTextView. 269 } 270 } 271 272 @UiThread applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged)273 public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) { 274 applyIconAndLabel(info); 275 setTag(info); 276 applyLoadingState(promiseStateChanged); 277 applyDotState(info, false /* animate */); 278 setDownloadStateContentDescription(info, info.getProgressLevel()); 279 } 280 281 @UiThread applyFromApplicationInfo(AppInfo info)282 public void applyFromApplicationInfo(AppInfo info) { 283 applyIconAndLabel(info); 284 285 // We don't need to check the info since it's not a WorkspaceItemInfo 286 super.setTag(info); 287 288 // Verify high res immediately 289 verifyHighRes(); 290 291 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { 292 applyProgressLevel(); 293 } 294 applyDotState(info, false /* animate */); 295 setDownloadStateContentDescription(info, info.getProgressLevel()); 296 } 297 298 /** 299 * Apply label and tag using a generic {@link ItemInfoWithIcon} 300 */ 301 @UiThread applyFromItemInfoWithIcon(ItemInfoWithIcon info)302 public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) { 303 applyIconAndLabel(info); 304 // We don't need to check the info since it's not a WorkspaceItemInfo 305 super.setTag(info); 306 307 // Verify high res immediately 308 verifyHighRes(); 309 310 setDownloadStateContentDescription(info, info.getProgressLevel()); 311 } 312 313 /** 314 * Apply label and tag using a {@link SearchActionItemInfo} 315 */ 316 @UiThread applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo)317 public void applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo) { 318 applyIconAndLabel(searchActionItemInfo); 319 setTag(searchActionItemInfo); 320 } 321 322 @UiThread applyIconAndLabel(ItemInfoWithIcon info)323 protected void applyIconAndLabel(ItemInfoWithIcon info) { 324 boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER; 325 FastBitmapDrawable iconDrawable = info.newIcon(getContext(), useTheme); 326 mDotParams.color = IconPalette.getMutedColor(iconDrawable.getIconColor(), 0.54f); 327 328 setIcon(iconDrawable); 329 applyLabel(info); 330 } 331 332 @UiThread applyLabel(ItemInfoWithIcon info)333 private void applyLabel(ItemInfoWithIcon info) { 334 setText(info.title); 335 if (info.contentDescription != null) { 336 setContentDescription(info.isDisabled() 337 ? getContext().getString(R.string.disabled_app_label, info.contentDescription) 338 : info.contentDescription); 339 } 340 } 341 342 /** 343 * Overrides the default long press timeout. 344 */ setLongPressTimeoutFactor(float longPressTimeoutFactor)345 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) { 346 mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor); 347 } 348 349 @Override refreshDrawableState()350 public void refreshDrawableState() { 351 if (!mIgnorePressedStateChange) { 352 super.refreshDrawableState(); 353 } 354 } 355 356 @Override onCreateDrawableState(int extraSpace)357 protected int[] onCreateDrawableState(int extraSpace) { 358 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 359 if (mStayPressed) { 360 mergeDrawableStates(drawableState, STATE_PRESSED); 361 } 362 return drawableState; 363 } 364 365 /** Returns the icon for this view. */ getIcon()366 public FastBitmapDrawable getIcon() { 367 return mIcon; 368 } 369 370 @Override onTouchEvent(MotionEvent event)371 public boolean onTouchEvent(MotionEvent event) { 372 // ignore events if they happen in padding area 373 if (event.getAction() == MotionEvent.ACTION_DOWN 374 && shouldIgnoreTouchDown(event.getX(), event.getY())) { 375 return false; 376 } 377 if (isLongClickable()) { 378 super.onTouchEvent(event); 379 mLongPressHelper.onTouchEvent(event); 380 // Keep receiving the rest of the events 381 return true; 382 } else { 383 return super.onTouchEvent(event); 384 } 385 } 386 387 /** 388 * Returns true if the touch down at the provided position be ignored 389 */ shouldIgnoreTouchDown(float x, float y)390 protected boolean shouldIgnoreTouchDown(float x, float y) { 391 return y < getPaddingTop() 392 || x < getPaddingLeft() 393 || y > getHeight() - getPaddingBottom() 394 || x > getWidth() - getPaddingRight(); 395 } 396 setStayPressed(boolean stayPressed)397 void setStayPressed(boolean stayPressed) { 398 mStayPressed = stayPressed; 399 refreshDrawableState(); 400 } 401 402 @Override onVisibilityAggregated(boolean isVisible)403 public void onVisibilityAggregated(boolean isVisible) { 404 super.onVisibilityAggregated(isVisible); 405 if (mIcon != null) { 406 mIcon.setVisible(isVisible, false); 407 } 408 } 409 clearPressedBackground()410 void clearPressedBackground() { 411 setPressed(false); 412 setStayPressed(false); 413 } 414 415 @Override onKeyUp(int keyCode, KeyEvent event)416 public boolean onKeyUp(int keyCode, KeyEvent event) { 417 // Unlike touch events, keypress event propagate pressed state change immediately, 418 // without waiting for onClickHandler to execute. Disable pressed state changes here 419 // to avoid flickering. 420 mIgnorePressedStateChange = true; 421 boolean result = super.onKeyUp(keyCode, event); 422 mIgnorePressedStateChange = false; 423 refreshDrawableState(); 424 return result; 425 } 426 427 @SuppressWarnings("wrongcall") drawWithoutDot(Canvas canvas)428 protected void drawWithoutDot(Canvas canvas) { 429 super.onDraw(canvas); 430 } 431 432 @Override onDraw(Canvas canvas)433 public void onDraw(Canvas canvas) { 434 super.onDraw(canvas); 435 drawDotIfNecessary(canvas); 436 } 437 438 /** 439 * Draws the notification dot in the top right corner of the icon bounds. 440 * 441 * @param canvas The canvas to draw to. 442 */ drawDotIfNecessary(Canvas canvas)443 protected void drawDotIfNecessary(Canvas canvas) { 444 if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) { 445 getIconBounds(mDotParams.iconBounds); 446 Utilities.scaleRectAboutCenter(mDotParams.iconBounds, 447 IconShape.getNormalizationScale()); 448 final int scrollX = getScrollX(); 449 final int scrollY = getScrollY(); 450 canvas.translate(scrollX, scrollY); 451 mDotRenderer.draw(canvas, mDotParams); 452 canvas.translate(-scrollX, -scrollY); 453 } 454 } 455 456 @Override setForceHideDot(boolean forceHideDot)457 public void setForceHideDot(boolean forceHideDot) { 458 if (mForceHideDot == forceHideDot) { 459 return; 460 } 461 mForceHideDot = forceHideDot; 462 463 if (forceHideDot) { 464 invalidate(); 465 } else if (hasDot()) { 466 animateDotScale(0, 1); 467 } 468 } 469 hasDot()470 private boolean hasDot() { 471 return mDotInfo != null; 472 } 473 getIconBounds(Rect outBounds)474 public void getIconBounds(Rect outBounds) { 475 getIconBounds(this, outBounds, mIconSize); 476 } 477 getIconBounds(View iconView, Rect outBounds, int iconSize)478 public static void getIconBounds(View iconView, Rect outBounds, int iconSize) { 479 int top = iconView.getPaddingTop(); 480 int left = (iconView.getWidth() - iconSize) / 2; 481 int right = left + iconSize; 482 int bottom = top + iconSize; 483 outBounds.set(left, top, right, bottom); 484 } 485 486 487 /** 488 * Sets whether to vertically center the content. 489 */ setCenterVertically(boolean centerVertically)490 public void setCenterVertically(boolean centerVertically) { 491 mCenterVertically = centerVertically; 492 } 493 494 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)495 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 496 if (mCenterVertically) { 497 Paint.FontMetrics fm = getPaint().getFontMetrics(); 498 int cellHeightPx = mIconSize + getCompoundDrawablePadding() + 499 (int) Math.ceil(fm.bottom - fm.top); 500 int height = MeasureSpec.getSize(heightMeasureSpec); 501 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 502 getPaddingBottom()); 503 } 504 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 505 } 506 507 @Override setTextColor(int color)508 public void setTextColor(int color) { 509 mTextColor = color; 510 super.setTextColor(getModifiedColor()); 511 } 512 513 @Override setTextColor(ColorStateList colors)514 public void setTextColor(ColorStateList colors) { 515 mTextColor = colors.getDefaultColor(); 516 if (Float.compare(mTextAlpha, 1) == 0) { 517 super.setTextColor(colors); 518 } else { 519 super.setTextColor(getModifiedColor()); 520 } 521 } 522 shouldTextBeVisible()523 public boolean shouldTextBeVisible() { 524 // Text should be visible everywhere but the hotseat. 525 Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag(); 526 ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null; 527 return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT 528 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION); 529 } 530 setTextVisibility(boolean visible)531 public void setTextVisibility(boolean visible) { 532 setTextAlpha(visible ? 1 : 0); 533 } 534 setTextAlpha(float alpha)535 private void setTextAlpha(float alpha) { 536 mTextAlpha = alpha; 537 super.setTextColor(getModifiedColor()); 538 } 539 getModifiedColor()540 private int getModifiedColor() { 541 if (mTextAlpha == 0) { 542 // Special case to prevent text shadows in high contrast mode 543 return Color.TRANSPARENT; 544 } 545 return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha)); 546 } 547 548 /** 549 * Creates an animator to fade the text in or out. 550 * 551 * @param fadeIn Whether the text should fade in or fade out. 552 */ createTextAlphaAnimator(boolean fadeIn)553 public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) { 554 float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0; 555 return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); 556 } 557 558 @Override cancelLongPress()559 public void cancelLongPress() { 560 super.cancelLongPress(); 561 mLongPressHelper.cancelLongPress(); 562 } 563 564 /** 565 * Applies the loading progress value to the progress bar. 566 * 567 * If this app is installing, the progress bar will be updated with the installation progress. 568 * If this app is installed and downloading incrementally, the progress bar will be updated 569 * with the total download progress. 570 */ applyLoadingState(boolean promiseStateChanged)571 public void applyLoadingState(boolean promiseStateChanged) { 572 if (getTag() instanceof ItemInfoWithIcon) { 573 WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); 574 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) 575 != 0) { 576 updateProgressBarUi(info.getProgressLevel() == 100); 577 } else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags 578 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { 579 updateProgressBarUi(promiseStateChanged); 580 } 581 } 582 } 583 updateProgressBarUi(boolean maybePerformFinishedAnimation)584 private void updateProgressBarUi(boolean maybePerformFinishedAnimation) { 585 PreloadIconDrawable preloadDrawable = applyProgressLevel(); 586 if (preloadDrawable != null && maybePerformFinishedAnimation) { 587 preloadDrawable.maybePerformFinishedAnimation(); 588 } 589 } 590 591 /** Applies the given progress level to the this icon's progress bar. */ 592 @Nullable applyProgressLevel()593 public PreloadIconDrawable applyProgressLevel() { 594 if (!(getTag() instanceof ItemInfoWithIcon)) { 595 return null; 596 } 597 598 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 599 int progressLevel = info.getProgressLevel(); 600 if (progressLevel >= 100) { 601 setContentDescription(info.contentDescription != null 602 ? info.contentDescription : ""); 603 } else if (progressLevel > 0) { 604 setDownloadStateContentDescription(info, progressLevel); 605 } else { 606 setContentDescription(getContext() 607 .getString(R.string.app_waiting_download_title, info.title)); 608 } 609 if (mIcon != null) { 610 PreloadIconDrawable preloadIconDrawable; 611 if (mIcon instanceof PreloadIconDrawable) { 612 preloadIconDrawable = (PreloadIconDrawable) mIcon; 613 preloadIconDrawable.setLevel(progressLevel); 614 preloadIconDrawable.setIsDisabled(!info.isAppStartable()); 615 } else { 616 preloadIconDrawable = makePreloadIcon(); 617 setIcon(preloadIconDrawable); 618 } 619 return preloadIconDrawable; 620 } 621 return null; 622 } 623 624 /** 625 * Creates a PreloadIconDrawable with the appropriate progress level without mutating this 626 * object. 627 */ 628 @Nullable makePreloadIcon()629 public PreloadIconDrawable makePreloadIcon() { 630 if (!(getTag() instanceof ItemInfoWithIcon)) { 631 return null; 632 } 633 634 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 635 int progressLevel = info.getProgressLevel(); 636 final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info); 637 638 preloadDrawable.setLevel(progressLevel); 639 preloadDrawable.setIsDisabled(!info.isAppStartable()); 640 641 return preloadDrawable; 642 } 643 applyDotState(ItemInfo itemInfo, boolean animate)644 public void applyDotState(ItemInfo itemInfo, boolean animate) { 645 if (mIcon instanceof FastBitmapDrawable) { 646 boolean wasDotted = mDotInfo != null; 647 mDotInfo = mActivity.getDotInfoForItem(itemInfo); 648 boolean isDotted = mDotInfo != null; 649 float newDotScale = isDotted ? 1f : 0; 650 if (mDisplay == DISPLAY_ALL_APPS) { 651 mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps; 652 } else { 653 mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace; 654 } 655 if (wasDotted || isDotted) { 656 // Animate when a dot is first added or when it is removed. 657 if (animate && (wasDotted ^ isDotted) && isShown()) { 658 animateDotScale(newDotScale); 659 } else { 660 cancelDotScaleAnim(); 661 mDotParams.scale = newDotScale; 662 invalidate(); 663 } 664 } 665 if (itemInfo.contentDescription != null) { 666 if (itemInfo.isDisabled()) { 667 setContentDescription(getContext().getString(R.string.disabled_app_label, 668 itemInfo.contentDescription)); 669 } else if (hasDot()) { 670 int count = mDotInfo.getNotificationCount(); 671 setContentDescription(getContext().getResources().getQuantityString( 672 R.plurals.dotted_app_label, count, itemInfo.contentDescription, count)); 673 } else { 674 setContentDescription(itemInfo.contentDescription); 675 } 676 } 677 } 678 } 679 setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel)680 private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) { 681 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) 682 != 0) { 683 String percentageString = NumberFormat.getPercentInstance() 684 .format(progressLevel * 0.01); 685 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) { 686 setContentDescription(getContext() 687 .getString( 688 R.string.app_installing_title, info.title, percentageString)); 689 } else if ((info.runtimeStatusFlags 690 & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) { 691 setContentDescription(getContext() 692 .getString( 693 R.string.app_downloading_title, info.title, percentageString)); 694 } 695 } 696 } 697 698 /** 699 * Sets the icon for this view based on the layout direction. 700 */ setIcon(FastBitmapDrawable icon)701 protected void setIcon(FastBitmapDrawable icon) { 702 if (mIsIconVisible) { 703 applyCompoundDrawables(icon); 704 } 705 mIcon = icon; 706 if (mIcon != null) { 707 mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); 708 } 709 } 710 711 @Override setIconVisible(boolean visible)712 public void setIconVisible(boolean visible) { 713 mIsIconVisible = visible; 714 if (!mIsIconVisible) { 715 resetIconScale(); 716 } 717 Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT); 718 applyCompoundDrawables(icon); 719 } 720 iconUpdateAnimationEnabled()721 protected boolean iconUpdateAnimationEnabled() { 722 return mEnableIconUpdateAnimation; 723 } 724 applyCompoundDrawables(Drawable icon)725 protected void applyCompoundDrawables(Drawable icon) { 726 // If we had already set an icon before, disable relayout as the icon size is the 727 // same as before. 728 mDisableRelayout = mIcon != null; 729 730 icon.setBounds(0, 0, mIconSize, mIconSize); 731 732 updateIcon(icon); 733 734 // If the current icon is a placeholder color, animate its update. 735 if (mIcon != null 736 && mIcon instanceof PlaceHolderIconDrawable 737 && iconUpdateAnimationEnabled()) { 738 ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon); 739 } 740 741 mDisableRelayout = false; 742 } 743 744 @Override requestLayout()745 public void requestLayout() { 746 if (!mDisableRelayout) { 747 super.requestLayout(); 748 } 749 } 750 751 /** 752 * Applies the item info if it is same as what the view is pointing to currently. 753 */ 754 @Override reapplyItemInfo(ItemInfoWithIcon info)755 public void reapplyItemInfo(ItemInfoWithIcon info) { 756 if (getTag() == info) { 757 mIconLoadRequest = null; 758 mDisableRelayout = true; 759 mEnableIconUpdateAnimation = true; 760 761 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 762 info.bitmap.icon.prepareToDraw(); 763 764 if (info instanceof AppInfo) { 765 applyFromApplicationInfo((AppInfo) info); 766 } else if (info instanceof WorkspaceItemInfo) { 767 applyFromWorkspaceItem((WorkspaceItemInfo) info); 768 mActivity.invalidateParent(info); 769 } else if (info instanceof PackageItemInfo) { 770 applyFromItemInfoWithIcon((PackageItemInfo) info); 771 } else if (info instanceof SearchActionItemInfo) { 772 applyFromSearchActionItemInfo((SearchActionItemInfo) info); 773 } 774 775 mDisableRelayout = false; 776 mEnableIconUpdateAnimation = false; 777 } 778 } 779 780 /** 781 * Verifies that the current icon is high-res otherwise posts a request to load the icon. 782 */ verifyHighRes()783 public void verifyHighRes() { 784 if (mIconLoadRequest != null) { 785 mIconLoadRequest.cancel(); 786 mIconLoadRequest = null; 787 } 788 if (getTag() instanceof ItemInfoWithIcon) { 789 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 790 if (info.usingLowResIcon()) { 791 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 792 .updateIconInBackground(BubbleTextView.this, info); 793 } 794 } 795 } 796 getIconSize()797 public int getIconSize() { 798 return mIconSize; 799 } 800 updateTranslation()801 private void updateTranslation() { 802 super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x); 803 super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y); 804 } 805 setReorderBounceOffset(float x, float y)806 public void setReorderBounceOffset(float x, float y) { 807 mTranslationForReorderBounce.set(x, y); 808 updateTranslation(); 809 } 810 getReorderBounceOffset(PointF offset)811 public void getReorderBounceOffset(PointF offset) { 812 offset.set(mTranslationForReorderBounce); 813 } 814 815 @Override setReorderPreviewOffset(float x, float y)816 public void setReorderPreviewOffset(float x, float y) { 817 mTranslationForReorderPreview.set(x, y); 818 updateTranslation(); 819 } 820 821 @Override getReorderPreviewOffset(PointF offset)822 public void getReorderPreviewOffset(PointF offset) { 823 offset.set(mTranslationForReorderPreview); 824 } 825 setReorderBounceScale(float scale)826 public void setReorderBounceScale(float scale) { 827 mScaleForReorderBounce = scale; 828 super.setScaleX(scale); 829 super.setScaleY(scale); 830 } 831 getReorderBounceScale()832 public float getReorderBounceScale() { 833 return mScaleForReorderBounce; 834 } 835 getView()836 public View getView() { 837 return this; 838 } 839 840 @Override getViewType()841 public int getViewType() { 842 return DRAGGABLE_ICON; 843 } 844 845 @Override getWorkspaceVisualDragBounds(Rect bounds)846 public void getWorkspaceVisualDragBounds(Rect bounds) { 847 DeviceProfile grid = mActivity.getDeviceProfile(); 848 BubbleTextView.getIconBounds(this, bounds, grid.iconSizePx); 849 } 850 getIconSizeForDisplay(int display)851 private int getIconSizeForDisplay(int display) { 852 DeviceProfile grid = mActivity.getDeviceProfile(); 853 switch (display) { 854 case DISPLAY_ALL_APPS: 855 return grid.allAppsIconSizePx; 856 case DISPLAY_WORKSPACE: 857 case DISPLAY_FOLDER: 858 default: 859 return grid.iconSizePx; 860 } 861 } 862 getSourceVisualDragBounds(Rect bounds)863 public void getSourceVisualDragBounds(Rect bounds) { 864 BubbleTextView.getIconBounds(this, bounds, getIconSizeForDisplay(mDisplay)); 865 } 866 867 @Override prepareDrawDragView()868 public SafeCloseable prepareDrawDragView() { 869 resetIconScale(); 870 setForceHideDot(true); 871 return () -> { }; 872 } 873 resetIconScale()874 private void resetIconScale() { 875 if (mIcon instanceof FastBitmapDrawable) { 876 ((FastBitmapDrawable) mIcon).resetScale(); 877 } 878 } 879 updateIcon(Drawable newIcon)880 private void updateIcon(Drawable newIcon) { 881 if (mLayoutHorizontal) { 882 setCompoundDrawablesRelative(newIcon, null, null, null); 883 } else { 884 setCompoundDrawables(null, newIcon, null, null); 885 } 886 } 887 } 888