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