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