blob: 4a4f7cff5e202c7d16a002403b4bd2112e76e176 [file] [log] [blame]
/*
* Copyright (C) 2008 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.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.launcher3.Alarm;
import com.android.launcher3.AppInfo;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.FolderInfo;
import com.android.launcher3.FolderInfo.FolderListener;
import com.android.launcher3.IconCache;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.OnAlarmListener;
import com.android.launcher3.PreloadIconDrawable;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.SimpleOnStylusPressListener;
import com.android.launcher3.StylusEventHelper;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.util.Thunk;
import java.util.ArrayList;
/**
* An icon that can appear on in the workspace representing an {@link Folder}.
*/
public class FolderIcon extends FrameLayout implements FolderListener {
@Thunk Launcher mLauncher;
@Thunk Folder mFolder;
private FolderInfo mInfo;
@Thunk static boolean sStaticValuesDirty = true;
public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW :
ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
private CheckLongPressHelper mLongPressHelper;
private StylusEventHelper mStylusEventHelper;
// The number of icons to display in the
private static final int CONSUMPTION_ANIMATION_DURATION = 100;
private static final int DROP_IN_ANIMATION_DURATION = 400;
private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
// Flag whether the folder should open itself when an item is dragged over is enabled.
public static final boolean SPRING_LOADING_ENABLED = true;
// Delay when drag enters until the folder opens, in miliseconds.
private static final int ON_OPEN_DELAY = 800;
@Thunk BubbleTextView mFolderName;
// 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 int mIntrinsicIconSize = -1;
private int mTotalWidth = -1;
private int mPrevTopPadding = -1;
PreviewBackground mBackground = new PreviewBackground();
private PreviewLayoutRule mPreviewLayoutRule;
boolean mAnimating = false;
private Rect mOldBounds = new Rect();
private float mSlop;
private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
private ArrayList<PreviewItemDrawingParams> mDrawingParams = new ArrayList<PreviewItemDrawingParams>();
private Drawable mReferenceDrawable = null;
Paint mBgPaint = new Paint();
private Alarm mOpenAlarm = new Alarm();
@Thunk
ItemInfo mDragInfo;
public FolderIcon(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FolderIcon(Context context) {
super(context);
init();
}
private void init() {
mLongPressHelper = new CheckLongPressHelper(this);
mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
mPreviewLayoutRule = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
new StackFolderIconLayoutRule() :
new ClippedFolderIconLayoutRule();
setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
}
public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
FolderInfo folderInfo, IconCache iconCache) {
@SuppressWarnings("all") // suppress dead code warning
final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
if (error) {
throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
"INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
"is dependent on this");
}
DeviceProfile grid = launcher.getDeviceProfile();
FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
// For performance and compatibility reasons we render the preview using a software layer.
// In particular, hardware path clipping has spotty ecosystem support and bad performance.
// Software rendering also allows us to use shadow layers.
icon.setLayerType(LAYER_TYPE_SOFTWARE, new Paint(Paint.FILTER_BITMAP_FLAG));
icon.setClipToPadding(false);
icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
icon.mFolderName.setText(folderInfo.title);
icon.mFolderName.setCompoundDrawablePadding(0);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
icon.setTag(folderInfo);
icon.setOnClickListener(launcher);
icon.mInfo = folderInfo;
icon.mLauncher = launcher;
icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
Folder folder = Folder.fromXml(launcher);
folder.setDragController(launcher.getDragController());
folder.setFolderIcon(icon);
folder.bind(folderInfo);
icon.setFolder(folder);
icon.setOnFocusChangeListener(launcher.mFocusHandler);
folderInfo.setListener(new MultiFolderListener(folder, icon));
return icon;
}
@Override
protected Parcelable onSaveInstanceState() {
sStaticValuesDirty = true;
return super.onSaveInstanceState();
}
public Folder getFolder() {
return mFolder;
}
private void setFolder(Folder folder) {
mFolder = folder;
updateItemDrawingParams(false);
}
public FolderInfo getFolderInfo() {
return mInfo;
}
private boolean willAcceptItem(ItemInfo item) {
final int itemType = item.itemType;
return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
!mFolder.isFull() && item != mInfo && !mInfo.opened);
}
public boolean acceptDrop(ItemInfo dragInfo) {
final ItemInfo item = dragInfo;
return !mFolder.isDestroyed() && willAcceptItem(item);
}
public void addItem(ShortcutInfo item) {
mInfo.add(item, true);
}
public void onDragEnter(ItemInfo dragInfo) {
if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
CellLayout cl = (CellLayout) getParent().getParent();
mBackground.animateToAccept(cl, lp.cellX, lp.cellY);
mOpenAlarm.setOnAlarmListener(mOnOpenListener);
if (SPRING_LOADING_ENABLED &&
((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) {
// TODO: we currently don't support spring-loading for PendingAddShortcutInfos even
// though widget-style shortcuts can be added to folders. The issue is that we need
// to deal with configuration activities which are currently handled in
// Workspace#onDropExternal.
mOpenAlarm.setAlarm(ON_OPEN_DELAY);
}
mDragInfo = dragInfo;
}
OnAlarmListener mOnOpenListener = new OnAlarmListener() {
public void onAlarm(Alarm alarm) {
ShortcutInfo item;
if (mDragInfo instanceof AppInfo) {
// Came from all apps -- make a copy.
item = ((AppInfo) mDragInfo).makeShortcut();
item.spanX = 1;
item.spanY = 1;
} else {
// ShortcutInfo
item = (ShortcutInfo) mDragInfo;
}
mFolder.beginExternalDrag(item);
mLauncher.openFolder(FolderIcon.this);
}
};
public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
// These correspond two the drawable and view that the icon was dropped _onto_
Drawable animateDrawable = getTopDrawable((TextView) destView);
computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
destView.getMeasuredWidth());
mReferenceDrawable = animateDrawable;
addItem(destInfo);
// This will animate the first item from it's position as an icon into its
// position as the first item in the preview
animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
// This will animate the dragView (srcView) into the new folder
onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null);
}
public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
Drawable animateDrawable = getTopDrawable((TextView) finalView);
computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
finalView.getMeasuredWidth());
// This will animate the first item from it's position as an icon into its
// position as the first item in the preview
animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
onCompleteRunnable);
}
public void onDragExit(Object dragInfo) {
onDragExit();
}
public void onDragExit() {
mBackground.animateToRest();
mOpenAlarm.cancelAlarm();
}
private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable,
DragObject d) {
item.cellX = -1;
item.cellY = -1;
// Typically, the animateView corresponds to the DragView; however, if this is being done
// after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
// will not have a view to animate
if (animateView != null) {
DragLayer dragLayer = mLauncher.getDragLayer();
Rect from = new Rect();
dragLayer.getViewRectRelativeToSelf(animateView, from);
Rect to = finalRect;
if (to == null) {
to = new Rect();
Workspace workspace = mLauncher.getWorkspace();
// Set cellLayout and this to it's final state to compute final animation locations
workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
float scaleX = getScaleX();
float scaleY = getScaleY();
setScaleX(1.0f);
setScaleY(1.0f);
scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
// Finished computing final animation locations, restore current state
setScaleX(scaleX);
setScaleY(scaleY);
workspace.resetTransitionTransform((CellLayout) getParent().getParent());
}
int[] center = new int[2];
float scale = getLocalCenterForIndex(index, index + 1, center);
center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
to.offset(center[0] - animateView.getMeasuredWidth() / 2,
center[1] - animateView.getMeasuredHeight() / 2);
float finalAlpha = index < mPreviewLayoutRule.numItems() ? 0.5f : 0f;
float finalScale = scale * scaleRelativeToDragLayer;
dragLayer.animateView(animateView, from, to, finalAlpha,
1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
new DecelerateInterpolator(2), new AccelerateInterpolator(2),
postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
addItem(item);
mFolder.hideItem(item);
final PreviewItemDrawingParams params = index < mDrawingParams.size() ?
mDrawingParams.get(index) : null;
if (params != null) params.hidden = true;
postDelayed(new Runnable() {
public void run() {
if (params != null) params.hidden = false;
mFolder.showItem(item);
invalidate();
}
}, DROP_IN_ANIMATION_DURATION);
} else {
addItem(item);
}
}
public void onDrop(DragObject d) {
ShortcutInfo item;
if (d.dragInfo instanceof AppInfo) {
// Came from all apps -- make a copy
item = ((AppInfo) d.dragInfo).makeShortcut();
} else {
item = (ShortcutInfo) d.dragInfo;
}
mFolder.notifyDrop();
onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d);
}
private void computePreviewDrawingParams(int drawableSize, int totalSize) {
if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
mPrevTopPadding != getPaddingTop()) {
DeviceProfile grid = mLauncher.getDeviceProfile();
mIntrinsicIconSize = drawableSize;
mTotalWidth = totalSize;
mPrevTopPadding = getPaddingTop();
mBackground.setup(getResources().getDisplayMetrics(), grid, this, mTotalWidth,
getPaddingTop());
mPreviewLayoutRule.init(mBackground.previewSize, mIntrinsicIconSize,
Utilities.isRtl(getResources()));
updateItemDrawingParams(false);
}
}
private void computePreviewDrawingParams(Drawable d) {
computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
}
static class PreviewItemDrawingParams {
PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
this.overlayAlpha = overlayAlpha;
}
public void update(float transX, float transY, float scale) {
// We ensure the update will not interfere with an animation on the layout params
// If the final values differ, we cancel the animation.
if (anim != null) {
if (anim.finalTransX == transX || anim.finalTransY == transY
|| anim.finalScale == scale) {
return;
}
anim.cancel();
}
this.transX = transX;
this.transY = transY;
this.scale = scale;
}
float transX;
float transY;
float scale;
public float overlayAlpha;
boolean hidden;
FolderPreviewItemAnim anim;
Drawable drawable;
}
private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
mTmpParams = computePreviewItemDrawingParams(Math.min(mPreviewLayoutRule.numItems(), index),
curNumItems, mTmpParams);
mTmpParams.transX += mBackground.basePreviewOffsetX;
mTmpParams.transY += mBackground.basePreviewOffsetY;
float offsetX = mTmpParams.transX + (mTmpParams.scale * mIntrinsicIconSize) / 2;
float offsetY = mTmpParams.transY + (mTmpParams.scale * mIntrinsicIconSize) / 2;
center[0] = (int) Math.round(offsetX);
center[1] = (int) Math.round(offsetY);
return mTmpParams.scale;
}
private 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 mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
}
private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
float iconSize = mLauncher.getDeviceProfile().iconSizePx;
final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
final float trans = (mBackground.previewSize - iconSize) / 2;
params.update(trans, trans, scale);
return params;
}
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) {
mOldBounds.set(d.getBounds());
d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
if (d instanceof FastBitmapDrawable) {
FastBitmapDrawable fd = (FastBitmapDrawable) d;
float oldBrightness = fd.getBrightness();
fd.setBrightness(params.overlayAlpha);
d.draw(canvas);
fd.setBrightness(oldBrightness);
} else {
d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
PorterDuff.Mode.SRC_ATOP);
d.draw(canvas);
d.clearColorFilter();
}
d.setBounds(mOldBounds);
}
canvas.restore();
}
/**
* This object represents a FolderIcon preview background. It stores drawing / measurement
* information, handles drawing, and animation (accept state <--> rest state).
*/
public static class PreviewBackground {
private float mScale = 1f;
private float mColorMultiplier = 1f;
private Path mClipPath = new Path();
private int mStrokeWidth;
private View mInvalidateDelegate;
public int previewSize;
private int basePreviewOffsetX;
private int basePreviewOffsetY;
private CellLayout mDrawingDelegate;
public int delegateCellX;
public int delegateCellY;
// When the PreviewBackground is drawn under an icon (for creating a folder) the border
// should not occlude the icon
public boolean isClipping = true;
// Drawing / animation configurations
private static final float ACCEPT_SCALE_FACTOR = 1.25f;
private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
// Expressed on a scale from 0 to 255.
private static final int BG_OPACITY = 160;
private static final int MAX_BG_OPACITY = 225;
private static final int BG_INTENSITY = 245;
private static final int SHADOW_OPACITY = 80;
ValueAnimator mScaleAnimator;
public void setup(DisplayMetrics dm, DeviceProfile grid, View invalidateDelegate,
int availableSpace, int topPadding) {
mInvalidateDelegate = invalidateDelegate;
final int previewSize = grid.folderIconSizePx;
final int previewPadding = grid.folderIconPreviewPadding;
this.previewSize = (previewSize - 2 * previewPadding);
basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
mStrokeWidth = Utilities.pxFromDp(1, dm);
invalidate();
}
int getRadius() {
return previewSize / 2;
}
int getScaledRadius() {
return (int) (mScale * getRadius());
}
int getOffsetX() {
return basePreviewOffsetX - (getScaledRadius() - getRadius());
}
int getOffsetY() {
return basePreviewOffsetY - (getScaledRadius() - getRadius());
}
void invalidate() {
int radius = getScaledRadius();
mClipPath.reset();
mClipPath.addCircle(radius, radius, radius, Path.Direction.CW);
if (mInvalidateDelegate != null) {
mInvalidateDelegate.invalidate();
}
if (mDrawingDelegate != null) {
mDrawingDelegate.invalidate();
}
}
void setInvalidateDelegate(View invalidateDelegate) {
mInvalidateDelegate = invalidateDelegate;
invalidate();
}
public void drawBackground(Canvas canvas, Paint paint) {
canvas.save();
canvas.translate(getOffsetX(), getOffsetY());
paint.reset();
paint.setStyle(Paint.Style.FILL);
paint.setXfermode(null);
paint.setAntiAlias(true);
int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
paint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
float radius = getScaledRadius();
canvas.drawCircle(radius, radius, radius, paint);
canvas.clipPath(mClipPath, Region.Op.DIFFERENCE);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.TRANSPARENT);
paint.setShadowLayer(mStrokeWidth, 0, mStrokeWidth, Color.argb(SHADOW_OPACITY, 0, 0, 0));
canvas.drawCircle(radius, radius, radius, paint);
canvas.restore();
}
public void drawBackgroundStroke(Canvas canvas, Paint paint) {
canvas.save();
canvas.translate(getOffsetX(), getOffsetY());
paint.reset();
paint.setAntiAlias(true);
paint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth);
float radius = getScaledRadius();
canvas.drawCircle(radius, radius, radius - 1, paint);
canvas.restore();
}
public void drawLeaveBehind(Canvas canvas, Paint paint) {
float originalScale = mScale;
mScale = 0.5f;
canvas.save();
canvas.translate(getOffsetX(), getOffsetY());
paint.reset();
paint.setAntiAlias(true);
paint.setColor(Color.argb(160, 245, 245, 245));
float radius = getScaledRadius();
canvas.drawCircle(radius, radius, radius, paint);
canvas.restore();
mScale = originalScale;
}
// It is the callers responsibility to save and restore the canvas.
private void clipCanvas(Canvas canvas) {
canvas.translate(getOffsetX(), getOffsetY());
canvas.clipPath(mClipPath);
canvas.translate(-getOffsetX(), -getOffsetY());
}
private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
if (mDrawingDelegate != delegate) {
delegate.addFolderBackground(this);
}
mDrawingDelegate = delegate;
delegateCellX = cellX;
delegateCellY = cellY;
invalidate();
}
private void clearDrawingDelegate() {
if (mDrawingDelegate != null) {
mDrawingDelegate.removeFolderBackground(this);
}
mDrawingDelegate = null;
invalidate();
}
private boolean drawingDelegated() {
return mDrawingDelegate != null;
}
private void animateScale(float finalScale, float finalMultiplier,
final Runnable onStart, final Runnable onEnd) {
final float scale0 = mScale;
final float scale1 = finalScale;
final float bgMultiplier0 = mColorMultiplier;
final float bgMultiplier1 = finalMultiplier;
if (mScaleAnimator != null) {
mScaleAnimator.cancel();
}
mScaleAnimator = LauncherAnimUtils.ofFloat(null, 0f, 1.0f);
mScaleAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float prog = animation.getAnimatedFraction();
mScale = prog * scale1 + (1 - prog) * scale0;
mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
invalidate();
}
});
mScaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (onStart != null) {
onStart.run();
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (onEnd != null) {
onEnd.run();
}
mScaleAnimator = null;
}
});
mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
mScaleAnimator.start();
}
public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
Runnable onStart = new Runnable() {
@Override
public void run() {
delegateDrawing(cl, cellX, cellY);
}
};
animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
}
public void animateToRest() {
// This can be called multiple times -- we need to make sure the drawing delegate
// is saved and restored at the beginning of the animation, since cancelling the
// existing animation can clear the delgate.
final CellLayout cl = mDrawingDelegate;
final int cellX = delegateCellX;
final int cellY = delegateCellY;
Runnable onStart = new Runnable() {
@Override
public void run() {
delegateDrawing(cl, cellX, cellY);
}
};
Runnable onEnd = new Runnable() {
@Override
public void run() {
clearDrawingDelegate();
}
};
animateScale(1f, 1f, onStart, onEnd);
}
}
public void setFolderBackground(PreviewBackground bg) {
mBackground = bg;
mBackground.setInvalidateDelegate(this);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mReferenceDrawable != null) {
computePreviewDrawingParams(mReferenceDrawable);
}
if (!mBackground.drawingDelegated()) {
mBackground.drawBackground(canvas, mBgPaint);
}
if (mFolder == null) return;
if (mFolder.getItemCount() == 0 && !mAnimating) return;
canvas.save();
if (mPreviewLayoutRule.clipToBackground()) {
mBackground.clipCanvas(canvas);
}
// The items are drawn in coordinates relative to the preview offset
canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY);
// The first item should be drawn last (ie. on top of later items)
for (int i = mDrawingParams.size() - 1; i >= 0; i--) {
PreviewItemDrawingParams p = mDrawingParams.get(i);
if (!p.hidden) {
drawPreviewItem(canvas, p);
}
}
canvas.restore();
if (mPreviewLayoutRule.clipToBackground() && !mBackground.drawingDelegated()) {
mBackground.drawBackgroundStroke(canvas, mBgPaint);
}
}
private Drawable getTopDrawable(TextView v) {
Drawable d = v.getCompoundDrawables()[1];
return (d instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) d).mIcon : d;
}
class FolderPreviewItemAnim {
ValueAnimator mValueAnimator;
float finalScale;
float finalTransX;
float finalTransY;
/**
*
* @param params layout params to animate
* @param index0 original index of the item to be animated
* @param nItems0 original number of items in the preview
* @param index1 new index of the item to be animated
* @param nItems1 new number of items in the preview
* @param duration duration in ms of the animation
* @param onCompleteRunnable runnable to execute upon animation completion
*/
public FolderPreviewItemAnim(final PreviewItemDrawingParams params, int index0, int nItems0,
int index1, int nItems1, int duration, final Runnable onCompleteRunnable) {
computePreviewItemDrawingParams(index1, nItems1, mTmpParams);
finalScale = mTmpParams.scale;
finalTransX = mTmpParams.transX;
finalTransY = mTmpParams.transY;
computePreviewItemDrawingParams(index0, nItems0, mTmpParams);
final float scale0 = mTmpParams.scale;
final float transX0 = mTmpParams.transX;
final float transY0 = mTmpParams.transY;
mValueAnimator = LauncherAnimUtils.ofFloat(FolderIcon.this, 0f, 1.0f);
mValueAnimator.addUpdateListener(new AnimatorUpdateListener(){
public void onAnimationUpdate(ValueAnimator animation) {
float progress = animation.getAnimatedFraction();
params.transX = transX0 + progress * (finalTransX - transX0);
params.transY = transY0 + progress * (finalTransY - transY0);
params.scale = scale0 + progress * (finalScale - scale0);
invalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
params.anim = null;
}
});
mValueAnimator.setDuration(duration);
}
public void start() {
mValueAnimator.start();
}
public void cancel() {
mValueAnimator.cancel();
}
public boolean hasEqualFinalState(FolderPreviewItemAnim anim) {
return finalTransY == anim.finalTransY && finalTransX == anim.finalTransX &&
finalScale == anim.finalScale;
}
}
private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
final Runnable onCompleteRunnable) {
FolderPreviewItemAnim anim;
if (!reverse) {
anim = new FolderPreviewItemAnim(mDrawingParams.get(0), -1, -1, 0, 2, duration,
onCompleteRunnable);
} else {
anim = new FolderPreviewItemAnim(mDrawingParams.get(0), 0, 2, -1, -1, duration,
onCompleteRunnable);
}
anim.start();
}
public void setTextVisible(boolean visible) {
if (visible) {
mFolderName.setVisibility(VISIBLE);
} else {
mFolderName.setVisibility(INVISIBLE);
}
}
public boolean getTextVisible() {
return mFolderName.getVisibility() == VISIBLE;
}
private void updateItemDrawingParams(boolean animate) {
ArrayList<View> items = mFolder.getItemsInReadingOrder();
int nItemsInPreview = Math.min(items.size(), mPreviewLayoutRule.numItems());
int prevNumItems = mDrawingParams.size();
// We adjust the size of the list to match the number of items in the preview
while (nItemsInPreview < mDrawingParams.size()) {
mDrawingParams.remove(mDrawingParams.size() - 1);
}
while (nItemsInPreview > mDrawingParams.size()) {
mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0));
}
for (int i = 0; i < mDrawingParams.size(); i++) {
PreviewItemDrawingParams p = mDrawingParams.get(i);
p.drawable = getTopDrawable((TextView) items.get(i));
if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
computePreviewItemDrawingParams(i, nItemsInPreview, p);
if (mReferenceDrawable == null) {
mReferenceDrawable = p.drawable;
}
} else {
FolderPreviewItemAnim anim = new FolderPreviewItemAnim(p, i, prevNumItems, i,
nItemsInPreview, 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();
}
}
}
@Override
public void onItemsChanged(boolean animate) {
updateItemDrawingParams(animate);
invalidate();
requestLayout();
}
@Override
public void onAdd(ShortcutInfo item) {
invalidate();
requestLayout();
}
@Override
public void onRemove(ShortcutInfo item) {
invalidate();
requestLayout();
}
public void onTitleChanged(CharSequence title) {
mFolderName.setText(title);
setContentDescription(getContext().getString(R.string.folder_name_format, title));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Call the superclass onTouchEvent first, because sometimes it changes the state to
// isPressed() on an ACTION_UP
boolean result = super.onTouchEvent(event);
// Check for a stylus button press, if it occurs cancel any long press checks.
if (mStylusEventHelper.onMotionEvent(event)) {
mLongPressHelper.cancelLongPress();
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLongPressHelper.postCheckForLongPress();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mLongPressHelper.cancelLongPress();
break;
case MotionEvent.ACTION_MOVE:
if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
mLongPressHelper.cancelLongPress();
}
break;
}
return result;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public void cancelLongPress() {
super.cancelLongPress();
mLongPressHelper.cancelLongPress();
}
public interface PreviewLayoutRule {
public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
PreviewItemDrawingParams params);
public void init(int availableSpace, int intrinsicIconSize, boolean rtl);
public int numItems();
public boolean clipToBackground();
}
}