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