2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.launcher3.popup;
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.SuppressLint;
26 import android.annotation.TargetApi;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Color;
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;
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.TriangleShape;
67 import com.android.launcher3.notification.NotificationItemView;
68 import com.android.launcher3.shortcuts.DeepShortcutView;
69 import com.android.launcher3.shortcuts.ShortcutsItemView;
70 import com.android.launcher3.util.PackageUserKey;
72 import java.util.Collections;
73 import java.util.List;
76 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
77 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
78 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
81 * A container for shortcuts to deep links within apps.
83 @TargetApi(Build.VERSION_CODES.N)
84 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
85 DragController.DragListener {
87 protected final Launcher mLauncher;
88 private final int mStartDragThreshold;
89 private LauncherAccessibilityDelegate mAccessibilityDelegate;
90 private final boolean mIsRtl;
92 public ShortcutsItemView mShortcutsItemView;
93 private NotificationItemView mNotificationItemView;
95 protected BubbleTextView mOriginalIcon;
96 private final Rect mTempRect = new Rect();
97 private PointF mInterceptTouchDown = new PointF();
98 private boolean mIsLeftAligned;
99 protected boolean mIsAboveIcon;
102 protected Animator mOpenCloseAnimator;
103 private boolean mDeferContainerRemoval;
105 public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
106 super(context, attrs, defStyleAttr);
107 mLauncher = Launcher.getLauncher(context);
109 mStartDragThreshold = getResources().getDimensionPixelSize(
110 R.dimen.deep_shortcuts_start_drag_threshold);
111 // TODO: make sure the delegate works for all items, not just shortcuts.
112 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
113 mIsRtl = Utilities.isRtl(getResources());
116 public PopupContainerWithArrow(Context context, AttributeSet attrs) {
117 this(context, attrs, 0);
120 public PopupContainerWithArrow(Context context) {
121 this(context, null, 0);
124 public LauncherAccessibilityDelegate getAccessibilityDelegate() {
125 return mAccessibilityDelegate;
129 * Shows the notifications and deep shortcuts associated with {@param icon}.
130 * @return the container if shown or null.
132 public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
133 Launcher launcher = Launcher.getLauncher(icon.getContext());
134 if (getOpen(launcher) != null) {
135 // There is already an items container open, so don't open this one.
139 ItemInfo itemInfo = (ItemInfo) icon.getTag();
140 List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
141 String[] notificationKeys = launcher.getPopupDataProvider()
142 .getNotificationKeysForItem(itemInfo);
143 if (shortcutIds.size() > 0 || notificationKeys.length > 0) {
144 final PopupContainerWithArrow container =
145 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
146 R.layout.popup_container, launcher.getDragLayer(), false);
147 container.setVisibility(View.INVISIBLE);
148 launcher.getDragLayer().addView(container);
149 container.populateAndShow(icon, shortcutIds, notificationKeys);
155 public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
156 final String[] notificationKeys) {
157 final Resources resources = getResources();
158 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
159 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
160 final int arrowHorizontalOffset = resources.getDimensionPixelSize(
161 R.dimen.deep_shortcuts_arrow_horizontal_offset);
162 final int arrowVerticalOffset = resources.getDimensionPixelSize(
163 R.dimen.deep_shortcuts_arrow_vertical_offset);
165 // Add dummy views first, and populate with real info when ready.
166 PopupPopulator.Item[] itemsToPopulate = PopupPopulator
167 .getItemsToPopulate(shortcutIds, notificationKeys);
168 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
170 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
171 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
173 boolean reverseOrder = mIsAboveIcon;
176 mNotificationItemView = null;
177 mShortcutsItemView = null;
178 itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
179 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
181 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
182 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
185 ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
186 List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
187 ? Collections.EMPTY_LIST
188 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
189 if (mNotificationItemView != null) {
190 BadgeInfo badgeInfo = mLauncher.getPopupDataProvider()
191 .getBadgeInfoForItem(originalItemInfo);
192 updateNotificationHeader(badgeInfo);
196 mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
197 mArrow.setPivotX(arrowWidth / 2);
198 mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
202 mOriginalIcon = originalIcon;
204 mLauncher.getDragController().addDragListener(this);
206 // Load the shortcuts on a background thread and update the container as it animates.
207 final Looper workerLooper = LauncherModel.getWorkerLooper();
208 new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
209 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
210 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView));
213 private void addDummyViews(BubbleTextView originalIcon,
214 PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
215 final Resources res = getResources();
216 final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
217 final LayoutInflater inflater = mLauncher.getLayoutInflater();
218 int numItems = itemTypesToPopulate.length;
219 for (int i = 0; i < numItems; i++) {
220 PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
221 final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
223 if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
224 mNotificationItemView = (NotificationItemView) item;
225 int footerHeight = notificationFooterHasIcons ?
226 res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
227 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
230 boolean itemIsFollowedByDifferentType = i < numItems - 1
231 && itemTypesToPopulate[i + 1] != itemTypeToPopulate;
233 item.setAccessibilityDelegate(mAccessibilityDelegate);
234 if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
235 if (mShortcutsItemView == null) {
236 mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
237 R.layout.shortcuts_item, this, false);
238 addView(mShortcutsItemView);
240 mShortcutsItemView.addDeepShortcutView((DeepShortcutView) item);
241 if (itemIsFollowedByDifferentType) {
242 ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
246 if (itemIsFollowedByDifferentType) {
247 ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
251 // TODO: update this, since not all items are shortcuts
252 setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
253 numItems, originalIcon.getContentDescription().toString()));
256 protected PopupItemView getItemViewAt(int index) {
258 // Opening down, so arrow is the first view.
261 return (PopupItemView) getChildAt(index);
264 protected int getItemCount() {
265 // All children except the arrow are items.
266 return getChildCount() - 1;
269 private void animateOpen() {
270 setVisibility(View.VISIBLE);
273 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
274 final int itemCount = getItemCount();
276 final long duration = getResources().getInteger(
277 R.integer.config_deepShortcutOpenDuration);
278 final long arrowScaleDuration = getResources().getInteger(
279 R.integer.config_deepShortcutArrowOpenDuration);
280 final long arrowScaleDelay = duration - arrowScaleDuration;
281 final long stagger = getResources().getInteger(
282 R.integer.config_deepShortcutOpenStagger);
283 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
286 DecelerateInterpolator interpolator = new DecelerateInterpolator();
287 for (int i = 0; i < itemCount; i++) {
288 final PopupItemView popupItemView = getItemViewAt(i);
289 popupItemView.setVisibility(INVISIBLE);
290 popupItemView.setAlpha(0);
292 Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
293 anim.addListener(new AnimatorListenerAdapter() {
295 public void onAnimationStart(Animator animation) {
296 popupItemView.setVisibility(VISIBLE);
299 anim.setDuration(duration);
300 int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
301 anim.setStartDelay(stagger * animationIndex);
302 anim.setInterpolator(interpolator);
303 shortcutAnims.play(anim);
305 Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
306 fadeAnim.setInterpolator(fadeInterpolator);
307 // We want the shortcut to be fully opaque before the arrow starts animating.
308 fadeAnim.setDuration(arrowScaleDelay);
309 shortcutAnims.play(fadeAnim);
311 shortcutAnims.addListener(new AnimatorListenerAdapter() {
313 public void onAnimationEnd(Animator animation) {
314 mOpenCloseAnimator = null;
315 Utilities.sendCustomAccessibilityEvent(
316 PopupContainerWithArrow.this,
317 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
318 getContext().getString(R.string.action_deep_shortcut));
325 Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
326 arrowScale.setStartDelay(arrowScaleDelay);
327 shortcutAnims.play(arrowScale);
329 mOpenCloseAnimator = shortcutAnims;
330 shortcutAnims.start();
334 * Orients this container above or below the given icon, aligning with the left or right.
336 * These are the preferred orientations, in order (RTL prefers right-aligned over left):
337 * - Above and left-aligned
338 * - Above and right-aligned
339 * - Below and left-aligned
340 * - Below and right-aligned
342 * So we always align left if there is enough horizontal space
343 * and align above if there is enough vertical space.
345 private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
346 int width = getMeasuredWidth();
347 int height = getMeasuredHeight() + arrowHeight;
349 DragLayer dragLayer = mLauncher.getDragLayer();
350 dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
351 Rect insets = dragLayer.getInsets();
353 // Align left (right in RTL) if there is room.
354 int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
355 int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
356 int x = leftAlignedX;
357 boolean canBeLeftAligned = leftAlignedX + width + insets.left
358 < dragLayer.getRight() - insets.right;
359 boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
360 if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
363 mIsLeftAligned = x == leftAlignedX;
365 x -= dragLayer.getWidth() - width;
368 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
369 int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
370 iconWidth *= icon.getScaleX();
371 Resources resources = getResources();
373 if (isAlignedWithStart()) {
374 // Aligning with the shortcut icon.
375 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
376 int shortcutPaddingStart = resources.getDimensionPixelSize(
377 R.dimen.deep_shortcut_padding_start);
378 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
380 // Aligning with the drag handle.
381 int shortcutDragHandleWidth = resources.getDimensionPixelSize(
382 R.dimen.deep_shortcut_drag_handle_size);
383 int shortcutPaddingEnd = resources.getDimensionPixelSize(
384 R.dimen.deep_shortcut_padding_end);
385 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
387 x += mIsLeftAligned ? xOffset : -xOffset;
389 // Open above icon if there is room.
390 int iconHeight = icon.getIcon().getBounds().height();
391 int y = mTempRect.top + icon.getPaddingTop() - height;
392 mIsAboveIcon = y > dragLayer.getTop() + insets.top;
394 y = mTempRect.top + icon.getPaddingTop() + iconHeight;
397 // Insets are added later, so subtract them now.
405 if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
406 // The container is opening off the screen, so just center it in the drag layer instead.
407 ((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
408 // Put the container next to the icon, preferring the right side in ltr (left in rtl).
409 int rightSide = leftAlignedX + iconWidth - insets.left;
410 int leftSide = rightAlignedX - iconWidth - insets.left;
412 if (rightSide + width < dragLayer.getRight()) {
414 mIsLeftAligned = true;
417 mIsLeftAligned = false;
420 if (leftSide > dragLayer.getLeft()) {
422 mIsLeftAligned = false;
425 mIsLeftAligned = true;
431 if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
432 // If we are still off screen, center horizontally too.
433 ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
436 int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
437 if (!Gravity.isHorizontal(gravity)) {
440 if (!Gravity.isVertical(gravity)) {
445 private boolean isAlignedWithStart() {
446 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
450 * Adds an arrow view pointing at the original icon.
451 * @param horizontalOffset the horizontal offset of the arrow, so that it
452 * points at the center of the original icon
454 private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
455 LayoutParams layoutParams = new LayoutParams(width, height);
456 if (mIsLeftAligned) {
457 layoutParams.gravity = Gravity.LEFT;
458 layoutParams.leftMargin = horizontalOffset;
460 layoutParams.gravity = Gravity.RIGHT;
461 layoutParams.rightMargin = horizontalOffset;
464 layoutParams.topMargin = verticalOffset;
466 layoutParams.bottomMargin = verticalOffset;
469 View arrowView = new View(getContext());
470 if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
471 // This is only true if there wasn't room for the container next to the icon,
472 // so we centered it instead. In that case we don't want to show the arrow.
473 arrowView.setVisibility(INVISIBLE);
475 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
476 width, height, !mIsAboveIcon));
477 arrowDrawable.getPaint().setColor(Color.WHITE);
478 arrowView.setBackground(arrowDrawable);
479 arrowView.setElevation(getElevation());
481 addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
486 public View getExtendedTouchView() {
487 return mOriginalIcon;
491 * Determines when the deferred drag should be started.
494 * - Start the drag if the touch passes a certain distance from the original touch down.
496 public DragOptions.PreDragCondition createPreDragCondition() {
497 return new DragOptions.PreDragCondition() {
499 public boolean shouldStartDrag(double distanceDragged) {
500 return distanceDragged > mStartDragThreshold;
504 public void onPreDragStart(DropTarget.DragObject dragObject) {
505 mOriginalIcon.setVisibility(INVISIBLE);
509 public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
511 mOriginalIcon.setVisibility(VISIBLE);
512 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
514 mOriginalIcon.setTextVisibility(false);
522 public boolean onInterceptTouchEvent(MotionEvent ev) {
523 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
524 mInterceptTouchDown.set(ev.getX(), ev.getY());
527 // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
528 return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
529 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
533 * We need to handle touch events to prevent them from falling through to the workspace below.
535 @SuppressLint("ClickableViewAccessibility")
537 public boolean onTouchEvent(MotionEvent ev) {
542 * Updates the notification header to reflect the badge info. Since this can be called
543 * for any badge info (not necessarily the one associated with this app), we first
544 * check that the ItemInfo matches the one of this popup.
546 public void updateNotificationHeader(BadgeInfo badgeInfo, ItemInfo originalItemInfo) {
547 if (originalItemInfo != mOriginalIcon.getTag()) {
550 updateNotificationHeader(badgeInfo);
553 private void updateNotificationHeader(BadgeInfo badgeInfo) {
554 if (mNotificationItemView != null && badgeInfo != null) {
555 mNotificationItemView.updateHeader(badgeInfo.getNotificationCount());
559 public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
560 if (mNotificationItemView == null) {
563 ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
564 BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
565 if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
566 AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
567 final int duration = getResources().getInteger(
568 R.integer.config_removeNotificationViewDuration);
569 final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
570 removeNotification.play(reduceNotificationViewHeight(
571 mNotificationItemView.getHeight() + spacing, duration, mNotificationItemView));
572 final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
573 : mNotificationItemView;
574 if (removeMarginView != null) {
575 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
576 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
578 public void onAnimationUpdate(ValueAnimator valueAnimator) {
579 ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
580 = (int) (spacing * (float) valueAnimator.getAnimatedValue());
583 removeNotification.play(removeMargin);
585 Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
586 .setDuration(duration);
587 fade.addListener(new AnimatorListenerAdapter() {
589 public void onAnimationEnd(Animator animation) {
590 removeView(mNotificationItemView);
591 if (getItemCount() == 0) {
597 removeNotification.play(fade);
598 final long arrowScaleDuration = getResources().getInteger(
599 R.integer.config_deepShortcutArrowOpenDuration);
600 Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
601 hideArrow.setStartDelay(0);
602 Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
603 showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
604 removeNotification.playSequentially(hideArrow, showArrow);
605 removeNotification.start();
608 mNotificationItemView.trimNotifications(badgeInfo.getNotificationKeys());
611 private ObjectAnimator createArrowScaleAnim(float scale) {
612 return LauncherAnimUtils.ofPropertyValuesHolder(
613 mArrow, new PropertyListBuilder().scale(scale).build());
616 * Animates the height of the notification item and the translationY of other items accordingly.
618 public Animator reduceNotificationViewHeight(int heightToRemove, int duration,
619 NotificationItemView notificationItem) {
620 final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
621 AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet();
622 animatorSet.play(notificationItem.animateHeightRemoval(heightToRemove));
623 PropertyResetListener<View, Float> resetTranslationYListener
624 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
625 for (int i = 0; i < getItemCount(); i++) {
626 final PopupItemView itemView = getItemViewAt(i);
627 if (!mIsAboveIcon && itemView == notificationItem) {
628 // The notification view is already in the right place when container is below icon.
631 ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
632 itemView.getTranslationY() + translateYBy).setDuration(duration);
633 translateItem.addListener(resetTranslationYListener);
634 animatorSet.play(translateItem);
637 // All the items, including the notification item, translated down, but the
638 // container itself did not. This means the items would jump back to their
639 // original translation unless we update the container's translationY here.
640 animatorSet.addListener(new AnimatorListenerAdapter() {
642 public void onAnimationEnd(Animator animation) {
643 setTranslationY(getTranslationY() + translateYBy);
650 public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
651 return reduceNotificationViewHeight(heightToRemove, duration, mNotificationItemView);
655 public boolean supportsAppInfoDropTarget() {
660 public boolean supportsDeleteDropTarget() {
665 public float getIntrinsicIconScaleFactor() {
670 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
674 mLauncher.showWorkspace(true);
675 mLauncher.getDropTargetBar().onDragEnd();
680 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
681 // Either the original icon or one of the shortcuts was dragged.
682 // Hide the container, but don't remove it yet because that interferes with touch events.
683 mDeferContainerRemoval = true;
688 public void onDragEnd() {
690 if (mOpenCloseAnimator != null) {
691 // Close animation is running.
692 mDeferContainerRemoval = false;
694 // Close animation is not running.
695 if (mDeferContainerRemoval) {
703 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
704 target.itemType = ItemType.DEEPSHORTCUT;
705 targetParent.containerType = ContainerType.DEEPSHORTCUTS;
709 protected void handleClose(boolean animate) {
717 protected void animateClose() {
721 if (mOpenCloseAnimator != null) {
722 mOpenCloseAnimator.cancel();
726 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
727 final int itemCount = getItemCount();
728 int numOpenShortcuts = 0;
729 for (int i = 0; i < itemCount; i++) {
730 if (getItemViewAt(i).isOpenOrOpening()) {
734 final long duration = getResources().getInteger(
735 R.integer.config_deepShortcutCloseDuration);
736 final long arrowScaleDuration = getResources().getInteger(
737 R.integer.config_deepShortcutArrowOpenDuration);
738 final long stagger = getResources().getInteger(
739 R.integer.config_deepShortcutCloseStagger);
740 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
742 int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
743 for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
744 final PopupItemView view = getItemViewAt(i);
746 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
747 int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
748 : numOpenShortcuts - i - 1;
749 anim.setStartDelay(stagger * animationIndex);
751 Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
752 // Don't start fading until the arrow is gone.
753 fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
754 fadeAnim.setDuration(duration - arrowScaleDuration);
755 fadeAnim.setInterpolator(fadeInterpolator);
756 shortcutAnims.play(fadeAnim);
757 anim.addListener(new AnimatorListenerAdapter() {
759 public void onAnimationEnd(Animator animation) {
760 view.setVisibility(INVISIBLE);
763 shortcutAnims.play(anim);
765 Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
766 arrowAnim.setStartDelay(0);
767 shortcutAnims.play(arrowAnim);
769 shortcutAnims.addListener(new AnimatorListenerAdapter() {
771 public void onAnimationEnd(Animator animation) {
772 mOpenCloseAnimator = null;
773 if (mDeferContainerRemoval) {
774 setVisibility(INVISIBLE);
780 mOpenCloseAnimator = shortcutAnims;
781 shortcutAnims.start();
785 * Closes the folder without animation.
787 protected void closeComplete() {
788 if (mOpenCloseAnimator != null) {
789 mOpenCloseAnimator.cancel();
790 mOpenCloseAnimator = null;
793 mDeferContainerRemoval = false;
794 boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
795 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
796 mOriginalIcon.setTextVisibility(!isInHotseat);
797 mLauncher.getDragController().removeDragListener(this);
798 mLauncher.getDragLayer().removeView(this);
802 protected boolean isOfType(int type) {
803 return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
807 * Returns a DeepShortcutsContainer which is already open or null
809 public static PopupContainerWithArrow getOpen(Launcher launcher) {
810 return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
814 public int getLogContainerType() {
815 return ContainerType.DEEPSHORTCUTS;