/* * 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 static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.FloatProperty; import android.view.View; import androidx.annotation.NonNull; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Utilities; import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.views.ActivityContext; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; /** * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}. */ public class PreviewItemManager { private static final FloatProperty CURRENT_PAGE_ITEMS_TRANS_X = new FloatProperty("currentPageItemsTransX") { @Override public void setValue(PreviewItemManager manager, float v) { manager.mCurrentPageItemsTransX = v; manager.onParamsChanged(); } @Override public Float get(PreviewItemManager manager) { return manager.mCurrentPageItemsTransX; } }; private final Context mContext; private final FolderIcon mIcon; private final int mIconSize; // 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<>(); // We clip the preview items during the middle of the animation, so that it does not go outside // of the visual shape. We stop clipping at this threshold, since the preview items ultimately // do not get cropped in their resting state. private final float mClipThreshold; 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) { mContext = icon.getContext(); mIcon = icon; mIconSize = ActivityContext.lookupContext( mContext).getDeviceProfile().folderChildIconSizePx; mClipThreshold = Utilities.dpToPx(1f); } /** * @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 = ((BubbleTextView) destView).getIcon(); 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.getContext(), mIcon.mActivity, mIcon, mTotalWidth, mIcon.getPaddingTop()); mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize, Utilities.isRtl(mIcon.getResources())); updatePreviewItems(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.mActivity.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, PointF offset, boolean shouldClipPath, Path clipPath) { // 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) { // Exiting param should always be clipped. boolean isExiting = p.index == EXIT_INDEX; drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath); } } } /** * Draws the preview items on {@param canvas}. */ public void draw(Canvas canvas) { int saveCount = canvas.getSaveCount(); // The items are drawn in coordinates relative to the preview offset PreviewBackground bg = mIcon.getFolderBackground(); Path clipPath = bg.getClipPath(); float firstPageItemsTransX = 0; if (mShouldSlideInFirstPage) { PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX, bg.basePreviewOffsetY); boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold; drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath); firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX; } PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX, bg.basePreviewOffsetY); boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold; drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath); canvas.restoreToCount(saveCount); } public void onParamsChanged() { mIcon.invalidate(); } /** * Draws each preview item. * * @param offset The offset needed to draw the preview items. * @param shouldClipPath Iff true, clip path using {@param clipPath}. * @param clipPath The clip path of the folder icon. */ private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset, boolean shouldClipPath, Path clipPath) { canvas.save(); if (shouldClipPath) { canvas.clipPath(clipPath); } canvas.translate(offset.x + params.transX, offset.y + 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) { // If there are more params than visible in the preview, they are used for enter/exit // animation purposes and they were added to the front of the list. // To index the params properly, we need to skip these params. index = index + Math.max(mFirstPageParams.size() - MAX_NUM_ITEMS_IN_PREVIEW, 0); 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)); } int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW; for (int i = 0; i < params.size(); i++) { PreviewItemDrawingParams p = params.get(i); setDrawable(p, items.get(i)); if (!animate) { if (p.anim != null) { p.anim.cancel(); } 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 = ObjectAnimator .ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX); 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 updatePreviewItems(boolean animate) { buildParamsForPage(0, mFirstPageParams, animate); } void updatePreviewItems(Predicate itemCheck) { boolean modified = false; for (PreviewItemDrawingParams param : mFirstPageParams) { if (itemCheck.test(param.item)) { setDrawable(param, param.item); modified = true; } } for (PreviewItemDrawingParams param : mCurrentPageParams) { if (itemCheck.test(param.item)) { setDrawable(param, param.item); modified = true; } } if (modified) { mIcon.invalidate(); } } 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 oldItems The list of items in the old preview. * @param newItems The list of items in the new preview. * @param dropped The item that was dropped onto the FolderIcon. */ public void onDrop(List oldItems, List newItems, WorkspaceItemInfo dropped) { int numItems = newItems.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 (WorkspaceItemInfo newItem : newItems) { if (!oldItems.contains(newItem) && !newItem.equals(dropped)) { moveIn.add(newItem); } } for (int i = 0; i < moveIn.size(); ++i) { int prevIndex = newItems.indexOf(moveIn.get(i)); PreviewItemDrawingParams p = params.get(prevIndex); computePreviewItemDrawingParams(prevIndex, numItems, p); updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)), numItems); } // Items that are moving into new positions within the preview. for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) { int oldIndex = oldItems.indexOf(newItems.get(newIndex)); if (oldIndex >= 0 && newIndex != oldIndex) { PreviewItemDrawingParams p = params.get(newIndex); updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems); } } // Old preview items that need to be moved out. List moveOut = new ArrayList<>(oldItems); moveOut.removeAll(newItems); for (int i = 0; i < moveOut.size(); ++i) { WorkspaceItemInfo item = moveOut.get(i); int oldIndex = oldItems.indexOf(item); PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null); updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems); 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, WorkspaceItemInfo item, int prevIndex, int newIndex, int numItems) { setDrawable(p, item); FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems, newIndex, numItems, DROP_IN_ANIMATION_DURATION, null); if (p.anim != null && !p.anim.hasEqualFinalState(anim)) { p.anim.cancel(); } p.anim = anim; } private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) { if (item.hasPromiseIconUi() || (item.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { PreloadIconDrawable drawable = newPendingIcon(mContext, item); drawable.setLevel(item.getProgressLevel()); p.drawable = drawable; } else { p.drawable = item.newIcon(mContext, true); } p.drawable.setBounds(0, 0, mIconSize, mIconSize); p.item = item; // 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); } }