Home | History | Annotate | Download | only in popup
      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 
    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 
    120     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
    121         this(context, attrs, 0);
    122     }
    123 
    124     public PopupContainerWithArrow(Context context) {
    125         this(context, null, 0);
    126     }
    127 
    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      */
    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 
    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 
    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
    538             public void onPreDragStart(DropTarget.DragObject dragObject) {
    539                 mOriginalIcon.setVisibility(INVISIBLE);
    540             }
    541 
    542             @Override
    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
    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      */
    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 
    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 
    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
    641     protected void onWidgetsBound() {
    642         if (mShortcutsItemView != null) {
    643             mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
    644         }
    645     }
    646 
    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      */
    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
    691     public boolean supportsAppInfoDropTarget() {
    692         return true;
    693     }
    694 
    695     @Override
    696     public boolean supportsDeleteDropTarget() {
    697         return false;
    698     }
    699 
    700     @Override
    701     public float getIntrinsicIconScaleFactor() {
    702         return 1f;
    703     }
    704 
    705     @Override
    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
    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
    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
    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
    745     protected void handleClose(boolean animate) {
    746         if (animate) {
    747             animateClose();
    748         } else {
    749             closeComplete();
    750         }
    751     }
    752 
    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      */
    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
    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      */
    847     public static PopupContainerWithArrow getOpen(Launcher launcher) {
    848         return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
    849     }
    850 
    851     @Override
    852     public int getLogContainerType() {
    853         return ContainerType.DEEPSHORTCUTS;
    854     }
    855 }
    856