• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.launcher3.shortcuts;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.TimeInterpolator;
23 import android.annotation.TargetApi;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.Bitmap;
28 import android.graphics.Color;
29 import android.graphics.Point;
30 import android.graphics.Rect;
31 import android.graphics.drawable.ShapeDrawable;
32 import android.os.Build;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.util.AttributeSet;
36 import android.view.Gravity;
37 import android.view.LayoutInflater;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.accessibility.AccessibilityEvent;
41 import android.view.animation.DecelerateInterpolator;
42 import android.widget.LinearLayout;
43 
44 import com.android.launcher3.BubbleTextView;
45 import com.android.launcher3.DragSource;
46 import com.android.launcher3.DropTarget;
47 import com.android.launcher3.IconCache;
48 import com.android.launcher3.ItemInfo;
49 import com.android.launcher3.Launcher;
50 import com.android.launcher3.LauncherAnimUtils;
51 import com.android.launcher3.LauncherAppState;
52 import com.android.launcher3.LauncherModel;
53 import com.android.launcher3.LauncherSettings;
54 import com.android.launcher3.LauncherViewPropertyAnimator;
55 import com.android.launcher3.LogAccelerateInterpolator;
56 import com.android.launcher3.R;
57 import com.android.launcher3.ShortcutInfo;
58 import com.android.launcher3.Utilities;
59 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
60 import com.android.launcher3.compat.UserHandleCompat;
61 import com.android.launcher3.dragndrop.DragController;
62 import com.android.launcher3.dragndrop.DragLayer;
63 import com.android.launcher3.dragndrop.DragOptions;
64 import com.android.launcher3.dragndrop.DragView;
65 import com.android.launcher3.graphics.TriangleShape;
66 import com.android.launcher3.userevent.nano.LauncherLogProto;
67 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
68 
69 import java.util.Collections;
70 import java.util.List;
71 
72 /**
73  * A container for shortcuts to deep links within apps.
74  */
75 @TargetApi(Build.VERSION_CODES.N)
76 public class DeepShortcutsContainer extends LinearLayout implements View.OnLongClickListener,
77         View.OnTouchListener, DragSource, DragController.DragListener {
78     private static final String TAG = "ShortcutsContainer";
79 
80     private final Point mIconShift = new Point();
81 
82     private final Launcher mLauncher;
83     private final DeepShortcutManager mDeepShortcutsManager;
84     private final int mStartDragThreshold;
85     private final ShortcutMenuAccessibilityDelegate mAccessibilityDelegate;
86     private final boolean mIsRtl;
87 
88     private BubbleTextView mDeferredDragIcon;
89     private final Rect mTempRect = new Rect();
90     private Point mIconLastTouchPos = new Point();
91     private boolean mIsLeftAligned;
92     private boolean mIsAboveIcon;
93     private View mArrow;
94 
95     private Animator mOpenCloseAnimator;
96     private boolean mDeferContainerRemoval;
97     private boolean mIsOpen;
98 
DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr)99     public DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr) {
100         super(context, attrs, defStyleAttr);
101         mLauncher = Launcher.getLauncher(context);
102         mDeepShortcutsManager = LauncherAppState.getInstance().getShortcutManager();
103 
104         mStartDragThreshold = getResources().getDimensionPixelSize(
105                 R.dimen.deep_shortcuts_start_drag_threshold);
106         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
107         mIsRtl = Utilities.isRtl(getResources());
108     }
109 
DeepShortcutsContainer(Context context, AttributeSet attrs)110     public DeepShortcutsContainer(Context context, AttributeSet attrs) {
111         this(context, attrs, 0);
112     }
113 
DeepShortcutsContainer(Context context)114     public DeepShortcutsContainer(Context context) {
115         this(context, null, 0);
116     }
117 
populateAndShow(final BubbleTextView originalIcon, final List<String> ids)118     public void populateAndShow(final BubbleTextView originalIcon, final List<String> ids) {
119         final Resources resources = getResources();
120         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
121         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
122         final int arrowHorizontalOffset = resources.getDimensionPixelSize(
123                 R.dimen.deep_shortcuts_arrow_horizontal_offset);
124         final int arrowVerticalOffset = resources.getDimensionPixelSize(
125                 R.dimen.deep_shortcuts_arrow_vertical_offset);
126 
127         // Add dummy views first, and populate with real shortcut info when ready.
128         final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
129         final LayoutInflater inflater = mLauncher.getLayoutInflater();
130         int numShortcuts = Math.min(ids.size(), ShortcutFilter.MAX_SHORTCUTS);
131         for (int i = 0; i < numShortcuts; i++) {
132             final DeepShortcutView shortcut =
133                     (DeepShortcutView) inflater.inflate(R.layout.deep_shortcut, this, false);
134             if (i < numShortcuts - 1) {
135                 ((LayoutParams) shortcut.getLayoutParams()).bottomMargin = spacing;
136             }
137             shortcut.getBubbleText().setAccessibilityDelegate(mAccessibilityDelegate);
138             addView(shortcut);
139         }
140         setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
141                 numShortcuts, originalIcon.getContentDescription().toString()));
142 
143         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
144         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
145 
146         // Add the arrow.
147         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
148         mArrow.setPivotX(arrowWidth / 2);
149         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
150 
151         animateOpen();
152 
153         deferDrag(originalIcon);
154 
155         // Load the shortcuts on a background thread and update the container as it animates.
156         final Looper workerLooper = LauncherModel.getWorkerLooper();
157         final Handler uiHandler = new Handler(Looper.getMainLooper());
158         final ItemInfo originalInfo = (ItemInfo) originalIcon.getTag();
159         final UserHandleCompat user = originalInfo.user;
160         final ComponentName activity = originalInfo.getTargetComponent();
161         new Handler(workerLooper).postAtFrontOfQueue(new Runnable() {
162             @Override
163             public void run() {
164                 final List<ShortcutInfoCompat> shortcuts = ShortcutFilter.sortAndFilterShortcuts(
165                         mDeepShortcutsManager.queryForShortcutsContainer(activity, ids, user));
166                 // We want the lowest rank to be closest to the user's finger.
167                 if (mIsAboveIcon) {
168                     Collections.reverse(shortcuts);
169                 }
170                 for (int i = 0; i < shortcuts.size(); i++) {
171                     final ShortcutInfoCompat shortcut = shortcuts.get(i);
172                     uiHandler.post(new UpdateShortcutChild(
173                             i, new UnbadgedShortcutInfo(shortcut, mLauncher)));
174                 }
175             }
176         });
177     }
178 
179     /** Updates the child of this container at the given index based on the given shortcut info. */
180     private class UpdateShortcutChild implements Runnable {
181         private int mShortcutChildIndex;
182         private UnbadgedShortcutInfo mShortcutChildInfo;
183 
UpdateShortcutChild(int shortcutChildIndex, UnbadgedShortcutInfo shortcutChildInfo)184         public UpdateShortcutChild(int shortcutChildIndex, UnbadgedShortcutInfo shortcutChildInfo) {
185             mShortcutChildIndex = shortcutChildIndex;
186             mShortcutChildInfo = shortcutChildInfo;
187         }
188 
189         @Override
run()190         public void run() {
191             getShortcutAt(mShortcutChildIndex)
192                     .applyShortcutInfo(mShortcutChildInfo, DeepShortcutsContainer.this);
193         }
194     }
195 
getShortcutAt(int index)196     private DeepShortcutView getShortcutAt(int index) {
197         if (!mIsAboveIcon) {
198             // Opening down, so arrow is the first view.
199             index++;
200         }
201         return (DeepShortcutView) getChildAt(index);
202     }
203 
getShortcutCount()204     private int getShortcutCount() {
205         // All children except the arrow are shortcuts.
206         return getChildCount() - 1;
207     }
208 
animateOpen()209     private void animateOpen() {
210         setVisibility(View.VISIBLE);
211         mIsOpen = true;
212 
213         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
214         final int shortcutCount = getShortcutCount();
215 
216         final long duration = getResources().getInteger(
217                 R.integer.config_deepShortcutOpenDuration);
218         final long arrowScaleDuration = getResources().getInteger(
219                 R.integer.config_deepShortcutArrowOpenDuration);
220         final long arrowScaleDelay = duration - arrowScaleDuration;
221         final long stagger = getResources().getInteger(
222                 R.integer.config_deepShortcutOpenStagger);
223         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
224 
225         // Animate shortcuts
226         DecelerateInterpolator interpolator = new DecelerateInterpolator();
227         for (int i = 0; i < shortcutCount; i++) {
228             final DeepShortcutView deepShortcutView = getShortcutAt(i);
229             deepShortcutView.setVisibility(INVISIBLE);
230             deepShortcutView.setAlpha(0);
231 
232             Animator anim = deepShortcutView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
233             anim.addListener(new AnimatorListenerAdapter() {
234                 @Override
235                 public void onAnimationStart(Animator animation) {
236                     deepShortcutView.setVisibility(VISIBLE);
237                 }
238             });
239             anim.setDuration(duration);
240             int animationIndex = mIsAboveIcon ? shortcutCount - i - 1 : i;
241             anim.setStartDelay(stagger * animationIndex);
242             anim.setInterpolator(interpolator);
243             shortcutAnims.play(anim);
244 
245             Animator fadeAnim = new LauncherViewPropertyAnimator(deepShortcutView).alpha(1);
246             fadeAnim.setInterpolator(fadeInterpolator);
247             // We want the shortcut to be fully opaque before the arrow starts animating.
248             fadeAnim.setDuration(arrowScaleDelay);
249             shortcutAnims.play(fadeAnim);
250         }
251         shortcutAnims.addListener(new AnimatorListenerAdapter() {
252             @Override
253             public void onAnimationEnd(Animator animation) {
254                 mOpenCloseAnimator = null;
255                 Utilities.sendCustomAccessibilityEvent(
256                         DeepShortcutsContainer.this,
257                         AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
258                         getContext().getString(R.string.action_deep_shortcut));
259             }
260         });
261 
262         // Animate the arrow
263         mArrow.setScaleX(0);
264         mArrow.setScaleY(0);
265         Animator arrowScale = new LauncherViewPropertyAnimator(mArrow).scaleX(1).scaleY(1);
266         arrowScale.setStartDelay(arrowScaleDelay);
267         arrowScale.setDuration(arrowScaleDuration);
268         shortcutAnims.play(arrowScale);
269 
270         mOpenCloseAnimator = shortcutAnims;
271         shortcutAnims.start();
272     }
273 
274     /**
275      * Orients this container above or below the given icon, aligning with the left or right.
276      *
277      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
278      * - Above and left-aligned
279      * - Above and right-aligned
280      * - Below and left-aligned
281      * - Below and right-aligned
282      *
283      * So we always align left if there is enough horizontal space
284      * and align above if there is enough vertical space.
285      */
orientAboutIcon(BubbleTextView icon, int arrowHeight)286     private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
287         int width = getMeasuredWidth();
288         int height = getMeasuredHeight() + arrowHeight;
289 
290         DragLayer dragLayer = mLauncher.getDragLayer();
291         dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
292         Rect insets = dragLayer.getInsets();
293 
294         // Align left (right in RTL) if there is room.
295         int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
296         int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
297         int x = leftAlignedX;
298         boolean canBeLeftAligned = leftAlignedX + width < dragLayer.getRight() - insets.right;
299         boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
300         if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
301             x = rightAlignedX;
302         }
303         mIsLeftAligned = x == leftAlignedX;
304         if (mIsRtl) {
305             x -= dragLayer.getWidth() - width;
306         }
307 
308         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
309         int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
310         iconWidth *= icon.getScaleX();
311         Resources resources = getResources();
312         int xOffset;
313         if (isAlignedWithStart()) {
314             // Aligning with the shortcut icon.
315             int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
316             int shortcutPaddingStart = resources.getDimensionPixelSize(
317                     R.dimen.deep_shortcut_padding_start);
318             xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
319         } else {
320             // Aligning with the drag handle.
321             int shortcutDragHandleWidth = resources.getDimensionPixelSize(
322                     R.dimen.deep_shortcut_drag_handle_size);
323             int shortcutPaddingEnd = resources.getDimensionPixelSize(
324                     R.dimen.deep_shortcut_padding_end);
325             xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
326         }
327         x += mIsLeftAligned ? xOffset : -xOffset;
328 
329         // Open above icon if there is room.
330         int iconHeight = icon.getIcon().getBounds().height();
331         int y = mTempRect.top + icon.getPaddingTop() - height;
332         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
333         if (!mIsAboveIcon) {
334             y = mTempRect.top + icon.getPaddingTop() + iconHeight;
335         }
336 
337         // Insets are added later, so subtract them now.
338         y -= insets.top;
339 
340         setX(x);
341         setY(y);
342     }
343 
isAlignedWithStart()344     private boolean isAlignedWithStart() {
345         return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
346     }
347 
348     /**
349      * Adds an arrow view pointing at the original icon.
350      * @param horizontalOffset the horizontal offset of the arrow, so that it
351      *                              points at the center of the original icon
352      */
addArrowView(int horizontalOffset, int verticalOffset, int width, int height)353     private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
354         LinearLayout.LayoutParams layoutParams = new LayoutParams(width, height);
355         if (mIsLeftAligned) {
356             layoutParams.gravity = Gravity.LEFT;
357             layoutParams.leftMargin = horizontalOffset;
358         } else {
359             layoutParams.gravity = Gravity.RIGHT;
360             layoutParams.rightMargin = horizontalOffset;
361         }
362         if (mIsAboveIcon) {
363             layoutParams.topMargin = verticalOffset;
364         } else {
365             layoutParams.bottomMargin = verticalOffset;
366         }
367 
368         View arrowView = new View(getContext());
369         ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
370                 width, height, !mIsAboveIcon));
371         arrowDrawable.getPaint().setColor(Color.WHITE);
372         arrowView.setBackground(arrowDrawable);
373         arrowView.setElevation(getElevation());
374         addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
375         return arrowView;
376     }
377 
deferDrag(BubbleTextView originalIcon)378     private void deferDrag(BubbleTextView originalIcon) {
379         mDeferredDragIcon = originalIcon;
380         mLauncher.getDragController().addDragListener(this);
381     }
382 
getDeferredDragIcon()383     public BubbleTextView getDeferredDragIcon() {
384         return mDeferredDragIcon;
385     }
386 
387     /**
388      * Determines when the deferred drag should be started.
389      *
390      * Current behavior:
391      * - Start the drag if the touch passes a certain distance from the original touch down.
392      */
createDeferDragCondition(final Runnable onDragStart)393     public DragOptions.DeferDragCondition createDeferDragCondition(final Runnable onDragStart) {
394         return new DragOptions.DeferDragCondition() {
395             @Override
396             public boolean shouldStartDeferredDrag(double distanceDragged) {
397                 return distanceDragged > mStartDragThreshold;
398             }
399 
400             @Override
401             public void onDeferredDragStart() {
402                 mDeferredDragIcon.setVisibility(INVISIBLE);
403             }
404 
405             @Override
406             public void onDropBeforeDeferredDrag() {
407                 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mDeferredDragIcon);
408                 if (!mIsAboveIcon) {
409                     mDeferredDragIcon.setTextVisibility(false);
410                 }
411             }
412 
413             @Override
414             public void onDragStart() {
415                 if (onDragStart != null) {
416                     onDragStart.run();
417                 }
418             }
419         };
420     }
421 
422     @Override
423     public boolean onTouch(View v, MotionEvent ev) {
424         // Touched a shortcut, update where it was touched so we can drag from there on long click.
425         switch (ev.getAction()) {
426             case MotionEvent.ACTION_DOWN:
427             case MotionEvent.ACTION_MOVE:
428                 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
429                 break;
430         }
431         return false;
432     }
433 
434     public boolean onLongClick(View v) {
435         // Return early if this is not initiated from a touch or not the correct view
436         if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false;
437         // Return if global dragging is not enabled
438         if (!mLauncher.isDraggingEnabled()) return false;
439 
440         // Long clicked on a shortcut.
441         mDeferContainerRemoval = true;
442         DeepShortcutView sv = (DeepShortcutView) v.getParent();
443         sv.setWillDrawIcon(false);
444 
445         // Move the icon to align with the center-top of the touch point
446         mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
447         mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
448 
449         DragView dv = mLauncher.getWorkspace().beginDragShared(
450                 sv.getBubbleText(), this, sv.getFinalInfo(),
451                 new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
452         dv.animateShift(-mIconShift.x, -mIconShift.y);
453 
454         // TODO: support dragging from within folder without having to close it
455         mLauncher.closeFolder();
456         return false;
457     }
458 
459     @Override
460     public boolean supportsFlingToDelete() {
461         return true;
462     }
463 
464     @Override
465     public boolean supportsAppInfoDropTarget() {
466         return true;
467     }
468 
469     @Override
470     public boolean supportsDeleteDropTarget() {
471         return false;
472     }
473 
474     @Override
475     public float getIntrinsicIconScaleFactor() {
476         return 1f;
477     }
478 
479     @Override
480     public void onFlingToDeleteCompleted() {
481         // Don't care; ignore.
482     }
483 
484     @Override
485     public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
486             boolean success) {
487         if (!success) {
488             d.dragView.remove();
489             mLauncher.showWorkspace(true);
490             mLauncher.getDropTargetBar().onDragEnd();
491         }
492     }
493 
494     @Override
495     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
496         // Either the original icon or one of the shortcuts was dragged.
497         // Hide the container, but don't remove it yet because that interferes with touch events.
498         animateClose();
499     }
500 
501     @Override
502     public void onDragEnd() {
503         if (!mIsOpen) {
504             if (mOpenCloseAnimator != null) {
505                 // Close animation is running.
506                 mDeferContainerRemoval = false;
507             } else {
508                 // Close animation is not running.
509                 if (mDeferContainerRemoval) {
510                     close();
511                 }
512             }
513         }
514         mDeferredDragIcon.setVisibility(VISIBLE);
515     }
516 
517     @Override
518     public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) {
519         target.itemType = LauncherLogProto.DEEPSHORTCUT;
520         // TODO: add target.rank
521         targetParent.containerType = LauncherLogProto.DEEPSHORTCUTS;
522     }
523 
524     public void animateClose() {
525         if (!mIsOpen) {
526             return;
527         }
528         if (mOpenCloseAnimator != null) {
529             mOpenCloseAnimator.cancel();
530         }
531         mIsOpen = false;
532 
533         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
534         final int shortcutCount = getShortcutCount();
535         int numOpenShortcuts = 0;
536         for (int i = 0; i < shortcutCount; i++) {
537             if (getShortcutAt(i).isOpenOrOpening()) {
538                 numOpenShortcuts++;
539             }
540         }
541         final long duration = getResources().getInteger(
542                 R.integer.config_deepShortcutCloseDuration);
543         final long arrowScaleDuration = getResources().getInteger(
544                 R.integer.config_deepShortcutArrowOpenDuration);
545         final long stagger = getResources().getInteger(
546                 R.integer.config_deepShortcutCloseStagger);
547         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
548 
549         int firstOpenShortcutIndex = mIsAboveIcon ? shortcutCount - numOpenShortcuts : 0;
550         for (int i = firstOpenShortcutIndex; i < firstOpenShortcutIndex + numOpenShortcuts; i++) {
551             final DeepShortcutView view = getShortcutAt(i);
552             Animator anim;
553             if (view.willDrawIcon()) {
554                 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
555                 int animationIndex = mIsAboveIcon ? i - firstOpenShortcutIndex
556                         : numOpenShortcuts - i - 1;
557                 anim.setStartDelay(stagger * animationIndex);
558 
559                 Animator fadeAnim = new LauncherViewPropertyAnimator(view).alpha(0);
560                 // Don't start fading until the arrow is gone.
561                 fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
562                 fadeAnim.setDuration(duration - arrowScaleDuration);
563                 fadeAnim.setInterpolator(fadeInterpolator);
564                 shortcutAnims.play(fadeAnim);
565             } else {
566                 // The view is being dragged. Animate it such that it collapses with the drag view
567                 anim = view.collapseToIcon();
568                 anim.setDuration(DragView.VIEW_ZOOM_DURATION);
569 
570                 // Scale and translate the view to follow the drag view.
571                 Point iconCenter = view.getIconCenter();
572                 view.setPivotX(iconCenter.x);
573                 view.setPivotY(iconCenter.y);
574 
575                 float scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / view.getHeight();
576                 LauncherViewPropertyAnimator anim2 = new LauncherViewPropertyAnimator(view)
577                         .scaleX(scale)
578                         .scaleY(scale)
579                         .translationX(mIconShift.x)
580                         .translationY(mIconShift.y);
581                 anim2.setDuration(DragView.VIEW_ZOOM_DURATION);
582                 shortcutAnims.play(anim2);
583             }
584             anim.addListener(new AnimatorListenerAdapter() {
585                 @Override
586                 public void onAnimationEnd(Animator animation) {
587                     view.setVisibility(INVISIBLE);
588                 }
589             });
590             shortcutAnims.play(anim);
591         }
592         Animator arrowAnim = new LauncherViewPropertyAnimator(mArrow)
593                 .scaleX(0).scaleY(0).setDuration(arrowScaleDuration);
594         arrowAnim.setStartDelay(0);
595         shortcutAnims.play(arrowAnim);
596 
597         shortcutAnims.addListener(new AnimatorListenerAdapter() {
598             @Override
599             public void onAnimationEnd(Animator animation) {
600                 mOpenCloseAnimator = null;
601                 if (mDeferContainerRemoval) {
602                     setVisibility(INVISIBLE);
603                 } else {
604                     close();
605                 }
606             }
607         });
608         mOpenCloseAnimator = shortcutAnims;
609         shortcutAnims.start();
610     }
611 
612     /**
613      * Closes the folder without animation.
614      */
615     public void close() {
616         if (mOpenCloseAnimator != null) {
617             mOpenCloseAnimator.cancel();
618             mOpenCloseAnimator = null;
619         }
620         mIsOpen = false;
621         mDeferContainerRemoval = false;
622         boolean isInHotseat = ((ItemInfo) mDeferredDragIcon.getTag()).container
623                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
624         mDeferredDragIcon.setTextVisibility(!isInHotseat);
625         mLauncher.getDragController().removeDragListener(this);
626         mLauncher.getDragLayer().removeView(this);
627     }
628 
629     public boolean isOpen() {
630         return mIsOpen;
631     }
632 
633     /**
634      * Shows the shortcuts container for {@param icon}
635      * @return the container if shown or null.
636      */
637     public static DeepShortcutsContainer showForIcon(BubbleTextView icon) {
638         Launcher launcher = Launcher.getLauncher(icon.getContext());
639         if (launcher.getOpenShortcutsContainer() != null) {
640             // There is already a shortcuts container open, so don't open this one.
641             icon.clearFocus();
642             return null;
643         }
644         List<String> ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag());
645         if (!ids.isEmpty()) {
646             // There are shortcuts associated with the app, so defer its drag.
647             final DeepShortcutsContainer container =
648                     (DeepShortcutsContainer) launcher.getLayoutInflater().inflate(
649                             R.layout.deep_shortcuts_container, launcher.getDragLayer(), false);
650             container.setVisibility(View.INVISIBLE);
651             launcher.getDragLayer().addView(container);
652             container.populateAndShow(icon, ids);
653             return container;
654         }
655         return null;
656     }
657 
658     /**
659      * Extension of {@link ShortcutInfo} which does not badge the icons.
660      */
661     static class UnbadgedShortcutInfo extends ShortcutInfo {
662         public final ShortcutInfoCompat mDetail;
663 
664         public UnbadgedShortcutInfo(ShortcutInfoCompat shortcutInfo, Context context) {
665             super(shortcutInfo, context);
666             mDetail = shortcutInfo;
667         }
668 
669         @Override
670         protected Bitmap getBadgedIcon(Bitmap unbadgedBitmap, ShortcutInfoCompat shortcutInfo,
671                 IconCache cache, Context context) {
672             return unbadgedBitmap;
673         }
674     }
675 }
676