• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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