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.icons.GraphicsUtils.setColorAlphaBound; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ObjectAnimator; 24 import android.content.Context; 25 import android.content.res.ColorStateList; 26 import android.content.res.TypedArray; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.text.TextUtils.TruncateAt; 34 import android.util.AttributeSet; 35 import android.util.Log; 36 import android.util.Property; 37 import android.util.TypedValue; 38 import android.view.KeyEvent; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.ViewConfiguration; 42 import android.view.ViewDebug; 43 import android.widget.TextView; 44 45 import com.android.launcher3.Launcher.OnResumeCallback; 46 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; 47 import com.android.launcher3.dot.DotInfo; 48 import com.android.launcher3.folder.FolderIcon; 49 import com.android.launcher3.graphics.DrawableFactory; 50 import com.android.launcher3.graphics.IconPalette; 51 import com.android.launcher3.graphics.IconShape; 52 import com.android.launcher3.graphics.PreloadIconDrawable; 53 import com.android.launcher3.icons.DotRenderer; 54 import com.android.launcher3.icons.IconCache.IconLoadRequest; 55 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 56 import com.android.launcher3.model.PackageItemInfo; 57 import com.android.launcher3.testing.TestProtocol; 58 import com.android.launcher3.views.ActivityContext; 59 60 import java.text.NumberFormat; 61 62 /** 63 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 64 * because we want to make the bubble taller than the text and TextView's clip is 65 * too aggressive. 66 */ 67 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback { 68 69 private static final int DISPLAY_WORKSPACE = 0; 70 private static final int DISPLAY_ALL_APPS = 1; 71 private static final int DISPLAY_FOLDER = 2; 72 73 private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed}; 74 75 76 private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY 77 = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") { 78 @Override 79 public Float get(BubbleTextView bubbleTextView) { 80 return bubbleTextView.mDotParams.scale; 81 } 82 83 @Override 84 public void set(BubbleTextView bubbleTextView, Float value) { 85 bubbleTextView.mDotParams.scale = value; 86 bubbleTextView.invalidate(); 87 } 88 }; 89 90 public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY 91 = new Property<BubbleTextView, Float>(Float.class, "textAlpha") { 92 @Override 93 public Float get(BubbleTextView bubbleTextView) { 94 return bubbleTextView.mTextAlpha; 95 } 96 97 @Override 98 public void set(BubbleTextView bubbleTextView, Float alpha) { 99 bubbleTextView.setTextAlpha(alpha); 100 } 101 }; 102 103 private final ActivityContext mActivity; 104 private Drawable mIcon; 105 private final boolean mCenterVertically; 106 107 private final CheckLongPressHelper mLongPressHelper; 108 private final StylusEventHelper mStylusEventHelper; 109 private final float mSlop; 110 111 private final boolean mLayoutHorizontal; 112 private final int mIconSize; 113 114 @ViewDebug.ExportedProperty(category = "launcher") 115 private boolean mIsIconVisible = true; 116 @ViewDebug.ExportedProperty(category = "launcher") 117 private int mTextColor; 118 @ViewDebug.ExportedProperty(category = "launcher") 119 private float mTextAlpha = 1; 120 121 @ViewDebug.ExportedProperty(category = "launcher") 122 private DotInfo mDotInfo; 123 private DotRenderer mDotRenderer; 124 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 125 private DotRenderer.DrawParams mDotParams; 126 private Animator mDotScaleAnim; 127 private boolean mForceHideDot; 128 129 @ViewDebug.ExportedProperty(category = "launcher") 130 private boolean mStayPressed; 131 @ViewDebug.ExportedProperty(category = "launcher") 132 private boolean mIgnorePressedStateChange; 133 @ViewDebug.ExportedProperty(category = "launcher") 134 private boolean mDisableRelayout = false; 135 136 private IconLoadRequest mIconLoadRequest; 137 BubbleTextView(Context context)138 public BubbleTextView(Context context) { 139 this(context, null, 0); 140 } 141 BubbleTextView(Context context, AttributeSet attrs)142 public BubbleTextView(Context context, AttributeSet attrs) { 143 this(context, attrs, 0); 144 } 145 BubbleTextView(Context context, AttributeSet attrs, int defStyle)146 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 147 super(context, attrs, defStyle); 148 mActivity = ActivityContext.lookupContext(context); 149 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 150 151 TypedArray a = context.obtainStyledAttributes(attrs, 152 R.styleable.BubbleTextView, defStyle, 0); 153 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 154 155 int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 156 final int defaultIconSize; 157 if (display == DISPLAY_WORKSPACE) { 158 DeviceProfile grid = mActivity.getWallpaperDeviceProfile(); 159 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); 160 setCompoundDrawablePadding(grid.iconDrawablePaddingPx); 161 defaultIconSize = grid.iconSizePx; 162 } else if (display == DISPLAY_ALL_APPS) { 163 DeviceProfile grid = mActivity.getDeviceProfile(); 164 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); 165 setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx); 166 defaultIconSize = grid.allAppsIconSizePx; 167 } else if (display == DISPLAY_FOLDER) { 168 DeviceProfile grid = mActivity.getDeviceProfile(); 169 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx); 170 setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx); 171 defaultIconSize = grid.folderChildIconSizePx; 172 } else { 173 defaultIconSize = mActivity.getDeviceProfile().iconSizePx; 174 } 175 mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false); 176 177 mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, 178 defaultIconSize); 179 a.recycle(); 180 181 mLongPressHelper = new CheckLongPressHelper(this); 182 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 183 184 mDotParams = new DotRenderer.DrawParams(); 185 186 setEllipsize(TruncateAt.END); 187 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 188 setTextAlpha(1f); 189 } 190 191 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)192 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 193 // Disable marques when not focused to that, so that updating text does not cause relayout. 194 setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END); 195 super.onFocusChanged(focused, direction, previouslyFocusedRect); 196 } 197 198 /** 199 * Resets the view so it can be recycled. 200 */ reset()201 public void reset() { 202 mDotInfo = null; 203 mDotParams.color = Color.TRANSPARENT; 204 cancelDotScaleAnim(); 205 mDotParams.scale = 0f; 206 mForceHideDot = false; 207 } 208 cancelDotScaleAnim()209 private void cancelDotScaleAnim() { 210 if (mDotScaleAnim != null) { 211 mDotScaleAnim.cancel(); 212 } 213 } 214 animateDotScale(float... dotScales)215 private void animateDotScale(float... dotScales) { 216 cancelDotScaleAnim(); 217 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 218 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 219 @Override 220 public void onAnimationEnd(Animator animation) { 221 mDotScaleAnim = null; 222 } 223 }); 224 mDotScaleAnim.start(); 225 } 226 applyFromWorkspaceItem(WorkspaceItemInfo info)227 public void applyFromWorkspaceItem(WorkspaceItemInfo info) { 228 applyFromWorkspaceItem(info, false); 229 } 230 231 @Override setAccessibilityDelegate(AccessibilityDelegate delegate)232 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 233 if (delegate instanceof LauncherAccessibilityDelegate) { 234 super.setAccessibilityDelegate(delegate); 235 } else { 236 // NO-OP 237 // Workaround for b/129745295 where RecyclerView is setting our Accessibility 238 // delegate incorrectly. There are no cases when we shouldn't be using the 239 // LauncherAccessibilityDelegate for BubbleTextView. 240 } 241 } 242 applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged)243 public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) { 244 applyIconAndLabel(info); 245 setTag(info); 246 if (promiseStateChanged || (info.hasPromiseIconUi())) { 247 applyPromiseState(promiseStateChanged); 248 } 249 250 applyDotState(info, false /* animate */); 251 } 252 applyFromApplicationInfo(AppInfo info)253 public void applyFromApplicationInfo(AppInfo info) { 254 applyIconAndLabel(info); 255 256 // We don't need to check the info since it's not a WorkspaceItemInfo 257 super.setTag(info); 258 259 // Verify high res immediately 260 verifyHighRes(); 261 262 if (info instanceof PromiseAppInfo) { 263 PromiseAppInfo promiseAppInfo = (PromiseAppInfo) info; 264 applyProgressLevel(promiseAppInfo.level); 265 } 266 applyDotState(info, false /* animate */); 267 } 268 applyFromPackageItemInfo(PackageItemInfo info)269 public void applyFromPackageItemInfo(PackageItemInfo info) { 270 applyIconAndLabel(info); 271 // We don't need to check the info since it's not a WorkspaceItemInfo 272 super.setTag(info); 273 274 // Verify high res immediately 275 verifyHighRes(); 276 } 277 applyIconAndLabel(ItemInfoWithIcon info)278 private void applyIconAndLabel(ItemInfoWithIcon info) { 279 FastBitmapDrawable iconDrawable = DrawableFactory.INSTANCE.get(getContext()) 280 .newIcon(getContext(), info); 281 mDotParams.color = IconPalette.getMutedColor(info.iconColor, 0.54f); 282 283 setIcon(iconDrawable); 284 setText(info.title); 285 if (info.contentDescription != null) { 286 setContentDescription(info.isDisabled() 287 ? getContext().getString(R.string.disabled_app_label, info.contentDescription) 288 : info.contentDescription); 289 } 290 } 291 292 /** 293 * Overrides the default long press timeout. 294 */ setLongPressTimeoutFactor(float longPressTimeoutFactor)295 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) { 296 mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor); 297 } 298 299 @Override refreshDrawableState()300 public void refreshDrawableState() { 301 if (!mIgnorePressedStateChange) { 302 super.refreshDrawableState(); 303 } 304 } 305 306 @Override onCreateDrawableState(int extraSpace)307 protected int[] onCreateDrawableState(int extraSpace) { 308 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 309 if (mStayPressed) { 310 mergeDrawableStates(drawableState, STATE_PRESSED); 311 } 312 return drawableState; 313 } 314 315 /** Returns the icon for this view. */ getIcon()316 public Drawable getIcon() { 317 return mIcon; 318 } 319 320 @Override onTouchEvent(MotionEvent event)321 public boolean onTouchEvent(MotionEvent event) { 322 if (TestProtocol.sDebugTracing) { 323 Log.d(TestProtocol.NO_START_TAG, "BubbleTextView.onTouchEvent " + event); 324 } 325 // Call the superclass onTouchEvent first, because sometimes it changes the state to 326 // isPressed() on an ACTION_UP 327 boolean result = super.onTouchEvent(event); 328 329 // Check for a stylus button press, if it occurs cancel any long press checks. 330 if (mStylusEventHelper.onMotionEvent(event)) { 331 mLongPressHelper.cancelLongPress(); 332 result = true; 333 } 334 335 switch (event.getAction()) { 336 case MotionEvent.ACTION_DOWN: 337 // If we're in a stylus button press, don't check for long press. 338 if (!mStylusEventHelper.inStylusButtonPressed()) { 339 mLongPressHelper.postCheckForLongPress(); 340 } 341 break; 342 case MotionEvent.ACTION_CANCEL: 343 case MotionEvent.ACTION_UP: 344 mLongPressHelper.cancelLongPress(); 345 break; 346 case MotionEvent.ACTION_MOVE: 347 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { 348 mLongPressHelper.cancelLongPress(); 349 } 350 break; 351 } 352 return result; 353 } 354 setStayPressed(boolean stayPressed)355 void setStayPressed(boolean stayPressed) { 356 mStayPressed = stayPressed; 357 refreshDrawableState(); 358 } 359 360 @Override onVisibilityAggregated(boolean isVisible)361 public void onVisibilityAggregated(boolean isVisible) { 362 super.onVisibilityAggregated(isVisible); 363 if (mIcon != null) { 364 mIcon.setVisible(isVisible, false); 365 } 366 } 367 368 @Override onLauncherResume()369 public void onLauncherResume() { 370 // Reset the pressed state of icon that was locked in the press state while activity 371 // was launching 372 setStayPressed(false); 373 } 374 clearPressedBackground()375 void clearPressedBackground() { 376 setPressed(false); 377 setStayPressed(false); 378 } 379 380 @Override onKeyUp(int keyCode, KeyEvent event)381 public boolean onKeyUp(int keyCode, KeyEvent event) { 382 // Unlike touch events, keypress event propagate pressed state change immediately, 383 // without waiting for onClickHandler to execute. Disable pressed state changes here 384 // to avoid flickering. 385 mIgnorePressedStateChange = true; 386 boolean result = super.onKeyUp(keyCode, event); 387 mIgnorePressedStateChange = false; 388 refreshDrawableState(); 389 return result; 390 } 391 392 @SuppressWarnings("wrongcall") drawWithoutDot(Canvas canvas)393 protected void drawWithoutDot(Canvas canvas) { 394 super.onDraw(canvas); 395 } 396 397 @Override onDraw(Canvas canvas)398 public void onDraw(Canvas canvas) { 399 super.onDraw(canvas); 400 drawDotIfNecessary(canvas); 401 } 402 403 /** 404 * Draws the notification dot in the top right corner of the icon bounds. 405 * @param canvas The canvas to draw to. 406 */ drawDotIfNecessary(Canvas canvas)407 protected void drawDotIfNecessary(Canvas canvas) { 408 if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) { 409 getIconBounds(mDotParams.iconBounds); 410 Utilities.scaleRectAboutCenter(mDotParams.iconBounds, IconShape.getNormalizationScale()); 411 final int scrollX = getScrollX(); 412 final int scrollY = getScrollY(); 413 canvas.translate(scrollX, scrollY); 414 mDotRenderer.draw(canvas, mDotParams); 415 canvas.translate(-scrollX, -scrollY); 416 } 417 } 418 forceHideDot(boolean forceHideDot)419 public void forceHideDot(boolean forceHideDot) { 420 if (mForceHideDot == forceHideDot) { 421 return; 422 } 423 mForceHideDot = forceHideDot; 424 425 if (forceHideDot) { 426 invalidate(); 427 } else if (hasDot()) { 428 animateDotScale(0, 1); 429 } 430 } 431 hasDot()432 private boolean hasDot() { 433 return mDotInfo != null; 434 } 435 getIconBounds(Rect outBounds)436 public void getIconBounds(Rect outBounds) { 437 getIconBounds(this, outBounds, mIconSize); 438 } 439 getIconBounds(View iconView, Rect outBounds, int iconSize)440 public static void getIconBounds(View iconView, Rect outBounds, int iconSize) { 441 int top = iconView.getPaddingTop(); 442 int left = (iconView.getWidth() - iconSize) / 2; 443 int right = left + iconSize; 444 int bottom = top + iconSize; 445 outBounds.set(left, top, right, bottom); 446 } 447 448 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)449 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 450 if (mCenterVertically) { 451 Paint.FontMetrics fm = getPaint().getFontMetrics(); 452 int cellHeightPx = mIconSize + getCompoundDrawablePadding() + 453 (int) Math.ceil(fm.bottom - fm.top); 454 int height = MeasureSpec.getSize(heightMeasureSpec); 455 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 456 getPaddingBottom()); 457 } 458 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 459 } 460 461 @Override setTextColor(int color)462 public void setTextColor(int color) { 463 mTextColor = color; 464 super.setTextColor(getModifiedColor()); 465 } 466 467 @Override setTextColor(ColorStateList colors)468 public void setTextColor(ColorStateList colors) { 469 mTextColor = colors.getDefaultColor(); 470 if (Float.compare(mTextAlpha, 1) == 0) { 471 super.setTextColor(colors); 472 } else { 473 super.setTextColor(getModifiedColor()); 474 } 475 } 476 shouldTextBeVisible()477 public boolean shouldTextBeVisible() { 478 // Text should be visible everywhere but the hotseat. 479 Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag(); 480 ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null; 481 return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT; 482 } 483 setTextVisibility(boolean visible)484 public void setTextVisibility(boolean visible) { 485 setTextAlpha(visible ? 1 : 0); 486 } 487 setTextAlpha(float alpha)488 private void setTextAlpha(float alpha) { 489 mTextAlpha = alpha; 490 super.setTextColor(getModifiedColor()); 491 } 492 getModifiedColor()493 private int getModifiedColor() { 494 if (mTextAlpha == 0) { 495 // Special case to prevent text shadows in high contrast mode 496 return Color.TRANSPARENT; 497 } 498 return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha)); 499 } 500 501 /** 502 * Creates an animator to fade the text in or out. 503 * @param fadeIn Whether the text should fade in or fade out. 504 */ createTextAlphaAnimator(boolean fadeIn)505 public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) { 506 float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0; 507 return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); 508 } 509 510 @Override cancelLongPress()511 public void cancelLongPress() { 512 super.cancelLongPress(); 513 514 mLongPressHelper.cancelLongPress(); 515 } 516 applyPromiseState(boolean promiseStateChanged)517 public void applyPromiseState(boolean promiseStateChanged) { 518 if (getTag() instanceof WorkspaceItemInfo) { 519 WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); 520 final boolean isPromise = info.hasPromiseIconUi(); 521 final int progressLevel = isPromise ? 522 ((info.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE) ? 523 info.getInstallProgress() : 0)) : 100; 524 525 PreloadIconDrawable preloadDrawable = applyProgressLevel(progressLevel); 526 if (preloadDrawable != null && promiseStateChanged) { 527 preloadDrawable.maybePerformFinishedAnimation(); 528 } 529 } 530 } 531 applyProgressLevel(int progressLevel)532 public PreloadIconDrawable applyProgressLevel(int progressLevel) { 533 if (getTag() instanceof ItemInfoWithIcon) { 534 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 535 if (progressLevel >= 100) { 536 setContentDescription(info.contentDescription != null 537 ? info.contentDescription : ""); 538 } else if (progressLevel > 0) { 539 setContentDescription(getContext() 540 .getString(R.string.app_downloading_title, info.title, 541 NumberFormat.getPercentInstance().format(progressLevel * 0.01))); 542 } else { 543 setContentDescription(getContext() 544 .getString(R.string.app_waiting_download_title, info.title)); 545 } 546 if (mIcon != null) { 547 final PreloadIconDrawable preloadDrawable; 548 if (mIcon instanceof PreloadIconDrawable) { 549 preloadDrawable = (PreloadIconDrawable) mIcon; 550 preloadDrawable.setLevel(progressLevel); 551 } else { 552 preloadDrawable = DrawableFactory.INSTANCE.get(getContext()) 553 .newPendingIcon(getContext(), info); 554 preloadDrawable.setLevel(progressLevel); 555 setIcon(preloadDrawable); 556 } 557 return preloadDrawable; 558 } 559 } 560 return null; 561 } 562 applyDotState(ItemInfo itemInfo, boolean animate)563 public void applyDotState(ItemInfo itemInfo, boolean animate) { 564 if (mIcon instanceof FastBitmapDrawable) { 565 boolean wasDotted = mDotInfo != null; 566 mDotInfo = mActivity.getDotInfoForItem(itemInfo); 567 boolean isDotted = mDotInfo != null; 568 float newDotScale = isDotted ? 1f : 0; 569 mDotRenderer = mActivity.getDeviceProfile().mDotRenderer; 570 if (wasDotted || isDotted) { 571 // Animate when a dot is first added or when it is removed. 572 if (animate && (wasDotted ^ isDotted) && isShown()) { 573 animateDotScale(newDotScale); 574 } else { 575 cancelDotScaleAnim(); 576 mDotParams.scale = newDotScale; 577 invalidate(); 578 } 579 } 580 if (itemInfo.contentDescription != null) { 581 if (itemInfo.isDisabled()) { 582 setContentDescription(getContext().getString(R.string.disabled_app_label, 583 itemInfo.contentDescription)); 584 } else if (hasDot()) { 585 int count = mDotInfo.getNotificationCount(); 586 setContentDescription(getContext().getResources().getQuantityString( 587 R.plurals.dotted_app_label, count, itemInfo.contentDescription, count)); 588 } else { 589 setContentDescription(itemInfo.contentDescription); 590 } 591 } 592 } 593 } 594 595 /** 596 * Sets the icon for this view based on the layout direction. 597 */ setIcon(Drawable icon)598 private void setIcon(Drawable icon) { 599 if (mIsIconVisible) { 600 applyCompoundDrawables(icon); 601 } 602 mIcon = icon; 603 if (mIcon != null) { 604 mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); 605 } 606 } 607 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.iconBitmap.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 } 681