OSDN Git Service

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