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