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