• 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.folder;
18 
19 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
20 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
21 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.ObjectAnimator;
29 import android.content.Context;
30 import android.graphics.Canvas;
31 import android.graphics.PointF;
32 import android.graphics.Rect;
33 import android.graphics.drawable.Drawable;
34 import android.util.AttributeSet;
35 import android.util.Property;
36 import android.view.LayoutInflater;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewDebug;
40 import android.view.ViewGroup;
41 import android.widget.FrameLayout;
42 
43 import androidx.annotation.NonNull;
44 
45 import com.android.launcher3.Alarm;
46 import com.android.launcher3.BubbleTextView;
47 import com.android.launcher3.CellLayout;
48 import com.android.launcher3.CheckLongPressHelper;
49 import com.android.launcher3.DeviceProfile;
50 import com.android.launcher3.DropTarget.DragObject;
51 import com.android.launcher3.Launcher;
52 import com.android.launcher3.LauncherSettings;
53 import com.android.launcher3.OnAlarmListener;
54 import com.android.launcher3.R;
55 import com.android.launcher3.Reorderable;
56 import com.android.launcher3.Utilities;
57 import com.android.launcher3.Workspace;
58 import com.android.launcher3.allapps.AllAppsContainerView;
59 import com.android.launcher3.anim.Interpolators;
60 import com.android.launcher3.config.FeatureFlags;
61 import com.android.launcher3.dot.FolderDotInfo;
62 import com.android.launcher3.dragndrop.BaseItemDragListener;
63 import com.android.launcher3.dragndrop.DragLayer;
64 import com.android.launcher3.dragndrop.DragView;
65 import com.android.launcher3.dragndrop.DraggableView;
66 import com.android.launcher3.icons.DotRenderer;
67 import com.android.launcher3.logger.LauncherAtom.FromState;
68 import com.android.launcher3.logger.LauncherAtom.ToState;
69 import com.android.launcher3.logging.InstanceId;
70 import com.android.launcher3.logging.StatsLogManager;
71 import com.android.launcher3.model.data.AppInfo;
72 import com.android.launcher3.model.data.FolderInfo;
73 import com.android.launcher3.model.data.FolderInfo.FolderListener;
74 import com.android.launcher3.model.data.FolderInfo.LabelState;
75 import com.android.launcher3.model.data.ItemInfo;
76 import com.android.launcher3.model.data.WorkspaceItemInfo;
77 import com.android.launcher3.touch.ItemClickHandler;
78 import com.android.launcher3.util.Executors;
79 import com.android.launcher3.util.Thunk;
80 import com.android.launcher3.views.ActivityContext;
81 import com.android.launcher3.views.IconLabelDotView;
82 import com.android.launcher3.widget.PendingAddShortcutInfo;
83 
84 import java.util.ArrayList;
85 import java.util.List;
86 import java.util.function.Predicate;
87 
88 
89 /**
90  * An icon that can appear on in the workspace representing an {@link Folder}.
91  */
92 public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
93         DraggableView, Reorderable {
94 
95     @Thunk ActivityContext mActivity;
96     @Thunk Folder mFolder;
97     public FolderInfo mInfo;
98 
99     private CheckLongPressHelper mLongPressHelper;
100 
101     static final int DROP_IN_ANIMATION_DURATION = 400;
102 
103     // Flag whether the folder should open itself when an item is dragged over is enabled.
104     public static final boolean SPRING_LOADING_ENABLED = true;
105 
106     // Delay when drag enters until the folder opens, in miliseconds.
107     private static final int ON_OPEN_DELAY = 800;
108 
109     @Thunk BubbleTextView mFolderName;
110 
111     PreviewBackground mBackground = new PreviewBackground();
112     private boolean mBackgroundIsVisible = true;
113 
114     FolderGridOrganizer mPreviewVerifier;
115     ClippedFolderIconLayoutRule mPreviewLayoutRule;
116     private PreviewItemManager mPreviewItemManager;
117     private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
118     private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
119 
120     boolean mAnimating = false;
121 
122     private Alarm mOpenAlarm = new Alarm();
123 
124     private boolean mForceHideDot;
125     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
126     private FolderDotInfo mDotInfo = new FolderDotInfo();
127     private DotRenderer mDotRenderer;
128     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
129     private DotRenderer.DrawParams mDotParams;
130     private float mDotScale;
131     private Animator mDotScaleAnim;
132 
133     private Rect mTouchArea = new Rect();
134 
135     private final PointF mTranslationForReorderBounce = new PointF(0, 0);
136     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
137     private float mScaleForReorderBounce = 1f;
138 
139     private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY
140             = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") {
141         @Override
142         public Float get(FolderIcon folderIcon) {
143             return folderIcon.mDotScale;
144         }
145 
146         @Override
147         public void set(FolderIcon folderIcon, Float value) {
148             folderIcon.mDotScale = value;
149             folderIcon.invalidate();
150         }
151     };
152 
FolderIcon(Context context, AttributeSet attrs)153     public FolderIcon(Context context, AttributeSet attrs) {
154         super(context, attrs);
155         init();
156     }
157 
FolderIcon(Context context)158     public FolderIcon(Context context) {
159         super(context);
160         init();
161     }
162 
init()163     private void init() {
164         mLongPressHelper = new CheckLongPressHelper(this);
165         mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
166         mPreviewItemManager = new PreviewItemManager(this);
167         mDotParams = new DotRenderer.DrawParams();
168     }
169 
inflateFolderAndIcon(int resId, T activityContext, ViewGroup group, FolderInfo folderInfo)170     public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
171             T activityContext, ViewGroup group, FolderInfo folderInfo) {
172         Folder folder = Folder.fromXml(activityContext);
173 
174         FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
175         folder.setFolderIcon(icon);
176         folder.bind(folderInfo);
177         icon.setFolder(folder);
178         return icon;
179     }
180 
inflateIcon(int resId, ActivityContext activity, ViewGroup group, FolderInfo folderInfo)181     public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
182             FolderInfo folderInfo) {
183         @SuppressWarnings("all") // suppress dead code warning
184         final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
185         if (error) {
186             throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
187                     "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
188                     "is dependent on this");
189         }
190 
191         DeviceProfile grid = activity.getDeviceProfile();
192         FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
193                 .inflate(resId, group, false);
194 
195         icon.setClipToPadding(false);
196         icon.mFolderName = icon.findViewById(R.id.folder_icon_name);
197         icon.mFolderName.setText(folderInfo.title);
198         icon.mFolderName.setCompoundDrawablePadding(0);
199         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
200         lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
201 
202         icon.setTag(folderInfo);
203         icon.setOnClickListener(ItemClickHandler.INSTANCE);
204         icon.mInfo = folderInfo;
205         icon.mActivity = activity;
206         icon.mDotRenderer = grid.mDotRendererWorkSpace;
207 
208         icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title));
209 
210         // Keep the notification dot up to date with the sum of all the content's dots.
211         FolderDotInfo folderDotInfo = new FolderDotInfo();
212         for (WorkspaceItemInfo si : folderInfo.contents) {
213             folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
214         }
215         icon.setDotInfo(folderDotInfo);
216 
217         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
218 
219         icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
220         icon.mPreviewVerifier.setFolderInfo(folderInfo);
221         icon.updatePreviewItems(false);
222 
223         folderInfo.addListener(icon);
224 
225         return icon;
226     }
227 
animateBgShadowAndStroke()228     public void animateBgShadowAndStroke() {
229         mBackground.fadeInBackgroundShadow();
230         mBackground.animateBackgroundStroke();
231     }
232 
getFolderName()233     public BubbleTextView getFolderName() {
234         return mFolderName;
235     }
236 
getPreviewBounds(Rect outBounds)237     public void getPreviewBounds(Rect outBounds) {
238         mPreviewItemManager.recomputePreviewDrawingParams();
239         mBackground.getBounds(outBounds);
240         // The preview items go outside of the bounds of the background.
241         Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR);
242     }
243 
getBackgroundStrokeWidth()244     public float getBackgroundStrokeWidth() {
245         return mBackground.getStrokeWidth();
246     }
247 
getFolder()248     public Folder getFolder() {
249         return mFolder;
250     }
251 
setFolder(Folder folder)252     private void setFolder(Folder folder) {
253         mFolder = folder;
254     }
255 
willAcceptItem(ItemInfo item)256     private boolean willAcceptItem(ItemInfo item) {
257         final int itemType = item.itemType;
258         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
259                 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
260                 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
261                 item != mInfo && !mFolder.isOpen());
262     }
263 
acceptDrop(ItemInfo dragInfo)264     public boolean acceptDrop(ItemInfo dragInfo) {
265         return !mFolder.isDestroyed() && willAcceptItem(dragInfo);
266     }
267 
addItem(WorkspaceItemInfo item)268     public void addItem(WorkspaceItemInfo item) {
269         mInfo.add(item, true);
270     }
271 
removeItem(WorkspaceItemInfo item, boolean animate)272     public void removeItem(WorkspaceItemInfo item, boolean animate) {
273         mInfo.remove(item, animate);
274     }
275 
onDragEnter(ItemInfo dragInfo)276     public void onDragEnter(ItemInfo dragInfo) {
277         if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
278         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
279         CellLayout cl = (CellLayout) getParent().getParent();
280 
281         mBackground.animateToAccept(cl, lp.cellX, lp.cellY);
282         mOpenAlarm.setOnAlarmListener(mOnOpenListener);
283         if (SPRING_LOADING_ENABLED &&
284                 ((dragInfo instanceof AppInfo)
285                         || (dragInfo instanceof WorkspaceItemInfo)
286                         || (dragInfo instanceof PendingAddShortcutInfo))) {
287             mOpenAlarm.setAlarm(ON_OPEN_DELAY);
288         }
289     }
290 
291     OnAlarmListener mOnOpenListener = new OnAlarmListener() {
292         public void onAlarm(Alarm alarm) {
293             mFolder.beginExternalDrag();
294         }
295     };
296 
prepareCreateAnimation(final View destView)297     public Drawable prepareCreateAnimation(final View destView) {
298         return mPreviewItemManager.prepareCreateAnimation(destView);
299     }
300 
performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)301     public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView,
302             final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect,
303             float scaleRelativeToDragLayer) {
304         final DragView srcView = d.dragView;
305         prepareCreateAnimation(destView);
306         addItem(destInfo);
307         // This will animate the first item from it's position as an icon into its
308         // position as the first item in the preview
309         mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null)
310                 .start();
311 
312         // This will animate the dragView (srcView) into the new folder
313         onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1,
314                 false /* itemReturnedOnFailedDrop */);
315     }
316 
performDestroyAnimation(Runnable onCompleteRunnable)317     public void performDestroyAnimation(Runnable onCompleteRunnable) {
318         // This will animate the final item in the preview to be full size.
319         mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable)
320                 .start();
321     }
322 
onDragExit()323     public void onDragExit() {
324         mBackground.animateToRest();
325         mOpenAlarm.cancelAlarm();
326     }
327 
onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)328     private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect,
329             float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) {
330         item.cellX = -1;
331         item.cellY = -1;
332         DragView animateView = d.dragView;
333         // Typically, the animateView corresponds to the DragView; however, if this is being done
334         // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
335         // will not have a view to animate
336         if (animateView != null && mActivity instanceof Launcher) {
337             final Launcher launcher = (Launcher) mActivity;
338             DragLayer dragLayer = launcher.getDragLayer();
339             Rect from = new Rect();
340             dragLayer.getViewRectRelativeToSelf(animateView, from);
341             Rect to = finalRect;
342             if (to == null) {
343                 to = new Rect();
344                 Workspace workspace = launcher.getWorkspace();
345                 // Set cellLayout and this to it's final state to compute final animation locations
346                 workspace.setFinalTransitionTransform();
347                 float scaleX = getScaleX();
348                 float scaleY = getScaleY();
349                 setScaleX(1.0f);
350                 setScaleY(1.0f);
351                 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
352                 // Finished computing final animation locations, restore current state
353                 setScaleX(scaleX);
354                 setScaleY(scaleY);
355                 workspace.resetTransitionTransform();
356             }
357 
358             int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1);
359             boolean itemAdded = false;
360             if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) {
361                 List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
362                 mInfo.add(item, index, false);
363                 mCurrentPreviewItems.clear();
364                 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
365 
366                 if (!oldPreviewItems.equals(mCurrentPreviewItems)) {
367                     int newIndex = mCurrentPreviewItems.indexOf(item);
368                     if (newIndex >= 0) {
369                         // If the item dropped is going to be in the preview, we update the
370                         // index here to reflect its position in the preview.
371                         index = newIndex;
372                     }
373 
374                     mPreviewItemManager.hidePreviewItem(index, true);
375                     mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item);
376                     itemAdded = true;
377                 } else {
378                     removeItem(item, false);
379                 }
380             }
381 
382             if (!itemAdded) {
383                 mInfo.add(item, index, true);
384             }
385 
386             int[] center = new int[2];
387             float scale = getLocalCenterForIndex(index, numItemsInPreview, center);
388             center[0] = Math.round(scaleRelativeToDragLayer * center[0]);
389             center[1] = Math.round(scaleRelativeToDragLayer * center[1]);
390 
391             to.offset(center[0] - animateView.getMeasuredWidth() / 2,
392                     center[1] - animateView.getMeasuredHeight() / 2);
393 
394             float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f;
395 
396             float finalScale = scale * scaleRelativeToDragLayer;
397 
398             // Account for potentially different icon sizes with non-default grid settings
399             if (d.dragSource instanceof AllAppsContainerView) {
400                 DeviceProfile grid = mActivity.getDeviceProfile();
401                 float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx);
402                 finalScale *= containerScale;
403             }
404 
405             final int finalIndex = index;
406             dragLayer.animateView(animateView, from, to, finalAlpha,
407                     1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
408                     Interpolators.DEACCEL_2, Interpolators.ACCEL_2,
409                     () -> {
410                         mPreviewItemManager.hidePreviewItem(finalIndex, false);
411                         mFolder.showItem(item);
412                     }, DragLayer.ANIMATION_END_DISAPPEAR, null);
413 
414             mFolder.hideItem(item);
415 
416             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
417 
418             FolderNameInfos nameInfos = new FolderNameInfos();
419             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
420                 Executors.MODEL_EXECUTOR.post(() -> {
421                     d.folderNameProvider.getSuggestedFolderName(
422                             getContext(), mInfo.contents, nameInfos);
423                     showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
424                 });
425             } else {
426                 showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
427             }
428         } else {
429             addItem(item);
430         }
431     }
432 
showFinalView(int finalIndex, final WorkspaceItemInfo item, FolderNameInfos nameInfos, InstanceId instanceId)433     private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
434             FolderNameInfos nameInfos, InstanceId instanceId) {
435         postDelayed(() -> {
436             setLabelSuggestion(nameInfos, instanceId);
437             invalidate();
438         }, DROP_IN_ANIMATION_DURATION);
439     }
440 
441     /**
442      * Set the suggested folder name.
443      */
setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId)444     public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) {
445         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
446             return;
447         }
448         if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) {
449             return;
450         }
451         if (nameInfos == null || !nameInfos.hasSuggestions()) {
452             StatsLogManager.newInstance(getContext()).logger()
453                     .withInstanceId(instanceId)
454                     .withItemInfo(mInfo)
455                     .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS);
456             return;
457         }
458         if (!nameInfos.hasPrimary()) {
459             StatsLogManager.newInstance(getContext()).logger()
460                     .withInstanceId(instanceId)
461                     .withItemInfo(mInfo)
462                     .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY);
463             return;
464         }
465         CharSequence newTitle = nameInfos.getLabels()[0];
466         FromState fromState = mInfo.getFromLabelState();
467 
468         mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
469         onTitleChanged(mInfo.title);
470         mFolder.mFolderName.setText(mInfo.title);
471 
472         // Logging for folder creation flow
473         StatsLogManager.newInstance(getContext()).logger()
474                 .withInstanceId(instanceId)
475                 .withItemInfo(mInfo)
476                 .withFromState(fromState)
477                 .withToState(ToState.TO_SUGGESTION0)
478                 // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter,
479                 // event is assumed to be folder creation on the server side.
480                 .withEditText(newTitle.toString())
481                 .log(LAUNCHER_FOLDER_AUTO_LABELED);
482     }
483 
484 
onDrop(DragObject d, boolean itemReturnedOnFailedDrop)485     public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) {
486         WorkspaceItemInfo item;
487         if (d.dragInfo instanceof AppInfo) {
488             // Came from all apps -- make a copy
489             item = ((AppInfo) d.dragInfo).makeWorkspaceItem();
490         } else if (d.dragSource instanceof BaseItemDragListener){
491             // Came from a different window -- make a copy
492             item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo);
493         } else {
494             item = (WorkspaceItemInfo) d.dragInfo;
495         }
496         mFolder.notifyDrop();
497         onDrop(item, d, null, 1.0f,
498                 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
499                 itemReturnedOnFailedDrop
500         );
501     }
502 
setDotInfo(FolderDotInfo dotInfo)503     public void setDotInfo(FolderDotInfo dotInfo) {
504         updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot());
505         mDotInfo = dotInfo;
506     }
507 
getLayoutRule()508     public ClippedFolderIconLayoutRule getLayoutRule() {
509         return mPreviewLayoutRule;
510     }
511 
512     @Override
setForceHideDot(boolean forceHideDot)513     public void setForceHideDot(boolean forceHideDot) {
514         if (mForceHideDot == forceHideDot) {
515             return;
516         }
517         mForceHideDot = forceHideDot;
518 
519         if (forceHideDot) {
520             invalidate();
521         } else if (hasDot()) {
522             animateDotScale(0, 1);
523         }
524     }
525 
526     /**
527      * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false
528      * (the dot is being added or removed).
529      */
updateDotScale(boolean wasDotted, boolean isDotted)530     private void updateDotScale(boolean wasDotted, boolean isDotted) {
531         float newDotScale = isDotted ? 1f : 0f;
532         // Animate when a dot is first added or when it is removed.
533         if ((wasDotted ^ isDotted) && isShown()) {
534             animateDotScale(newDotScale);
535         } else {
536             cancelDotScaleAnim();
537             mDotScale = newDotScale;
538             invalidate();
539         }
540     }
541 
cancelDotScaleAnim()542     private void cancelDotScaleAnim() {
543         if (mDotScaleAnim != null) {
544             mDotScaleAnim.cancel();
545         }
546     }
547 
animateDotScale(float... dotScales)548     public void animateDotScale(float... dotScales) {
549         cancelDotScaleAnim();
550         mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
551         mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
552             @Override
553             public void onAnimationEnd(Animator animation) {
554                 mDotScaleAnim = null;
555             }
556         });
557         mDotScaleAnim.start();
558     }
559 
hasDot()560     public boolean hasDot() {
561         return mDotInfo != null && mDotInfo.hasDot();
562     }
563 
getLocalCenterForIndex(int index, int curNumItems, int[] center)564     private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
565         mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams(
566                 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams);
567 
568         mTmpParams.transX += mBackground.basePreviewOffsetX;
569         mTmpParams.transY += mBackground.basePreviewOffsetY;
570 
571         float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize();
572         float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2;
573         float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2;
574 
575         center[0] = Math.round(offsetX);
576         center[1] = Math.round(offsetY);
577         return mTmpParams.scale;
578     }
579 
setFolderBackground(PreviewBackground bg)580     public void setFolderBackground(PreviewBackground bg) {
581         mBackground = bg;
582         mBackground.setInvalidateDelegate(this);
583     }
584 
585     @Override
setIconVisible(boolean visible)586     public void setIconVisible(boolean visible) {
587         mBackgroundIsVisible = visible;
588         invalidate();
589     }
590 
getIconVisible()591     public boolean getIconVisible() {
592         return mBackgroundIsVisible;
593     }
594 
getFolderBackground()595     public PreviewBackground getFolderBackground() {
596         return mBackground;
597     }
598 
getPreviewItemManager()599     public PreviewItemManager getPreviewItemManager() {
600         return mPreviewItemManager;
601     }
602 
603     @Override
dispatchDraw(Canvas canvas)604     protected void dispatchDraw(Canvas canvas) {
605         super.dispatchDraw(canvas);
606 
607         if (!mBackgroundIsVisible) return;
608 
609         mPreviewItemManager.recomputePreviewDrawingParams();
610 
611         if (!mBackground.drawingDelegated()) {
612             mBackground.drawBackground(canvas);
613         }
614 
615         if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
616 
617         mPreviewItemManager.draw(canvas);
618 
619         if (!mBackground.drawingDelegated()) {
620             mBackground.drawBackgroundStroke(canvas);
621         }
622 
623         drawDot(canvas);
624     }
625 
drawDot(Canvas canvas)626     public void drawDot(Canvas canvas) {
627         if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
628             Rect iconBounds = mDotParams.iconBounds;
629             BubbleTextView.getIconBounds(this, iconBounds, mActivity.getDeviceProfile().iconSizePx);
630             float iconScale = (float) mBackground.previewSize / iconBounds.width();
631             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
632 
633             // If we are animating to the accepting state, animate the dot out.
634             mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
635             mDotParams.color = mBackground.getDotColor();
636             mDotRenderer.draw(canvas, mDotParams);
637         }
638     }
639 
setTextVisible(boolean visible)640     public void setTextVisible(boolean visible) {
641         if (visible) {
642             mFolderName.setVisibility(VISIBLE);
643         } else {
644             mFolderName.setVisibility(INVISIBLE);
645         }
646     }
647 
getTextVisible()648     public boolean getTextVisible() {
649         return mFolderName.getVisibility() == VISIBLE;
650     }
651 
652     /**
653      * Returns the list of items which should be visible in the preview
654      */
getPreviewItemsOnPage(int page)655     public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
656         return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
657     }
658 
659     @Override
verifyDrawable(@onNull Drawable who)660     protected boolean verifyDrawable(@NonNull Drawable who) {
661         return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who);
662     }
663 
664     @Override
onItemsChanged(boolean animate)665     public void onItemsChanged(boolean animate) {
666         updatePreviewItems(animate);
667         invalidate();
668         requestLayout();
669     }
670 
updatePreviewItems(boolean animate)671     private void updatePreviewItems(boolean animate) {
672         mPreviewItemManager.updatePreviewItems(animate);
673         mCurrentPreviewItems.clear();
674         mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
675     }
676 
677     /**
678      * Updates the preview items which match the provided condition
679      */
updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck)680     public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
681         mPreviewItemManager.updatePreviewItems(itemCheck);
682     }
683 
684     @Override
onAdd(WorkspaceItemInfo item, int rank)685     public void onAdd(WorkspaceItemInfo item, int rank) {
686         boolean wasDotted = mDotInfo.hasDot();
687         mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
688         boolean isDotted = mDotInfo.hasDot();
689         updateDotScale(wasDotted, isDotted);
690         setContentDescription(getAccessiblityTitle(mInfo.title));
691         invalidate();
692         requestLayout();
693     }
694 
695     @Override
onRemove(List<WorkspaceItemInfo> items)696     public void onRemove(List<WorkspaceItemInfo> items) {
697         boolean wasDotted = mDotInfo.hasDot();
698         items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo);
699         boolean isDotted = mDotInfo.hasDot();
700         updateDotScale(wasDotted, isDotted);
701         setContentDescription(getAccessiblityTitle(mInfo.title));
702         invalidate();
703         requestLayout();
704     }
705 
onTitleChanged(CharSequence title)706     public void onTitleChanged(CharSequence title) {
707         mFolderName.setText(title);
708         setContentDescription(getAccessiblityTitle(title));
709     }
710 
711     @Override
onTouchEvent(MotionEvent event)712     public boolean onTouchEvent(MotionEvent event) {
713         if (event.getAction() == MotionEvent.ACTION_DOWN
714                 && shouldIgnoreTouchDown(event.getX(), event.getY())) {
715             return false;
716         }
717 
718         // Call the superclass onTouchEvent first, because sometimes it changes the state to
719         // isPressed() on an ACTION_UP
720         super.onTouchEvent(event);
721         mLongPressHelper.onTouchEvent(event);
722         // Keep receiving the rest of the events
723         return true;
724     }
725 
726     /**
727      * Returns true if the touch down at the provided position be ignored
728      */
shouldIgnoreTouchDown(float x, float y)729     protected boolean shouldIgnoreTouchDown(float x, float y) {
730         mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
731                 getHeight() - getPaddingBottom());
732         return !mTouchArea.contains((int) x, (int) y);
733     }
734 
735     @Override
cancelLongPress()736     public void cancelLongPress() {
737         super.cancelLongPress();
738         mLongPressHelper.cancelLongPress();
739     }
740 
removeListeners()741     public void removeListeners() {
742         mInfo.removeListener(this);
743         mInfo.removeListener(mFolder);
744     }
745 
isInHotseat()746     private boolean isInHotseat() {
747         return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
748     }
749 
clearLeaveBehindIfExists()750     public void clearLeaveBehindIfExists() {
751         if (getParent() instanceof FolderIconParent) {
752             ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
753         }
754     }
755 
drawLeaveBehindIfExists()756     public void drawLeaveBehindIfExists() {
757         if (getParent() instanceof FolderIconParent) {
758             ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
759         }
760     }
761 
onFolderClose(int currentPage)762     public void onFolderClose(int currentPage) {
763         mPreviewItemManager.onFolderClose(currentPage);
764     }
765 
updateTranslation()766     private void updateTranslation() {
767         super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
768         super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
769     }
770 
setReorderBounceOffset(float x, float y)771     public void setReorderBounceOffset(float x, float y) {
772         mTranslationForReorderBounce.set(x, y);
773         updateTranslation();
774     }
775 
getReorderBounceOffset(PointF offset)776     public void getReorderBounceOffset(PointF offset) {
777         offset.set(mTranslationForReorderBounce);
778     }
779 
780     @Override
setReorderPreviewOffset(float x, float y)781     public void setReorderPreviewOffset(float x, float y) {
782         mTranslationForReorderPreview.set(x, y);
783         updateTranslation();
784     }
785 
786     @Override
getReorderPreviewOffset(PointF offset)787     public void getReorderPreviewOffset(PointF offset) {
788         offset.set(mTranslationForReorderPreview);
789     }
790 
setReorderBounceScale(float scale)791     public void setReorderBounceScale(float scale) {
792         mScaleForReorderBounce = scale;
793         super.setScaleX(scale);
794         super.setScaleY(scale);
795     }
796 
getReorderBounceScale()797     public float getReorderBounceScale() {
798         return mScaleForReorderBounce;
799     }
800 
getView()801     public View getView() {
802         return this;
803     }
804 
805     @Override
getViewType()806     public int getViewType() {
807         return DRAGGABLE_ICON;
808     }
809 
810     @Override
getWorkspaceVisualDragBounds(Rect bounds)811     public void getWorkspaceVisualDragBounds(Rect bounds) {
812         getPreviewBounds(bounds);
813     }
814 
815     /**
816      * Returns a formatted accessibility title for folder
817      */
getAccessiblityTitle(CharSequence title)818     public String getAccessiblityTitle(CharSequence title) {
819         int size = mInfo.contents.size();
820         if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
821             return getContext().getString(R.string.folder_name_format_exact, title, size);
822         } else {
823             return getContext().getString(R.string.folder_name_format_overflow, title,
824                     MAX_NUM_ITEMS_IN_PREVIEW);
825         }
826     }
827 
828     /**
829      * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
830      */
831     public interface FolderIconParent {
832         /**
833          * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
834          * gap where the FolderIcon would be when the Folder is closed.
835          */
drawFolderLeaveBehindForIcon(FolderIcon child)836         void drawFolderLeaveBehindForIcon(FolderIcon child);
837         /**
838          * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
839          */
clearFolderLeaveBehind(FolderIcon child)840         void clearFolderLeaveBehind(FolderIcon child);
841     }
842 }
843