OSDN Git Service

Merge "Moving Event dispatcher definition from LAuncher to BaseActivity so that it...
[android-x86/packages-apps-Launcher3.git] / src / com / android / launcher3 / popup / PopupContainerWithArrow.java
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.popup;
18
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.TimeInterpolator;
24 import android.animation.ValueAnimator;
25 import android.annotation.SuppressLint;
26 import android.annotation.TargetApi;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Color;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.graphics.drawable.ShapeDrawable;
33 import android.os.Build;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.util.AttributeSet;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewConfiguration;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.view.animation.DecelerateInterpolator;
44 import android.widget.FrameLayout;
45
46 import com.android.launcher3.AbstractFloatingView;
47 import com.android.launcher3.BubbleTextView;
48 import com.android.launcher3.DragSource;
49 import com.android.launcher3.DropTarget;
50 import com.android.launcher3.ItemInfo;
51 import com.android.launcher3.Launcher;
52 import com.android.launcher3.LauncherAnimUtils;
53 import com.android.launcher3.LauncherModel;
54 import com.android.launcher3.LauncherSettings;
55 import com.android.launcher3.LogAccelerateInterpolator;
56 import com.android.launcher3.R;
57 import com.android.launcher3.Utilities;
58 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
59 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
60 import com.android.launcher3.anim.PropertyListBuilder;
61 import com.android.launcher3.anim.PropertyResetListener;
62 import com.android.launcher3.badge.BadgeInfo;
63 import com.android.launcher3.dragndrop.DragController;
64 import com.android.launcher3.dragndrop.DragLayer;
65 import com.android.launcher3.dragndrop.DragOptions;
66 import com.android.launcher3.graphics.TriangleShape;
67 import com.android.launcher3.notification.NotificationItemView;
68 import com.android.launcher3.shortcuts.DeepShortcutView;
69 import com.android.launcher3.shortcuts.ShortcutsItemView;
70 import com.android.launcher3.util.PackageUserKey;
71
72 import java.util.Collections;
73 import java.util.List;
74 import java.util.Map;
75
76 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
77 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
78 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
79
80 /**
81  * A container for shortcuts to deep links within apps.
82  */
83 @TargetApi(Build.VERSION_CODES.N)
84 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
85         DragController.DragListener {
86
87     protected final Launcher mLauncher;
88     private final int mStartDragThreshold;
89     private LauncherAccessibilityDelegate mAccessibilityDelegate;
90     private final boolean mIsRtl;
91
92     public ShortcutsItemView mShortcutsItemView;
93     private NotificationItemView mNotificationItemView;
94
95     protected BubbleTextView mOriginalIcon;
96     private final Rect mTempRect = new Rect();
97     private PointF mInterceptTouchDown = new PointF();
98     private boolean mIsLeftAligned;
99     protected boolean mIsAboveIcon;
100     private View mArrow;
101
102     protected Animator mOpenCloseAnimator;
103     private boolean mDeferContainerRemoval;
104
105     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
106         super(context, attrs, defStyleAttr);
107         mLauncher = Launcher.getLauncher(context);
108
109         mStartDragThreshold = getResources().getDimensionPixelSize(
110                 R.dimen.deep_shortcuts_start_drag_threshold);
111         // TODO: make sure the delegate works for all items, not just shortcuts.
112         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
113         mIsRtl = Utilities.isRtl(getResources());
114     }
115
116     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
117         this(context, attrs, 0);
118     }
119
120     public PopupContainerWithArrow(Context context) {
121         this(context, null, 0);
122     }
123
124     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
125         return mAccessibilityDelegate;
126     }
127
128     /**
129      * Shows the notifications and deep shortcuts associated with {@param icon}.
130      * @return the container if shown or null.
131      */
132     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
133         Launcher launcher = Launcher.getLauncher(icon.getContext());
134         if (getOpen(launcher) != null) {
135             // There is already an items container open, so don't open this one.
136             icon.clearFocus();
137             return null;
138         }
139         ItemInfo itemInfo = (ItemInfo) icon.getTag();
140         List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
141         String[] notificationKeys = launcher.getPopupDataProvider()
142                 .getNotificationKeysForItem(itemInfo);
143         if (shortcutIds.size() > 0 || notificationKeys.length > 0) {
144             final PopupContainerWithArrow container =
145                     (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
146                             R.layout.popup_container, launcher.getDragLayer(), false);
147             container.setVisibility(View.INVISIBLE);
148             launcher.getDragLayer().addView(container);
149             container.populateAndShow(icon, shortcutIds, notificationKeys);
150             return container;
151         }
152         return null;
153     }
154
155     public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
156             final String[] notificationKeys) {
157         final Resources resources = getResources();
158         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
159         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
160         final int arrowHorizontalOffset = resources.getDimensionPixelSize(
161                 R.dimen.deep_shortcuts_arrow_horizontal_offset);
162         final int arrowVerticalOffset = resources.getDimensionPixelSize(
163                 R.dimen.deep_shortcuts_arrow_vertical_offset);
164
165         // Add dummy views first, and populate with real info when ready.
166         PopupPopulator.Item[] itemsToPopulate = PopupPopulator
167                 .getItemsToPopulate(shortcutIds, notificationKeys);
168         addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
169
170         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
171         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
172
173         boolean reverseOrder = mIsAboveIcon;
174         if (reverseOrder) {
175             removeAllViews();
176             mNotificationItemView = null;
177             mShortcutsItemView = null;
178             itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
179             addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
180
181             measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
182             orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
183         }
184
185         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
186         List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
187                 ? Collections.EMPTY_LIST
188                 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
189         if (mNotificationItemView != null) {
190             BadgeInfo badgeInfo = mLauncher.getPopupDataProvider()
191                     .getBadgeInfoForItem(originalItemInfo);
192             updateNotificationHeader(badgeInfo);
193         }
194
195         // Add the arrow.
196         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
197         mArrow.setPivotX(arrowWidth / 2);
198         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
199
200         animateOpen();
201
202         mOriginalIcon = originalIcon;
203
204         mLauncher.getDragController().addDragListener(this);
205
206         // Load the shortcuts on a background thread and update the container as it animates.
207         final Looper workerLooper = LauncherModel.getWorkerLooper();
208         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
209                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
210                 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView));
211     }
212
213     private void addDummyViews(BubbleTextView originalIcon,
214             PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
215         final Resources res = getResources();
216         final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
217         final LayoutInflater inflater = mLauncher.getLayoutInflater();
218         int numItems = itemTypesToPopulate.length;
219         for (int i = 0; i < numItems; i++) {
220             PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
221             final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
222
223             if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
224                 mNotificationItemView = (NotificationItemView) item;
225                 int footerHeight = notificationFooterHasIcons ?
226                         res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
227                 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
228             }
229
230             boolean itemIsFollowedByDifferentType = i < numItems - 1
231                     && itemTypesToPopulate[i + 1] != itemTypeToPopulate;
232
233             item.setAccessibilityDelegate(mAccessibilityDelegate);
234             if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
235                 if (mShortcutsItemView == null) {
236                     mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
237                             R.layout.shortcuts_item, this, false);
238                     addView(mShortcutsItemView);
239                 }
240                 mShortcutsItemView.addDeepShortcutView((DeepShortcutView) item);
241                 if (itemIsFollowedByDifferentType) {
242                     ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
243                 }
244             } else {
245                 addView(item);
246                 if (itemIsFollowedByDifferentType) {
247                     ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
248                 }
249             }
250         }
251         // TODO: update this, since not all items are shortcuts
252         setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
253                 numItems, originalIcon.getContentDescription().toString()));
254     }
255
256     protected PopupItemView getItemViewAt(int index) {
257         if (!mIsAboveIcon) {
258             // Opening down, so arrow is the first view.
259             index++;
260         }
261         return (PopupItemView) getChildAt(index);
262     }
263
264     protected int getItemCount() {
265         // All children except the arrow are items.
266         return getChildCount() - 1;
267     }
268
269     private void animateOpen() {
270         setVisibility(View.VISIBLE);
271         mIsOpen = true;
272
273         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
274         final int itemCount = getItemCount();
275
276         final long duration = getResources().getInteger(
277                 R.integer.config_deepShortcutOpenDuration);
278         final long arrowScaleDuration = getResources().getInteger(
279                 R.integer.config_deepShortcutArrowOpenDuration);
280         final long arrowScaleDelay = duration - arrowScaleDuration;
281         final long stagger = getResources().getInteger(
282                 R.integer.config_deepShortcutOpenStagger);
283         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
284
285         // Animate shortcuts
286         DecelerateInterpolator interpolator = new DecelerateInterpolator();
287         for (int i = 0; i < itemCount; i++) {
288             final PopupItemView popupItemView = getItemViewAt(i);
289             popupItemView.setVisibility(INVISIBLE);
290             popupItemView.setAlpha(0);
291
292             Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
293             anim.addListener(new AnimatorListenerAdapter() {
294                 @Override
295                 public void onAnimationStart(Animator animation) {
296                     popupItemView.setVisibility(VISIBLE);
297                 }
298             });
299             anim.setDuration(duration);
300             int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
301             anim.setStartDelay(stagger * animationIndex);
302             anim.setInterpolator(interpolator);
303             shortcutAnims.play(anim);
304
305             Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
306             fadeAnim.setInterpolator(fadeInterpolator);
307             // We want the shortcut to be fully opaque before the arrow starts animating.
308             fadeAnim.setDuration(arrowScaleDelay);
309             shortcutAnims.play(fadeAnim);
310         }
311         shortcutAnims.addListener(new AnimatorListenerAdapter() {
312             @Override
313             public void onAnimationEnd(Animator animation) {
314                 mOpenCloseAnimator = null;
315                 Utilities.sendCustomAccessibilityEvent(
316                         PopupContainerWithArrow.this,
317                         AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
318                         getContext().getString(R.string.action_deep_shortcut));
319             }
320         });
321
322         // Animate the arrow
323         mArrow.setScaleX(0);
324         mArrow.setScaleY(0);
325         Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
326         arrowScale.setStartDelay(arrowScaleDelay);
327         shortcutAnims.play(arrowScale);
328
329         mOpenCloseAnimator = shortcutAnims;
330         shortcutAnims.start();
331     }
332
333     /**
334      * Orients this container above or below the given icon, aligning with the left or right.
335      *
336      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
337      * - Above and left-aligned
338      * - Above and right-aligned
339      * - Below and left-aligned
340      * - Below and right-aligned
341      *
342      * So we always align left if there is enough horizontal space
343      * and align above if there is enough vertical space.
344      */
345     private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
346         int width = getMeasuredWidth();
347         int height = getMeasuredHeight() + arrowHeight;
348
349         DragLayer dragLayer = mLauncher.getDragLayer();
350         dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
351         Rect insets = dragLayer.getInsets();
352
353         // Align left (right in RTL) if there is room.
354         int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
355         int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
356         int x = leftAlignedX;
357         boolean canBeLeftAligned = leftAlignedX + width + insets.left
358                 < dragLayer.getRight() - insets.right;
359         boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
360         if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
361             x = rightAlignedX;
362         }
363         mIsLeftAligned = x == leftAlignedX;
364         if (mIsRtl) {
365             x -= dragLayer.getWidth() - width;
366         }
367
368         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
369         int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
370         iconWidth *= icon.getScaleX();
371         Resources resources = getResources();
372         int xOffset;
373         if (isAlignedWithStart()) {
374             // Aligning with the shortcut icon.
375             int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
376             int shortcutPaddingStart = resources.getDimensionPixelSize(
377                     R.dimen.deep_shortcut_padding_start);
378             xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
379         } else {
380             // Aligning with the drag handle.
381             int shortcutDragHandleWidth = resources.getDimensionPixelSize(
382                     R.dimen.deep_shortcut_drag_handle_size);
383             int shortcutPaddingEnd = resources.getDimensionPixelSize(
384                     R.dimen.deep_shortcut_padding_end);
385             xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
386         }
387         x += mIsLeftAligned ? xOffset : -xOffset;
388
389         // Open above icon if there is room.
390         int iconHeight = icon.getIcon().getBounds().height();
391         int y = mTempRect.top + icon.getPaddingTop() - height;
392         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
393         if (!mIsAboveIcon) {
394             y = mTempRect.top + icon.getPaddingTop() + iconHeight;
395         }
396
397         // Insets are added later, so subtract them now.
398         if (mIsRtl) {
399             x += insets.right;
400         } else {
401             x -= insets.left;
402         }
403         y -= insets.top;
404
405         if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
406             // The container is opening off the screen, so just center it in the drag layer instead.
407             ((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
408             // Put the container next to the icon, preferring the right side in ltr (left in rtl).
409             int rightSide = leftAlignedX + iconWidth - insets.left;
410             int leftSide = rightAlignedX - iconWidth - insets.left;
411             if (!mIsRtl) {
412                 if (rightSide + width < dragLayer.getRight()) {
413                     x = rightSide;
414                     mIsLeftAligned = true;
415                 } else {
416                     x = leftSide;
417                     mIsLeftAligned = false;
418                 }
419             } else {
420                 if (leftSide > dragLayer.getLeft()) {
421                     x = leftSide;
422                     mIsLeftAligned = false;
423                 } else {
424                     x = rightSide;
425                     mIsLeftAligned = true;
426                 }
427             }
428             mIsAboveIcon = true;
429         }
430
431         if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
432             // If we are still off screen, center horizontally too.
433             ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
434         }
435
436         int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
437         if (!Gravity.isHorizontal(gravity)) {
438             setX(x);
439         }
440         if (!Gravity.isVertical(gravity)) {
441             setY(y);
442         }
443     }
444
445     private boolean isAlignedWithStart() {
446         return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
447     }
448
449     /**
450      * Adds an arrow view pointing at the original icon.
451      * @param horizontalOffset the horizontal offset of the arrow, so that it
452      *                              points at the center of the original icon
453      */
454     private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
455         LayoutParams layoutParams = new LayoutParams(width, height);
456         if (mIsLeftAligned) {
457             layoutParams.gravity = Gravity.LEFT;
458             layoutParams.leftMargin = horizontalOffset;
459         } else {
460             layoutParams.gravity = Gravity.RIGHT;
461             layoutParams.rightMargin = horizontalOffset;
462         }
463         if (mIsAboveIcon) {
464             layoutParams.topMargin = verticalOffset;
465         } else {
466             layoutParams.bottomMargin = verticalOffset;
467         }
468
469         View arrowView = new View(getContext());
470         if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
471             // This is only true if there wasn't room for the container next to the icon,
472             // so we centered it instead. In that case we don't want to show the arrow.
473             arrowView.setVisibility(INVISIBLE);
474         } else {
475             ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
476                     width, height, !mIsAboveIcon));
477             arrowDrawable.getPaint().setColor(Color.WHITE);
478             arrowView.setBackground(arrowDrawable);
479             arrowView.setElevation(getElevation());
480         }
481         addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
482         return arrowView;
483     }
484
485     @Override
486     public View getExtendedTouchView() {
487         return mOriginalIcon;
488     }
489
490     /**
491      * Determines when the deferred drag should be started.
492      *
493      * Current behavior:
494      * - Start the drag if the touch passes a certain distance from the original touch down.
495      */
496     public DragOptions.PreDragCondition createPreDragCondition() {
497         return new DragOptions.PreDragCondition() {
498             @Override
499             public boolean shouldStartDrag(double distanceDragged) {
500                 return distanceDragged > mStartDragThreshold;
501             }
502
503             @Override
504             public void onPreDragStart(DropTarget.DragObject dragObject) {
505                 mOriginalIcon.setVisibility(INVISIBLE);
506             }
507
508             @Override
509             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
510                 if (!dragStarted) {
511                     mOriginalIcon.setVisibility(VISIBLE);
512                     mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
513                     if (!mIsAboveIcon) {
514                         mOriginalIcon.setTextVisibility(false);
515                     }
516                 }
517             }
518         };
519     }
520
521     @Override
522     public boolean onInterceptTouchEvent(MotionEvent ev) {
523         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
524             mInterceptTouchDown.set(ev.getX(), ev.getY());
525             return false;
526         }
527         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
528         return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
529                 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
530     }
531
532     /**
533      * We need to handle touch events to prevent them from falling through to the workspace below.
534      */
535     @SuppressLint("ClickableViewAccessibility")
536     @Override
537     public boolean onTouchEvent(MotionEvent ev) {
538         return true;
539     }
540
541     /**
542      * Updates the notification header to reflect the badge info. Since this can be called
543      * for any badge info (not necessarily the one associated with this app), we first
544      * check that the ItemInfo matches the one of this popup.
545      */
546     public void updateNotificationHeader(BadgeInfo badgeInfo, ItemInfo originalItemInfo) {
547         if (originalItemInfo != mOriginalIcon.getTag()) {
548             return;
549         }
550         updateNotificationHeader(badgeInfo);
551     }
552
553     private void updateNotificationHeader(BadgeInfo badgeInfo) {
554         if (mNotificationItemView != null && badgeInfo != null) {
555             mNotificationItemView.updateHeader(badgeInfo.getNotificationCount());
556         }
557     }
558
559     public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
560         if (mNotificationItemView == null) {
561             return;
562         }
563         ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
564         BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
565         if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
566             AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
567             final int duration = getResources().getInteger(
568                     R.integer.config_removeNotificationViewDuration);
569             final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
570             removeNotification.play(reduceNotificationViewHeight(
571                     mNotificationItemView.getHeight() + spacing, duration, mNotificationItemView));
572             final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
573                     : mNotificationItemView;
574             if (removeMarginView != null) {
575                 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
576                 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
577                     @Override
578                     public void onAnimationUpdate(ValueAnimator valueAnimator) {
579                         ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
580                                 = (int) (spacing * (float) valueAnimator.getAnimatedValue());
581                     }
582                 });
583                 removeNotification.play(removeMargin);
584             }
585             Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
586                     .setDuration(duration);
587             fade.addListener(new AnimatorListenerAdapter() {
588                 @Override
589                 public void onAnimationEnd(Animator animation) {
590                     removeView(mNotificationItemView);
591                     if (getItemCount() == 0) {
592                         close(false);
593                         return;
594                     }
595                 }
596             });
597             removeNotification.play(fade);
598             final long arrowScaleDuration = getResources().getInteger(
599                     R.integer.config_deepShortcutArrowOpenDuration);
600             Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
601             hideArrow.setStartDelay(0);
602             Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
603             showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
604             removeNotification.playSequentially(hideArrow, showArrow);
605             removeNotification.start();
606             return;
607         }
608         mNotificationItemView.trimNotifications(badgeInfo.getNotificationKeys());
609     }
610
611     private ObjectAnimator createArrowScaleAnim(float scale) {
612         return LauncherAnimUtils.ofPropertyValuesHolder(
613                 mArrow, new PropertyListBuilder().scale(scale).build());
614     }
615     /**
616      * Animates the height of the notification item and the translationY of other items accordingly.
617      */
618     public Animator reduceNotificationViewHeight(int heightToRemove, int duration,
619             NotificationItemView notificationItem) {
620         final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
621         AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet();
622         animatorSet.play(notificationItem.animateHeightRemoval(heightToRemove));
623         PropertyResetListener<View, Float> resetTranslationYListener
624                 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
625         for (int i = 0; i < getItemCount(); i++) {
626             final PopupItemView itemView = getItemViewAt(i);
627             if (!mIsAboveIcon && itemView == notificationItem) {
628                 // The notification view is already in the right place when container is below icon.
629                 continue;
630             }
631             ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
632                     itemView.getTranslationY() + translateYBy).setDuration(duration);
633             translateItem.addListener(resetTranslationYListener);
634             animatorSet.play(translateItem);
635         }
636         if (mIsAboveIcon) {
637             // All the items, including the notification item, translated down, but the
638             // container itself did not. This means the items would jump back to their
639             // original translation unless we update the container's translationY here.
640             animatorSet.addListener(new AnimatorListenerAdapter() {
641                 @Override
642                 public void onAnimationEnd(Animator animation) {
643                     setTranslationY(getTranslationY() + translateYBy);
644                 }
645             });
646         }
647         return animatorSet;
648     }
649
650     public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
651         return reduceNotificationViewHeight(heightToRemove, duration, mNotificationItemView);
652     }
653
654     @Override
655     public boolean supportsAppInfoDropTarget() {
656         return true;
657     }
658
659     @Override
660     public boolean supportsDeleteDropTarget() {
661         return false;
662     }
663
664     @Override
665     public float getIntrinsicIconScaleFactor() {
666         return 1f;
667     }
668
669     @Override
670     public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
671             boolean success) {
672         if (!success) {
673             d.dragView.remove();
674             mLauncher.showWorkspace(true);
675             mLauncher.getDropTargetBar().onDragEnd();
676         }
677     }
678
679     @Override
680     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
681         // Either the original icon or one of the shortcuts was dragged.
682         // Hide the container, but don't remove it yet because that interferes with touch events.
683         mDeferContainerRemoval = true;
684         animateClose();
685     }
686
687     @Override
688     public void onDragEnd() {
689         if (!mIsOpen) {
690             if (mOpenCloseAnimator != null) {
691                 // Close animation is running.
692                 mDeferContainerRemoval = false;
693             } else {
694                 // Close animation is not running.
695                 if (mDeferContainerRemoval) {
696                     closeComplete();
697                 }
698             }
699         }
700     }
701
702     @Override
703     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
704         target.itemType = ItemType.DEEPSHORTCUT;
705         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
706     }
707
708     @Override
709     protected void handleClose(boolean animate) {
710         if (animate) {
711             animateClose();
712         } else {
713             closeComplete();
714         }
715     }
716
717     protected void animateClose() {
718         if (!mIsOpen) {
719             return;
720         }
721         if (mOpenCloseAnimator != null) {
722             mOpenCloseAnimator.cancel();
723         }
724         mIsOpen = false;
725
726         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
727         final int itemCount = getItemCount();
728         int numOpenShortcuts = 0;
729         for (int i = 0; i < itemCount; i++) {
730             if (getItemViewAt(i).isOpenOrOpening()) {
731                 numOpenShortcuts++;
732             }
733         }
734         final long duration = getResources().getInteger(
735                 R.integer.config_deepShortcutCloseDuration);
736         final long arrowScaleDuration = getResources().getInteger(
737                 R.integer.config_deepShortcutArrowOpenDuration);
738         final long stagger = getResources().getInteger(
739                 R.integer.config_deepShortcutCloseStagger);
740         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
741
742         int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
743         for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
744             final PopupItemView view = getItemViewAt(i);
745             Animator anim;
746             anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
747             int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
748                     : numOpenShortcuts - i - 1;
749             anim.setStartDelay(stagger * animationIndex);
750
751             Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
752             // Don't start fading until the arrow is gone.
753             fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
754             fadeAnim.setDuration(duration - arrowScaleDuration);
755             fadeAnim.setInterpolator(fadeInterpolator);
756             shortcutAnims.play(fadeAnim);
757             anim.addListener(new AnimatorListenerAdapter() {
758                 @Override
759                 public void onAnimationEnd(Animator animation) {
760                     view.setVisibility(INVISIBLE);
761                 }
762             });
763             shortcutAnims.play(anim);
764         }
765         Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
766         arrowAnim.setStartDelay(0);
767         shortcutAnims.play(arrowAnim);
768
769         shortcutAnims.addListener(new AnimatorListenerAdapter() {
770             @Override
771             public void onAnimationEnd(Animator animation) {
772                 mOpenCloseAnimator = null;
773                 if (mDeferContainerRemoval) {
774                     setVisibility(INVISIBLE);
775                 } else {
776                     closeComplete();
777                 }
778             }
779         });
780         mOpenCloseAnimator = shortcutAnims;
781         shortcutAnims.start();
782     }
783
784     /**
785      * Closes the folder without animation.
786      */
787     protected void closeComplete() {
788         if (mOpenCloseAnimator != null) {
789             mOpenCloseAnimator.cancel();
790             mOpenCloseAnimator = null;
791         }
792         mIsOpen = false;
793         mDeferContainerRemoval = false;
794         boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
795                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
796         mOriginalIcon.setTextVisibility(!isInHotseat);
797         mLauncher.getDragController().removeDragListener(this);
798         mLauncher.getDragLayer().removeView(this);
799     }
800
801     @Override
802     protected boolean isOfType(int type) {
803         return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
804     }
805
806     /**
807      * Returns a DeepShortcutsContainer which is already open or null
808      */
809     public static PopupContainerWithArrow getOpen(Launcher launcher) {
810         return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
811     }
812
813     @Override
814     public int getLogContainerType() {
815         return ContainerType.DEEPSHORTCUTS;
816     }
817 }