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