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