/*
* 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.launcher2;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.launcher.R;
import com.android.launcher2.DropTarget.DragObject;
import com.android.launcher2.FolderInfo.FolderListener;
import java.util.ArrayList;
/**
* An icon that can appear on in the workspace representing an {@link UserFolder}.
*/
public class FolderIcon extends LinearLayout implements FolderListener {
private Launcher mLauncher;
private Folder mFolder;
private FolderInfo mInfo;
private static boolean sStaticValuesDirty = true;
private CheckLongPressHelper mLongPressHelper;
// The number of icons to display in the
private static final int NUM_ITEMS_IN_PREVIEW = 3;
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;
// The degree to which the inner ring grows when accepting drop
private static final float INNER_RING_GROWTH_FACTOR = 0.15f;
// The degree to which the outer ring is scaled in its natural state
private static final float OUTER_RING_GROWTH_FACTOR = 0.3f;
// The amount of vertical spread between items in the stack [0...1]
private static final float PERSPECTIVE_SHIFT_FACTOR = 0.24f;
// The degree to which the item in the back of the stack is scaled [0...1]
// (0 means it's not scaled at all, 1 means it's scaled to nothing)
private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f;
public static Drawable sSharedFolderLeaveBehind = null;
private ImageView mPreviewBackground;
private BubbleTextView mFolderName;
FolderRingAnimator mFolderRingAnimator = null;
// 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;
private float mBaselineIconScale;
private int mBaselineIconSize;
private int mAvailableSpaceInPreview;
private int mTotalWidth = -1;
private int mPreviewOffsetX;
private int mPreviewOffsetY;
private float mMaxPerspectiveShift;
boolean mAnimating = false;
private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0);
private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0);
private ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>();
public FolderIcon(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FolderIcon(Context context) {
super(context);
init();
}
private void init() {
mLongPressHelper = new CheckLongPressHelper(this);
}
public boolean isDropEnabled() {
final ViewGroup cellLayoutChildren = (ViewGroup) getParent();
final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent();
final Workspace workspace = (Workspace) cellLayout.getParent();
return !workspace.isSmall();
}
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");
}
FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
icon.mFolderName.setText(folderInfo.title);
icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background);
icon.setTag(folderInfo);
icon.setOnClickListener(launcher);
icon.mInfo = folderInfo;
icon.mLauncher = launcher;
icon.setContentDescription(String.format(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.mFolder = folder;
icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon);
folderInfo.addListener(icon);
return icon;
}
@Override
protected Parcelable onSaveInstanceState() {
sStaticValuesDirty = true;
return super.onSaveInstanceState();
}
public static class FolderRingAnimator {
public int mCellX;
public int mCellY;
private CellLayout mCellLayout;
public float mOuterRingSize;
public float mInnerRingSize;
public FolderIcon mFolderIcon = null;
public Drawable mOuterRingDrawable = null;
public Drawable mInnerRingDrawable = null;
public static Drawable sSharedOuterRingDrawable = null;
public static Drawable sSharedInnerRingDrawable = null;
public static int sPreviewSize = -1;
public static int sPreviewPadding = -1;
private ValueAnimator mAcceptAnimator;
private ValueAnimator mNeutralAnimator;
public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) {
mFolderIcon = folderIcon;
Resources res = launcher.getResources();
mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo);
mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo);
// We need to reload the static values when configuration changes in case they are
// different in another configuration
if (sStaticValuesDirty) {
sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size);
sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding);
sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo);
sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo);
sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest);
sStaticValuesDirty = false;
}
}
public void animateToAcceptState() {
if (mNeutralAnimator != null) {
mNeutralAnimator.cancel();
}
mAcceptAnimator = LauncherAnimUtils.ofFloat(0f, 1f);
mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
final int previewSize = sPreviewSize;
mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
final float percent = (Float) animation.getAnimatedValue();
mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize;
mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize;
if (mCellLayout != null) {
mCellLayout.invalidate();
}
}
});
mAcceptAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (mFolderIcon != null) {
mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE);
}
}
});
mAcceptAnimator.start();
}
public void animateToNaturalState() {
if (mAcceptAnimator != null) {
mAcceptAnimator.cancel();
}
mNeutralAnimator = LauncherAnimUtils.ofFloat(0f, 1f);
mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
final int previewSize = sPreviewSize;
mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
final float percent = (Float) animation.getAnimatedValue();
mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize;
mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize;
if (mCellLayout != null) {
mCellLayout.invalidate();
}
}
});
mNeutralAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mCellLayout != null) {
mCellLayout.hideFolderAccept(FolderRingAnimator.this);
}
if (mFolderIcon != null) {
mFolderIcon.mPreviewBackground.setVisibility(VISIBLE);
}
}
});
mNeutralAnimator.start();
}
// Location is expressed in window coordinates
public void getCell(int[] loc) {
loc[0] = mCellX;
loc[1] = mCellY;
}
// Location is expressed in window coordinates
public void setCell(int x, int y) {
mCellX = x;
mCellY = y;
}
public void setCellLayout(CellLayout layout) {
mCellLayout = layout;
}
public float getOuterRingSize() {
return mOuterRingSize;
}
public float getInnerRingSize() {
return mInnerRingSize;
}
}
Folder getFolder() {
return mFolder;
}
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(Object dragInfo) {
final ItemInfo item = (ItemInfo) dragInfo;
return !mFolder.isDestroyed() && willAcceptItem(item);
}
public void addItem(ShortcutInfo item) {
mInfo.add(item);
}
public void onDragEnter(Object dragInfo) {
if (mFolder.isDestroyed() || !willAcceptItem((ItemInfo) dragInfo)) return;
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
CellLayout layout = (CellLayout) getParent().getParent();
mFolderRingAnimator.setCell(lp.cellX, lp.cellY);
mFolderRingAnimator.setCellLayout(layout);
mFolderRingAnimator.animateToAcceptState();
layout.showFolderAccept(mFolderRingAnimator);
}
public void onDragOver(Object dragInfo) {
}
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 = ((TextView) destView).getCompoundDrawables()[1];
computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
destView.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, INITIAL_ITEM_ANIMATION_DURATION, false, null);
addItem(destInfo);
// 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 = ((TextView) finalView).getCompoundDrawables()[1];
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() {
mFolderRingAnimator.animateToNaturalState();
}
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, 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 < NUM_ITEMS_IN_PREVIEW ? 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);
mHiddenItems.add(item);
postDelayed(new Runnable() {
public void run() {
mHiddenItems.remove(item);
invalidate();
}
}, DROP_IN_ANIMATION_DURATION);
} else {
addItem(item);
}
}
public void onDrop(DragObject d) {
ShortcutInfo item;
if (d.dragInfo instanceof ApplicationInfo) {
// Came from all apps -- make a copy
item = ((ApplicationInfo) d.dragInfo).makeShortcut();
} else {
item = (ShortcutInfo) d.dragInfo;
}
mFolder.notifyDrop();
onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d);
}
public DropTarget getDropTargetDelegate(DragObject d) {
return null;
}
private void computePreviewDrawingParams(int drawableSize, int totalSize) {
if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) {
mIntrinsicIconSize = drawableSize;
mTotalWidth = totalSize;
final int previewSize = FolderRingAnimator.sPreviewSize;
final int previewPadding = FolderRingAnimator.sPreviewPadding;
mAvailableSpaceInPreview = (previewSize - 2 * previewPadding);
// cos(45) = 0.707 + ~= 0.1) = 0.8f
int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f));
int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR));
mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight);
mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale);
mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR;
mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2;
mPreviewOffsetY = previewPadding;
}
}
private void computePreviewDrawingParams(Drawable d) {
computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
}
class PreviewItemDrawingParams {
PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
this.overlayAlpha = overlayAlpha;
}
float transX;
float transY;
float scale;
int overlayAlpha;
Drawable drawable;
}
private float getLocalCenterForIndex(int index, int[] center) {
mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams);
mParams.transX += mPreviewOffsetX;
mParams.transY += mPreviewOffsetY;
float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2;
float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2;
center[0] = (int) Math.round(offsetX);
center[1] = (int) Math.round(offsetY);
return mParams.scale;
}
private PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
PreviewItemDrawingParams params) {
index = NUM_ITEMS_IN_PREVIEW - index - 1;
float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1);
float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r));
float offset = (1 - r) * mMaxPerspectiveShift;
float scaledSize = scale * mBaselineIconSize;
float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize;
// We want to imagine our coordinates from the bottom left, growing up and to the
// right. This is natural for the x-axis, but for the y-axis, we have to invert things.
float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection);
float transX = offset + scaleOffsetCorrection;
float totalScale = mBaselineIconScale * scale;
final int overlayAlpha = (int) (80 * (1 - r));
if (params == null) {
params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
} else {
params.transX = transX;
params.transY = transY;
params.scale = totalScale;
params.overlayAlpha = overlayAlpha;
}
return params;
}
private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
canvas.save();
canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY);
canvas.scale(params.scale, params.scale);
Drawable d = params.drawable;
if (d != null) {
d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
d.setFilterBitmap(true);
d.setColorFilter(Color.argb(params.overlayAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP);
d.draw(canvas);
d.clearColorFilter();
d.setFilterBitmap(false);
}
canvas.restore();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mFolder == null) return;
if (mFolder.getItemCount() == 0 && !mAnimating) return;
ArrayList<View> items = mFolder.getItemsInReadingOrder(false);
Drawable d;
TextView v;
// Update our drawing parameters if necessary
if (mAnimating) {
computePreviewDrawingParams(mAnimParams.drawable);
} else {
v = (TextView) items.get(0);
d = v.getCompoundDrawables()[1];
computePreviewDrawingParams(d);
}
int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW);
if (!mAnimating) {
for (int i = nItemsInPreview - 1; i >= 0; i--) {
v = (TextView) items.get(i);
if (!mHiddenItems.contains(v.getTag())) {
d = v.getCompoundDrawables()[1];
mParams = computePreviewItemDrawingParams(i, mParams);
mParams.drawable = d;
drawPreviewItem(canvas, mParams);
}
}
} else {
drawPreviewItem(canvas, mAnimParams);
}
}
private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
final Runnable onCompleteRunnable) {
final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null);
final float scale0 = 1.0f;
final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2;
final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2;
mAnimParams.drawable = d;
ValueAnimator va = LauncherAnimUtils.ofFloat(0f, 1.0f);
va.addUpdateListener(new AnimatorUpdateListener(){
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (Float) animation.getAnimatedValue();
if (reverse) {
progress = 1 - progress;
mPreviewBackground.setAlpha(progress);
}
mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0);
mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0);
mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0);
invalidate();
}
});
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mAnimating = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimating = false;
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
}
});
va.setDuration(duration);
va.start();
}
public void setTextVisible(boolean visible) {
if (visible) {
mFolderName.setVisibility(VISIBLE);
} else {
mFolderName.setVisibility(INVISIBLE);
}
}
public boolean getTextVisible() {
return mFolderName.getVisibility() == VISIBLE;
}
public void onItemsChanged() {
invalidate();
requestLayout();
}
public void onAdd(ShortcutInfo item) {
invalidate();
requestLayout();
}
public void onRemove(ShortcutInfo item) {
invalidate();
requestLayout();
}
public void onTitleChanged(CharSequence title) {
mFolderName.setText(title.toString());
setContentDescription(String.format(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);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLongPressHelper.postCheckForLongPress();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mLongPressHelper.cancelLongPress();
break;
}
return result;
}
@Override
public void cancelLongPress() {
super.cancelLongPress();
mLongPressHelper.cancelLongPress();
}
}