/*
|
* 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 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.view.View;
|
import android.widget.TextView;
|
|
import com.android.launcher3.BubbleTextView;
|
import com.android.launcher3.WorkspaceItemInfo;
|
import com.android.launcher3.Utilities;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
|
import androidx.annotation.NonNull;
|
|
/**
|
* 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<PreviewItemDrawingParams> mFirstPageParams = new ArrayList<>();
|
// These hold the current page preview items. It is empty if the current page is the first page.
|
private ArrayList<PreviewItemDrawingParams> 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.mLauncher, 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.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<PreviewItemDrawingParams> 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.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) {
|
// 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<PreviewItemDrawingParams> params, boolean animate) {
|
List<BubbleTextView> 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() : MAX_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) {
|
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 updatePreviewItems(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<BubbleTextView> oldParams, List<BubbleTextView> newParams,
|
WorkspaceItemInfo dropped) {
|
int numItems = newParams.size();
|
final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
|
buildParamsForPage(0, params, false);
|
|
// New preview items for items that are moving in (except for the dropped item).
|
List<BubbleTextView> 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), ENTER_INDEX, newParams.indexOf(moveIn.get(i)),
|
numItems);
|
}
|
|
// 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, numItems);
|
}
|
}
|
|
// Old preview items that need to be moved out.
|
List<BubbleTextView> 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, 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, BubbleTextView btv,
|
int prevIndex, int newIndex, int numItems) {
|
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, numItems,
|
newIndex, numItems, DROP_IN_ANIMATION_DURATION, null);
|
if (p.anim != null && !p.anim.hasEqualFinalState(anim)) {
|
p.anim.cancel();
|
}
|
p.anim = anim;
|
}
|
}
|