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