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.Property; 36 import android.util.TypedValue; 37 import android.view.KeyEvent; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewConfiguration; 41 import android.view.ViewDebug; 42 import android.widget.TextView; 43 44 import com.android.launcher3.Launcher.OnResumeCallback; 45 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; 46 import com.android.launcher3.dot.DotInfo; 47 import com.android.launcher3.folder.FolderIcon; 48 import com.android.launcher3.graphics.DrawableFactory; 49 import com.android.launcher3.graphics.IconPalette; 50 import com.android.launcher3.graphics.IconShape; 51 import com.android.launcher3.graphics.PreloadIconDrawable; 52 import com.android.launcher3.icons.DotRenderer; 53 import com.android.launcher3.icons.IconCache.IconLoadRequest; 54 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 55 import com.android.launcher3.model.PackageItemInfo; 56 import com.android.launcher3.views.ActivityContext; 57 import com.android.launcher3.views.IconLabelDotView; 58 59 import java.text.NumberFormat; 60 61 /** 62 * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan 63 * because we want to make the bubble taller than the text and TextView's clip is 64 * too aggressive. 65 */ 66 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback, 67 IconLabelDotView { 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 int mDisplay; 108 109 private final CheckLongPressHelper mLongPressHelper; 110 private final StylusEventHelper mStylusEventHelper; 111 private final float mSlop; 112 113 private final boolean mLayoutHorizontal; 114 private final int mIconSize; 115 116 @ViewDebug.ExportedProperty(category = "launcher") 117 private boolean mIsIconVisible = true; 118 @ViewDebug.ExportedProperty(category = "launcher") 119 private int mTextColor; 120 @ViewDebug.ExportedProperty(category = "launcher") 121 private float mTextAlpha = 1; 122 123 @ViewDebug.ExportedProperty(category = "launcher") 124 private DotInfo mDotInfo; 125 private DotRenderer mDotRenderer; 126 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 127 private DotRenderer.DrawParams mDotParams; 128 private Animator mDotScaleAnim; 129 private boolean mForceHideDot; 130 131 @ViewDebug.ExportedProperty(category = "launcher") 132 private boolean mStayPressed; 133 @ViewDebug.ExportedProperty(category = "launcher") 134 private boolean mIgnorePressedStateChange; 135 @ViewDebug.ExportedProperty(category = "launcher") 136 private boolean mDisableRelayout = false; 137 138 @ViewDebug.ExportedProperty(category = "launcher") 139 private final boolean mIgnorePaddingTouch; 140 141 private IconLoadRequest mIconLoadRequest; 142 BubbleTextView(Context context)143 public BubbleTextView(Context context) { 144 this(context, null, 0); 145 } 146 BubbleTextView(Context context, AttributeSet attrs)147 public BubbleTextView(Context context, AttributeSet attrs) { 148 this(context, attrs, 0); 149 } 150 BubbleTextView(Context context, AttributeSet attrs, int defStyle)151 public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { 152 super(context, attrs, defStyle); 153 mActivity = ActivityContext.lookupContext(context); 154 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 155 156 TypedArray a = context.obtainStyledAttributes(attrs, 157 R.styleable.BubbleTextView, defStyle, 0); 158 mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); 159 160 mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); 161 final int defaultIconSize; 162 if (mDisplay == DISPLAY_WORKSPACE) { 163 DeviceProfile grid = mActivity.getWallpaperDeviceProfile(); 164 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); 165 setCompoundDrawablePadding(grid.iconDrawablePaddingPx); 166 defaultIconSize = grid.iconSizePx; 167 mIgnorePaddingTouch = true; 168 } else if (mDisplay == DISPLAY_ALL_APPS) { 169 DeviceProfile grid = mActivity.getDeviceProfile(); 170 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); 171 setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx); 172 defaultIconSize = grid.allAppsIconSizePx; 173 mIgnorePaddingTouch = true; 174 } else if (mDisplay == DISPLAY_FOLDER) { 175 DeviceProfile grid = mActivity.getDeviceProfile(); 176 setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx); 177 setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx); 178 defaultIconSize = grid.folderChildIconSizePx; 179 mIgnorePaddingTouch = true; 180 } else { 181 // widget_selection or shortcut_popup 182 defaultIconSize = mActivity.getDeviceProfile().iconSizePx; 183 mIgnorePaddingTouch = false; 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 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 194 195 mDotParams = new DotRenderer.DrawParams(); 196 197 setEllipsize(TruncateAt.END); 198 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 199 setTextAlpha(1f); 200 } 201 202 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)203 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 204 // Disable marques when not focused to that, so that updating text does not cause relayout. 205 setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END); 206 super.onFocusChanged(focused, direction, previouslyFocusedRect); 207 } 208 209 /** 210 * Resets the view so it can be recycled. 211 */ reset()212 public void reset() { 213 mDotInfo = null; 214 mDotParams.color = Color.TRANSPARENT; 215 cancelDotScaleAnim(); 216 mDotParams.scale = 0f; 217 mForceHideDot = false; 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 = DrawableFactory.INSTANCE.get(getContext()) 291 .newIcon(getContext(), info); 292 mDotParams.color = IconPalette.getMutedColor(info.iconColor, 0.54f); 293 294 setIcon(iconDrawable); 295 setText(info.title); 296 if (info.contentDescription != null) { 297 setContentDescription(info.isDisabled() 298 ? getContext().getString(R.string.disabled_app_label, info.contentDescription) 299 : info.contentDescription); 300 } 301 } 302 303 /** 304 * Overrides the default long press timeout. 305 */ setLongPressTimeoutFactor(float longPressTimeoutFactor)306 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) { 307 mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor); 308 } 309 310 @Override refreshDrawableState()311 public void refreshDrawableState() { 312 if (!mIgnorePressedStateChange) { 313 super.refreshDrawableState(); 314 } 315 } 316 317 @Override onCreateDrawableState(int extraSpace)318 protected int[] onCreateDrawableState(int extraSpace) { 319 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 320 if (mStayPressed) { 321 mergeDrawableStates(drawableState, STATE_PRESSED); 322 } 323 return drawableState; 324 } 325 326 /** Returns the icon for this view. */ getIcon()327 public Drawable getIcon() { 328 return mIcon; 329 } 330 331 @Override onTouchEvent(MotionEvent event)332 public boolean onTouchEvent(MotionEvent event) { 333 // ignore events if they happen in padding area 334 if (event.getAction() == MotionEvent.ACTION_DOWN && mIgnorePaddingTouch 335 && (event.getY() < getPaddingTop() 336 || event.getX() < getPaddingLeft() 337 || event.getY() > getHeight() - getPaddingBottom() 338 || event.getX() > getWidth() - getPaddingRight())) { 339 return false; 340 } 341 342 // Call the superclass onTouchEvent first, because sometimes it changes the state to 343 // isPressed() on an ACTION_UP 344 boolean result = super.onTouchEvent(event); 345 346 // Check for a stylus button press, if it occurs cancel any long press checks. 347 if (mStylusEventHelper.onMotionEvent(event)) { 348 mLongPressHelper.cancelLongPress(); 349 result = true; 350 } 351 352 switch (event.getAction()) { 353 case MotionEvent.ACTION_DOWN: 354 // If we're in a stylus button press, don't check for long press. 355 if (!mStylusEventHelper.inStylusButtonPressed()) { 356 mLongPressHelper.postCheckForLongPress(); 357 } 358 break; 359 case MotionEvent.ACTION_CANCEL: 360 case MotionEvent.ACTION_UP: 361 mLongPressHelper.cancelLongPress(); 362 break; 363 case MotionEvent.ACTION_MOVE: 364 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { 365 mLongPressHelper.cancelLongPress(); 366 } 367 break; 368 } 369 return result; 370 } 371 setStayPressed(boolean stayPressed)372 void setStayPressed(boolean stayPressed) { 373 mStayPressed = stayPressed; 374 refreshDrawableState(); 375 } 376 377 @Override onVisibilityAggregated(boolean isVisible)378 public void onVisibilityAggregated(boolean isVisible) { 379 super.onVisibilityAggregated(isVisible); 380 if (mIcon != null) { 381 mIcon.setVisible(isVisible, false); 382 } 383 } 384 385 @Override onLauncherResume()386 public void onLauncherResume() { 387 // Reset the pressed state of icon that was locked in the press state while activity 388 // was launching 389 setStayPressed(false); 390 } 391 clearPressedBackground()392 void clearPressedBackground() { 393 setPressed(false); 394 setStayPressed(false); 395 } 396 397 @Override onKeyUp(int keyCode, KeyEvent event)398 public boolean onKeyUp(int keyCode, KeyEvent event) { 399 // Unlike touch events, keypress event propagate pressed state change immediately, 400 // without waiting for onClickHandler to execute. Disable pressed state changes here 401 // to avoid flickering. 402 mIgnorePressedStateChange = true; 403 boolean result = super.onKeyUp(keyCode, event); 404 mIgnorePressedStateChange = false; 405 refreshDrawableState(); 406 return result; 407 } 408 409 @SuppressWarnings("wrongcall") drawWithoutDot(Canvas canvas)410 protected void drawWithoutDot(Canvas canvas) { 411 super.onDraw(canvas); 412 } 413 414 @Override onDraw(Canvas canvas)415 public void onDraw(Canvas canvas) { 416 super.onDraw(canvas); 417 drawDotIfNecessary(canvas); 418 } 419 420 /** 421 * Draws the notification dot in the top right corner of the icon bounds. 422 * @param canvas The canvas to draw to. 423 */ drawDotIfNecessary(Canvas canvas)424 protected void drawDotIfNecessary(Canvas canvas) { 425 if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) { 426 getIconBounds(mDotParams.iconBounds); 427 Utilities.scaleRectAboutCenter(mDotParams.iconBounds, IconShape.getNormalizationScale()); 428 final int scrollX = getScrollX(); 429 final int scrollY = getScrollY(); 430 canvas.translate(scrollX, scrollY); 431 mDotRenderer.draw(canvas, mDotParams); 432 canvas.translate(-scrollX, -scrollY); 433 } 434 } 435 436 @Override setForceHideDot(boolean forceHideDot)437 public void setForceHideDot(boolean forceHideDot) { 438 if (mForceHideDot == forceHideDot) { 439 return; 440 } 441 mForceHideDot = forceHideDot; 442 443 if (forceHideDot) { 444 invalidate(); 445 } else if (hasDot()) { 446 animateDotScale(0, 1); 447 } 448 } 449 hasDot()450 private boolean hasDot() { 451 return mDotInfo != null; 452 } 453 getIconBounds(Rect outBounds)454 public void getIconBounds(Rect outBounds) { 455 getIconBounds(this, outBounds, mIconSize); 456 } 457 getIconBounds(View iconView, Rect outBounds, int iconSize)458 public static void getIconBounds(View iconView, Rect outBounds, int iconSize) { 459 int top = iconView.getPaddingTop(); 460 int left = (iconView.getWidth() - iconSize) / 2; 461 int right = left + iconSize; 462 int bottom = top + iconSize; 463 outBounds.set(left, top, right, bottom); 464 } 465 466 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)467 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 468 if (mCenterVertically) { 469 Paint.FontMetrics fm = getPaint().getFontMetrics(); 470 int cellHeightPx = mIconSize + getCompoundDrawablePadding() + 471 (int) Math.ceil(fm.bottom - fm.top); 472 int height = MeasureSpec.getSize(heightMeasureSpec); 473 setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(), 474 getPaddingBottom()); 475 } 476 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 477 } 478 479 @Override setTextColor(int color)480 public void setTextColor(int color) { 481 mTextColor = color; 482 super.setTextColor(getModifiedColor()); 483 } 484 485 @Override setTextColor(ColorStateList colors)486 public void setTextColor(ColorStateList colors) { 487 mTextColor = colors.getDefaultColor(); 488 if (Float.compare(mTextAlpha, 1) == 0) { 489 super.setTextColor(colors); 490 } else { 491 super.setTextColor(getModifiedColor()); 492 } 493 } 494 shouldTextBeVisible()495 public boolean shouldTextBeVisible() { 496 // Text should be visible everywhere but the hotseat. 497 Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag(); 498 ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null; 499 return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT; 500 } 501 setTextVisibility(boolean visible)502 public void setTextVisibility(boolean visible) { 503 setTextAlpha(visible ? 1 : 0); 504 } 505 setTextAlpha(float alpha)506 private void setTextAlpha(float alpha) { 507 mTextAlpha = alpha; 508 super.setTextColor(getModifiedColor()); 509 } 510 getModifiedColor()511 private int getModifiedColor() { 512 if (mTextAlpha == 0) { 513 // Special case to prevent text shadows in high contrast mode 514 return Color.TRANSPARENT; 515 } 516 return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha)); 517 } 518 519 /** 520 * Creates an animator to fade the text in or out. 521 * @param fadeIn Whether the text should fade in or fade out. 522 */ createTextAlphaAnimator(boolean fadeIn)523 public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) { 524 float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0; 525 return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha); 526 } 527 528 @Override cancelLongPress()529 public void cancelLongPress() { 530 super.cancelLongPress(); 531 532 mLongPressHelper.cancelLongPress(); 533 } 534 applyPromiseState(boolean promiseStateChanged)535 public void applyPromiseState(boolean promiseStateChanged) { 536 if (getTag() instanceof WorkspaceItemInfo) { 537 WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); 538 final boolean isPromise = info.hasPromiseIconUi(); 539 final int progressLevel = isPromise ? 540 ((info.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE) ? 541 info.getInstallProgress() : 0)) : 100; 542 543 PreloadIconDrawable preloadDrawable = applyProgressLevel(progressLevel); 544 if (preloadDrawable != null && promiseStateChanged) { 545 preloadDrawable.maybePerformFinishedAnimation(); 546 } 547 } 548 } 549 applyProgressLevel(int progressLevel)550 public PreloadIconDrawable applyProgressLevel(int progressLevel) { 551 if (getTag() instanceof ItemInfoWithIcon) { 552 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 553 if (progressLevel >= 100) { 554 setContentDescription(info.contentDescription != null 555 ? info.contentDescription : ""); 556 } else if (progressLevel > 0) { 557 setContentDescription(getContext() 558 .getString(R.string.app_downloading_title, info.title, 559 NumberFormat.getPercentInstance().format(progressLevel * 0.01))); 560 } else { 561 setContentDescription(getContext() 562 .getString(R.string.app_waiting_download_title, info.title)); 563 } 564 if (mIcon != null) { 565 final PreloadIconDrawable preloadDrawable; 566 if (mIcon instanceof PreloadIconDrawable) { 567 preloadDrawable = (PreloadIconDrawable) mIcon; 568 preloadDrawable.setLevel(progressLevel); 569 } else { 570 preloadDrawable = DrawableFactory.INSTANCE.get(getContext()) 571 .newPendingIcon(getContext(), info); 572 preloadDrawable.setLevel(progressLevel); 573 setIcon(preloadDrawable); 574 } 575 return preloadDrawable; 576 } 577 } 578 return null; 579 } 580 applyDotState(ItemInfo itemInfo, boolean animate)581 public void applyDotState(ItemInfo itemInfo, boolean animate) { 582 if (mIcon instanceof FastBitmapDrawable) { 583 boolean wasDotted = mDotInfo != null; 584 mDotInfo = mActivity.getDotInfoForItem(itemInfo); 585 boolean isDotted = mDotInfo != null; 586 float newDotScale = isDotted ? 1f : 0; 587 if (mDisplay == DISPLAY_ALL_APPS) { 588 mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps; 589 } else { 590 mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace; 591 } 592 if (wasDotted || isDotted) { 593 // Animate when a dot is first added or when it is removed. 594 if (animate && (wasDotted ^ isDotted) && isShown()) { 595 animateDotScale(newDotScale); 596 } else { 597 cancelDotScaleAnim(); 598 mDotParams.scale = newDotScale; 599 invalidate(); 600 } 601 } 602 if (itemInfo.contentDescription != null) { 603 if (itemInfo.isDisabled()) { 604 setContentDescription(getContext().getString(R.string.disabled_app_label, 605 itemInfo.contentDescription)); 606 } else if (hasDot()) { 607 int count = mDotInfo.getNotificationCount(); 608 setContentDescription(getContext().getResources().getQuantityString( 609 R.plurals.dotted_app_label, count, itemInfo.contentDescription, count)); 610 } else { 611 setContentDescription(itemInfo.contentDescription); 612 } 613 } 614 } 615 } 616 617 /** 618 * Sets the icon for this view based on the layout direction. 619 */ setIcon(Drawable icon)620 private void setIcon(Drawable icon) { 621 if (mIsIconVisible) { 622 applyCompoundDrawables(icon); 623 } 624 mIcon = icon; 625 if (mIcon != null) { 626 mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); 627 } 628 } 629 630 @Override setIconVisible(boolean visible)631 public void setIconVisible(boolean visible) { 632 mIsIconVisible = visible; 633 Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT); 634 applyCompoundDrawables(icon); 635 } 636 applyCompoundDrawables(Drawable icon)637 protected void applyCompoundDrawables(Drawable icon) { 638 // If we had already set an icon before, disable relayout as the icon size is the 639 // same as before. 640 mDisableRelayout = mIcon != null; 641 642 icon.setBounds(0, 0, mIconSize, mIconSize); 643 if (mLayoutHorizontal) { 644 setCompoundDrawablesRelative(icon, null, null, null); 645 } else { 646 setCompoundDrawables(null, icon, null, null); 647 } 648 mDisableRelayout = false; 649 } 650 651 @Override requestLayout()652 public void requestLayout() { 653 if (!mDisableRelayout) { 654 super.requestLayout(); 655 } 656 } 657 658 /** 659 * Applies the item info if it is same as what the view is pointing to currently. 660 */ 661 @Override reapplyItemInfo(ItemInfoWithIcon info)662 public void reapplyItemInfo(ItemInfoWithIcon info) { 663 if (getTag() == info) { 664 mIconLoadRequest = null; 665 mDisableRelayout = true; 666 667 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 668 info.iconBitmap.prepareToDraw(); 669 670 if (info instanceof AppInfo) { 671 applyFromApplicationInfo((AppInfo) info); 672 } else if (info instanceof WorkspaceItemInfo) { 673 applyFromWorkspaceItem((WorkspaceItemInfo) info); 674 mActivity.invalidateParent(info); 675 } else if (info instanceof PackageItemInfo) { 676 applyFromPackageItemInfo((PackageItemInfo) info); 677 } 678 679 mDisableRelayout = false; 680 } 681 } 682 683 /** 684 * Verifies that the current icon is high-res otherwise posts a request to load the icon. 685 */ verifyHighRes()686 public void verifyHighRes() { 687 if (mIconLoadRequest != null) { 688 mIconLoadRequest.cancel(); 689 mIconLoadRequest = null; 690 } 691 if (getTag() instanceof ItemInfoWithIcon) { 692 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 693 if (info.usingLowResIcon()) { 694 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 695 .updateIconInBackground(BubbleTextView.this, info); 696 } 697 } 698 } 699 getIconSize()700 public int getIconSize() { 701 return mIconSize; 702 } 703 } 704