• 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.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
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.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 import androidx.annotation.Nullable;
45 
46 import com.android.app.animation.Interpolators;
47 import com.android.launcher3.Alarm;
48 import com.android.launcher3.BubbleTextView;
49 import com.android.launcher3.CellLayout;
50 import com.android.launcher3.CheckLongPressHelper;
51 import com.android.launcher3.DeviceProfile;
52 import com.android.launcher3.DropTarget.DragObject;
53 import com.android.launcher3.Launcher;
54 import com.android.launcher3.LauncherSettings;
55 import com.android.launcher3.OnAlarmListener;
56 import com.android.launcher3.R;
57 import com.android.launcher3.Reorderable;
58 import com.android.launcher3.Utilities;
59 import com.android.launcher3.Workspace;
60 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
61 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
62 import com.android.launcher3.dot.FolderDotInfo;
63 import com.android.launcher3.dragndrop.BaseItemDragListener;
64 import com.android.launcher3.dragndrop.DragLayer;
65 import com.android.launcher3.dragndrop.DragView;
66 import com.android.launcher3.dragndrop.DraggableView;
67 import com.android.launcher3.icons.DotRenderer;
68 import com.android.launcher3.logger.LauncherAtom.FromState;
69 import com.android.launcher3.logger.LauncherAtom.ToState;
70 import com.android.launcher3.logging.InstanceId;
71 import com.android.launcher3.logging.StatsLogManager;
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.WorkspaceItemFactory;
77 import com.android.launcher3.model.data.WorkspaceItemInfo;
78 import com.android.launcher3.util.Executors;
79 import com.android.launcher3.util.MultiTranslateDelegate;
80 import com.android.launcher3.util.Thunk;
81 import com.android.launcher3.views.ActivityContext;
82 import com.android.launcher3.views.IconLabelDotView;
83 import com.android.launcher3.widget.PendingAddShortcutInfo;
84 
85 import java.util.ArrayList;
86 import java.util.List;
87 import java.util.function.Predicate;
88 
89 
90 /**
91  * An icon that can appear on in the workspace representing an {@link Folder}.
92  */
93 public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
94         DraggableView, Reorderable {
95 
96     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
97     @Thunk ActivityContext mActivity;
98     @Thunk Folder mFolder;
99     public FolderInfo mInfo;
100 
101     private CheckLongPressHelper mLongPressHelper;
102 
103     static final int DROP_IN_ANIMATION_DURATION = 400;
104 
105     // Flag whether the folder should open itself when an item is dragged over is enabled.
106     public static final boolean SPRING_LOADING_ENABLED = true;
107 
108     // Delay when drag enters until the folder opens, in miliseconds.
109     private static final int ON_OPEN_DELAY = 800;
110 
111     @Thunk BubbleTextView mFolderName;
112 
113     PreviewBackground mBackground = new PreviewBackground();
114     private boolean mBackgroundIsVisible = true;
115 
116     FolderGridOrganizer mPreviewVerifier;
117     ClippedFolderIconLayoutRule mPreviewLayoutRule;
118     private PreviewItemManager mPreviewItemManager;
119     private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
120     private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
121 
122     boolean mAnimating = false;
123 
124     private Alarm mOpenAlarm = new Alarm();
125 
126     private boolean mForceHideDot;
127     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
128     private FolderDotInfo mDotInfo = new FolderDotInfo();
129     private DotRenderer mDotRenderer;
130     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
131     private DotRenderer.DrawParams mDotParams;
132     private float mDotScale;
133     private Animator mDotScaleAnim;
134 
135     private Rect mTouchArea = new Rect();
136 
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 
181     /**
182      * Builds a FolderIcon to be added to the Launcher
183      */
inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, FolderInfo folderInfo)184     public static FolderIcon inflateIcon(int resId, ActivityContext activity,
185             @Nullable ViewGroup group, 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         LayoutInflater inflater = (group != null)
196                 ? LayoutInflater.from(group.getContext())
197                 : activity.getLayoutInflater();
198         FolderIcon icon = (FolderIcon) inflater.inflate(resId, group, false);
199 
200         icon.setClipToPadding(false);
201         icon.mFolderName = icon.findViewById(R.id.folder_icon_name);
202         icon.mFolderName.setText(folderInfo.title);
203         icon.mFolderName.setCompoundDrawablePadding(0);
204         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
205         lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
206 
207         icon.setTag(folderInfo);
208         icon.setOnClickListener(activity.getItemOnClickListener());
209         icon.mInfo = folderInfo;
210         icon.mActivity = activity;
211         icon.mDotRenderer = grid.mDotRendererWorkSpace;
212 
213         icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title));
214 
215         // Keep the notification dot up to date with the sum of all the content's dots.
216         FolderDotInfo folderDotInfo = new FolderDotInfo();
217         for (WorkspaceItemInfo si : folderInfo.contents) {
218             folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
219         }
220         icon.setDotInfo(folderDotInfo);
221 
222         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
223 
224         icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
225         icon.mPreviewVerifier.setFolderInfo(folderInfo);
226         icon.updatePreviewItems(false);
227 
228         folderInfo.addListener(icon);
229 
230         return icon;
231     }
232 
animateBgShadowAndStroke()233     public void animateBgShadowAndStroke() {
234         mBackground.fadeInBackgroundShadow();
235         mBackground.animateBackgroundStroke();
236     }
237 
getFolderName()238     public BubbleTextView getFolderName() {
239         return mFolderName;
240     }
241 
getPreviewBounds(Rect outBounds)242     public void getPreviewBounds(Rect outBounds) {
243         mPreviewItemManager.recomputePreviewDrawingParams();
244         mBackground.getBounds(outBounds);
245         // The preview items go outside of the bounds of the background.
246         Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR);
247     }
248 
getBackgroundStrokeWidth()249     public float getBackgroundStrokeWidth() {
250         return mBackground.getStrokeWidth();
251     }
252 
getFolder()253     public Folder getFolder() {
254         return mFolder;
255     }
256 
setFolder(Folder folder)257     private void setFolder(Folder folder) {
258         mFolder = folder;
259     }
260 
willAcceptItem(ItemInfo item)261     private boolean willAcceptItem(ItemInfo item) {
262         final int itemType = item.itemType;
263         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
264                 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
265                 item != mInfo && !mFolder.isOpen());
266     }
267 
acceptDrop(ItemInfo dragInfo)268     public boolean acceptDrop(ItemInfo dragInfo) {
269         return !mFolder.isDestroyed() && willAcceptItem(dragInfo);
270     }
271 
addItem(WorkspaceItemInfo item)272     public void addItem(WorkspaceItemInfo item) {
273         mInfo.add(item, true);
274     }
275 
removeItem(WorkspaceItemInfo item, boolean animate)276     public void removeItem(WorkspaceItemInfo 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 WorkspaceItemInfo)
290                         || (dragInfo instanceof PendingAddShortcutInfo))) {
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 WorkspaceItemInfo destInfo, final View destView, final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)305     public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView,
306             final WorkspaceItemInfo 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 WorkspaceItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)332     private void onDrop(final WorkspaceItemInfo 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<WorkspaceItemInfo> 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.contents, 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         WorkspaceItemInfo 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             item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo);
484         } else {
485             item = (WorkspaceItemInfo) d.dragInfo;
486         }
487         mFolder.notifyDrop();
488         onDrop(item, d, null, 1.0f,
489                 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
490                 itemReturnedOnFailedDrop
491         );
492     }
493 
setDotInfo(FolderDotInfo dotInfo)494     public void setDotInfo(FolderDotInfo dotInfo) {
495         updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot());
496         mDotInfo = dotInfo;
497     }
498 
getLayoutRule()499     public ClippedFolderIconLayoutRule getLayoutRule() {
500         return mPreviewLayoutRule;
501     }
502 
503     @Override
setForceHideDot(boolean forceHideDot)504     public void setForceHideDot(boolean forceHideDot) {
505         if (mForceHideDot == forceHideDot) {
506             return;
507         }
508         mForceHideDot = forceHideDot;
509 
510         if (forceHideDot) {
511             invalidate();
512         } else if (hasDot()) {
513             animateDotScale(0, 1);
514         }
515     }
516 
517     /**
518      * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false
519      * (the dot is being added or removed).
520      */
updateDotScale(boolean wasDotted, boolean isDotted)521     private void updateDotScale(boolean wasDotted, boolean isDotted) {
522         float newDotScale = isDotted ? 1f : 0f;
523         // Animate when a dot is first added or when it is removed.
524         if ((wasDotted ^ isDotted) && isShown()) {
525             animateDotScale(newDotScale);
526         } else {
527             cancelDotScaleAnim();
528             mDotScale = newDotScale;
529             invalidate();
530         }
531     }
532 
cancelDotScaleAnim()533     private void cancelDotScaleAnim() {
534         if (mDotScaleAnim != null) {
535             mDotScaleAnim.cancel();
536         }
537     }
538 
animateDotScale(float... dotScales)539     public void animateDotScale(float... dotScales) {
540         cancelDotScaleAnim();
541         mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
542         mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
543             @Override
544             public void onAnimationEnd(Animator animation) {
545                 mDotScaleAnim = null;
546             }
547         });
548         mDotScaleAnim.start();
549     }
550 
hasDot()551     public boolean hasDot() {
552         return mDotInfo != null && mDotInfo.hasDot();
553     }
554 
getLocalCenterForIndex(int index, int curNumItems, int[] center)555     private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
556         mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams(
557                 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams);
558 
559         mTmpParams.transX += mBackground.basePreviewOffsetX;
560         mTmpParams.transY += mBackground.basePreviewOffsetY;
561 
562         float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize();
563         float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2;
564         float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2;
565 
566         center[0] = Math.round(offsetX);
567         center[1] = Math.round(offsetY);
568         return mTmpParams.scale;
569     }
570 
setFolderBackground(PreviewBackground bg)571     public void setFolderBackground(PreviewBackground bg) {
572         mBackground = bg;
573         mBackground.setInvalidateDelegate(this);
574     }
575 
576     @Override
setIconVisible(boolean visible)577     public void setIconVisible(boolean visible) {
578         mBackgroundIsVisible = visible;
579         invalidate();
580     }
581 
getIconVisible()582     public boolean getIconVisible() {
583         return mBackgroundIsVisible;
584     }
585 
getFolderBackground()586     public PreviewBackground getFolderBackground() {
587         return mBackground;
588     }
589 
getPreviewItemManager()590     public PreviewItemManager getPreviewItemManager() {
591         return mPreviewItemManager;
592     }
593 
594     @Override
dispatchDraw(Canvas canvas)595     protected void dispatchDraw(Canvas canvas) {
596         super.dispatchDraw(canvas);
597 
598         if (!mBackgroundIsVisible) return;
599 
600         mPreviewItemManager.recomputePreviewDrawingParams();
601 
602         if (!mBackground.drawingDelegated()) {
603             mBackground.drawBackground(canvas);
604         }
605 
606         if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
607 
608         mPreviewItemManager.draw(canvas);
609 
610         if (!mBackground.drawingDelegated()) {
611             mBackground.drawBackgroundStroke(canvas);
612         }
613 
614         drawDot(canvas);
615     }
616 
drawDot(Canvas canvas)617     public void drawDot(Canvas canvas) {
618         if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
619             Rect iconBounds = mDotParams.iconBounds;
620             // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered
621             int iconSize = mActivity.getDeviceProfile().iconSizePx;
622             iconBounds.left = (getWidth() - iconSize) / 2;
623             iconBounds.right = iconBounds.left + iconSize;
624             iconBounds.top = getPaddingTop();
625             iconBounds.bottom = iconBounds.top + iconSize;
626 
627             float iconScale = (float) mBackground.previewSize / iconSize;
628             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
629 
630             // If we are animating to the accepting state, animate the dot out.
631             mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress());
632             mDotParams.dotColor = mBackground.getDotColor();
633             mDotRenderer.draw(canvas, mDotParams);
634         }
635     }
636 
setTextVisible(boolean visible)637     public void setTextVisible(boolean visible) {
638         if (visible) {
639             mFolderName.setVisibility(VISIBLE);
640         } else {
641             mFolderName.setVisibility(INVISIBLE);
642         }
643     }
644 
getTextVisible()645     public boolean getTextVisible() {
646         return mFolderName.getVisibility() == VISIBLE;
647     }
648 
649     /**
650      * Returns the list of items which should be visible in the preview
651      */
getPreviewItemsOnPage(int page)652     public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
653         return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
654     }
655 
656     @Override
verifyDrawable(@onNull Drawable who)657     protected boolean verifyDrawable(@NonNull Drawable who) {
658         return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who);
659     }
660 
661     @Override
onItemsChanged(boolean animate)662     public void onItemsChanged(boolean animate) {
663         updatePreviewItems(animate);
664         invalidate();
665         requestLayout();
666     }
667 
updatePreviewItems(boolean animate)668     private void updatePreviewItems(boolean animate) {
669         mPreviewItemManager.updatePreviewItems(animate);
670         mCurrentPreviewItems.clear();
671         mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
672     }
673 
674     /**
675      * Updates the preview items which match the provided condition
676      */
updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck)677     public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
678         mPreviewItemManager.updatePreviewItems(itemCheck);
679     }
680 
681     @Override
onAdd(WorkspaceItemInfo item, int rank)682     public void onAdd(WorkspaceItemInfo item, int rank) {
683         updatePreviewItems(false);
684         boolean wasDotted = mDotInfo.hasDot();
685         mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
686         boolean isDotted = mDotInfo.hasDot();
687         updateDotScale(wasDotted, isDotted);
688         setContentDescription(getAccessiblityTitle(mInfo.title));
689         invalidate();
690         requestLayout();
691     }
692 
693     @Override
onRemove(List<WorkspaceItemInfo> items)694     public void onRemove(List<WorkspaceItemInfo> items) {
695         updatePreviewItems(false);
696         boolean wasDotted = mDotInfo.hasDot();
697         items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo);
698         boolean isDotted = mDotInfo.hasDot();
699         updateDotScale(wasDotted, isDotted);
700         setContentDescription(getAccessiblityTitle(mInfo.title));
701         invalidate();
702         requestLayout();
703     }
704 
onTitleChanged(CharSequence title)705     public void onTitleChanged(CharSequence title) {
706         mFolderName.setText(title);
707         setContentDescription(getAccessiblityTitle(title));
708     }
709 
710     @Override
onTouchEvent(MotionEvent event)711     public boolean onTouchEvent(MotionEvent event) {
712         if (event.getAction() == MotionEvent.ACTION_DOWN
713                 && shouldIgnoreTouchDown(event.getX(), event.getY())) {
714             return false;
715         }
716 
717         // Call the superclass onTouchEvent first, because sometimes it changes the state to
718         // isPressed() on an ACTION_UP
719         super.onTouchEvent(event);
720         mLongPressHelper.onTouchEvent(event);
721         // Keep receiving the rest of the events
722         return true;
723     }
724 
725     /**
726      * Returns true if the touch down at the provided position be ignored
727      */
shouldIgnoreTouchDown(float x, float y)728     protected boolean shouldIgnoreTouchDown(float x, float y) {
729         mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
730                 getHeight() - getPaddingBottom());
731         return !mTouchArea.contains((int) x, (int) y);
732     }
733 
734     @Override
cancelLongPress()735     public void cancelLongPress() {
736         super.cancelLongPress();
737         mLongPressHelper.cancelLongPress();
738     }
739 
removeListeners()740     public void removeListeners() {
741         mInfo.removeListener(this);
742         mInfo.removeListener(mFolder);
743     }
744 
isInHotseat()745     private boolean isInHotseat() {
746         return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
747     }
748 
clearLeaveBehindIfExists()749     public void clearLeaveBehindIfExists() {
750         if (getParent() instanceof FolderIconParent) {
751             ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
752         }
753     }
754 
drawLeaveBehindIfExists()755     public void drawLeaveBehindIfExists() {
756         if (getParent() instanceof FolderIconParent) {
757             ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
758         }
759     }
760 
onFolderClose(int currentPage)761     public void onFolderClose(int currentPage) {
762         mPreviewItemManager.onFolderClose(currentPage);
763     }
764 
765     @Override
getTranslateDelegate()766     public MultiTranslateDelegate getTranslateDelegate() {
767         return mTranslateDelegate;
768     }
769 
770     @Override
setReorderBounceScale(float scale)771     public void setReorderBounceScale(float scale) {
772         mScaleForReorderBounce = scale;
773         super.setScaleX(scale);
774         super.setScaleY(scale);
775     }
776 
777     @Override
getReorderBounceScale()778     public float getReorderBounceScale() {
779         return mScaleForReorderBounce;
780     }
781 
782     @Override
getViewType()783     public int getViewType() {
784         return DRAGGABLE_ICON;
785     }
786 
787     @Override
getWorkspaceVisualDragBounds(Rect bounds)788     public void getWorkspaceVisualDragBounds(Rect bounds) {
789         getPreviewBounds(bounds);
790     }
791 
792     /**
793      * Returns a formatted accessibility title for folder
794      */
getAccessiblityTitle(CharSequence title)795     public String getAccessiblityTitle(CharSequence title) {
796         int size = mInfo.contents.size();
797         if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
798             return getContext().getString(R.string.folder_name_format_exact, title, size);
799         } else {
800             return getContext().getString(R.string.folder_name_format_overflow, title,
801                     MAX_NUM_ITEMS_IN_PREVIEW);
802         }
803     }
804 
805     @Override
onHoverChanged(boolean hovered)806     public void onHoverChanged(boolean hovered) {
807         super.onHoverChanged(hovered);
808         if (ENABLE_CURSOR_HOVER_STATES.get()) {
809             mBackground.setHovered(hovered);
810         }
811     }
812 
813     /**
814      * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
815      */
816     public interface FolderIconParent {
817         /**
818          * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
819          * gap where the FolderIcon would be when the Folder is closed.
820          */
drawFolderLeaveBehindForIcon(FolderIcon child)821         void drawFolderLeaveBehindForIcon(FolderIcon child);
822         /**
823          * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
824          */
clearFolderLeaveBehind(FolderIcon child)825         void clearFolderLeaveBehind(FolderIcon child);
826     }
827 }
828