OSDN Git Service

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