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