OSDN Git Service

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