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