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