• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.BubbleTextView.DISPLAY_FOLDER;
20 import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
22 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
23 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
24 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
25 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
26 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
27 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
28 
29 import android.animation.Animator;
30 import android.animation.AnimatorListenerAdapter;
31 import android.animation.ObjectAnimator;
32 import android.animation.ValueAnimator;
33 import android.content.Context;
34 import android.graphics.Canvas;
35 import android.graphics.Path;
36 import android.graphics.PointF;
37 import android.graphics.Rect;
38 import android.graphics.drawable.Drawable;
39 import android.util.FloatProperty;
40 import android.view.View;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.VisibleForTesting;
44 
45 import com.android.launcher3.BubbleTextView;
46 import com.android.launcher3.Flags;
47 import com.android.launcher3.LauncherAppState;
48 import com.android.launcher3.Utilities;
49 import com.android.launcher3.apppairs.AppPairIcon;
50 import com.android.launcher3.apppairs.AppPairIconDrawingParams;
51 import com.android.launcher3.apppairs.AppPairIconGraphic;
52 import com.android.launcher3.model.data.AppPairInfo;
53 import com.android.launcher3.model.data.ItemInfo;
54 import com.android.launcher3.model.data.ItemInfoWithIcon;
55 import com.android.launcher3.model.data.WorkspaceItemInfo;
56 import com.android.launcher3.views.ActivityContext;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.function.Predicate;
61 
62 /**
63  * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}.
64  */
65 public class PreviewItemManager {
66 
67     private static final FloatProperty<PreviewItemManager> CURRENT_PAGE_ITEMS_TRANS_X =
68             new FloatProperty<PreviewItemManager>("currentPageItemsTransX") {
69                 @Override
70                 public void setValue(PreviewItemManager manager, float v) {
71                     manager.mCurrentPageItemsTransX = v;
72                     manager.onParamsChanged();
73                 }
74 
75                 @Override
76                 public Float get(PreviewItemManager manager) {
77                     return manager.mCurrentPageItemsTransX;
78                 }
79             };
80 
81     private final Context mContext;
82     private final FolderIcon mIcon;
83     @VisibleForTesting
84     public final int mIconSize;
85 
86     // These variables are all associated with the drawing of the preview; they are stored
87     // as member variables for shared usage and to avoid computation on each frame
88     private float mIntrinsicIconSize = -1;
89     private int mTotalWidth = -1;
90     private int mPrevTopPadding = -1;
91     private Drawable mReferenceDrawable = null;
92 
93     private int mNumOfPrevItems = 0;
94 
95     // These hold the first page preview items
96     private ArrayList<PreviewItemDrawingParams> mFirstPageParams = new ArrayList<>();
97     // These hold the current page preview items. It is empty if the current page is the first page.
98     private ArrayList<PreviewItemDrawingParams> mCurrentPageParams = new ArrayList<>();
99 
100     // We clip the preview items during the middle of the animation, so that it does not go outside
101     // of the visual shape. We stop clipping at this threshold, since the preview items ultimately
102     // do not get cropped in their resting state.
103     private final float mClipThreshold;
104     private float mCurrentPageItemsTransX = 0;
105     private boolean mShouldSlideInFirstPage;
106 
107     static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
108     private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
109 
110     private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY = 100;
111     private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION = 300;
112     private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200;
113 
PreviewItemManager(FolderIcon icon)114     public PreviewItemManager(FolderIcon icon) {
115         mContext = icon.getContext();
116         mIcon = icon;
117         mIconSize = ActivityContext.lookupContext(
118                 mContext).getDeviceProfile().folderChildIconSizePx;
119         mClipThreshold = Utilities.dpToPx(1f);
120     }
121 
122     /**
123      * @param reverse If true, animates the final item in the preview to be full size. If false,
124      *                animates the first item to its position in the preview.
125      */
createFirstItemAnimation(final boolean reverse, final Runnable onCompleteRunnable)126     public FolderPreviewItemAnim createFirstItemAnimation(final boolean reverse,
127             final Runnable onCompleteRunnable) {
128         return reverse
129                 ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1,
130                 FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
131                 : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2,
132                         INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable);
133     }
134 
prepareCreateAnimation(final View destView)135     Drawable prepareCreateAnimation(final View destView) {
136         Drawable animateDrawable = destView instanceof AppPairIcon
137                 ? ((AppPairIcon) destView).getIconDrawableArea().getDrawable()
138                 : ((BubbleTextView) destView).getIcon();
139         computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
140                 destView.getMeasuredWidth());
141         mReferenceDrawable = animateDrawable;
142         return animateDrawable;
143     }
144 
recomputePreviewDrawingParams()145     public void recomputePreviewDrawingParams() {
146         if (mReferenceDrawable != null) {
147             computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(),
148                     mIcon.getMeasuredWidth());
149         }
150     }
151 
computePreviewDrawingParams(int drawableSize, int totalSize)152     private void computePreviewDrawingParams(int drawableSize, int totalSize) {
153         if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
154                 mPrevTopPadding != mIcon.getPaddingTop()) {
155             mIntrinsicIconSize = drawableSize;
156             mTotalWidth = totalSize;
157             mPrevTopPadding = mIcon.getPaddingTop();
158 
159             mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
160                     mIcon.getPaddingTop());
161             mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
162                     Utilities.isRtl(mIcon.getResources()));
163 
164             updatePreviewItems(false);
165         }
166     }
167 
computePreviewItemDrawingParams(int index, int curNumItems, PreviewItemDrawingParams params)168     PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
169             PreviewItemDrawingParams params) {
170         // We use an index of -1 to represent an icon on the workspace for the destroy and
171         // create animations
172         if (index == -1) {
173             return getFinalIconParams(params);
174         }
175         return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
176     }
177 
getFinalIconParams(PreviewItemDrawingParams params)178     private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
179         float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
180 
181         final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
182         final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
183 
184         params.update(trans, trans, scale);
185         return params;
186     }
187 
drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params, PointF offset, boolean shouldClipPath, Path clipPath)188     public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
189             PointF offset, boolean shouldClipPath, Path clipPath) {
190         // The first item should be drawn last (ie. on top of later items)
191         for (int i = params.size() - 1; i >= 0; i--) {
192             PreviewItemDrawingParams p = params.get(i);
193             if (!p.hidden) {
194                 // Exiting param should always be clipped.
195                 boolean isExiting = p.index == EXIT_INDEX;
196                 drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
197             }
198         }
199     }
200 
201     /**
202      * Draws the preview items on {@param canvas}.
203      */
draw(Canvas canvas)204     public void draw(Canvas canvas) {
205         int saveCount = canvas.getSaveCount();
206         // The items are drawn in coordinates relative to the preview offset
207         PreviewBackground bg = mIcon.getFolderBackground();
208         Path clipPath = bg.getClipPath();
209         float firstPageItemsTransX = 0;
210         if (mShouldSlideInFirstPage) {
211             PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX,
212                     bg.basePreviewOffsetY);
213             boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold;
214             drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath);
215             firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
216         }
217 
218         PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX,
219                 bg.basePreviewOffsetY);
220         boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold;
221         drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath);
222         canvas.restoreToCount(saveCount);
223     }
224 
225     public void onParamsChanged() {
226         mIcon.invalidate();
227     }
228 
229     /**
230      * Draws each preview item.
231      *
232      * @param offset         The offset needed to draw the preview items.
233      * @param shouldClipPath Iff true, clip path using {@param clipPath}.
234      * @param clipPath       The clip path of the folder icon.
235      */
236     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
237             boolean shouldClipPath, Path clipPath) {
238         canvas.save();
239         if (shouldClipPath) {
240             canvas.clipPath(clipPath);
241         }
242         canvas.translate(offset.x + params.transX, offset.y + params.transY);
243         canvas.scale(params.scale, params.scale);
244         Drawable d = params.drawable;
245 
246         if (d != null) {
247             Rect bounds = d.getBounds();
248             canvas.save();
249             canvas.translate(-bounds.left, -bounds.top);
250             canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
251             d.draw(canvas);
252             canvas.restore();
253         }
254         canvas.restore();
255     }
256 
257     public void hidePreviewItem(int index, boolean hidden) {
258         // If there are more params than visible in the preview, they are used for enter/exit
259         // animation purposes and they were added to the front of the list.
260         // To index the params properly, we need to skip these params.
261         index = index + Math.max(mFirstPageParams.size() - MAX_NUM_ITEMS_IN_PREVIEW, 0);
262 
263         PreviewItemDrawingParams params = index < mFirstPageParams.size() ?
264                 mFirstPageParams.get(index) : null;
265         if (params != null) {
266             params.hidden = hidden;
267         }
268     }
269 
270     void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
271         List<ItemInfo> items = mIcon.getPreviewItemsOnPage(page);
272 
273         // We adjust the size of the list to match the number of items in the preview.
274         while (items.size() < params.size()) {
275             params.remove(params.size() - 1);
276         }
277         while (items.size() > params.size()) {
278             params.add(new PreviewItemDrawingParams(0, 0, 0));
279         }
280 
281         int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
282         for (int i = 0; i < params.size(); i++) {
283             PreviewItemDrawingParams p = params.get(i);
284             setDrawable(p, items.get(i));
285 
286             if (!animate) {
287                 if (p.anim != null) {
288                     p.anim.cancel();
289                 }
290                 computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
291                 if (mReferenceDrawable == null) {
292                     mReferenceDrawable = p.drawable;
293                 }
294             } else {
295                 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i,
296                         mNumOfPrevItems, i, numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION,
297                         null);
298 
299                 if (p.anim != null) {
300                     if (p.anim.hasEqualFinalState(anim)) {
301                         // do nothing, let the current animation finish
302                         continue;
303                     }
304                     p.anim.cancel();
305                 }
306                 p.anim = anim;
307                 p.anim.start();
308             }
309         }
310     }
311 
312     void onFolderClose(int currentPage) {
313         // If we are not closing on the first page, we animate the current page preview items
314         // out, and animate the first page preview items in.
315         mShouldSlideInFirstPage = currentPage != 0;
316         if (mShouldSlideInFirstPage) {
317             mCurrentPageItemsTransX = 0;
318             buildParamsForPage(currentPage, mCurrentPageParams, false);
319             onParamsChanged();
320 
321             ValueAnimator slideAnimator = ObjectAnimator
322                     .ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
323             slideAnimator.addListener(new AnimatorListenerAdapter() {
324                 @Override
325                 public void onAnimationEnd(Animator animation) {
326                     mCurrentPageParams.clear();
327                 }
328             });
329             slideAnimator.setStartDelay(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY);
330             slideAnimator.setDuration(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION);
331             slideAnimator.start();
332         }
333     }
334 
335     void updatePreviewItems(boolean animate) {
336         int numOfPrevItemsAux = mFirstPageParams.size();
337         buildParamsForPage(0, mFirstPageParams, animate);
338         mNumOfPrevItems = numOfPrevItemsAux;
339     }
340 
341     void updatePreviewItems(Predicate<ItemInfo> itemCheck) {
342         boolean modified = false;
343         for (PreviewItemDrawingParams param : mFirstPageParams) {
344             if (itemCheck.test(param.item)
345                     || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) {
346                 setDrawable(param, param.item);
347                 modified = true;
348             }
349         }
350         for (PreviewItemDrawingParams param : mCurrentPageParams) {
351             if (itemCheck.test(param.item)
352                     || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) {
353                 setDrawable(param, param.item);
354                 modified = true;
355             }
356         }
357         if (modified) {
358             mIcon.invalidate();
359         }
360     }
361 
362     boolean verifyDrawable(@NonNull Drawable who) {
363         for (int i = 0; i < mFirstPageParams.size(); i++) {
364             if (mFirstPageParams.get(i).drawable == who) {
365                 return true;
366             }
367         }
368         return false;
369     }
370 
371     float getIntrinsicIconSize() {
372         return mIntrinsicIconSize;
373     }
374 
375     /**
376      * Handles the case where items in the preview are either:
377      * - Moving into the preview
378      * - Moving into a new position
379      * - Moving out of the preview
380      *
381      * @param oldItems The list of items in the old preview.
382      * @param newItems The list of items in the new preview.
383      * @param dropped  The item that was dropped onto the FolderIcon.
384      */
385     public void onDrop(List<ItemInfo> oldItems, List<ItemInfo> newItems, ItemInfo dropped) {
386         int numItems = newItems.size();
387         final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
388         buildParamsForPage(0, params, false);
389 
390         // New preview items for items that are moving in (except for the dropped item).
391         List<ItemInfo> moveIn = new ArrayList<>();
392         for (ItemInfo newItem : newItems) {
393             if (!oldItems.contains(newItem) && !newItem.equals(dropped)) {
394                 moveIn.add(newItem);
395             }
396         }
397         for (int i = 0; i < moveIn.size(); ++i) {
398             int prevIndex = newItems.indexOf(moveIn.get(i));
399             PreviewItemDrawingParams p = params.get(prevIndex);
400             computePreviewItemDrawingParams(prevIndex, numItems, p);
401             updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)),
402                     numItems);
403         }
404 
405         // Items that are moving into new positions within the preview.
406         for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) {
407             int oldIndex = oldItems.indexOf(newItems.get(newIndex));
408             if (oldIndex >= 0 && newIndex != oldIndex) {
409                 PreviewItemDrawingParams p = params.get(newIndex);
410                 updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems);
411             }
412         }
413 
414         // Old preview items that need to be moved out.
415         List<ItemInfo> moveOut = new ArrayList<>(oldItems);
416         moveOut.removeAll(newItems);
417         for (int i = 0; i < moveOut.size(); ++i) {
418             ItemInfo item = moveOut.get(i);
419             int oldIndex = oldItems.indexOf(item);
420             PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null);
421             updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems);
422             params.add(0, p); // We want these items first so that they are on drawn last.
423         }
424 
425         for (int i = 0; i < params.size(); ++i) {
426             if (params.get(i).anim != null) {
427                 params.get(i).anim.start();
428             }
429         }
430     }
431 
432     private void updateTransitionParam(final PreviewItemDrawingParams p, ItemInfo item,
433             int prevIndex, int newIndex, int numItems) {
434         setDrawable(p, item);
435 
436         FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems,
437                 newIndex, numItems, DROP_IN_ANIMATION_DURATION, null);
438         if (p.anim != null && !p.anim.hasEqualFinalState(anim)) {
439             p.anim.cancel();
440         }
441         p.anim = anim;
442     }
443 
444     @VisibleForTesting
445     public void setDrawable(PreviewItemDrawingParams p, ItemInfo item) {
446         if (item instanceof WorkspaceItemInfo wii) {
447             if (isActivePendingIcon(wii)) {
448                 p.drawable = newPendingIcon(mContext, wii);
449             } else {
450                 p.drawable = wii.newIcon(mContext, FLAG_THEMED);
451             }
452             p.drawable.setBounds(0, 0, mIconSize, mIconSize);
453         } else if (item instanceof AppPairInfo api) {
454             AppPairIconDrawingParams appPairParams =
455                     new AppPairIconDrawingParams(mContext, DISPLAY_FOLDER);
456             p.drawable = AppPairIconGraphic.composeDrawable(api, appPairParams);
457             p.drawable.setBounds(0, 0, mIconSize, mIconSize);
458         }
459 
460         p.item = item;
461         // Set the callback to FolderIcon as it is responsible to drawing the icon. The
462         // callback will be released when the folder is opened.
463         p.drawable.setCallback(mIcon);
464 
465         // Verify high res
466         if (item instanceof ItemInfoWithIcon info
467                 && info.getMatchingLookupFlag().isVisuallyLessThan(DESKTOP_ICON_FLAG)) {
468             LauncherAppState.getInstance(mContext).getIconCache().updateIconInBackground(
469                     newInfo -> {
470                         if (p.item == newInfo) {
471                             setDrawable(p, newInfo);
472                             mIcon.invalidate();
473                         }
474                     }, info);
475         }
476     }
477 
478     /**
479      * Returns true if item is a Promise Icon or actively downloading, and the item is not an
480      * inactive archived app.
481      */
482     private boolean isActivePendingIcon(WorkspaceItemInfo item) {
483         return (item.hasPromiseIconUi()
484                 || (item.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0)
485                 && !(Flags.useNewIconForArchivedApps() && item.isInactiveArchive());
486     }
487 }
488