• 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.Outline;
30 import android.graphics.Paint;
31 import android.graphics.Point;
32 import android.graphics.PointF;
33 import android.graphics.Rect;
34 import android.graphics.drawable.ShapeDrawable;
35 import android.os.Build;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.support.annotation.IntDef;
39 import android.util.AttributeSet;
40 import android.view.Gravity;
41 import android.view.LayoutInflater;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewConfiguration;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.animation.AccelerateDecelerateInterpolator;
47 
48 import com.android.launcher3.AbstractFloatingView;
49 import com.android.launcher3.BubbleTextView;
50 import com.android.launcher3.DragSource;
51 import com.android.launcher3.DropTarget;
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.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.anim.RoundedRectRevealOutlineProvider;
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 import com.android.launcher3.util.Themes;
76 
77 import java.lang.annotation.Retention;
78 import java.lang.annotation.RetentionPolicy;
79 import java.util.Collections;
80 import java.util.List;
81 import java.util.Map;
82 import java.util.Set;
83 
84 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
85 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
86 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
87 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
88 
89 /**
90  * A container for shortcuts to deep links within apps.
91  */
92 @TargetApi(Build.VERSION_CODES.N)
93 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
94         DragController.DragListener {
95 
96     public static final int ROUNDED_TOP_CORNERS = 1 << 0;
97     public static final int ROUNDED_BOTTOM_CORNERS = 1 << 1;
98 
99     @IntDef(flag = true, value = {
100             ROUNDED_TOP_CORNERS,
101             ROUNDED_BOTTOM_CORNERS
102     })
103     @Retention(RetentionPolicy.SOURCE)
104     public  @interface RoundedCornerFlags {}
105 
106     protected final Launcher mLauncher;
107     private final int mStartDragThreshold;
108     private LauncherAccessibilityDelegate mAccessibilityDelegate;
109     private final boolean mIsRtl;
110 
111     public ShortcutsItemView mShortcutsItemView;
112     private NotificationItemView mNotificationItemView;
113 
114     protected BubbleTextView mOriginalIcon;
115     private final Rect mTempRect = new Rect();
116     private PointF mInterceptTouchDown = new PointF();
117     private boolean mIsLeftAligned;
118     protected boolean mIsAboveIcon;
119     private View mArrow;
120     private int mGravity;
121 
122     protected Animator mOpenCloseAnimator;
123     private boolean mDeferContainerRemoval;
124     private AnimatorSet mReduceHeightAnimatorSet;
125     private final Rect mStartRect = new Rect();
126     private final Rect mEndRect = new Rect();
127 
PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr)128     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
129         super(context, attrs, defStyleAttr);
130         mLauncher = Launcher.getLauncher(context);
131 
132         mStartDragThreshold = getResources().getDimensionPixelSize(
133                 R.dimen.deep_shortcuts_start_drag_threshold);
134         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
135         mIsRtl = Utilities.isRtl(getResources());
136     }
137 
PopupContainerWithArrow(Context context, AttributeSet attrs)138     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
139         this(context, attrs, 0);
140     }
141 
PopupContainerWithArrow(Context context)142     public PopupContainerWithArrow(Context context) {
143         this(context, null, 0);
144     }
145 
getAccessibilityDelegate()146     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
147         return mAccessibilityDelegate;
148     }
149 
150     /**
151      * Shows the notifications and deep shortcuts associated with {@param icon}.
152      * @return the container if shown or null.
153      */
showForIcon(BubbleTextView icon)154     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
155         Launcher launcher = Launcher.getLauncher(icon.getContext());
156         if (getOpen(launcher) != null) {
157             // There is already an items container open, so don't open this one.
158             icon.clearFocus();
159             return null;
160         }
161         ItemInfo itemInfo = (ItemInfo) icon.getTag();
162         if (!DeepShortcutManager.supportsShortcuts(itemInfo)) {
163             return null;
164         }
165 
166         PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
167         List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo);
168         List<NotificationKeyData> notificationKeys = popupDataProvider
169                 .getNotificationKeysForItem(itemInfo);
170         List<SystemShortcut> systemShortcuts = popupDataProvider
171                 .getEnabledSystemShortcutsForItem(itemInfo);
172 
173         final PopupContainerWithArrow container =
174                 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
175                         R.layout.popup_container, launcher.getDragLayer(), false);
176         container.setVisibility(View.INVISIBLE);
177         launcher.getDragLayer().addView(container);
178         container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts);
179         return container;
180     }
181 
populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts)182     public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
183             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
184         final Resources resources = getResources();
185         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
186         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
187         final int arrowVerticalOffset = resources.getDimensionPixelSize(
188                 R.dimen.popup_arrow_vertical_offset);
189 
190         mOriginalIcon = originalIcon;
191 
192         // Add dummy views first, and populate with real info when ready.
193         PopupPopulator.Item[] itemsToPopulate = PopupPopulator
194                 .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
195         addDummyViews(itemsToPopulate, notificationKeys.size());
196 
197         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
198         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
199 
200         boolean reverseOrder = mIsAboveIcon;
201         if (reverseOrder) {
202             removeAllViews();
203             mNotificationItemView = null;
204             mShortcutsItemView = null;
205             itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
206             addDummyViews(itemsToPopulate, notificationKeys.size());
207 
208             measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
209             orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
210         }
211 
212         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
213         List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
214                 ? Collections.EMPTY_LIST
215                 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
216         List<View> systemShortcutViews = mShortcutsItemView == null
217                 ? Collections.EMPTY_LIST
218                 : mShortcutsItemView.getSystemShortcutViews(reverseOrder);
219         if (mNotificationItemView != null) {
220             updateNotificationHeader();
221         }
222 
223         int numShortcuts = shortcutViews.size() + systemShortcutViews.size();
224         int numNotifications = notificationKeys.size();
225         if (numNotifications == 0) {
226             setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
227                     numShortcuts, originalIcon.getContentDescription().toString()));
228         } else {
229             setContentDescription(getContext().getString(
230                     R.string.shortcuts_menu_with_notifications_description, numShortcuts,
231                     numNotifications, originalIcon.getContentDescription().toString()));
232         }
233 
234         // Add the arrow.
235         final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
236                 R.dimen.popup_arrow_horizontal_offset_start :
237                 R.dimen.popup_arrow_horizontal_offset_end);
238         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
239         mArrow.setPivotX(arrowWidth / 2);
240         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
241 
242         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
243         animateOpen();
244 
245         mLauncher.getDragController().addDragListener(this);
246         mOriginalIcon.forceHideBadge(true);
247 
248         // Load the shortcuts on a background thread and update the container as it animates.
249         final Looper workerLooper = LauncherModel.getWorkerLooper();
250         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
251                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
252                 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
253                 systemShortcuts, systemShortcutViews));
254     }
255 
addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications)256     private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) {
257         final Resources res = getResources();
258         final LayoutInflater inflater = mLauncher.getLayoutInflater();
259 
260         int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
261         int numItems = itemTypesToPopulate.length;
262         for (int i = 0; i < numItems; i++) {
263             PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
264             PopupPopulator.Item prevItemTypeToPopulate =
265                     i > 0 ? itemTypesToPopulate[i - 1] : null;
266             PopupPopulator.Item nextItemTypeToPopulate =
267                     i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
268             final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
269 
270             boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null
271                     && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut;
272             boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null
273                     && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
274 
275             if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
276                 mNotificationItemView = (NotificationItemView) item;
277                 boolean notificationFooterHasIcons = numNotifications > 1;
278                 int footerHeight = res.getDimensionPixelSize(
279                         notificationFooterHasIcons ? R.dimen.notification_footer_height
280                                 : R.dimen.notification_empty_footer_height);
281                 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
282                 if (notificationFooterHasIcons) {
283                     mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
284                 }
285 
286                 int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
287                 if (shouldUnroundTopCorners) {
288                     roundedCorners &= ~ROUNDED_TOP_CORNERS;
289                     mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
290                 }
291                 if (shouldUnroundBottomCorners) {
292                     roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
293                     mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
294                 }
295                 int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
296                 mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
297 
298                 mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
299             } else if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
300                 item.setAccessibilityDelegate(mAccessibilityDelegate);
301             }
302 
303             if (itemTypeToPopulate.isShortcut) {
304                 if (mShortcutsItemView == null) {
305                     mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
306                             R.layout.shortcuts_item, this, false);
307                     addView(mShortcutsItemView);
308                     if (shouldUnroundTopCorners) {
309                         shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS;
310                     }
311                 }
312                 if (itemTypeToPopulate != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON
313                         && numNotifications > 0) {
314                     int prevHeight = item.getLayoutParams().height;
315                     // Condense shortcuts height when there are notifications.
316                     item.getLayoutParams().height = res.getDimensionPixelSize(
317                             R.dimen.bg_popup_item_condensed_height);
318                     if (item instanceof DeepShortcutView) {
319                         float iconScale = (float) item.getLayoutParams().height / prevHeight;
320                         ((DeepShortcutView) item).getIconView().setScaleX(iconScale);
321                         ((DeepShortcutView) item).getIconView().setScaleY(iconScale);
322                     }
323                 }
324                 mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
325                 if (shouldUnroundBottomCorners) {
326                     shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
327                 }
328             } else {
329                 addView(item);
330             }
331         }
332         int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
333         mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners);
334         if (numNotifications > 0) {
335             mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
336         }
337     }
338 
getItemViewAt(int index)339     protected PopupItemView getItemViewAt(int index) {
340         if (!mIsAboveIcon) {
341             // Opening down, so arrow is the first view.
342             index++;
343         }
344         return (PopupItemView) getChildAt(index);
345     }
346 
getItemCount()347     protected int getItemCount() {
348         // All children except the arrow are items.
349         return getChildCount() - 1;
350     }
351 
animateOpen()352     private void animateOpen() {
353         setVisibility(View.VISIBLE);
354         mIsOpen = true;
355 
356         final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
357         final Resources res = getResources();
358         final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
359         final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
360 
361         // Rectangular reveal.
362         int itemsTotalHeight = 0;
363         for (int i = 0; i < getItemCount(); i++) {
364             itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
365         }
366         Point startPoint = computeAnimStartPoint(itemsTotalHeight);
367         int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
368         float radius = getItemViewAt(0).getBackgroundRadius();
369         mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
370         mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
371         final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider
372                 (radius, radius, mStartRect, mEndRect).createRevealAnimator(this, false);
373         revealAnim.setDuration(revealDuration);
374         revealAnim.setInterpolator(revealInterpolator);
375 
376         Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
377         fadeIn.setDuration(revealDuration);
378         fadeIn.setInterpolator(revealInterpolator);
379         openAnim.play(fadeIn);
380 
381         // Animate the arrow.
382         mArrow.setScaleX(0);
383         mArrow.setScaleY(0);
384         Animator arrowScale = createArrowScaleAnim(1).setDuration(res.getInteger(
385                 R.integer.config_popupArrowOpenDuration));
386 
387         openAnim.addListener(new AnimatorListenerAdapter() {
388             @Override
389             public void onAnimationEnd(Animator animation) {
390                 mOpenCloseAnimator = null;
391                 Utilities.sendCustomAccessibilityEvent(
392                         PopupContainerWithArrow.this,
393                         AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
394                         getContext().getString(R.string.action_deep_shortcut));
395             }
396         });
397 
398         mOpenCloseAnimator = openAnim;
399         openAnim.playSequentially(revealAnim, arrowScale);
400         openAnim.start();
401     }
402 
403     @Override
onLayout(boolean changed, int l, int t, int r, int b)404     protected void onLayout(boolean changed, int l, int t, int r, int b) {
405         super.onLayout(changed, l, t, r, b);
406         enforceContainedWithinScreen(l, r);
407 
408     }
409 
enforceContainedWithinScreen(int left, int right)410     private void enforceContainedWithinScreen(int left, int right) {
411         DragLayer dragLayer = mLauncher.getDragLayer();
412         if (getTranslationX() + left < 0 ||
413                 getTranslationX() + right > dragLayer.getWidth()) {
414             // If we are still off screen, center horizontally too.
415             mGravity |= Gravity.CENTER_HORIZONTAL;
416         }
417 
418         if (Gravity.isHorizontal(mGravity)) {
419             setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
420         }
421         if (Gravity.isVertical(mGravity)) {
422             setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
423         }
424     }
425 
426     /**
427      * Returns the point at which the center of the arrow merges with the first popup item.
428      */
computeAnimStartPoint(int itemsTotalHeight)429     private Point computeAnimStartPoint(int itemsTotalHeight) {
430         int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
431                 R.dimen.popup_arrow_horizontal_center_start:
432                 R.dimen.popup_arrow_horizontal_center_end);
433         if (!mIsLeftAligned) {
434             arrowCenterX = getMeasuredWidth() - arrowCenterX;
435         }
436         int arrowHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
437                 - itemsTotalHeight;
438         // The y-coordinate of edge between the arrow and the first popup item.
439         int arrowEdge = getPaddingTop() + (mIsAboveIcon ? itemsTotalHeight : arrowHeight);
440         return new Point(arrowCenterX, arrowEdge);
441     }
442 
443     /**
444      * Orients this container above or below the given icon, aligning with the left or right.
445      *
446      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
447      * - Above and left-aligned
448      * - Above and right-aligned
449      * - Below and left-aligned
450      * - Below and right-aligned
451      *
452      * So we always align left if there is enough horizontal space
453      * and align above if there is enough vertical space.
454      */
orientAboutIcon(BubbleTextView icon, int arrowHeight)455     private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
456         int width = getMeasuredWidth();
457         int height = getMeasuredHeight() + arrowHeight;
458 
459         DragLayer dragLayer = mLauncher.getDragLayer();
460         dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
461         Rect insets = dragLayer.getInsets();
462 
463         // Align left (right in RTL) if there is room.
464         int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
465         int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
466         int x = leftAlignedX;
467         boolean canBeLeftAligned = leftAlignedX + width + insets.left
468                 < dragLayer.getRight() - insets.right;
469         boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
470         if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
471             x = rightAlignedX;
472         }
473         mIsLeftAligned = x == leftAlignedX;
474         if (mIsRtl) {
475             x -= dragLayer.getWidth() - width;
476         }
477 
478         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
479         int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
480         iconWidth *= icon.getScaleX();
481         Resources resources = getResources();
482         int xOffset;
483         if (isAlignedWithStart()) {
484             // Aligning with the shortcut icon.
485             int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
486             int shortcutPaddingStart = resources.getDimensionPixelSize(
487                     R.dimen.popup_padding_start);
488             xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
489         } else {
490             // Aligning with the drag handle.
491             int shortcutDragHandleWidth = resources.getDimensionPixelSize(
492                     R.dimen.deep_shortcut_drag_handle_size);
493             int shortcutPaddingEnd = resources.getDimensionPixelSize(
494                     R.dimen.popup_padding_end);
495             xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
496         }
497         x += mIsLeftAligned ? xOffset : -xOffset;
498 
499         // Open above icon if there is room.
500         int iconHeight = icon.getIcon() != null
501                 ? icon.getIcon().getBounds().height()
502                 : icon.getHeight();
503         int y = mTempRect.top + icon.getPaddingTop() - height;
504         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
505         if (!mIsAboveIcon) {
506             y = mTempRect.top + icon.getPaddingTop() + iconHeight;
507         }
508 
509         // Insets are added later, so subtract them now.
510         if (mIsRtl) {
511             x += insets.right;
512         } else {
513             x -= insets.left;
514         }
515         y -= insets.top;
516 
517         mGravity = 0;
518         if (y + height > dragLayer.getBottom() - insets.bottom) {
519             // The container is opening off the screen, so just center it in the drag layer instead.
520             mGravity = Gravity.CENTER_VERTICAL;
521             // Put the container next to the icon, preferring the right side in ltr (left in rtl).
522             int rightSide = leftAlignedX + iconWidth - insets.left;
523             int leftSide = rightAlignedX - iconWidth - insets.left;
524             if (!mIsRtl) {
525                 if (rightSide + width < dragLayer.getRight()) {
526                     x = rightSide;
527                     mIsLeftAligned = true;
528                 } else {
529                     x = leftSide;
530                     mIsLeftAligned = false;
531                 }
532             } else {
533                 if (leftSide > dragLayer.getLeft()) {
534                     x = leftSide;
535                     mIsLeftAligned = false;
536                 } else {
537                     x = rightSide;
538                     mIsLeftAligned = true;
539                 }
540             }
541             mIsAboveIcon = true;
542         }
543 
544         setX(x);
545         setY(y);
546     }
547 
isAlignedWithStart()548     private boolean isAlignedWithStart() {
549         return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
550     }
551 
552     /**
553      * Adds an arrow view pointing at the original icon.
554      * @param horizontalOffset the horizontal offset of the arrow, so that it
555      *                              points at the center of the original icon
556      */
addArrowView(int horizontalOffset, int verticalOffset, int width, int height)557     private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
558         LayoutParams layoutParams = new LayoutParams(width, height);
559         if (mIsLeftAligned) {
560             layoutParams.gravity = Gravity.LEFT;
561             layoutParams.leftMargin = horizontalOffset;
562         } else {
563             layoutParams.gravity = Gravity.RIGHT;
564             layoutParams.rightMargin = horizontalOffset;
565         }
566         if (mIsAboveIcon) {
567             layoutParams.topMargin = verticalOffset;
568         } else {
569             layoutParams.bottomMargin = verticalOffset;
570         }
571 
572         View arrowView = new View(getContext());
573         if (Gravity.isVertical(mGravity)) {
574             // This is only true if there wasn't room for the container next to the icon,
575             // so we centered it instead. In that case we don't want to show the arrow.
576             arrowView.setVisibility(INVISIBLE);
577         } else {
578             ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
579                     width, height, !mIsAboveIcon));
580             Paint arrowPaint = arrowDrawable.getPaint();
581             // Note that we have to use getChildAt() instead of getItemViewAt(),
582             // since the latter expects the arrow which hasn't been added yet.
583             PopupItemView itemAttachedToArrow = (PopupItemView)
584                     (getChildAt(mIsAboveIcon ? getChildCount() - 1 : 0));
585             arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
586             // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
587             int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
588             arrowPaint.setPathEffect(new CornerPathEffect(radius));
589             arrowView.setBackground(arrowDrawable);
590             arrowView.setElevation(getElevation());
591         }
592         addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
593         return arrowView;
594     }
595 
596     @Override
getExtendedTouchView()597     public View getExtendedTouchView() {
598         return mOriginalIcon;
599     }
600 
601     /**
602      * Determines when the deferred drag should be started.
603      *
604      * Current behavior:
605      * - Start the drag if the touch passes a certain distance from the original touch down.
606      */
createPreDragCondition()607     public DragOptions.PreDragCondition createPreDragCondition() {
608         return new DragOptions.PreDragCondition() {
609 
610             @Override
611             public boolean shouldStartDrag(double distanceDragged) {
612                 return distanceDragged > mStartDragThreshold;
613             }
614 
615             @Override
616             public void onPreDragStart(DropTarget.DragObject dragObject) {
617                 if (mIsAboveIcon) {
618                     // Hide only the icon, keep the text visible.
619                     mOriginalIcon.setIconVisible(false);
620                     mOriginalIcon.setVisibility(VISIBLE);
621                 } else {
622                     // Hide both the icon and text.
623                     mOriginalIcon.setVisibility(INVISIBLE);
624                 }
625             }
626 
627             @Override
628             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
629                 mOriginalIcon.setIconVisible(true);
630                 if (dragStarted) {
631                     // Make sure we keep the original icon hidden while it is being dragged.
632                     mOriginalIcon.setVisibility(INVISIBLE);
633                 } else {
634                     mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
635                     if (!mIsAboveIcon) {
636                         // Show the icon but keep the text hidden.
637                         mOriginalIcon.setVisibility(VISIBLE);
638                         mOriginalIcon.setTextVisibility(false);
639                     }
640                 }
641             }
642         };
643     }
644 
645     @Override
646     public boolean onInterceptTouchEvent(MotionEvent ev) {
647         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
648             mInterceptTouchDown.set(ev.getX(), ev.getY());
649             return false;
650         }
651         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
652         return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
653                 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
654     }
655 
656     /**
657      * Updates the notification header if the original icon's badge updated.
658      */
659     public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) {
660         ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
661         PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
662         if (updatedBadges.contains(packageUser)) {
663             updateNotificationHeader();
664         }
665     }
666 
667     private void updateNotificationHeader() {
668         ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
669         BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
670         if (mNotificationItemView != null && badgeInfo != null) {
671             IconPalette palette = mOriginalIcon.getBadgePalette();
672             mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette);
673         }
674     }
675 
676     public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
677         if (mNotificationItemView == null) {
678             return;
679         }
680         ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
681         BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
682         if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) {
683             // There are no more notifications, so create an animation to remove
684             // the notifications view and expand the shortcuts view (if possible).
685             AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
686             int hiddenShortcutsHeight = 0;
687             if (mShortcutsItemView != null) {
688                 hiddenShortcutsHeight = mShortcutsItemView.getHiddenShortcutsHeight();
689                 int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
690                 // With notifications gone, all corners of shortcuts item should be rounded.
691                 mShortcutsItemView.setBackgroundWithCorners(backgroundColor,
692                         ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS);
693                 removeNotification.play(mShortcutsItemView.showAllShortcuts(mIsAboveIcon));
694             }
695             final int duration = getResources().getInteger(
696                     R.integer.config_removeNotificationViewDuration);
697             removeNotification.play(adjustItemHeights(mNotificationItemView.getHeightMinusFooter(),
698                     hiddenShortcutsHeight, duration));
699             Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
700                     .setDuration(duration);
701             fade.addListener(new AnimatorListenerAdapter() {
702                 @Override
703                 public void onAnimationEnd(Animator animation) {
704                     removeView(mNotificationItemView);
705                     mNotificationItemView = null;
706                     if (getItemCount() == 0) {
707                         close(false);
708                     }
709                 }
710             });
711             removeNotification.play(fade);
712             final long arrowScaleDuration = getResources().getInteger(
713                     R.integer.config_popupArrowOpenDuration);
714             Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
715             hideArrow.setStartDelay(0);
716             Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
717             showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
718             removeNotification.playSequentially(hideArrow, showArrow);
719             removeNotification.start();
720             return;
721         }
722         mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
723                 badgeInfo.getNotificationKeys()));
724     }
725 
726     @Override
727     protected void onWidgetsBound() {
728         if (mShortcutsItemView != null) {
729             mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
730         }
731     }
732 
733     private ObjectAnimator createArrowScaleAnim(float scale) {
734         return LauncherAnimUtils.ofPropertyValuesHolder(
735                 mArrow, new PropertyListBuilder().scale(scale).build());
736     }
737 
738     public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
739         return adjustItemHeights(heightToRemove, 0, duration);
740     }
741 
742     /**
743      * Animates the height of the notification item and the translationY of other items accordingly.
744      */
745     public Animator adjustItemHeights(int notificationHeightToRemove, int shortcutHeightToAdd,
746             int duration) {
747         if (mReduceHeightAnimatorSet != null) {
748             mReduceHeightAnimatorSet.cancel();
749         }
750         final int translateYBy = mIsAboveIcon ? notificationHeightToRemove - shortcutHeightToAdd
751                 : -notificationHeightToRemove;
752         mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet();
753         boolean removingNotification =
754                 notificationHeightToRemove == mNotificationItemView.getHeightMinusFooter();
755         boolean shouldRemoveNotificationHeightFromTop = mIsAboveIcon && removingNotification;
756         mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval(
757                 notificationHeightToRemove, shouldRemoveNotificationHeightFromTop));
758         PropertyResetListener<View, Float> resetTranslationYListener
759                 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
760         boolean itemIsAfterShortcuts = false;
761         for (int i = 0; i < getItemCount(); i++) {
762             final PopupItemView itemView = getItemViewAt(i);
763             if (itemIsAfterShortcuts) {
764                 // Every item after the shortcuts item needs to adjust for the new height.
765                 itemView.setTranslationY(itemView.getTranslationY() - shortcutHeightToAdd);
766             }
767             if (itemView == mNotificationItemView && (!mIsAboveIcon || removingNotification)) {
768                 // The notification view is already in the right place.
769                 continue;
770             }
771             ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
772                     itemView.getTranslationY() + translateYBy).setDuration(duration);
773             translateItem.addListener(resetTranslationYListener);
774             mReduceHeightAnimatorSet.play(translateItem);
775             if (itemView == mShortcutsItemView) {
776                 itemIsAfterShortcuts = true;
777             }
778         }
779         if (mIsAboveIcon) {
780             // We also need to adjust the arrow position to account for the new shortcuts height.
781             mArrow.setTranslationY(mArrow.getTranslationY() - shortcutHeightToAdd);
782         }
783         mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() {
784             @Override
785             public void onAnimationEnd(Animator animation) {
786                 if (mIsAboveIcon) {
787                     // All the items, including the notification item, translated down, but the
788                     // container itself did not. This means the items would jump back to their
789                     // original translation unless we update the container's translationY here.
790                     setTranslationY(getTranslationY() + translateYBy);
791                     mArrow.setTranslationY(0);
792                 }
793                 mReduceHeightAnimatorSet = null;
794             }
795         });
796         return mReduceHeightAnimatorSet;
797     }
798 
799     @Override
800     public boolean supportsAppInfoDropTarget() {
801         return true;
802     }
803 
804     @Override
805     public boolean supportsDeleteDropTarget() {
806         return false;
807     }
808 
809     @Override
810     public float getIntrinsicIconScaleFactor() {
811         return 1f;
812     }
813 
814     @Override
815     public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
816             boolean success) {
817         if (!success) {
818             d.dragView.remove();
819             mLauncher.showWorkspace(true);
820             mLauncher.getDropTargetBar().onDragEnd();
821         }
822     }
823 
824     @Override
825     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
826         // Either the original icon or one of the shortcuts was dragged.
827         // Hide the container, but don't remove it yet because that interferes with touch events.
828         mDeferContainerRemoval = true;
829         animateClose();
830     }
831 
832     @Override
833     public void onDragEnd() {
834         if (!mIsOpen) {
835             if (mOpenCloseAnimator != null) {
836                 // Close animation is running.
837                 mDeferContainerRemoval = false;
838             } else {
839                 // Close animation is not running.
840                 if (mDeferContainerRemoval) {
841                     closeComplete();
842                 }
843             }
844         }
845     }
846 
847     @Override
848     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
849         target.itemType = ItemType.DEEPSHORTCUT;
850         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
851     }
852 
853     @Override
854     protected void handleClose(boolean animate) {
855         if (animate) {
856             animateClose();
857         } else {
858             closeComplete();
859         }
860     }
861 
862     protected void animateClose() {
863         if (!mIsOpen) {
864             return;
865         }
866         mEndRect.setEmpty();
867         if (mOpenCloseAnimator != null) {
868             Outline outline = new Outline();
869             getOutlineProvider().getOutline(this, outline);
870             outline.getRect(mEndRect);
871             mOpenCloseAnimator.cancel();
872         }
873         mIsOpen = false;
874 
875         final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
876         final Resources res = getResources();
877         final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
878         final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
879 
880         // Rectangular reveal (reversed).
881         int itemsTotalHeight = 0;
882         for (int i = 0; i < getItemCount(); i++) {
883             itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
884         }
885         Point startPoint = computeAnimStartPoint(itemsTotalHeight);
886         int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
887         float radius = getItemViewAt(0).getBackgroundRadius();
888         mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
889         if (mEndRect.isEmpty()) {
890             mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
891         }
892         final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider(
893                 radius, radius, mStartRect, mEndRect).createRevealAnimator(this, true);
894         revealAnim.setDuration(revealDuration);
895         revealAnim.setInterpolator(revealInterpolator);
896         closeAnim.play(revealAnim);
897 
898         Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
899         fadeOut.setDuration(revealDuration);
900         fadeOut.setInterpolator(revealInterpolator);
901         closeAnim.play(fadeOut);
902 
903         // Animate original icon's text back in.
904         Animator fadeText = mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */);
905         fadeText.setDuration(revealDuration);
906         closeAnim.play(fadeText);
907 
908         closeAnim.addListener(new AnimatorListenerAdapter() {
909             @Override
910             public void onAnimationEnd(Animator animation) {
911                 mOpenCloseAnimator = null;
912                 if (mDeferContainerRemoval) {
913                     setVisibility(INVISIBLE);
914                 } else {
915                     closeComplete();
916                 }
917             }
918         });
919         mOpenCloseAnimator = closeAnim;
920         closeAnim.start();
921         mOriginalIcon.forceHideBadge(false);
922     }
923 
924     /**
925      * Closes the folder without animation.
926      */
927     protected void closeComplete() {
928         if (mOpenCloseAnimator != null) {
929             mOpenCloseAnimator.cancel();
930             mOpenCloseAnimator = null;
931         }
932         mIsOpen = false;
933         mDeferContainerRemoval = false;
934         mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
935         mOriginalIcon.forceHideBadge(false);
936         mLauncher.getDragController().removeDragListener(this);
937         mLauncher.getDragLayer().removeView(this);
938     }
939 
940     @Override
941     protected boolean isOfType(int type) {
942         return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
943     }
944 
945     /**
946      * Returns a DeepShortcutsContainer which is already open or null
947      */
948     public static PopupContainerWithArrow getOpen(Launcher launcher) {
949         return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
950     }
951 
952     @Override
953     public int getLogContainerType() {
954         return ContainerType.DEEPSHORTCUTS;
955     }
956 }
957