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