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