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