2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.launcher3.folder;
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.animation.ValueAnimator.AnimatorUpdateListener;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Matrix;
28 import android.graphics.Paint;
29 import android.graphics.Path;
30 import android.graphics.Point;
31 import android.graphics.PorterDuff;
32 import android.graphics.PorterDuffXfermode;
33 import android.graphics.RadialGradient;
34 import android.graphics.Rect;
35 import android.graphics.Region;
36 import android.graphics.Shader;
37 import android.graphics.drawable.Drawable;
38 import android.os.Parcelable;
39 import android.util.AttributeSet;
40 import android.util.DisplayMetrics;
41 import android.util.Property;
42 import android.view.LayoutInflater;
43 import android.view.MotionEvent;
44 import android.view.View;
45 import android.view.ViewConfiguration;
46 import android.view.ViewGroup;
47 import android.view.animation.AccelerateInterpolator;
48 import android.view.animation.DecelerateInterpolator;
49 import android.widget.FrameLayout;
50 import android.widget.TextView;
52 import com.android.launcher3.Alarm;
53 import com.android.launcher3.AppInfo;
54 import com.android.launcher3.BubbleTextView;
55 import com.android.launcher3.CellLayout;
56 import com.android.launcher3.CheckLongPressHelper;
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.DropTarget.DragObject;
59 import com.android.launcher3.FastBitmapDrawable;
60 import com.android.launcher3.FolderInfo;
61 import com.android.launcher3.FolderInfo.FolderListener;
62 import com.android.launcher3.ItemInfo;
63 import com.android.launcher3.Launcher;
64 import com.android.launcher3.LauncherAnimUtils;
65 import com.android.launcher3.LauncherSettings;
66 import com.android.launcher3.OnAlarmListener;
67 import com.android.launcher3.R;
68 import com.android.launcher3.ShortcutInfo;
69 import com.android.launcher3.SimpleOnStylusPressListener;
70 import com.android.launcher3.StylusEventHelper;
71 import com.android.launcher3.Utilities;
72 import com.android.launcher3.Workspace;
73 import com.android.launcher3.badge.BadgeRenderer;
74 import com.android.launcher3.badge.FolderBadgeInfo;
75 import com.android.launcher3.config.FeatureFlags;
76 import com.android.launcher3.dragndrop.DragLayer;
77 import com.android.launcher3.dragndrop.DragView;
78 import com.android.launcher3.graphics.IconPalette;
79 import com.android.launcher3.util.Thunk;
81 import java.util.ArrayList;
82 import java.util.List;
85 * An icon that can appear on in the workspace representing an {@link Folder}.
87 public class FolderIcon extends FrameLayout implements FolderListener {
88 @Thunk Launcher mLauncher;
89 @Thunk Folder mFolder;
90 private FolderInfo mInfo;
91 @Thunk static boolean sStaticValuesDirty = true;
93 public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
94 StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW :
95 ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
97 private CheckLongPressHelper mLongPressHelper;
98 private StylusEventHelper mStylusEventHelper;
100 // The number of icons to display in the
101 private static final int CONSUMPTION_ANIMATION_DURATION = 100;
102 private static final int DROP_IN_ANIMATION_DURATION = 400;
103 private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
104 private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
106 // Flag whether the folder should open itself when an item is dragged over is enabled.
107 public static final boolean SPRING_LOADING_ENABLED = true;
109 // Delay when drag enters until the folder opens, in miliseconds.
110 private static final int ON_OPEN_DELAY = 800;
112 @Thunk BubbleTextView mFolderName;
114 // These variables are all associated with the drawing of the preview; they are stored
115 // as member variables for shared usage and to avoid computation on each frame
116 private int mIntrinsicIconSize = -1;
117 private int mTotalWidth = -1;
118 private int mPrevTopPadding = -1;
120 PreviewBackground mBackground = new PreviewBackground();
122 private PreviewLayoutRule mPreviewLayoutRule;
124 boolean mAnimating = false;
125 private Rect mTempBounds = new Rect();
129 private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
130 private ArrayList<PreviewItemDrawingParams> mDrawingParams = new ArrayList<PreviewItemDrawingParams>();
131 private Drawable mReferenceDrawable = null;
133 private Alarm mOpenAlarm = new Alarm();
135 private FolderBadgeInfo mBadgeInfo = new FolderBadgeInfo();
136 private BadgeRenderer mBadgeRenderer;
137 private float mBadgeScale;
138 private Point mTempSpaceForBadgeOffset = new Point();
140 private static final Property<FolderIcon, Float> BADGE_SCALE_PROPERTY
141 = new Property<FolderIcon, Float>(Float.TYPE, "badgeScale") {
143 public Float get(FolderIcon folderIcon) {
144 return folderIcon.mBadgeScale;
148 public void set(FolderIcon folderIcon, Float value) {
149 folderIcon.mBadgeScale = value;
150 folderIcon.invalidate();
154 public FolderIcon(Context context, AttributeSet attrs) {
155 super(context, attrs);
159 public FolderIcon(Context context) {
164 private void init() {
165 mLongPressHelper = new CheckLongPressHelper(this);
166 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
167 mPreviewLayoutRule = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
168 new StackFolderIconLayoutRule() :
169 new ClippedFolderIconLayoutRule();
170 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
173 public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
174 FolderInfo folderInfo) {
175 @SuppressWarnings("all") // suppress dead code warning
176 final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
178 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
179 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
180 "is dependent on this");
183 DeviceProfile grid = launcher.getDeviceProfile();
184 FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
186 icon.setClipToPadding(false);
187 icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
188 icon.mFolderName.setText(folderInfo.title);
189 icon.mFolderName.setCompoundDrawablePadding(0);
190 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
191 lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
193 icon.setTag(folderInfo);
194 icon.setOnClickListener(launcher);
195 icon.mInfo = folderInfo;
196 icon.mLauncher = launcher;
197 icon.mBadgeRenderer = launcher.getDeviceProfile().mBadgeRenderer;
198 icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
199 Folder folder = Folder.fromXml(launcher);
200 folder.setDragController(launcher.getDragController());
201 folder.setFolderIcon(icon);
202 folder.bind(folderInfo);
203 icon.setFolder(folder);
204 icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
206 folderInfo.addListener(icon);
208 icon.setOnFocusChangeListener(launcher.mFocusHandler);
213 protected Parcelable onSaveInstanceState() {
214 sStaticValuesDirty = true;
215 return super.onSaveInstanceState();
218 public Folder getFolder() {
222 private void setFolder(Folder folder) {
224 updateItemDrawingParams(false);
227 private boolean willAcceptItem(ItemInfo item) {
228 final int itemType = item.itemType;
229 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
230 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
231 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
232 !mFolder.isFull() && item != mInfo && !mFolder.isOpen());
235 public boolean acceptDrop(ItemInfo dragInfo) {
236 final ItemInfo item = dragInfo;
237 return !mFolder.isDestroyed() && willAcceptItem(item);
240 public void addItem(ShortcutInfo item) {
241 mInfo.add(item, true);
244 public void onDragEnter(ItemInfo dragInfo) {
245 if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
246 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
247 CellLayout cl = (CellLayout) getParent().getParent();
249 mBackground.animateToAccept(cl, lp.cellX, lp.cellY);
250 mOpenAlarm.setOnAlarmListener(mOnOpenListener);
251 if (SPRING_LOADING_ENABLED &&
252 ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) {
253 // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even
254 // though widget-style shortcuts can be added to folders. The issue is that we need
255 // to deal with configuration activities which are currently handled in
256 // Workspace#onDropExternal.
257 mOpenAlarm.setAlarm(ON_OPEN_DELAY);
261 OnAlarmListener mOnOpenListener = new OnAlarmListener() {
262 public void onAlarm(Alarm alarm) {
263 mFolder.beginExternalDrag();
264 mFolder.animateOpen();
268 public Drawable prepareCreate(final View destView) {
269 Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
270 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
271 destView.getMeasuredWidth());
272 return animateDrawable;
275 public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
276 final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
277 float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
279 // These correspond two the drawable and view that the icon was dropped _onto_
280 Drawable animateDrawable = prepareCreate(destView);
282 mReferenceDrawable = animateDrawable;
285 // This will animate the first item from it's position as an icon into its
286 // position as the first item in the preview
287 animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
289 // This will animate the dragView (srcView) into the new folder
290 onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable);
293 public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
294 Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1];
295 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
296 finalView.getMeasuredWidth());
298 // This will animate the first item from it's position as an icon into its
299 // position as the first item in the preview
300 animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
304 public void onDragExit() {
305 mBackground.animateToRest();
306 mOpenAlarm.cancelAlarm();
309 private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
310 float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable) {
314 // Typically, the animateView corresponds to the DragView; however, if this is being done
315 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
316 // will not have a view to animate
317 if (animateView != null) {
318 DragLayer dragLayer = mLauncher.getDragLayer();
319 Rect from = new Rect();
320 dragLayer.getViewRectRelativeToSelf(animateView, from);
324 Workspace workspace = mLauncher.getWorkspace();
325 // Set cellLayout and this to it's final state to compute final animation locations
326 workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
327 float scaleX = getScaleX();
328 float scaleY = getScaleY();
331 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
332 // Finished computing final animation locations, restore current state
335 workspace.resetTransitionTransform((CellLayout) getParent().getParent());
338 int[] center = new int[2];
339 float scale = getLocalCenterForIndex(index, index + 1, center);
340 center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
341 center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
343 to.offset(center[0] - animateView.getMeasuredWidth() / 2,
344 center[1] - animateView.getMeasuredHeight() / 2);
346 float finalAlpha = index < mPreviewLayoutRule.maxNumItems() ? 0.5f : 0f;
348 float finalScale = scale * scaleRelativeToDragLayer;
349 dragLayer.animateView(animateView, from, to, finalAlpha,
350 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
351 new DecelerateInterpolator(2), new AccelerateInterpolator(2),
352 postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
354 mFolder.hideItem(item);
356 final PreviewItemDrawingParams params = index < mDrawingParams.size() ?
357 mDrawingParams.get(index) : null;
358 if (params != null) params.hidden = true;
359 postDelayed(new Runnable() {
361 if (params != null) params.hidden = false;
362 mFolder.showItem(item);
365 }, DROP_IN_ANIMATION_DURATION);
371 public void onDrop(DragObject d) {
373 if (d.dragInfo instanceof AppInfo) {
374 // Came from all apps -- make a copy
375 item = ((AppInfo) d.dragInfo).makeShortcut();
377 item = (ShortcutInfo) d.dragInfo;
379 mFolder.notifyDrop();
380 onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable);
383 private void computePreviewDrawingParams(int drawableSize, int totalSize) {
384 if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
385 mPrevTopPadding != getPaddingTop()) {
386 DeviceProfile grid = mLauncher.getDeviceProfile();
388 mIntrinsicIconSize = drawableSize;
389 mTotalWidth = totalSize;
390 mPrevTopPadding = getPaddingTop();
392 mBackground.setup(getResources().getDisplayMetrics(), grid, this, mTotalWidth,
394 mPreviewLayoutRule.init(mBackground.previewSize, mIntrinsicIconSize,
395 Utilities.isRtl(getResources()));
397 updateItemDrawingParams(false);
401 private void computePreviewDrawingParams(Drawable d) {
402 computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
405 public void setBadgeInfo(FolderBadgeInfo badgeInfo) {
406 updateBadgeScale(mBadgeInfo.hasBadge(), badgeInfo.hasBadge());
407 mBadgeInfo = badgeInfo;
411 * Sets mBadgeScale to 1 or 0, animating if wasBadged or isBadged is false
412 * (the badge is being added or removed).
414 private void updateBadgeScale(boolean wasBadged, boolean isBadged) {
415 float newBadgeScale = isBadged ? 1f : 0f;
416 // Animate when a badge is first added or when it is removed.
417 if ((wasBadged ^ isBadged) && isShown()) {
418 ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start();
420 mBadgeScale = newBadgeScale;
425 static class PreviewItemDrawingParams {
426 PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
427 this.transX = transX;
428 this.transY = transY;
430 this.overlayAlpha = overlayAlpha;
433 public void update(float transX, float transY, float scale) {
434 // We ensure the update will not interfere with an animation on the layout params
435 // If the final values differ, we cancel the animation.
437 if (anim.finalTransX == transX || anim.finalTransY == transY
438 || anim.finalScale == scale) {
444 this.transX = transX;
445 this.transY = transY;
452 public float overlayAlpha;
454 FolderPreviewItemAnim anim;
458 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
459 mTmpParams = computePreviewItemDrawingParams(
460 Math.min(mPreviewLayoutRule.maxNumItems(), index), curNumItems, mTmpParams);
462 mTmpParams.transX += mBackground.basePreviewOffsetX;
463 mTmpParams.transY += mBackground.basePreviewOffsetY;
464 float offsetX = mTmpParams.transX + (mTmpParams.scale * mIntrinsicIconSize) / 2;
465 float offsetY = mTmpParams.transY + (mTmpParams.scale * mIntrinsicIconSize) / 2;
467 center[0] = (int) Math.round(offsetX);
468 center[1] = (int) Math.round(offsetY);
469 return mTmpParams.scale;
472 private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
473 PreviewItemDrawingParams params) {
474 // We use an index of -1 to represent an icon on the workspace for the destroy and
477 return getFinalIconParams(params);
479 return mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
482 private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
483 float iconSize = mLauncher.getDeviceProfile().iconSizePx;
485 final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
486 final float trans = (mBackground.previewSize - iconSize) / 2;
488 params.update(trans, trans, scale);
492 private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
493 canvas.save(Canvas.MATRIX_SAVE_FLAG);
494 canvas.translate(params.transX, params.transY);
495 canvas.scale(params.scale, params.scale);
496 Drawable d = params.drawable;
499 mTempBounds.set(d.getBounds());
500 d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
501 if (d instanceof FastBitmapDrawable) {
502 FastBitmapDrawable fd = (FastBitmapDrawable) d;
503 fd.drawWithBrightness(canvas, params.overlayAlpha);
505 d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
506 PorterDuff.Mode.SRC_ATOP);
508 d.clearColorFilter();
510 d.setBounds(mTempBounds);
516 * This object represents a FolderIcon preview background. It stores drawing / measurement
517 * information, handles drawing, and animation (accept state <--> rest state).
519 public static class PreviewBackground {
521 private final PorterDuffXfermode mClipPorterDuffXfermode
522 = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
523 // Create a RadialGradient such that it draws a black circle and then extends with
524 // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
525 // just at the edge quickly change it to transparent.
526 private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
527 new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
528 new float[] {0, 0.999f, 1},
529 Shader.TileMode.CLAMP);
531 private final PorterDuffXfermode mShadowPorterDuffXfermode
532 = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
533 private RadialGradient mShadowShader = null;
535 private final Matrix mShaderMatrix = new Matrix();
536 private final Path mPath = new Path();
538 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
540 private float mScale = 1f;
541 private float mColorMultiplier = 1f;
542 private float mStrokeWidth;
543 private View mInvalidateDelegate;
545 public int previewSize;
546 private int basePreviewOffsetX;
547 private int basePreviewOffsetY;
549 private CellLayout mDrawingDelegate;
550 public int delegateCellX;
551 public int delegateCellY;
553 // When the PreviewBackground is drawn under an icon (for creating a folder) the border
554 // should not occlude the icon
555 public boolean isClipping = true;
557 // Drawing / animation configurations
558 private static final float ACCEPT_SCALE_FACTOR = 1.25f;
559 private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
561 // Expressed on a scale from 0 to 255.
562 private static final int BG_OPACITY = 160;
563 private static final int MAX_BG_OPACITY = 225;
564 private static final int BG_INTENSITY = 245;
565 private static final int SHADOW_OPACITY = 40;
567 ValueAnimator mScaleAnimator;
569 public void setup(DisplayMetrics dm, DeviceProfile grid, View invalidateDelegate,
570 int availableSpace, int topPadding) {
571 mInvalidateDelegate = invalidateDelegate;
573 final int previewSize = grid.folderIconSizePx;
574 final int previewPadding = grid.folderIconPreviewPadding;
576 this.previewSize = (previewSize - 2 * previewPadding);
578 basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
579 basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
581 // Stroke width is 1dp
582 mStrokeWidth = dm.density;
584 float radius = getScaledRadius();
585 float shadowRadius = radius + mStrokeWidth;
586 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
587 mShadowShader = new RadialGradient(0, 0, 1,
588 new int[] {shadowColor, Color.TRANSPARENT},
589 new float[] {radius / shadowRadius, 1},
590 Shader.TileMode.CLAMP);
596 return previewSize / 2;
599 int getScaledRadius() {
600 return (int) (mScale * getRadius());
604 return basePreviewOffsetX - (getScaledRadius() - getRadius());
608 return basePreviewOffsetY - (getScaledRadius() - getRadius());
612 * Returns the progress of the scale animation, where 0 means the scale is at 1f
613 * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
615 float getScaleProgress() {
616 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
620 if (mInvalidateDelegate != null) {
621 mInvalidateDelegate.invalidate();
624 if (mDrawingDelegate != null) {
625 mDrawingDelegate.invalidate();
629 void setInvalidateDelegate(View invalidateDelegate) {
630 mInvalidateDelegate = invalidateDelegate;
634 public void drawBackground(Canvas canvas) {
635 mPaint.setStyle(Paint.Style.FILL);
636 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
637 mPaint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
639 drawCircle(canvas, 0 /* deltaRadius */);
642 if (mShadowShader == null) {
645 float radius = getScaledRadius();
646 float shadowRadius = radius + mStrokeWidth;
647 mPaint.setColor(Color.BLACK);
648 int offsetX = getOffsetX();
649 int offsetY = getOffsetY();
651 if (canvas.isHardwareAccelerated()) {
652 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
653 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
654 null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
657 saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
658 clipCanvasSoftware(canvas, Region.Op.DIFFERENCE);
661 mShaderMatrix.setScale(shadowRadius, shadowRadius);
662 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
663 mShadowShader.setLocalMatrix(mShaderMatrix);
664 mPaint.setShader(mShadowShader);
665 canvas.drawPaint(mPaint);
666 mPaint.setShader(null);
668 if (canvas.isHardwareAccelerated()) {
669 mPaint.setXfermode(mShadowPorterDuffXfermode);
670 canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
671 mPaint.setXfermode(null);
674 canvas.restoreToCount(saveCount);
677 public void drawBackgroundStroke(Canvas canvas) {
678 mPaint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
679 mPaint.setStyle(Paint.Style.STROKE);
680 mPaint.setStrokeWidth(mStrokeWidth);
681 drawCircle(canvas, 1 /* deltaRadius */);
684 public void drawLeaveBehind(Canvas canvas) {
685 float originalScale = mScale;
688 mPaint.setStyle(Paint.Style.FILL);
689 mPaint.setColor(Color.argb(160, 245, 245, 245));
690 drawCircle(canvas, 0 /* deltaRadius */);
692 mScale = originalScale;
695 private void drawCircle(Canvas canvas,float deltaRadius) {
696 float radius = getScaledRadius();
697 canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
698 radius - deltaRadius, mPaint);
701 // It is the callers responsibility to save and restore the canvas layers.
702 private void clipCanvasSoftware(Canvas canvas, Region.Op op) {
704 float r = getScaledRadius();
705 mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
706 canvas.clipPath(mPath, op);
709 // It is the callers responsibility to save and restore the canvas layers.
710 private void clipCanvasHardware(Canvas canvas) {
711 mPaint.setColor(Color.BLACK);
712 mPaint.setXfermode(mClipPorterDuffXfermode);
714 float radius = getScaledRadius();
715 mShaderMatrix.setScale(radius, radius);
716 mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
717 mClipShader.setLocalMatrix(mShaderMatrix);
718 mPaint.setShader(mClipShader);
719 canvas.drawPaint(mPaint);
720 mPaint.setXfermode(null);
721 mPaint.setShader(null);
724 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
725 if (mDrawingDelegate != delegate) {
726 delegate.addFolderBackground(this);
729 mDrawingDelegate = delegate;
730 delegateCellX = cellX;
731 delegateCellY = cellY;
736 private void clearDrawingDelegate() {
737 if (mDrawingDelegate != null) {
738 mDrawingDelegate.removeFolderBackground(this);
741 mDrawingDelegate = null;
745 private boolean drawingDelegated() {
746 return mDrawingDelegate != null;
749 private void animateScale(float finalScale, float finalMultiplier,
750 final Runnable onStart, final Runnable onEnd) {
751 final float scale0 = mScale;
752 final float scale1 = finalScale;
754 final float bgMultiplier0 = mColorMultiplier;
755 final float bgMultiplier1 = finalMultiplier;
757 if (mScaleAnimator != null) {
758 mScaleAnimator.cancel();
761 mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
763 mScaleAnimator.addUpdateListener(new AnimatorUpdateListener() {
765 public void onAnimationUpdate(ValueAnimator animation) {
766 float prog = animation.getAnimatedFraction();
767 mScale = prog * scale1 + (1 - prog) * scale0;
768 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
772 mScaleAnimator.addListener(new AnimatorListenerAdapter() {
774 public void onAnimationStart(Animator animation) {
775 if (onStart != null) {
781 public void onAnimationEnd(Animator animation) {
785 mScaleAnimator = null;
789 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
790 mScaleAnimator.start();
793 public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
794 Runnable onStart = new Runnable() {
797 delegateDrawing(cl, cellX, cellY);
800 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
803 public void animateToRest() {
804 // This can be called multiple times -- we need to make sure the drawing delegate
805 // is saved and restored at the beginning of the animation, since cancelling the
806 // existing animation can clear the delgate.
807 final CellLayout cl = mDrawingDelegate;
808 final int cellX = delegateCellX;
809 final int cellY = delegateCellY;
811 Runnable onStart = new Runnable() {
814 delegateDrawing(cl, cellX, cellY);
817 Runnable onEnd = new Runnable() {
820 clearDrawingDelegate();
823 animateScale(1f, 1f, onStart, onEnd);
827 public void setFolderBackground(PreviewBackground bg) {
829 mBackground.setInvalidateDelegate(this);
833 protected void dispatchDraw(Canvas canvas) {
834 super.dispatchDraw(canvas);
836 if (mReferenceDrawable != null) {
837 computePreviewDrawingParams(mReferenceDrawable);
840 if (!mBackground.drawingDelegated()) {
841 mBackground.drawBackground(canvas);
844 if (mFolder == null) return;
845 if (mFolder.getItemCount() == 0 && !mAnimating) return;
849 if (canvas.isHardwareAccelerated()) {
850 saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
851 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
853 saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
854 if (mPreviewLayoutRule.clipToBackground()) {
855 mBackground.clipCanvasSoftware(canvas, Region.Op.INTERSECT);
859 // The items are drawn in coordinates relative to the preview offset
860 canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY);
862 // The first item should be drawn last (ie. on top of later items)
863 for (int i = mDrawingParams.size() - 1; i >= 0; i--) {
864 PreviewItemDrawingParams p = mDrawingParams.get(i);
866 drawPreviewItem(canvas, p);
869 canvas.translate(-mBackground.basePreviewOffsetX, -mBackground.basePreviewOffsetY);
871 if (mPreviewLayoutRule.clipToBackground() && canvas.isHardwareAccelerated()) {
872 mBackground.clipCanvasHardware(canvas);
874 canvas.restoreToCount(saveCount);
876 if (mPreviewLayoutRule.clipToBackground() && !mBackground.drawingDelegated()) {
877 mBackground.drawBackgroundStroke(canvas);
880 if ((mBadgeInfo != null && mBadgeInfo.hasBadge()) || mBadgeScale > 0) {
881 int offsetX = mBackground.getOffsetX();
882 int offsetY = mBackground.getOffsetY();
883 int previewSize = (int) (mBackground.previewSize * mBackground.mScale);
884 mTempBounds.set(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize);
886 // If we are animating to the accepting state, animate the badge out.
887 float badgeScale = Math.max(0, mBadgeScale - mBackground.getScaleProgress());
888 mTempSpaceForBadgeOffset.set(getWidth() - mTempBounds.right, mTempBounds.top);
889 mBadgeRenderer.draw(canvas, IconPalette.FOLDER_ICON_PALETTE, mBadgeInfo, mTempBounds,
890 badgeScale, mTempSpaceForBadgeOffset);
894 class FolderPreviewItemAnim {
895 ValueAnimator mValueAnimator;
902 * @param params layout params to animate
903 * @param index0 original index of the item to be animated
904 * @param nItems0 original number of items in the preview
905 * @param index1 new index of the item to be animated
906 * @param nItems1 new number of items in the preview
907 * @param duration duration in ms of the animation
908 * @param onCompleteRunnable runnable to execute upon animation completion
910 public FolderPreviewItemAnim(final PreviewItemDrawingParams params, int index0, int nItems0,
911 int index1, int nItems1, int duration, final Runnable onCompleteRunnable) {
913 computePreviewItemDrawingParams(index1, nItems1, mTmpParams);
915 finalScale = mTmpParams.scale;
916 finalTransX = mTmpParams.transX;
917 finalTransY = mTmpParams.transY;
919 computePreviewItemDrawingParams(index0, nItems0, mTmpParams);
921 final float scale0 = mTmpParams.scale;
922 final float transX0 = mTmpParams.transX;
923 final float transY0 = mTmpParams.transY;
925 mValueAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
926 mValueAnimator.addUpdateListener(new AnimatorUpdateListener(){
927 public void onAnimationUpdate(ValueAnimator animation) {
928 float progress = animation.getAnimatedFraction();
930 params.transX = transX0 + progress * (finalTransX - transX0);
931 params.transY = transY0 + progress * (finalTransY - transY0);
932 params.scale = scale0 + progress * (finalScale - scale0);
937 mValueAnimator.addListener(new AnimatorListenerAdapter() {
939 public void onAnimationStart(Animator animation) {
943 public void onAnimationEnd(Animator animation) {
944 if (onCompleteRunnable != null) {
945 onCompleteRunnable.run();
950 mValueAnimator.setDuration(duration);
953 public void start() {
954 mValueAnimator.start();
957 public void cancel() {
958 mValueAnimator.cancel();
961 public boolean hasEqualFinalState(FolderPreviewItemAnim anim) {
962 return finalTransY == anim.finalTransY && finalTransX == anim.finalTransX &&
963 finalScale == anim.finalScale;
968 private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
969 final Runnable onCompleteRunnable) {
971 FolderPreviewItemAnim anim;
973 anim = new FolderPreviewItemAnim(mDrawingParams.get(0), -1, -1, 0, 2, duration,
976 anim = new FolderPreviewItemAnim(mDrawingParams.get(0), 0, 2, -1, -1, duration,
982 public void setTextVisible(boolean visible) {
984 mFolderName.setVisibility(VISIBLE);
986 mFolderName.setVisibility(INVISIBLE);
990 public boolean getTextVisible() {
991 return mFolderName.getVisibility() == VISIBLE;
994 private void updateItemDrawingParams(boolean animate) {
995 List<View> items = mPreviewLayoutRule.getItemsToDisplay(mFolder);
996 int nItemsInPreview = items.size();
998 int prevNumItems = mDrawingParams.size();
1000 // We adjust the size of the list to match the number of items in the preview
1001 while (nItemsInPreview < mDrawingParams.size()) {
1002 mDrawingParams.remove(mDrawingParams.size() - 1);
1004 while (nItemsInPreview > mDrawingParams.size()) {
1005 mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0));
1008 for (int i = 0; i < mDrawingParams.size(); i++) {
1009 PreviewItemDrawingParams p = mDrawingParams.get(i);
1010 p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1];
1012 if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
1013 computePreviewItemDrawingParams(i, nItemsInPreview, p);
1014 if (mReferenceDrawable == null) {
1015 mReferenceDrawable = p.drawable;
1018 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(p, i, prevNumItems, i,
1019 nItemsInPreview, DROP_IN_ANIMATION_DURATION, null);
1021 if (p.anim != null) {
1022 if (p.anim.hasEqualFinalState(anim)) {
1023 // do nothing, let the current animation finish
1035 public void onItemsChanged(boolean animate) {
1036 updateItemDrawingParams(animate);
1042 public void prepareAutoUpdate() {
1046 public void onAdd(ShortcutInfo item) {
1047 boolean wasBadged = mBadgeInfo.hasBadge();
1048 mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1049 boolean isBadged = mBadgeInfo.hasBadge();
1050 updateBadgeScale(wasBadged, isBadged);
1056 public void onRemove(ShortcutInfo item) {
1057 boolean wasBadged = mBadgeInfo.hasBadge();
1058 mBadgeInfo.subtractBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1059 boolean isBadged = mBadgeInfo.hasBadge();
1060 updateBadgeScale(wasBadged, isBadged);
1066 public void onTitleChanged(CharSequence title) {
1067 mFolderName.setText(title);
1068 setContentDescription(getContext().getString(R.string.folder_name_format, title));
1072 public boolean onTouchEvent(MotionEvent event) {
1073 // Call the superclass onTouchEvent first, because sometimes it changes the state to
1074 // isPressed() on an ACTION_UP
1075 boolean result = super.onTouchEvent(event);
1077 // Check for a stylus button press, if it occurs cancel any long press checks.
1078 if (mStylusEventHelper.onMotionEvent(event)) {
1079 mLongPressHelper.cancelLongPress();
1083 switch (event.getAction()) {
1084 case MotionEvent.ACTION_DOWN:
1085 mLongPressHelper.postCheckForLongPress();
1087 case MotionEvent.ACTION_CANCEL:
1088 case MotionEvent.ACTION_UP:
1089 mLongPressHelper.cancelLongPress();
1091 case MotionEvent.ACTION_MOVE:
1092 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
1093 mLongPressHelper.cancelLongPress();
1101 public void cancelLongPress() {
1102 super.cancelLongPress();
1103 mLongPressHelper.cancelLongPress();
1106 public void removeListeners() {
1107 mInfo.removeListener(this);
1108 mInfo.removeListener(mFolder);
1111 public void shrinkAndFadeIn(boolean animate) {
1112 final CellLayout cl = (CellLayout) getParent().getParent();
1113 ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
1115 // We remove and re-draw the FolderIcon in-case it has changed
1116 final PreviewImageView previewImage = PreviewImageView.get(getContext());
1117 previewImage.removeFromParent();
1118 copyToPreview(previewImage);
1121 cl.clearFolderLeaveBehind();
1124 ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 1, 1, 1);
1125 oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1126 oa.addListener(new AnimatorListenerAdapter() {
1128 public void onAnimationEnd(Animator animation) {
1130 // Remove the ImageView copy of the FolderIcon and make the original visible.
1131 previewImage.removeFromParent();
1132 setVisibility(View.VISIBLE);
1142 public void growAndFadeOut() {
1143 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
1144 // While the folder is open, the position of the icon cannot change.
1145 lp.canReorder = false;
1146 if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1147 CellLayout cl = (CellLayout) getParent().getParent();
1148 cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
1151 // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original
1152 PreviewImageView previewImage = PreviewImageView.get(getContext());
1153 copyToPreview(previewImage);
1154 setVisibility(View.INVISIBLE);
1156 ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 0, 1.5f, 1.5f);
1157 oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1162 * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView
1163 * in the DragLayer in the exact absolute location of the original FolderIcon.
1165 private void copyToPreview(PreviewImageView previewImageView) {
1166 previewImageView.copy(this);
1167 if (mFolder != null) {
1168 previewImageView.setPivotX(mFolder.getPivotXForIconAnimation());
1169 previewImageView.setPivotY(mFolder.getPivotYForIconAnimation());
1170 mFolder.bringToFront();
1174 public interface PreviewLayoutRule {
1175 PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
1176 PreviewItemDrawingParams params);
1177 void init(int availableSpace, int intrinsicIconSize, boolean rtl);
1178 float scaleForItem(int index, int totalNumItems);
1180 boolean clipToBackground();
1181 List<View> getItemsToDisplay(Folder folder);