• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.folder;
18 
19 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
20 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
21 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.ObjectAnimator;
29 import android.content.Context;
30 import android.graphics.Canvas;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.util.AttributeSet;
34 import android.util.Property;
35 import android.view.LayoutInflater;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewDebug;
39 import android.view.ViewGroup;
40 import android.widget.FrameLayout;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 
45 import com.android.launcher3.Alarm;
46 import com.android.launcher3.BubbleTextView;
47 import com.android.launcher3.CellLayout;
48 import com.android.launcher3.CheckLongPressHelper;
49 import com.android.launcher3.DeviceProfile;
50 import com.android.launcher3.DropTarget.DragObject;
51 import com.android.launcher3.Launcher;
52 import com.android.launcher3.LauncherSettings;
53 import com.android.launcher3.OnAlarmListener;
54 import com.android.launcher3.R;
55 import com.android.launcher3.Reorderable;
56 import com.android.launcher3.Utilities;
57 import com.android.launcher3.Workspace;
58 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
59 import com.android.launcher3.anim.Interpolators;
60 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
61 import com.android.launcher3.dot.FolderDotInfo;
62 import com.android.launcher3.dragndrop.BaseItemDragListener;
63 import com.android.launcher3.dragndrop.DragLayer;
64 import com.android.launcher3.dragndrop.DragView;
65 import com.android.launcher3.dragndrop.DraggableView;
66 import com.android.launcher3.icons.DotRenderer;
67 import com.android.launcher3.logger.LauncherAtom.FromState;
68 import com.android.launcher3.logger.LauncherAtom.ToState;
69 import com.android.launcher3.logging.InstanceId;
70 import com.android.launcher3.logging.StatsLogManager;
71 import com.android.launcher3.model.data.FolderInfo;
72 import com.android.launcher3.model.data.FolderInfo.FolderListener;
73 import com.android.launcher3.model.data.FolderInfo.LabelState;
74 import com.android.launcher3.model.data.ItemInfo;
75 import com.android.launcher3.model.data.WorkspaceItemFactory;
76 import com.android.launcher3.model.data.WorkspaceItemInfo;
77 import com.android.launcher3.touch.ItemClickHandler;
78 import com.android.launcher3.util.Executors;
79 import com.android.launcher3.util.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(ItemClickHandler.INSTANCE);
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_SHORTCUT ||
265                 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
266                 item != mInfo && !mFolder.isOpen());
267     }
268 
acceptDrop(ItemInfo dragInfo)269     public boolean acceptDrop(ItemInfo dragInfo) {
270         return !mFolder.isDestroyed() && willAcceptItem(dragInfo);
271     }
272 
addItem(WorkspaceItemInfo item)273     public void addItem(WorkspaceItemInfo item) {
274         mInfo.add(item, true);
275     }
276 
removeItem(WorkspaceItemInfo item, boolean animate)277     public void removeItem(WorkspaceItemInfo item, boolean animate) {
278         mInfo.remove(item, animate);
279     }
280 
onDragEnter(ItemInfo dragInfo)281     public void onDragEnter(ItemInfo dragInfo) {
282         if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
283         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) getLayoutParams();
284         CellLayout cl = (CellLayout) getParent().getParent();
285 
286         mBackground.animateToAccept(cl, lp.getCellX(), lp.getCellY());
287         mOpenAlarm.setOnAlarmListener(mOnOpenListener);
288         if (SPRING_LOADING_ENABLED &&
289                 ((dragInfo instanceof WorkspaceItemFactory)
290                         || (dragInfo instanceof WorkspaceItemInfo)
291                         || (dragInfo instanceof PendingAddShortcutInfo))) {
292             mOpenAlarm.setAlarm(ON_OPEN_DELAY);
293         }
294     }
295 
296     OnAlarmListener mOnOpenListener = new OnAlarmListener() {
297         public void onAlarm(Alarm alarm) {
298             mFolder.beginExternalDrag();
299         }
300     };
301 
prepareCreateAnimation(final View destView)302     public Drawable prepareCreateAnimation(final View destView) {
303         return mPreviewItemManager.prepareCreateAnimation(destView);
304     }
305 
performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)306     public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView,
307             final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect,
308             float scaleRelativeToDragLayer) {
309         final DragView srcView = d.dragView;
310         prepareCreateAnimation(destView);
311         addItem(destInfo);
312         // This will animate the first item from it's position as an icon into its
313         // position as the first item in the preview
314         mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null)
315                 .start();
316 
317         // This will animate the dragView (srcView) into the new folder
318         onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1,
319                 false /* itemReturnedOnFailedDrop */);
320     }
321 
performDestroyAnimation(Runnable onCompleteRunnable)322     public void performDestroyAnimation(Runnable onCompleteRunnable) {
323         // This will animate the final item in the preview to be full size.
324         mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable)
325                 .start();
326     }
327 
onDragExit()328     public void onDragExit() {
329         mBackground.animateToRest();
330         mOpenAlarm.cancelAlarm();
331     }
332 
onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)333     private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect,
334             float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) {
335         item.cellX = -1;
336         item.cellY = -1;
337         DragView animateView = d.dragView;
338         // Typically, the animateView corresponds to the DragView; however, if this is being done
339         // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
340         // will not have a view to animate
341         if (animateView != null && mActivity instanceof Launcher) {
342             final Launcher launcher = (Launcher) mActivity;
343             DragLayer dragLayer = launcher.getDragLayer();
344             Rect to = finalRect;
345             if (to == null) {
346                 to = new Rect();
347                 Workspace<?> workspace = launcher.getWorkspace();
348                 // Set cellLayout and this to it's final state to compute final animation locations
349                 workspace.setFinalTransitionTransform();
350                 float scaleX = getScaleX();
351                 float scaleY = getScaleY();
352                 setScaleX(1.0f);
353                 setScaleY(1.0f);
354                 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
355                 // Finished computing final animation locations, restore current state
356                 setScaleX(scaleX);
357                 setScaleY(scaleY);
358                 workspace.resetTransitionTransform();
359             }
360 
361             int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1);
362             boolean itemAdded = false;
363             if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) {
364                 List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
365                 mInfo.add(item, index, false);
366                 mCurrentPreviewItems.clear();
367                 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
368 
369                 if (!oldPreviewItems.equals(mCurrentPreviewItems)) {
370                     int newIndex = mCurrentPreviewItems.indexOf(item);
371                     if (newIndex >= 0) {
372                         // If the item dropped is going to be in the preview, we update the
373                         // index here to reflect its position in the preview.
374                         index = newIndex;
375                     }
376 
377                     mPreviewItemManager.hidePreviewItem(index, true);
378                     mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item);
379                     itemAdded = true;
380                 } else {
381                     removeItem(item, false);
382                 }
383             }
384 
385             if (!itemAdded) {
386                 mInfo.add(item, index, true);
387             }
388 
389             int[] center = new int[2];
390             float scale = getLocalCenterForIndex(index, numItemsInPreview, center);
391             center[0] = Math.round(scaleRelativeToDragLayer * center[0]);
392             center[1] = Math.round(scaleRelativeToDragLayer * center[1]);
393 
394             to.offset(center[0] - animateView.getMeasuredWidth() / 2,
395                     center[1] - animateView.getMeasuredHeight() / 2);
396 
397             float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f;
398 
399             float finalScale = scale * scaleRelativeToDragLayer;
400 
401             // Account for potentially different icon sizes with non-default grid settings
402             if (d.dragSource instanceof ActivityAllAppsContainerView) {
403                 DeviceProfile grid = mActivity.getDeviceProfile();
404                 float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx);
405                 finalScale *= containerScale;
406             }
407 
408             final int finalIndex = index;
409             dragLayer.animateView(animateView, to, finalAlpha,
410                     finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
411                     Interpolators.DEACCEL_2,
412                     () -> {
413                         mPreviewItemManager.hidePreviewItem(finalIndex, false);
414                         mFolder.showItem(item);
415                     },
416                     DragLayer.ANIMATION_END_DISAPPEAR, null);
417 
418             mFolder.hideItem(item);
419 
420             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
421 
422             FolderNameInfos nameInfos = new FolderNameInfos();
423             Executors.MODEL_EXECUTOR.post(() -> {
424                 d.folderNameProvider.getSuggestedFolderName(
425                         getContext(), mInfo.contents, nameInfos);
426                 postDelayed(() -> {
427                     setLabelSuggestion(nameInfos, d.logInstanceId);
428                     invalidate();
429                 }, DROP_IN_ANIMATION_DURATION);
430             });
431         } else {
432             addItem(item);
433         }
434     }
435 
436     /**
437      * Set the suggested folder name.
438      */
setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId)439     public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) {
440         if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) {
441             return;
442         }
443         if (nameInfos == null || !nameInfos.hasSuggestions()) {
444             StatsLogManager.newInstance(getContext()).logger()
445                     .withInstanceId(instanceId)
446                     .withItemInfo(mInfo)
447                     .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS);
448             return;
449         }
450         if (!nameInfos.hasPrimary()) {
451             StatsLogManager.newInstance(getContext()).logger()
452                     .withInstanceId(instanceId)
453                     .withItemInfo(mInfo)
454                     .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY);
455             return;
456         }
457         CharSequence newTitle = nameInfos.getLabels()[0];
458         FromState fromState = mInfo.getFromLabelState();
459 
460         mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
461         onTitleChanged(mInfo.title);
462         mFolder.mFolderName.setText(mInfo.title);
463 
464         // Logging for folder creation flow
465         StatsLogManager.newInstance(getContext()).logger()
466                 .withInstanceId(instanceId)
467                 .withItemInfo(mInfo)
468                 .withFromState(fromState)
469                 .withToState(ToState.TO_SUGGESTION0)
470                 // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter,
471                 // event is assumed to be folder creation on the server side.
472                 .withEditText(newTitle.toString())
473                 .log(LAUNCHER_FOLDER_AUTO_LABELED);
474     }
475 
476 
onDrop(DragObject d, boolean itemReturnedOnFailedDrop)477     public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) {
478         WorkspaceItemInfo item;
479         if (d.dragInfo instanceof WorkspaceItemFactory) {
480             // Came from all apps -- make a copy
481             item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext());
482         } else if (d.dragSource instanceof BaseItemDragListener){
483             // Came from a different window -- make a copy
484             item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo);
485         } else {
486             item = (WorkspaceItemInfo) d.dragInfo;
487         }
488         mFolder.notifyDrop();
489         onDrop(item, d, null, 1.0f,
490                 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
491                 itemReturnedOnFailedDrop
492         );
493     }
494 
setDotInfo(FolderDotInfo dotInfo)495     public void setDotInfo(FolderDotInfo dotInfo) {
496         updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot());
497         mDotInfo = dotInfo;
498     }
499 
getLayoutRule()500     public ClippedFolderIconLayoutRule getLayoutRule() {
501         return mPreviewLayoutRule;
502     }
503 
504     @Override
setForceHideDot(boolean forceHideDot)505     public void setForceHideDot(boolean forceHideDot) {
506         if (mForceHideDot == forceHideDot) {
507             return;
508         }
509         mForceHideDot = forceHideDot;
510 
511         if (forceHideDot) {
512             invalidate();
513         } else if (hasDot()) {
514             animateDotScale(0, 1);
515         }
516     }
517 
518     /**
519      * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false
520      * (the dot is being added or removed).
521      */
updateDotScale(boolean wasDotted, boolean isDotted)522     private void updateDotScale(boolean wasDotted, boolean isDotted) {
523         float newDotScale = isDotted ? 1f : 0f;
524         // Animate when a dot is first added or when it is removed.
525         if ((wasDotted ^ isDotted) && isShown()) {
526             animateDotScale(newDotScale);
527         } else {
528             cancelDotScaleAnim();
529             mDotScale = newDotScale;
530             invalidate();
531         }
532     }
533 
cancelDotScaleAnim()534     private void cancelDotScaleAnim() {
535         if (mDotScaleAnim != null) {
536             mDotScaleAnim.cancel();
537         }
538     }
539 
animateDotScale(float... dotScales)540     public void animateDotScale(float... dotScales) {
541         cancelDotScaleAnim();
542         mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
543         mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
544             @Override
545             public void onAnimationEnd(Animator animation) {
546                 mDotScaleAnim = null;
547             }
548         });
549         mDotScaleAnim.start();
550     }
551 
hasDot()552     public boolean hasDot() {
553         return mDotInfo != null && mDotInfo.hasDot();
554     }
555 
getLocalCenterForIndex(int index, int curNumItems, int[] center)556     private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
557         mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams(
558                 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams);
559 
560         mTmpParams.transX += mBackground.basePreviewOffsetX;
561         mTmpParams.transY += mBackground.basePreviewOffsetY;
562 
563         float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize();
564         float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2;
565         float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2;
566 
567         center[0] = Math.round(offsetX);
568         center[1] = Math.round(offsetY);
569         return mTmpParams.scale;
570     }
571 
setFolderBackground(PreviewBackground bg)572     public void setFolderBackground(PreviewBackground bg) {
573         mBackground = bg;
574         mBackground.setInvalidateDelegate(this);
575     }
576 
577     @Override
setIconVisible(boolean visible)578     public void setIconVisible(boolean visible) {
579         mBackgroundIsVisible = visible;
580         invalidate();
581     }
582 
getIconVisible()583     public boolean getIconVisible() {
584         return mBackgroundIsVisible;
585     }
586 
getFolderBackground()587     public PreviewBackground getFolderBackground() {
588         return mBackground;
589     }
590 
getPreviewItemManager()591     public PreviewItemManager getPreviewItemManager() {
592         return mPreviewItemManager;
593     }
594 
595     @Override
dispatchDraw(Canvas canvas)596     protected void dispatchDraw(Canvas canvas) {
597         super.dispatchDraw(canvas);
598 
599         if (!mBackgroundIsVisible) return;
600 
601         mPreviewItemManager.recomputePreviewDrawingParams();
602 
603         if (!mBackground.drawingDelegated()) {
604             mBackground.drawBackground(canvas);
605         }
606 
607         if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
608 
609         mPreviewItemManager.draw(canvas);
610 
611         if (!mBackground.drawingDelegated()) {
612             mBackground.drawBackgroundStroke(canvas);
613         }
614 
615         drawDot(canvas);
616     }
617 
drawDot(Canvas canvas)618     public void drawDot(Canvas canvas) {
619         if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
620             Rect iconBounds = mDotParams.iconBounds;
621             // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered
622             int iconSize = mActivity.getDeviceProfile().iconSizePx;
623             iconBounds.left = (getWidth() - iconSize) / 2;
624             iconBounds.right = iconBounds.left + iconSize;
625             iconBounds.top = getPaddingTop();
626             iconBounds.bottom = iconBounds.top + iconSize;
627 
628             float iconScale = (float) mBackground.previewSize / iconSize;
629             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
630 
631             // If we are animating to the accepting state, animate the dot out.
632             mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
633             mDotParams.dotColor = mBackground.getDotColor();
634             mDotRenderer.draw(canvas, mDotParams);
635         }
636     }
637 
setTextVisible(boolean visible)638     public void setTextVisible(boolean visible) {
639         if (visible) {
640             mFolderName.setVisibility(VISIBLE);
641         } else {
642             mFolderName.setVisibility(INVISIBLE);
643         }
644     }
645 
getTextVisible()646     public boolean getTextVisible() {
647         return mFolderName.getVisibility() == VISIBLE;
648     }
649 
650     /**
651      * Returns the list of items which should be visible in the preview
652      */
getPreviewItemsOnPage(int page)653     public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
654         return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
655     }
656 
657     @Override
verifyDrawable(@onNull Drawable who)658     protected boolean verifyDrawable(@NonNull Drawable who) {
659         return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who);
660     }
661 
662     @Override
onItemsChanged(boolean animate)663     public void onItemsChanged(boolean animate) {
664         updatePreviewItems(animate);
665         invalidate();
666         requestLayout();
667     }
668 
updatePreviewItems(boolean animate)669     private void updatePreviewItems(boolean animate) {
670         mPreviewItemManager.updatePreviewItems(animate);
671         mCurrentPreviewItems.clear();
672         mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
673     }
674 
675     /**
676      * Updates the preview items which match the provided condition
677      */
updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck)678     public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
679         mPreviewItemManager.updatePreviewItems(itemCheck);
680     }
681 
682     @Override
onAdd(WorkspaceItemInfo item, int rank)683     public void onAdd(WorkspaceItemInfo item, int rank) {
684         updatePreviewItems(false);
685         boolean wasDotted = mDotInfo.hasDot();
686         mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
687         boolean isDotted = mDotInfo.hasDot();
688         updateDotScale(wasDotted, isDotted);
689         setContentDescription(getAccessiblityTitle(mInfo.title));
690         invalidate();
691         requestLayout();
692     }
693 
694     @Override
onRemove(List<WorkspaceItemInfo> items)695     public void onRemove(List<WorkspaceItemInfo> items) {
696         updatePreviewItems(false);
697         boolean wasDotted = mDotInfo.hasDot();
698         items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo);
699         boolean isDotted = mDotInfo.hasDot();
700         updateDotScale(wasDotted, isDotted);
701         setContentDescription(getAccessiblityTitle(mInfo.title));
702         invalidate();
703         requestLayout();
704     }
705 
onTitleChanged(CharSequence title)706     public void onTitleChanged(CharSequence title) {
707         mFolderName.setText(title);
708         setContentDescription(getAccessiblityTitle(title));
709     }
710 
711     @Override
onTouchEvent(MotionEvent event)712     public boolean onTouchEvent(MotionEvent event) {
713         if (event.getAction() == MotionEvent.ACTION_DOWN
714                 && shouldIgnoreTouchDown(event.getX(), event.getY())) {
715             return false;
716         }
717 
718         // Call the superclass onTouchEvent first, because sometimes it changes the state to
719         // isPressed() on an ACTION_UP
720         super.onTouchEvent(event);
721         mLongPressHelper.onTouchEvent(event);
722         // Keep receiving the rest of the events
723         return true;
724     }
725 
726     /**
727      * Returns true if the touch down at the provided position be ignored
728      */
shouldIgnoreTouchDown(float x, float y)729     protected boolean shouldIgnoreTouchDown(float x, float y) {
730         mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
731                 getHeight() - getPaddingBottom());
732         return !mTouchArea.contains((int) x, (int) y);
733     }
734 
735     @Override
cancelLongPress()736     public void cancelLongPress() {
737         super.cancelLongPress();
738         mLongPressHelper.cancelLongPress();
739     }
740 
removeListeners()741     public void removeListeners() {
742         mInfo.removeListener(this);
743         mInfo.removeListener(mFolder);
744     }
745 
isInHotseat()746     private boolean isInHotseat() {
747         return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
748     }
749 
clearLeaveBehindIfExists()750     public void clearLeaveBehindIfExists() {
751         if (getParent() instanceof FolderIconParent) {
752             ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
753         }
754     }
755 
drawLeaveBehindIfExists()756     public void drawLeaveBehindIfExists() {
757         if (getParent() instanceof FolderIconParent) {
758             ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
759         }
760     }
761 
onFolderClose(int currentPage)762     public void onFolderClose(int currentPage) {
763         mPreviewItemManager.onFolderClose(currentPage);
764     }
765 
766     @Override
getTranslateDelegate()767     public MultiTranslateDelegate getTranslateDelegate() {
768         return mTranslateDelegate;
769     }
770 
771     @Override
setReorderBounceScale(float scale)772     public void setReorderBounceScale(float scale) {
773         mScaleForReorderBounce = scale;
774         super.setScaleX(scale);
775         super.setScaleY(scale);
776     }
777 
778     @Override
getReorderBounceScale()779     public float getReorderBounceScale() {
780         return mScaleForReorderBounce;
781     }
782 
783     @Override
getViewType()784     public int getViewType() {
785         return DRAGGABLE_ICON;
786     }
787 
788     @Override
getWorkspaceVisualDragBounds(Rect bounds)789     public void getWorkspaceVisualDragBounds(Rect bounds) {
790         getPreviewBounds(bounds);
791     }
792 
793     /**
794      * Returns a formatted accessibility title for folder
795      */
getAccessiblityTitle(CharSequence title)796     public String getAccessiblityTitle(CharSequence title) {
797         int size = mInfo.contents.size();
798         if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
799             return getContext().getString(R.string.folder_name_format_exact, title, size);
800         } else {
801             return getContext().getString(R.string.folder_name_format_overflow, title,
802                     MAX_NUM_ITEMS_IN_PREVIEW);
803         }
804     }
805 
806     /**
807      * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
808      */
809     public interface FolderIconParent {
810         /**
811          * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
812          * gap where the FolderIcon would be when the Folder is closed.
813          */
drawFolderLeaveBehindForIcon(FolderIcon child)814         void drawFolderLeaveBehindForIcon(FolderIcon child);
815         /**
816          * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
817          */
clearFolderLeaveBehind(FolderIcon child)818         void clearFolderLeaveBehind(FolderIcon child);
819     }
820 }
821