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 android.text.Layout.Alignment.ALIGN_NORMAL; 20 21 import static com.android.launcher3.Flags.enableCursorHoverStates; 22 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; 23 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE; 24 import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE; 25 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; 26 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 27 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE; 28 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; 29 30 import android.animation.Animator; 31 import android.animation.AnimatorListenerAdapter; 32 import android.animation.ObjectAnimator; 33 import android.content.Context; 34 import android.content.res.ColorStateList; 35 import android.content.res.TypedArray; 36 import android.graphics.Canvas; 37 import android.graphics.Color; 38 import android.graphics.Paint; 39 import android.graphics.Rect; 40 import android.graphics.drawable.ColorDrawable; 41 import android.graphics.drawable.Drawable; 42 import android.icu.text.MessageFormat; 43 import android.text.StaticLayout; 44 import android.text.TextPaint; 45 import android.text.TextUtils; 46 import android.text.TextUtils.TruncateAt; 47 import android.util.AttributeSet; 48 import android.util.Property; 49 import android.util.Size; 50 import android.util.TypedValue; 51 import android.view.KeyEvent; 52 import android.view.MotionEvent; 53 import android.view.View; 54 import android.view.ViewDebug; 55 import android.widget.TextView; 56 57 import androidx.annotation.Nullable; 58 import androidx.annotation.UiThread; 59 import androidx.annotation.VisibleForTesting; 60 61 import com.android.launcher3.accessibility.BaseAccessibilityDelegate; 62 import com.android.launcher3.dot.DotInfo; 63 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; 64 import com.android.launcher3.dragndrop.DraggableView; 65 import com.android.launcher3.folder.FolderIcon; 66 import com.android.launcher3.graphics.IconShape; 67 import com.android.launcher3.graphics.PreloadIconDrawable; 68 import com.android.launcher3.icons.DotRenderer; 69 import com.android.launcher3.icons.FastBitmapDrawable; 70 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 71 import com.android.launcher3.icons.PlaceHolderIconDrawable; 72 import com.android.launcher3.model.data.AppInfo; 73 import com.android.launcher3.model.data.ItemInfo; 74 import com.android.launcher3.model.data.ItemInfoWithIcon; 75 import com.android.launcher3.model.data.WorkspaceItemInfo; 76 import com.android.launcher3.popup.PopupContainerWithArrow; 77 import com.android.launcher3.search.StringMatcherUtility; 78 import com.android.launcher3.util.CancellableTask; 79 import com.android.launcher3.util.IntArray; 80 import com.android.launcher3.util.MultiTranslateDelegate; 81 import com.android.launcher3.util.SafeCloseable; 82 import com.android.launcher3.util.ShortcutUtil; 83 import com.android.launcher3.util.Themes; 84 import com.android.launcher3.views.ActivityContext; 85 import com.android.launcher3.views.IconLabelDotView; 86 87 import java.text.NumberFormat; 88 import java.util.HashMap; 89 import java.util.Locale; 90 91 /** 92 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 93 * because we want to make the bubble taller than the text and TextView's clip is 94 * too aggressive. 95 */ 96 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 97 IconLabelDotView, DraggableView, Reorderable { 98 99 public static final int DISPLAY_WORKSPACE = 0; 100 public static final int DISPLAY_ALL_APPS = 1; 101 public static final int DISPLAY_FOLDER = 2; 102 public static final int DISPLAY_TASKBAR = 5; 103 public static final int DISPLAY_SEARCH_RESULT = 6; 104 public static final int DISPLAY_SEARCH_RESULT_SMALL = 7; 105 public static final int DISPLAY_PREDICTION_ROW = 8; 106 public static final int DISPLAY_SEARCH_RESULT_APP_ROW = 9; 107 108 private static final float MIN_LETTER_SPACING = -0.05f; 109 private static final int MAX_SEARCH_LOOP_COUNT = 20; 110 private static final Character NEW_LINE = '\n'; 111 private static final String EMPTY = ""; 112 private static final StringMatcherUtility.StringMatcher MATCHER = 113 StringMatcherUtility.StringMatcher.getInstance(); 114 115 private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; 116 117 private float mScaleForReorderBounce = 1f; 118 119 private IntArray mBreakPointsIntArray; 120 private CharSequence mLastOriginalText; 121 private CharSequence mLastModifiedText; 122 123 private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY 124 = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") { 125 @Override 126 public Float get(BubbleTextView bubbleTextView) { 127 return bubbleTextView.mDotParams.scale; 128 } 129 130 @Override 131 public void set(BubbleTextView bubbleTextView, Float value) { 132 bubbleTextView.mDotParams.scale = value; 133 bubbleTextView.invalidate(); 134 } 135 }; 136 137 public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY 138 = new Property<BubbleTextView, Float>(Float.class, "textAlpha") { 139 @Override 140 public Float get(BubbleTextView bubbleTextView) { 141 return bubbleTextView.mTextAlpha; 142 } 143 144 @Override 145 public void set(BubbleTextView bubbleTextView, Float alpha) { 146 bubbleTextView.setTextAlpha(alpha); 147 } 148 }; 149 150 private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); 151 private final ActivityContext mActivity; 152 private FastBitmapDrawable mIcon; 153 private DeviceProfile mDeviceProfile; 154 private boolean mCenterVertically; 155 156 protected int mDisplay; 157 158 private final CheckLongPressHelper mLongPressHelper; 159 160 private boolean mLayoutHorizontal; 161 private final boolean mIsRtl; 162 private final int mIconSize; 163 164 @ViewDebug.ExportedProperty(category = "launcher") 165 private boolean mHideBadge = false; 166 @ViewDebug.ExportedProperty(category = "launcher") 167 private boolean mSkipUserBadge = false; 168 @ViewDebug.ExportedProperty(category = "launcher") 169 private boolean mIsIconVisible = true; 170 @ViewDebug.ExportedProperty(category = "launcher") 171 private int mTextColor; 172 @ViewDebug.ExportedProperty(category = "launcher") 173 private ColorStateList mTextColorStateList; 174 @ViewDebug.ExportedProperty(category = "launcher") 175 private float mTextAlpha = 1; 176 177 @ViewDebug.ExportedProperty(category = "launcher") 178 private DotInfo mDotInfo; 179 private DotRenderer mDotRenderer; 180 private Locale mCurrentLocale; 181 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 182 protected DotRenderer.DrawParams mDotParams; 183 private Animator mDotScaleAnim; 184 private boolean mForceHideDot; 185 186 // These fields, related to showing running apps, are only used for Taskbar. 187 private final Size mRunningAppIndicatorSize; 188 private final int mRunningAppIndicatorTopMargin; 189 private final Size mMinimizedAppIndicatorSize; 190 private final int mMinimizedAppIndicatorTopMargin; 191 private final Paint mRunningAppIndicatorPaint; 192 private final Rect mRunningAppIconBounds = new Rect(); 193 private RunningAppState mRunningAppState; 194 195 /** 196 * Various options for the running state of an app. 197 */ 198 public enum RunningAppState { 199 NOT_RUNNING, 200 RUNNING, 201 MINIMIZED, 202 } 203 204 @ViewDebug.ExportedProperty(category = "launcher") 205 private boolean mStayPressed; 206 @ViewDebug.ExportedProperty(category = "launcher") 207 private boolean mIgnorePressedStateChange; 208 @ViewDebug.ExportedProperty(category = "launcher") 209 private boolean mDisableRelayout = false; 210 211 private CancellableTask mIconLoadRequest; 212 213 private boolean mEnableIconUpdateAnimation = false; 214 BubbleTextView(Context context)215 public BubbleTextView(Context context) { 216 this(context, null, 0); 217 } 218 BubbleTextView(Context context, AttributeSet attrs)219 public BubbleTextView(Context context, AttributeSet attrs) { 220 this(context, attrs, 0); 221 } 222 BubbleTextView(Context context, AttributeSet attrs, int defStyle)223 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 224 super(context, attrs, defStyle); 225 mActivity = ActivityContext.lookupContext(context); 226 FastBitmapDrawable.setFlagHoverEnabled(enableCursorHoverStates()); 227 228 TypedArray a = context.obtainStyledAttributes(attrs, 229 R.styleable.BubbleTextView, defStyle, 0); 230 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 231 mIsRtl = (getResources().getConfiguration().getLayoutDirection() 232 == View.LAYOUT_DIRECTION_RTL); 233 mDeviceProfile = mActivity.getDeviceProfile(); 234 mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); 235 236 mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 237 final int defaultIconSize; 238 if (mDisplay == DISPLAY_WORKSPACE) { 239 setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.iconTextSizePx); 240 setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); 241 defaultIconSize = mDeviceProfile.iconSizePx; 242 setCenterVertically(mDeviceProfile.iconCenterVertically); 243 } else if (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW 244 || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW) { 245 setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx); 246 setCompoundDrawablePadding(mDeviceProfile.allAppsIconDrawablePaddingPx); 247 defaultIconSize = mDeviceProfile.allAppsIconSizePx; 248 } else if (mDisplay == DISPLAY_FOLDER) { 249 setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.folderChildTextSizePx); 250 setCompoundDrawablePadding(mDeviceProfile.folderChildDrawablePaddingPx); 251 defaultIconSize = mDeviceProfile.folderChildIconSizePx; 252 } else if (mDisplay == DISPLAY_SEARCH_RESULT) { 253 setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx); 254 defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size); 255 } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) { 256 defaultIconSize = getResources().getDimensionPixelSize( 257 R.dimen.search_row_small_icon_size); 258 } else if (mDisplay == DISPLAY_TASKBAR) { 259 defaultIconSize = mDeviceProfile.taskbarIconSize; 260 } else { 261 // widget_selection or shortcut_popup 262 defaultIconSize = mDeviceProfile.iconSizePx; 263 } 264 265 266 mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, 267 defaultIconSize); 268 a.recycle(); 269 270 mRunningAppIndicatorSize = new Size( 271 getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width), 272 getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height)); 273 mMinimizedAppIndicatorSize = new Size( 274 getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width), 275 getResources().getDimensionPixelSize( 276 R.dimen.taskbar_minimized_app_indicator_height)); 277 mRunningAppIndicatorTopMargin = 278 getResources().getDimensionPixelSize( 279 R.dimen.taskbar_running_app_indicator_top_margin); 280 mMinimizedAppIndicatorTopMargin = 281 getResources().getDimensionPixelSize( 282 R.dimen.taskbar_minimized_app_indicator_top_margin); 283 mRunningAppIndicatorPaint = new Paint(); 284 mRunningAppIndicatorPaint.setColor(getResources().getColor( 285 R.color.taskbar_running_app_indicator_color, context.getTheme())); 286 287 mLongPressHelper = new CheckLongPressHelper(this); 288 289 mDotParams = new DotRenderer.DrawParams(); 290 291 mCurrentLocale = context.getResources().getConfiguration().locale; 292 setEllipsize(TruncateAt.END); 293 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 294 setTextAlpha(1f); 295 } 296 297 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)298 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 299 // Disable marques when not focused to that, so that updating text does not cause relayout. 300 setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END); 301 super.onFocusChanged(focused, direction, previouslyFocusedRect); 302 } 303 setHideBadge(boolean hideBadge)304 public void setHideBadge(boolean hideBadge) { 305 mHideBadge = hideBadge; 306 } 307 setSkipUserBadge(boolean skipUserBadge)308 public void setSkipUserBadge(boolean skipUserBadge) { 309 mSkipUserBadge = skipUserBadge; 310 } 311 312 /** 313 * Resets the view so it can be recycled. 314 */ reset()315 public void reset() { 316 mDotInfo = null; 317 mDotParams.dotColor = Color.TRANSPARENT; 318 mDotParams.appColor = Color.TRANSPARENT; 319 cancelDotScaleAnim(); 320 mDotParams.scale = 0f; 321 mForceHideDot = false; 322 setBackground(null); 323 324 setTag(null); 325 if (mIconLoadRequest != null) { 326 mIconLoadRequest.cancel(); 327 mIconLoadRequest = null; 328 } 329 // Reset any shifty arrangements in case animation is disrupted. 330 setPivotY(0); 331 setAlpha(1); 332 setScaleY(1); 333 setTranslationY(0); 334 setMaxLines(1); 335 setVisibility(VISIBLE); 336 } 337 cancelDotScaleAnim()338 private void cancelDotScaleAnim() { 339 if (mDotScaleAnim != null) { 340 mDotScaleAnim.cancel(); 341 } 342 } 343 animateDotScale(float... dotScales)344 public void animateDotScale(float... dotScales) { 345 cancelDotScaleAnim(); 346 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 347 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 348 @Override 349 public void onAnimationEnd(Animator animation) { 350 mDotScaleAnim = null; 351 } 352 }); 353 mDotScaleAnim.start(); 354 } 355 356 @UiThread applyFromWorkspaceItem(WorkspaceItemInfo info)357 public void applyFromWorkspaceItem(WorkspaceItemInfo info) { 358 applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0); 359 } 360 361 @UiThread applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex)362 public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) { 363 applyFromWorkspaceItem(info, null); 364 } 365 366 /** 367 * Returns whether the newInfo differs from the current getTag(). 368 */ shouldAnimateIconChange(WorkspaceItemInfo newInfo)369 public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) { 370 WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo 371 ? (WorkspaceItemInfo) getTag() 372 : null; 373 boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null 374 && newInfo.getTargetComponent() != null 375 && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent()); 376 return changedIcons && isShown(); 377 } 378 379 @Override setAccessibilityDelegate(AccessibilityDelegate delegate)380 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 381 if (delegate instanceof BaseAccessibilityDelegate) { 382 super.setAccessibilityDelegate(delegate); 383 } else { 384 // NO-OP 385 // Workaround for b/129745295 where RecyclerView is setting our Accessibility 386 // delegate incorrectly. There are no cases when we shouldn't be using the 387 // LauncherAccessibilityDelegate for BubbleTextView. 388 } 389 } 390 391 @UiThread applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon)392 public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) { 393 applyIconAndLabel(info); 394 setItemInfo(info); 395 applyLoadingState(icon); 396 applyDotState(info, false /* animate */); 397 setDownloadStateContentDescription(info, info.getProgressLevel()); 398 } 399 400 @UiThread applyFromApplicationInfo(AppInfo info)401 public void applyFromApplicationInfo(AppInfo info) { 402 applyIconAndLabel(info); 403 404 // We don't need to check the info since it's not a WorkspaceItemInfo 405 setItemInfo(info); 406 407 408 // Verify high res immediately 409 verifyHighRes(); 410 411 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { 412 applyProgressLevel(); 413 } 414 applyDotState(info, false /* animate */); 415 setDownloadStateContentDescription(info, info.getProgressLevel()); 416 } 417 418 /** 419 * Apply label and tag using a generic {@link ItemInfoWithIcon} 420 */ 421 @UiThread applyFromItemInfoWithIcon(ItemInfoWithIcon info)422 public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) { 423 applyIconAndLabel(info); 424 // We don't need to check the info since it's not a WorkspaceItemInfo 425 setItemInfo(info); 426 427 // Verify high res immediately 428 verifyHighRes(); 429 430 setDownloadStateContentDescription(info, info.getProgressLevel()); 431 } 432 433 /** Updates whether the app this view represents is currently running. */ 434 @UiThread updateRunningState(RunningAppState runningAppState)435 public void updateRunningState(RunningAppState runningAppState) { 436 mRunningAppState = runningAppState; 437 } 438 setItemInfo(ItemInfoWithIcon itemInfo)439 protected void setItemInfo(ItemInfoWithIcon itemInfo) { 440 setTag(itemInfo); 441 } 442 443 @VisibleForTesting 444 @UiThread applyIconAndLabel(ItemInfoWithIcon info)445 public void applyIconAndLabel(ItemInfoWithIcon info) { 446 int flags = shouldUseTheme() ? FLAG_THEMED : 0; 447 // Remove badge on icons smaller than 48dp. 448 if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) { 449 flags |= FLAG_NO_BADGE; 450 } 451 if (mSkipUserBadge) { 452 flags |= FLAG_SKIP_USER_BADGE; 453 } 454 FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags); 455 mDotParams.appColor = iconDrawable.getIconColor(); 456 mDotParams.dotColor = Themes.getAttrColor(getContext(), R.attr.notificationDotColor); 457 setIcon(iconDrawable); 458 applyLabel(info); 459 } 460 shouldUseTheme()461 protected boolean shouldUseTheme() { 462 return (mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER 463 || mDisplay == DISPLAY_TASKBAR) && Themes.isThemedIconEnabled(getContext()); 464 } 465 466 /** 467 * Only if actual text can be displayed in two line, the {@code true} value will be effective. 468 */ shouldUseTwoLine()469 protected boolean shouldUseTwoLine() { 470 return isCurrentLanguageEnglish() && (mDisplay == DISPLAY_ALL_APPS 471 || mDisplay == DISPLAY_PREDICTION_ROW) && (Flags.enableTwolineToggle() 472 && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())); 473 } 474 isCurrentLanguageEnglish()475 protected boolean isCurrentLanguageEnglish() { 476 return mCurrentLocale.equals(Locale.US); 477 } 478 479 @UiThread applyLabel(ItemInfo info)480 public void applyLabel(ItemInfo info) { 481 CharSequence label = info.title; 482 if (label != null) { 483 mLastOriginalText = label; 484 mLastModifiedText = mLastOriginalText; 485 mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER); 486 setText(label); 487 } 488 if (info.contentDescription != null) { 489 setContentDescription(info.isDisabled() 490 ? getContext().getString(R.string.disabled_app_label, info.contentDescription) 491 : info.contentDescription); 492 } 493 } 494 495 /** This is used for testing to forcefully set the display. */ 496 @VisibleForTesting setDisplay(int display)497 public void setDisplay(int display) { 498 mDisplay = display; 499 } 500 501 /** 502 * Overrides the default long press timeout. 503 */ setLongPressTimeoutFactor(float longPressTimeoutFactor)504 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) { 505 mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor); 506 } 507 508 @Override refreshDrawableState()509 public void refreshDrawableState() { 510 if (!mIgnorePressedStateChange) { 511 super.refreshDrawableState(); 512 } 513 } 514 515 @Override onCreateDrawableState(int extraSpace)516 protected int[] onCreateDrawableState(int extraSpace) { 517 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 518 if (mStayPressed) { 519 mergeDrawableStates(drawableState, STATE_PRESSED); 520 } 521 return drawableState; 522 } 523 524 /** Returns the icon for this view. */ getIcon()525 public FastBitmapDrawable getIcon() { 526 return mIcon; 527 } 528 529 @Override onTouchEvent(MotionEvent event)530 public boolean onTouchEvent(MotionEvent event) { 531 // ignore events if they happen in padding area 532 if (event.getAction() == MotionEvent.ACTION_DOWN 533 && shouldIgnoreTouchDown(event.getX(), event.getY())) { 534 return false; 535 } 536 if (isLongClickable()) { 537 super.onTouchEvent(event); 538 mLongPressHelper.onTouchEvent(event); 539 // Keep receiving the rest of the events 540 return true; 541 } else { 542 return super.onTouchEvent(event); 543 } 544 } 545 546 /** 547 * Returns true if the touch down at the provided position be ignored 548 */ shouldIgnoreTouchDown(float x, float y)549 protected boolean shouldIgnoreTouchDown(float x, float y) { 550 if (mDisplay == DISPLAY_TASKBAR) { 551 // Allow touching within padding on taskbar, given icon sizes are smaller. 552 return false; 553 } 554 return y < getPaddingTop() 555 || x < getPaddingLeft() 556 || y > getHeight() - getPaddingBottom() 557 || x > getWidth() - getPaddingRight(); 558 } 559 setStayPressed(boolean stayPressed)560 void setStayPressed(boolean stayPressed) { 561 mStayPressed = stayPressed; 562 refreshDrawableState(); 563 } 564 565 @Override onVisibilityAggregated(boolean isVisible)566 public void onVisibilityAggregated(boolean isVisible) { 567 super.onVisibilityAggregated(isVisible); 568 if (mIcon != null) { 569 mIcon.setVisible(isVisible, false); 570 } 571 } 572 clearPressedBackground()573 public void clearPressedBackground() { 574 setPressed(false); 575 setStayPressed(false); 576 } 577 578 @Override onKeyUp(int keyCode, KeyEvent event)579 public boolean onKeyUp(int keyCode, KeyEvent event) { 580 // Unlike touch events, keypress event propagate pressed state change immediately, 581 // without waiting for onClickHandler to execute. Disable pressed state changes here 582 // to avoid flickering. 583 mIgnorePressedStateChange = true; 584 boolean result = super.onKeyUp(keyCode, event); 585 mIgnorePressedStateChange = false; 586 refreshDrawableState(); 587 return result; 588 } 589 590 @Override onSizeChanged(int w, int h, int oldw, int oldh)591 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 592 super.onSizeChanged(w, h, oldw, oldh); 593 checkForEllipsis(); 594 } 595 596 @Override onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)597 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { 598 super.onTextChanged(text, start, lengthBefore, lengthAfter); 599 checkForEllipsis(); 600 } 601 checkForEllipsis()602 private void checkForEllipsis() { 603 float width = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); 604 if (width <= 0) { 605 return; 606 } 607 setLetterSpacing(0); 608 609 String text = getText().toString(); 610 TextPaint paint = getPaint(); 611 if (paint.measureText(text) < width) { 612 return; 613 } 614 615 float spacing = findBestSpacingValue(paint, text, width, MIN_LETTER_SPACING); 616 // Reset the paint value so that the call to TextView does appropriate diff. 617 paint.setLetterSpacing(0); 618 setLetterSpacing(spacing); 619 } 620 621 /** 622 * Find the appropriate text spacing to display the provided text 623 * 624 * @param paint the paint used by the text view 625 * @param text the text to display 626 * @param allowedWidthPx available space to render the text 627 * @param minSpacingEm minimum spacing allowed between characters 628 * @return the final textSpacing value 629 * @see #setLetterSpacing(float) 630 */ findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx, float minSpacingEm)631 private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx, 632 float minSpacingEm) { 633 paint.setLetterSpacing(minSpacingEm); 634 if (paint.measureText(text) > allowedWidthPx) { 635 // If there is no result at high limit, we can do anything more 636 return minSpacingEm; 637 } 638 639 float lowLimit = 0; 640 float highLimit = minSpacingEm; 641 642 for (int i = 0; i < MAX_SEARCH_LOOP_COUNT; i++) { 643 float value = (lowLimit + highLimit) / 2; 644 paint.setLetterSpacing(value); 645 if (paint.measureText(text) < allowedWidthPx) { 646 highLimit = value; 647 } else { 648 lowLimit = value; 649 } 650 } 651 652 // At the end error on the higher side 653 return highLimit; 654 } 655 656 @SuppressWarnings("wrongcall") drawWithoutDot(Canvas canvas)657 protected void drawWithoutDot(Canvas canvas) { 658 super.onDraw(canvas); 659 } 660 661 @Override onDraw(Canvas canvas)662 public void onDraw(Canvas canvas) { 663 super.onDraw(canvas); 664 drawDotIfNecessary(canvas); 665 drawRunningAppIndicatorIfNecessary(canvas); 666 } 667 668 /** 669 * Draws the notification dot in the top right corner of the icon bounds. 670 * 671 * @param canvas The canvas to draw to. 672 */ drawDotIfNecessary(Canvas canvas)673 protected void drawDotIfNecessary(Canvas canvas) { 674 if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) { 675 getIconBounds(mDotParams.iconBounds); 676 Utilities.scaleRectAboutCenter(mDotParams.iconBounds, 677 IconShape.INSTANCE.get(getContext()).getNormalizationScale()); 678 final int scrollX = getScrollX(); 679 final int scrollY = getScrollY(); 680 canvas.translate(scrollX, scrollY); 681 mDotRenderer.draw(canvas, mDotParams); 682 canvas.translate(-scrollX, -scrollY); 683 } 684 } 685 686 /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */ drawRunningAppIndicatorIfNecessary(Canvas canvas)687 protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) { 688 if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) { 689 return; 690 } 691 getIconBounds(mRunningAppIconBounds); 692 // TODO(b/333872717): update color, shape, and size of indicator 693 boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED; 694 int indicatorTop = 695 mRunningAppIconBounds.bottom + (isMinimized ? mMinimizedAppIndicatorTopMargin 696 : mRunningAppIndicatorTopMargin); 697 final Size indicatorSize = 698 isMinimized ? mMinimizedAppIndicatorSize : mRunningAppIndicatorSize; 699 canvas.drawRect(mRunningAppIconBounds.centerX() - indicatorSize.getWidth() / 2, 700 indicatorTop, mRunningAppIconBounds.centerX() + indicatorSize.getWidth() / 2, 701 indicatorTop + indicatorSize.getHeight(), mRunningAppIndicatorPaint); 702 } 703 704 @Override setForceHideDot(boolean forceHideDot)705 public void setForceHideDot(boolean forceHideDot) { 706 if (mForceHideDot == forceHideDot) { 707 return; 708 } 709 mForceHideDot = forceHideDot; 710 711 if (forceHideDot) { 712 invalidate(); 713 } else if (hasDot()) { 714 animateDotScale(0, 1); 715 } 716 } 717 718 @VisibleForTesting getForceHideDot()719 public boolean getForceHideDot() { 720 return mForceHideDot; 721 } 722 hasDot()723 public boolean hasDot() { 724 return mDotInfo != null; 725 } 726 727 /** 728 * Get the icon bounds on the view depending on the layout type. 729 */ getIconBounds(Rect outBounds)730 public void getIconBounds(Rect outBounds) { 731 getIconBounds(mIconSize, outBounds); 732 } 733 734 /** 735 * Get the icon bounds on the view depending on the layout type. 736 */ getIconBounds(int iconSize, Rect outBounds)737 public void getIconBounds(int iconSize, Rect outBounds) { 738 outBounds.set(0, 0, iconSize, iconSize); 739 if (mLayoutHorizontal) { 740 int top = (getHeight() - iconSize) / 2; 741 if (mIsRtl) { 742 outBounds.offsetTo(getWidth() - iconSize - getPaddingRight(), top); 743 } else { 744 outBounds.offsetTo(getPaddingLeft(), top); 745 } 746 } else { 747 outBounds.offset((getWidth() - iconSize) / 2, getPaddingTop()); 748 } 749 } 750 751 /** 752 * Sets whether the layout is horizontal. 753 */ setLayoutHorizontal(boolean layoutHorizontal)754 public void setLayoutHorizontal(boolean layoutHorizontal) { 755 if (mLayoutHorizontal == layoutHorizontal) { 756 return; 757 } 758 759 mLayoutHorizontal = layoutHorizontal; 760 applyCompoundDrawables(getIconOrTransparentColor()); 761 } 762 763 /** 764 * Sets whether to vertically center the content. 765 */ setCenterVertically(boolean centerVertically)766 public void setCenterVertically(boolean centerVertically) { 767 mCenterVertically = centerVertically; 768 } 769 770 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)771 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 772 int height = MeasureSpec.getSize(heightMeasureSpec); 773 if (mCenterVertically) { 774 Paint.FontMetrics fm = getPaint().getFontMetrics(); 775 int cellHeightPx = mIconSize + getCompoundDrawablePadding() + 776 (int) Math.ceil(fm.bottom - fm.top); 777 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 778 getPaddingBottom()); 779 } 780 // Only apply two line for all_apps and device search only if necessary. 781 if (shouldUseTwoLine() && (mLastOriginalText != null)) { 782 int allowedVerticalSpace = height - getPaddingTop() - getPaddingBottom() 783 - mDeviceProfile.allAppsIconSizePx 784 - mDeviceProfile.allAppsIconDrawablePaddingPx; 785 CharSequence modifiedString = modifyTitleToSupportMultiLine( 786 MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft() 787 - getCompoundPaddingRight(), 788 allowedVerticalSpace, 789 mLastOriginalText, 790 getPaint(), 791 mBreakPointsIntArray, 792 getLineSpacingMultiplier(), 793 getLineSpacingExtra()); 794 if (!TextUtils.equals(modifiedString, mLastModifiedText)) { 795 mLastModifiedText = modifiedString; 796 setText(modifiedString); 797 // if text contains NEW_LINE, set max lines to 2 798 if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) { 799 setSingleLine(false); 800 setMaxLines(2); 801 } else { 802 setSingleLine(true); 803 setMaxLines(1); 804 } 805 } 806 } 807 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 808 } 809 810 @Override setTextColor(int color)811 public void setTextColor(int color) { 812 mTextColor = color; 813 mTextColorStateList = null; 814 super.setTextColor(getModifiedColor()); 815 } 816 817 @Override setTextColor(ColorStateList colors)818 public void setTextColor(ColorStateList colors) { 819 mTextColor = colors.getDefaultColor(); 820 mTextColorStateList = colors; 821 if (Float.compare(mTextAlpha, 1) == 0) { 822 super.setTextColor(colors); 823 } else { 824 super.setTextColor(getModifiedColor()); 825 } 826 } 827 shouldTextBeVisible()828 public boolean shouldTextBeVisible() { 829 // Text should be visible everywhere but the hotseat. 830 Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag(); 831 ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null; 832 return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT 833 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION); 834 } 835 setTextVisibility(boolean visible)836 public void setTextVisibility(boolean visible) { 837 setTextAlpha(visible ? 1 : 0); 838 } 839 setTextAlpha(float alpha)840 private void setTextAlpha(float alpha) { 841 mTextAlpha = alpha; 842 if (mTextColorStateList != null) { 843 setTextColor(mTextColorStateList); 844 } else { 845 super.setTextColor(getModifiedColor()); 846 } 847 } 848 getModifiedColor()849 private int getModifiedColor() { 850 if (mTextAlpha == 0) { 851 // Special case to prevent text shadows in high contrast mode 852 return Color.TRANSPARENT; 853 } 854 return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha)); 855 } 856 857 /** 858 * Creates an animator to fade the text in or out. 859 * 860 * @param fadeIn Whether the text should fade in or fade out. 861 */ createTextAlphaAnimator(boolean fadeIn)862 public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) { 863 float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0; 864 return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); 865 } 866 867 /** 868 * Generate a new string that will support two line text depending on the current string. 869 * This method calculates the limited width of a text view and creates a string to fit as 870 * many words as it can until the limit is reached. Once the limit is reached, we decide to 871 * either return the original title or continue on a new line. How to get the new string is by 872 * iterating through the list of break points and determining if the strings between the break 873 * points can fit within the line it is in. We will show the modified string if there is enough 874 * horizontal and vertical space, otherwise this method will just return the original string. 875 * Example assuming each character takes up one spot: 876 * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7 877 * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery, 878 * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth 879 * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking 880 * if the first char is a SPACE, we trim to append "Stats". So resulting string would be 881 * "Battery\nStats" 882 */ modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight, CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier, float spacingExtra)883 public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight, 884 CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier, 885 float spacingExtra) { 886 // current title is less than the width allowed so we can just skip 887 if (title == null || paint.measureText(title, 0, title.length()) <= limitedWidth) { 888 return title; 889 } 890 float currentWordWidth, runningWidth = 0; 891 CharSequence currentWord; 892 StringBuilder newString = new StringBuilder(); 893 paint.setLetterSpacing(MIN_LETTER_SPACING); 894 int stringPtr = 0; 895 for (int i = 0; i < breakPoints.size() + 1; i++) { 896 if (i < breakPoints.size()) { 897 currentWord = title.subSequence(stringPtr, breakPoints.get(i) + 1); 898 } else { 899 // last word from recent breakpoint until the end of the string 900 currentWord = title.subSequence(stringPtr, title.length()); 901 } 902 currentWordWidth = paint.measureText(currentWord, 0, currentWord.length()); 903 runningWidth += currentWordWidth; 904 if (runningWidth <= limitedWidth) { 905 newString.append(currentWord); 906 } else { 907 if (i != 0) { 908 // If putting word onto a new line, make sure there is no space or new line 909 // character in the beginning of the current word and just put in the rest of 910 // the characters. 911 CharSequence lastCharacters = title.subSequence(stringPtr, title.length()); 912 int beginningLetterType = 913 Character.getType(Character.codePointAt(lastCharacters, 0)); 914 if (beginningLetterType == Character.SPACE_SEPARATOR 915 || beginningLetterType == Character.LINE_SEPARATOR) { 916 lastCharacters = lastCharacters.length() > 1 917 ? lastCharacters.subSequence(1, lastCharacters.length()) 918 : EMPTY; 919 } 920 newString.append(NEW_LINE).append(lastCharacters); 921 StaticLayout staticLayout = new StaticLayout(newString, paint, limitedWidth, 922 ALIGN_NORMAL, spacingMultiplier, spacingExtra, false); 923 if (staticLayout.getHeight() < limitedHeight) { 924 return newString.toString(); 925 } 926 } 927 // if the first words exceeds width, just return as the first line will ellipse 928 return title; 929 } 930 if (i >= breakPoints.size()) { 931 // no need to look forward into the string if we've already finished processing 932 break; 933 } 934 stringPtr = breakPoints.get(i) + 1; 935 } 936 return newString.toString(); 937 } 938 939 @Override cancelLongPress()940 public void cancelLongPress() { 941 super.cancelLongPress(); 942 mLongPressHelper.cancelLongPress(); 943 } 944 945 /** 946 * Applies the loading progress value to the progress bar. 947 * 948 * If this app is installing, the progress bar will be updated with the installation progress. 949 * If this app is installed and downloading incrementally, the progress bar will be updated 950 * with the total download progress. 951 */ applyLoadingState(PreloadIconDrawable icon)952 public void applyLoadingState(PreloadIconDrawable icon) { 953 if (getTag() instanceof ItemInfoWithIcon) { 954 WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); 955 if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0 956 || info.hasPromiseIconUi() 957 || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0 958 || (icon != null)) { 959 updateProgressBarUi(info.getProgressLevel() == 100 ? icon : null); 960 } 961 } 962 } 963 updateProgressBarUi(PreloadIconDrawable oldIcon)964 private void updateProgressBarUi(PreloadIconDrawable oldIcon) { 965 FastBitmapDrawable originalIcon = mIcon; 966 PreloadIconDrawable preloadDrawable = applyProgressLevel(); 967 if (preloadDrawable != null && oldIcon != null) { 968 preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon)); 969 } 970 } 971 972 /** Applies the given progress level to the this icon's progress bar. */ 973 @Nullable applyProgressLevel()974 public PreloadIconDrawable applyProgressLevel() { 975 if (!(getTag() instanceof ItemInfoWithIcon) 976 || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) { 977 return null; 978 } 979 980 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 981 int progressLevel = info.getProgressLevel(); 982 if (progressLevel >= 100) { 983 setContentDescription(info.contentDescription != null 984 ? info.contentDescription : ""); 985 } else if (progressLevel > 0) { 986 setDownloadStateContentDescription(info, progressLevel); 987 } else { 988 setContentDescription(getContext() 989 .getString(R.string.app_waiting_download_title, info.title)); 990 } 991 if (mIcon != null) { 992 PreloadIconDrawable preloadIconDrawable; 993 if (mIcon instanceof PreloadIconDrawable) { 994 preloadIconDrawable = (PreloadIconDrawable) mIcon; 995 preloadIconDrawable.setLevel(progressLevel); 996 preloadIconDrawable.setIsDisabled(isIconDisabled(info)); 997 } else { 998 preloadIconDrawable = makePreloadIcon(); 999 setIcon(preloadIconDrawable); 1000 } 1001 return preloadIconDrawable; 1002 } 1003 return null; 1004 } 1005 1006 /** 1007 * Creates a PreloadIconDrawable with the appropriate progress level without mutating this 1008 * object. 1009 */ 1010 @Nullable makePreloadIcon()1011 public PreloadIconDrawable makePreloadIcon() { 1012 if (!(getTag() instanceof ItemInfoWithIcon)) { 1013 return null; 1014 } 1015 1016 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 1017 int progressLevel = info.getProgressLevel(); 1018 final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info); 1019 1020 preloadDrawable.setLevel(progressLevel); 1021 preloadDrawable.setIsDisabled(isIconDisabled(info)); 1022 return preloadDrawable; 1023 } 1024 1025 /** 1026 * Returns true to grey the icon if the icon is either suspended or if the icon is pending 1027 * download 1028 */ isIconDisabled(ItemInfoWithIcon info)1029 public boolean isIconDisabled(ItemInfoWithIcon info) { 1030 return info.isDisabled() || info.isPendingDownload(); 1031 } 1032 1033 applyDotState(ItemInfo itemInfo, boolean animate)1034 public void applyDotState(ItemInfo itemInfo, boolean animate) { 1035 if (mIcon instanceof FastBitmapDrawable) { 1036 boolean wasDotted = mDotInfo != null; 1037 mDotInfo = mActivity.getDotInfoForItem(itemInfo); 1038 boolean isDotted = mDotInfo != null; 1039 float newDotScale = isDotted ? 1f : 0; 1040 if (mDisplay == DISPLAY_ALL_APPS) { 1041 mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps; 1042 } else { 1043 mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace; 1044 } 1045 if (wasDotted || isDotted) { 1046 // Animate when a dot is first added or when it is removed. 1047 if (animate && (wasDotted ^ isDotted) && isShown()) { 1048 animateDotScale(newDotScale); 1049 } else { 1050 cancelDotScaleAnim(); 1051 mDotParams.scale = newDotScale; 1052 invalidate(); 1053 } 1054 } 1055 if (!TextUtils.isEmpty(itemInfo.contentDescription)) { 1056 if (itemInfo.isDisabled()) { 1057 setContentDescription(getContext().getString(R.string.disabled_app_label, 1058 itemInfo.contentDescription)); 1059 } else if (hasDot()) { 1060 int count = mDotInfo.getNotificationCount(); 1061 setContentDescription( 1062 getAppLabelPluralString(itemInfo.contentDescription.toString(), count)); 1063 } else { 1064 setContentDescription(itemInfo.contentDescription); 1065 } 1066 } 1067 } 1068 } 1069 setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel)1070 private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) { 1071 if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0 && progressLevel == 0) { 1072 setContentDescription(getContext().getString(R.string.app_archived_title, info.title)); 1073 } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) 1074 != 0) { 1075 String percentageString = NumberFormat.getPercentInstance() 1076 .format(progressLevel * 0.01); 1077 if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) { 1078 setContentDescription(getContext() 1079 .getString( 1080 R.string.app_installing_title, info.title, percentageString)); 1081 } else if ((info.runtimeStatusFlags 1082 & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) { 1083 setContentDescription(getContext() 1084 .getString( 1085 R.string.app_downloading_title, info.title, percentageString)); 1086 } 1087 } 1088 } 1089 1090 /** 1091 * Sets the icon for this view based on the layout direction. 1092 */ setIcon(FastBitmapDrawable icon)1093 protected void setIcon(FastBitmapDrawable icon) { 1094 if (mIsIconVisible) { 1095 applyCompoundDrawables(icon); 1096 } 1097 mIcon = icon; 1098 if (mIcon != null) { 1099 mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); 1100 } 1101 } 1102 1103 @Override setIconVisible(boolean visible)1104 public void setIconVisible(boolean visible) { 1105 mIsIconVisible = visible; 1106 if (!mIsIconVisible) { 1107 resetIconScale(); 1108 } 1109 Drawable icon = getIconOrTransparentColor(); 1110 applyCompoundDrawables(icon); 1111 } 1112 getIconOrTransparentColor()1113 private Drawable getIconOrTransparentColor() { 1114 return mIsIconVisible ? mIcon : new ColorDrawable(Color.TRANSPARENT); 1115 } 1116 1117 /** Sets the icon visual state to disabled or not. */ setIconDisabled(boolean isDisabled)1118 public void setIconDisabled(boolean isDisabled) { 1119 if (mIcon != null) { 1120 mIcon.setIsDisabled(isDisabled); 1121 } 1122 } 1123 iconUpdateAnimationEnabled()1124 protected boolean iconUpdateAnimationEnabled() { 1125 return mEnableIconUpdateAnimation; 1126 } 1127 applyCompoundDrawables(Drawable icon)1128 protected void applyCompoundDrawables(Drawable icon) { 1129 if (icon == null) { 1130 // Icon can be null when we use the BubbleTextView for text only. 1131 return; 1132 } 1133 1134 // If we had already set an icon before, disable relayout as the icon size is the 1135 // same as before. 1136 mDisableRelayout = mIcon != null; 1137 1138 icon.setBounds(0, 0, mIconSize, mIconSize); 1139 1140 updateIcon(icon); 1141 1142 // If the current icon is a placeholder color, animate its update. 1143 if (mIcon != null 1144 && mIcon instanceof PlaceHolderIconDrawable 1145 && iconUpdateAnimationEnabled()) { 1146 ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon); 1147 } 1148 1149 mDisableRelayout = false; 1150 } 1151 1152 @Override requestLayout()1153 public void requestLayout() { 1154 if (!mDisableRelayout) { 1155 super.requestLayout(); 1156 } 1157 } 1158 1159 /** 1160 * Applies the item info if it is same as what the view is pointing to currently. 1161 */ 1162 @Override reapplyItemInfo(ItemInfoWithIcon info)1163 public void reapplyItemInfo(ItemInfoWithIcon info) { 1164 if (getTag() == info) { 1165 mIconLoadRequest = null; 1166 mDisableRelayout = true; 1167 mEnableIconUpdateAnimation = true; 1168 1169 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 1170 info.bitmap.icon.prepareToDraw(); 1171 1172 if (info instanceof AppInfo) { 1173 applyFromApplicationInfo((AppInfo) info); 1174 } else if (info instanceof WorkspaceItemInfo) { 1175 applyFromWorkspaceItem((WorkspaceItemInfo) info); 1176 mActivity.invalidateParent(info); 1177 } else if (info != null) { 1178 applyFromItemInfoWithIcon(info); 1179 } 1180 1181 mDisableRelayout = false; 1182 mEnableIconUpdateAnimation = false; 1183 } 1184 } 1185 1186 /** 1187 * Verifies that the current icon is high-res otherwise posts a request to load the icon. 1188 */ verifyHighRes()1189 public void verifyHighRes() { 1190 if (mIconLoadRequest != null) { 1191 mIconLoadRequest.cancel(); 1192 mIconLoadRequest = null; 1193 } 1194 if (getTag() instanceof ItemInfoWithIcon) { 1195 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 1196 if (info.usingLowResIcon()) { 1197 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 1198 .updateIconInBackground(BubbleTextView.this, info); 1199 } 1200 } 1201 } 1202 getIconSize()1203 public int getIconSize() { 1204 return mIconSize; 1205 } 1206 isDisplaySearchResult()1207 public boolean isDisplaySearchResult() { 1208 return mDisplay == DISPLAY_SEARCH_RESULT 1209 || mDisplay == DISPLAY_SEARCH_RESULT_SMALL 1210 || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW; 1211 } 1212 getIconDisplay()1213 public int getIconDisplay() { 1214 return mDisplay; 1215 } 1216 1217 @Override getTranslateDelegate()1218 public MultiTranslateDelegate getTranslateDelegate() { 1219 return mTranslateDelegate; 1220 } 1221 1222 @Override setReorderBounceScale(float scale)1223 public void setReorderBounceScale(float scale) { 1224 mScaleForReorderBounce = scale; 1225 super.setScaleX(scale); 1226 super.setScaleY(scale); 1227 } 1228 1229 @Override getReorderBounceScale()1230 public float getReorderBounceScale() { 1231 return mScaleForReorderBounce; 1232 } 1233 1234 @Override getViewType()1235 public int getViewType() { 1236 return DRAGGABLE_ICON; 1237 } 1238 1239 @Override getWorkspaceVisualDragBounds(Rect bounds)1240 public void getWorkspaceVisualDragBounds(Rect bounds) { 1241 getIconBounds(mIconSize, bounds); 1242 } 1243 getSourceVisualDragBounds(Rect bounds)1244 public void getSourceVisualDragBounds(Rect bounds) { 1245 getIconBounds(mIconSize, bounds); 1246 } 1247 1248 @Override prepareDrawDragView()1249 public SafeCloseable prepareDrawDragView() { 1250 resetIconScale(); 1251 setForceHideDot(true); 1252 return () -> { 1253 }; 1254 } 1255 resetIconScale()1256 private void resetIconScale() { 1257 if (mIcon != null) { 1258 mIcon.resetScale(); 1259 } 1260 } 1261 updateIcon(Drawable newIcon)1262 private void updateIcon(Drawable newIcon) { 1263 if (mLayoutHorizontal) { 1264 setCompoundDrawablesRelative(newIcon, null, null, null); 1265 } else { 1266 setCompoundDrawables(null, newIcon, null, null); 1267 } 1268 } 1269 getAppLabelPluralString(String appName, int notificationCount)1270 private String getAppLabelPluralString(String appName, int notificationCount) { 1271 MessageFormat icuCountFormat = new MessageFormat( 1272 getResources().getString(R.string.dotted_app_label), 1273 Locale.getDefault()); 1274 HashMap<String, Object> args = new HashMap(); 1275 args.put("app_name", appName); 1276 args.put("count", notificationCount); 1277 return icuCountFormat.format(args); 1278 } 1279 1280 /** 1281 * Starts a long press action and returns the corresponding pre-drag condition 1282 */ startLongPressAction()1283 public PreDragCondition startLongPressAction() { 1284 PopupContainerWithArrow popup = PopupContainerWithArrow.showForIcon(this); 1285 return popup != null ? popup.createPreDragCondition(true) : null; 1286 } 1287 1288 /** 1289 * Returns true if the view can show long-press popup 1290 */ canShowLongPressPopup()1291 public boolean canShowLongPressPopup() { 1292 return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag()); 1293 } 1294 1295 /** Returns the package name of the app this icon represents. */ getTargetPackageName()1296 public String getTargetPackageName() { 1297 Object tag = getTag(); 1298 if (tag instanceof ItemInfo itemInfo) { 1299 return itemInfo.getTargetPackage(); 1300 } 1301 return null; 1302 } 1303 } 1304