/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.folder; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.view.View; import android.widget.TextView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import java.util.ArrayList; import java.util.List; import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION; /** * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}. */ public class PreviewItemManager { private FolderIcon mIcon; // These variables are all associated with the drawing of the preview; they are stored // as member variables for shared usage and to avoid computation on each frame private float mIntrinsicIconSize = -1; private int mTotalWidth = -1; private int mPrevTopPadding = -1; private Drawable mReferenceDrawable = null; // These hold the first page preview items private ArrayList mFirstPageParams = new ArrayList<>(); // These hold the current page preview items. It is empty if the current page is the first page. private ArrayList mCurrentPageParams = new ArrayList<>(); private float mCurrentPageItemsTransX = 0; private boolean mShouldSlideInFirstPage; static final int INITIAL_ITEM_ANIMATION_DURATION = 350; private static final int FINAL_ITEM_ANIMATION_DURATION = 200; private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY = 100; private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION = 300; private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200; public PreviewItemManager(FolderIcon icon) { mIcon = icon; } /** * @param reverse If true, animates the final item in the preview to be full size. If false, * animates the first item to its position in the preview. */ public FolderPreviewItemAnim createFirstItemAnimation(final boolean reverse, final Runnable onCompleteRunnable) { return reverse ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1, FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable) : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2, INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable); } Drawable prepareCreateAnimation(final View destView) { Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); mReferenceDrawable = animateDrawable; return animateDrawable; } public void recomputePreviewDrawingParams() { if (mReferenceDrawable != null) { computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(), mIcon.getMeasuredWidth()); } } private void computePreviewDrawingParams(int drawableSize, int totalSize) { if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize || mPrevTopPadding != mIcon.getPaddingTop()) { mIntrinsicIconSize = drawableSize; mTotalWidth = totalSize; mPrevTopPadding = mIcon.getPaddingTop(); mIcon.mBackground.setup(mIcon.mLauncher, mIcon, mTotalWidth, mIcon.getPaddingTop()); mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize, Utilities.isRtl(mIcon.getResources())); updateItemDrawingParams(false); } } PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems, PreviewItemDrawingParams params) { // We use an index of -1 to represent an icon on the workspace for the destroy and // create animations if (index == -1) { return getFinalIconParams(params); } return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params); } private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) { float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx; final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth(); final float trans = (mIcon.mBackground.previewSize - iconSize) / 2; params.update(trans, trans, scale); return params; } public void drawParams(Canvas canvas, ArrayList params, float transX) { canvas.translate(transX, 0); // The first item should be drawn last (ie. on top of later items) for (int i = params.size() - 1; i >= 0; i--) { PreviewItemDrawingParams p = params.get(i); if (!p.hidden) { drawPreviewItem(canvas, p); } } canvas.translate(-transX, 0); } public void draw(Canvas canvas) { // The items are drawn in coordinates relative to the preview offset PreviewBackground bg = mIcon.getFolderBackground(); canvas.translate(bg.basePreviewOffsetX, bg.basePreviewOffsetY); float firstPageItemsTransX = 0; if (mShouldSlideInFirstPage) { drawParams(canvas, mCurrentPageParams, mCurrentPageItemsTransX); firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX; } drawParams(canvas, mFirstPageParams, firstPageItemsTransX); canvas.translate(-bg.basePreviewOffsetX, -bg.basePreviewOffsetY); } public void onParamsChanged() { mIcon.invalidate(); } private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(params.transX, params.transY); canvas.scale(params.scale, params.scale); Drawable d = params.drawable; if (d != null) { Rect bounds = d.getBounds(); canvas.save(); canvas.translate(-bounds.left, -bounds.top); canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height()); d.draw(canvas); canvas.restore(); } canvas.restore(); } public void hidePreviewItem(int index, boolean hidden) { PreviewItemDrawingParams params = index < mFirstPageParams.size() ? mFirstPageParams.get(index) : null; if (params != null) { params.hidden = hidden; } } void buildParamsForPage(int page, ArrayList params, boolean animate) { List items = mIcon.getPreviewItemsOnPage(page); int prevNumItems = params.size(); // We adjust the size of the list to match the number of items in the preview. while (items.size() < params.size()) { params.remove(params.size() - 1); } while (items.size() > params.size()) { params.add(new PreviewItemDrawingParams(0, 0, 0, 0)); } int numItemsInFirstPagePreview = page == 0 ? items.size() : FolderIcon.NUM_ITEMS_IN_PREVIEW; for (int i = 0; i < params.size(); i++) { PreviewItemDrawingParams p = params.get(i); p.drawable = items.get(i).getCompoundDrawables()[1]; if (p.drawable != null && !mIcon.mFolder.isOpen()) { // Set the callback to FolderIcon as it is responsible to drawing the icon. The // callback will be released when the folder is opened. p.drawable.setCallback(mIcon); } if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) { computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p); if (mReferenceDrawable == null) { mReferenceDrawable = p.drawable; } } else { FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i, prevNumItems, i, numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION, null); if (p.anim != null) { if (p.anim.hasEqualFinalState(anim)) { // do nothing, let the current animation finish continue; } p.anim.cancel(); } p.anim = anim; p.anim.start(); } } } void onFolderClose(int currentPage) { // If we are not closing on the first page, we animate the current page preview items // out, and animate the first page preview items in. mShouldSlideInFirstPage = currentPage != 0; if (mShouldSlideInFirstPage) { mCurrentPageItemsTransX = 0; buildParamsForPage(currentPage, mCurrentPageParams, false); onParamsChanged(); ValueAnimator slideAnimator = ValueAnimator.ofFloat(0, ITEM_SLIDE_IN_OUT_DISTANCE_PX); slideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mCurrentPageItemsTransX = (float) valueAnimator.getAnimatedValue(); onParamsChanged(); } }); slideAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentPageParams.clear(); } }); slideAnimator.setStartDelay(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY); slideAnimator.setDuration(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION); slideAnimator.start(); } } void updateItemDrawingParams(boolean animate) { buildParamsForPage(0, mFirstPageParams, animate); } boolean verifyDrawable(@NonNull Drawable who) { for (int i = 0; i < mFirstPageParams.size(); i++) { if (mFirstPageParams.get(i).drawable == who) { return true; } } return false; } float getIntrinsicIconSize() { return mIntrinsicIconSize; } /** * Handles the case where items in the preview are either: * - Moving into the preview * - Moving into a new position * - Moving out of the preview * * @param oldParams The list of items in the old preview. * @param newParams The list of items in the new preview. * @param dropped The item that was dropped onto the FolderIcon. */ public void onDrop(List oldParams, List newParams, ShortcutInfo dropped) { int numItems = newParams.size(); final ArrayList params = mFirstPageParams; buildParamsForPage(0, params, false); // New preview items for items that are moving in (except for the dropped item). List moveIn = new ArrayList<>(); for (BubbleTextView btv : newParams) { if (!oldParams.contains(btv) && !btv.getTag().equals(dropped)) { moveIn.add(btv); } } for (int i = 0; i < moveIn.size(); ++i) { int prevIndex = newParams.indexOf(moveIn.get(i)); PreviewItemDrawingParams p = params.get(prevIndex); computePreviewItemDrawingParams(prevIndex, numItems, p); updateTransitionParam(p, moveIn.get(i), mIcon.mPreviewLayoutRule.getEnterIndex(), newParams.indexOf(moveIn.get(i))); } // Items that are moving into new positions within the preview. for (int newIndex = 0; newIndex < newParams.size(); ++newIndex) { int oldIndex = oldParams.indexOf(newParams.get(newIndex)); if (oldIndex >= 0 && newIndex != oldIndex) { PreviewItemDrawingParams p = params.get(newIndex); updateTransitionParam(p, newParams.get(newIndex), oldIndex, newIndex); } } // Old preview items that need to be moved out. List moveOut = new ArrayList<>(oldParams); moveOut.removeAll(newParams); for (int i = 0; i < moveOut.size(); ++i) { BubbleTextView item = moveOut.get(i); int oldIndex = oldParams.indexOf(item); PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null); updateTransitionParam(p, item, oldIndex, mIcon.mPreviewLayoutRule.getExitIndex()); params.add(0, p); // We want these items first so that they are on drawn last. } for (int i = 0; i < params.size(); ++i) { if (params.get(i).anim != null) { params.get(i).anim.start(); } } } private void updateTransitionParam(final PreviewItemDrawingParams p, BubbleTextView btv, int prevIndex, int newIndex) { p.drawable = btv.getCompoundDrawables()[1]; if (!mIcon.mFolder.isOpen()) { // Set the callback to FolderIcon as it is responsible to drawing the icon. The // callback will be released when the folder is opened. p.drawable.setCallback(mIcon); } FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, FolderIcon.NUM_ITEMS_IN_PREVIEW, newIndex, FolderIcon.NUM_ITEMS_IN_PREVIEW, DROP_IN_ANIMATION_DURATION, null); if (p.anim != null && !p.anim.hasEqualFinalState(anim)) { p.anim.cancel(); } p.anim = anim; } }