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.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;
46 import com.android.launcher3.AbstractFloatingView;
47 import com.android.launcher3.BubbleTextView;
48 import com.android.launcher3.DragSource;
49 import com.android.launcher3.DropTarget;
50 import com.android.launcher3.FastBitmapDrawable;
51 import com.android.launcher3.ItemInfo;
52 import com.android.launcher3.Launcher;
53 import com.android.launcher3.LauncherAnimUtils;
54 import com.android.launcher3.LauncherModel;
55 import com.android.launcher3.LauncherSettings;
56 import com.android.launcher3.LogAccelerateInterpolator;
57 import com.android.launcher3.R;
58 import com.android.launcher3.Utilities;
59 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
60 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
61 import com.android.launcher3.anim.PropertyListBuilder;
62 import com.android.launcher3.anim.PropertyResetListener;
63 import com.android.launcher3.badge.BadgeInfo;
64 import com.android.launcher3.dragndrop.DragController;
65 import com.android.launcher3.dragndrop.DragLayer;
66 import com.android.launcher3.dragndrop.DragOptions;
67 import com.android.launcher3.graphics.IconPalette;
68 import com.android.launcher3.graphics.TriangleShape;
69 import com.android.launcher3.notification.NotificationItemView;
70 import com.android.launcher3.notification.NotificationKeyData;
71 import com.android.launcher3.shortcuts.DeepShortcutManager;
72 import com.android.launcher3.shortcuts.DeepShortcutView;
73 import com.android.launcher3.shortcuts.ShortcutsItemView;
74 import com.android.launcher3.util.PackageUserKey;
76 import java.util.Collections;
77 import java.util.List;
81 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
82 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
83 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
86 * A container for shortcuts to deep links within apps.
88 @TargetApi(Build.VERSION_CODES.N)
89 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
90 DragController.DragListener {
92 protected final Launcher mLauncher;
93 private final int mStartDragThreshold;
94 private LauncherAccessibilityDelegate mAccessibilityDelegate;
95 private final boolean mIsRtl;
97 public ShortcutsItemView mShortcutsItemView;
98 private NotificationItemView mNotificationItemView;
100 protected BubbleTextView mOriginalIcon;
101 private final Rect mTempRect = new Rect();
102 private PointF mInterceptTouchDown = new PointF();
103 private boolean mIsLeftAligned;
104 protected boolean mIsAboveIcon;
107 protected Animator mOpenCloseAnimator;
108 private boolean mDeferContainerRemoval;
109 private AnimatorSet mReduceHeightAnimatorSet;
111 public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
112 super(context, attrs, defStyleAttr);
113 mLauncher = Launcher.getLauncher(context);
115 mStartDragThreshold = getResources().getDimensionPixelSize(
116 R.dimen.deep_shortcuts_start_drag_threshold);
117 // TODO: make sure the delegate works for all items, not just shortcuts.
118 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
119 mIsRtl = Utilities.isRtl(getResources());
122 public PopupContainerWithArrow(Context context, AttributeSet attrs) {
123 this(context, attrs, 0);
126 public PopupContainerWithArrow(Context context) {
127 this(context, null, 0);
130 public LauncherAccessibilityDelegate getAccessibilityDelegate() {
131 return mAccessibilityDelegate;
135 * Shows the notifications and deep shortcuts associated with {@param icon}.
136 * @return the container if shown or null.
138 public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
139 Launcher launcher = Launcher.getLauncher(icon.getContext());
140 if (getOpen(launcher) != null) {
141 // There is already an items container open, so don't open this one.
145 ItemInfo itemInfo = (ItemInfo) icon.getTag();
146 if (!DeepShortcutManager.supportsShortcuts(itemInfo)) {
150 PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
151 List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo);
152 List<NotificationKeyData> notificationKeys = popupDataProvider
153 .getNotificationKeysForItem(itemInfo);
154 List<SystemShortcut> systemShortcuts = popupDataProvider
155 .getEnabledSystemShortcutsForItem(itemInfo);
157 final PopupContainerWithArrow container =
158 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
159 R.layout.popup_container, launcher.getDragLayer(), false);
160 container.setVisibility(View.INVISIBLE);
161 launcher.getDragLayer().addView(container);
162 container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts);
166 public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
167 final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
168 final Resources resources = getResources();
169 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
170 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
171 final int arrowVerticalOffset = resources.getDimensionPixelSize(
172 R.dimen.popup_arrow_vertical_offset);
174 mOriginalIcon = originalIcon;
176 // Add dummy views first, and populate with real info when ready.
177 PopupPopulator.Item[] itemsToPopulate = PopupPopulator
178 .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
179 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
181 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
182 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
184 boolean reverseOrder = mIsAboveIcon;
187 mNotificationItemView = null;
188 mShortcutsItemView = null;
189 itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
190 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
192 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
193 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
196 ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
197 List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
198 ? Collections.EMPTY_LIST
199 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
200 List<View> systemShortcutViews = mShortcutsItemView == null
201 ? Collections.EMPTY_LIST
202 : mShortcutsItemView.getSystemShortcutViews(reverseOrder);
203 if (mNotificationItemView != null) {
204 updateNotificationHeader();
208 final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
209 R.dimen.popup_arrow_horizontal_offset_start :
210 R.dimen.popup_arrow_horizontal_offset_end);
211 mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
212 mArrow.setPivotX(arrowWidth / 2);
213 mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
217 mLauncher.getDragController().addDragListener(this);
218 mOriginalIcon.forceHideBadge(true);
220 // Load the shortcuts on a background thread and update the container as it animates.
221 final Looper workerLooper = LauncherModel.getWorkerLooper();
222 new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
223 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
224 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
225 systemShortcuts, systemShortcutViews));
228 private void addDummyViews(BubbleTextView originalIcon,
229 PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
230 final Resources res = getResources();
231 final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
232 final LayoutInflater inflater = mLauncher.getLayoutInflater();
234 int numItems = itemTypesToPopulate.length;
235 for (int i = 0; i < numItems; i++) {
236 PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
237 PopupPopulator.Item nextItemTypeToPopulate =
238 i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
239 final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
241 if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
242 mNotificationItemView = (NotificationItemView) item;
243 int footerHeight = notificationFooterHasIcons ?
244 res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
245 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
248 boolean shouldAddBottomMargin = nextItemTypeToPopulate != null
249 && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
251 item.setAccessibilityDelegate(mAccessibilityDelegate);
252 if (itemTypeToPopulate.isShortcut) {
253 if (mShortcutsItemView == null) {
254 mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
255 R.layout.shortcuts_item, this, false);
256 addView(mShortcutsItemView);
258 mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
259 if (shouldAddBottomMargin) {
260 ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
264 if (shouldAddBottomMargin) {
265 ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
269 // TODO: update this, since not all items are shortcuts
270 setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
271 numItems, originalIcon.getContentDescription().toString()));
274 protected PopupItemView getItemViewAt(int index) {
276 // Opening down, so arrow is the first view.
279 return (PopupItemView) getChildAt(index);
282 protected int getItemCount() {
283 // All children except the arrow are items.
284 return getChildCount() - 1;
287 private void animateOpen() {
288 setVisibility(View.VISIBLE);
291 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
292 final int itemCount = getItemCount();
294 final long duration = getResources().getInteger(
295 R.integer.config_deepShortcutOpenDuration);
296 final long arrowScaleDuration = getResources().getInteger(
297 R.integer.config_deepShortcutArrowOpenDuration);
298 final long arrowScaleDelay = duration - arrowScaleDuration;
299 final long stagger = getResources().getInteger(
300 R.integer.config_deepShortcutOpenStagger);
301 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
304 DecelerateInterpolator interpolator = new DecelerateInterpolator();
305 for (int i = 0; i < itemCount; i++) {
306 final PopupItemView popupItemView = getItemViewAt(i);
307 popupItemView.setVisibility(INVISIBLE);
308 popupItemView.setAlpha(0);
310 Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
311 anim.addListener(new AnimatorListenerAdapter() {
313 public void onAnimationStart(Animator animation) {
314 popupItemView.setVisibility(VISIBLE);
317 anim.setDuration(duration);
318 int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
319 anim.setStartDelay(stagger * animationIndex);
320 anim.setInterpolator(interpolator);
321 shortcutAnims.play(anim);
323 Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
324 fadeAnim.setInterpolator(fadeInterpolator);
325 // We want the shortcut to be fully opaque before the arrow starts animating.
326 fadeAnim.setDuration(arrowScaleDelay);
327 shortcutAnims.play(fadeAnim);
329 shortcutAnims.addListener(new AnimatorListenerAdapter() {
331 public void onAnimationEnd(Animator animation) {
332 mOpenCloseAnimator = null;
333 Utilities.sendCustomAccessibilityEvent(
334 PopupContainerWithArrow.this,
335 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
336 getContext().getString(R.string.action_deep_shortcut));
343 Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
344 arrowScale.setStartDelay(arrowScaleDelay);
345 shortcutAnims.play(arrowScale);
347 mOpenCloseAnimator = shortcutAnims;
348 shortcutAnims.start();
352 * Orients this container above or below the given icon, aligning with the left or right.
354 * These are the preferred orientations, in order (RTL prefers right-aligned over left):
355 * - Above and left-aligned
356 * - Above and right-aligned
357 * - Below and left-aligned
358 * - Below and right-aligned
360 * So we always align left if there is enough horizontal space
361 * and align above if there is enough vertical space.
363 private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
364 int width = getMeasuredWidth();
365 int height = getMeasuredHeight() + arrowHeight;
367 DragLayer dragLayer = mLauncher.getDragLayer();
368 dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
369 Rect insets = dragLayer.getInsets();
371 // Align left (right in RTL) if there is room.
372 int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
373 int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
374 int x = leftAlignedX;
375 boolean canBeLeftAligned = leftAlignedX + width + insets.left
376 < dragLayer.getRight() - insets.right;
377 boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
378 if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
381 mIsLeftAligned = x == leftAlignedX;
383 x -= dragLayer.getWidth() - width;
386 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
387 int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
388 iconWidth *= icon.getScaleX();
389 Resources resources = getResources();
391 if (isAlignedWithStart()) {
392 // Aligning with the shortcut icon.
393 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
394 int shortcutPaddingStart = resources.getDimensionPixelSize(
395 R.dimen.popup_padding_start);
396 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
398 // Aligning with the drag handle.
399 int shortcutDragHandleWidth = resources.getDimensionPixelSize(
400 R.dimen.deep_shortcut_drag_handle_size);
401 int shortcutPaddingEnd = resources.getDimensionPixelSize(
402 R.dimen.popup_padding_end);
403 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
405 x += mIsLeftAligned ? xOffset : -xOffset;
407 // Open above icon if there is room.
408 int iconHeight = icon.getIcon().getBounds().height();
409 int y = mTempRect.top + icon.getPaddingTop() - height;
410 mIsAboveIcon = y > dragLayer.getTop() + insets.top;
412 y = mTempRect.top + icon.getPaddingTop() + iconHeight;
415 // Insets are added later, so subtract them now.
423 if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
424 // The container is opening off the screen, so just center it in the drag layer instead.
425 ((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
426 // Put the container next to the icon, preferring the right side in ltr (left in rtl).
427 int rightSide = leftAlignedX + iconWidth - insets.left;
428 int leftSide = rightAlignedX - iconWidth - insets.left;
430 if (rightSide + width < dragLayer.getRight()) {
432 mIsLeftAligned = true;
435 mIsLeftAligned = false;
438 if (leftSide > dragLayer.getLeft()) {
440 mIsLeftAligned = false;
443 mIsLeftAligned = true;
449 if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
450 // If we are still off screen, center horizontally too.
451 ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
454 int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
455 if (!Gravity.isHorizontal(gravity)) {
458 if (!Gravity.isVertical(gravity)) {
463 private boolean isAlignedWithStart() {
464 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
468 * Adds an arrow view pointing at the original icon.
469 * @param horizontalOffset the horizontal offset of the arrow, so that it
470 * points at the center of the original icon
472 private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
473 LayoutParams layoutParams = new LayoutParams(width, height);
474 if (mIsLeftAligned) {
475 layoutParams.gravity = Gravity.LEFT;
476 layoutParams.leftMargin = horizontalOffset;
478 layoutParams.gravity = Gravity.RIGHT;
479 layoutParams.rightMargin = horizontalOffset;
482 layoutParams.topMargin = verticalOffset;
484 layoutParams.bottomMargin = verticalOffset;
487 View arrowView = new View(getContext());
488 if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
489 // This is only true if there wasn't room for the container next to the icon,
490 // so we centered it instead. In that case we don't want to show the arrow.
491 arrowView.setVisibility(INVISIBLE);
493 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
494 width, height, !mIsAboveIcon));
495 Paint arrowPaint = arrowDrawable.getPaint();
496 // Note that we have to use getChildAt() instead of getItemViewAt(),
497 // since the latter expects the arrow which hasn't been added yet.
498 PopupItemView itemAttachedToArrow = (PopupItemView)
499 (getChildAt(mIsAboveIcon ? getChildCount() - 1 : 0));
500 arrowPaint.setColor(itemAttachedToArrow.getArrowColor(mIsAboveIcon));
501 // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
502 int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
503 arrowPaint.setPathEffect(new CornerPathEffect(radius));
504 arrowView.setBackground(arrowDrawable);
505 arrowView.setElevation(getElevation());
507 addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
512 public View getExtendedTouchView() {
513 return mOriginalIcon;
517 * Determines when the deferred drag should be started.
520 * - Start the drag if the touch passes a certain distance from the original touch down.
522 public DragOptions.PreDragCondition createPreDragCondition() {
523 return new DragOptions.PreDragCondition() {
525 public boolean shouldStartDrag(double distanceDragged) {
526 return distanceDragged > mStartDragThreshold;
530 public void onPreDragStart(DropTarget.DragObject dragObject) {
531 mOriginalIcon.setVisibility(INVISIBLE);
535 public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
537 mOriginalIcon.setVisibility(VISIBLE);
538 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
540 mOriginalIcon.setTextVisibility(false);
548 public boolean onInterceptTouchEvent(MotionEvent ev) {
549 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
550 mInterceptTouchDown.set(ev.getX(), ev.getY());
553 // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
554 return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
555 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
559 * Updates the notification header if the original icon's badge updated.
561 public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) {
562 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
563 PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
564 if (updatedBadges.contains(packageUser)) {
565 updateNotificationHeader();
569 private void updateNotificationHeader() {
570 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
571 BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
572 if (mNotificationItemView != null && badgeInfo != null) {
573 IconPalette palette = mOriginalIcon.getIcon() instanceof FastBitmapDrawable
574 ? ((FastBitmapDrawable) mOriginalIcon.getIcon()).getIconPalette()
576 mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette);
580 public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
581 if (mNotificationItemView == null) {
584 ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
585 BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
586 if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) {
587 AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
588 final int duration = getResources().getInteger(
589 R.integer.config_removeNotificationViewDuration);
590 final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
591 removeNotification.play(reduceNotificationViewHeight(
592 mNotificationItemView.getHeightMinusFooter() + spacing, duration));
593 final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
594 : mNotificationItemView;
595 if (removeMarginView != null) {
596 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
597 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
599 public void onAnimationUpdate(ValueAnimator valueAnimator) {
600 ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
601 = (int) (spacing * (float) valueAnimator.getAnimatedValue());
604 removeNotification.play(removeMargin);
606 Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
607 .setDuration(duration);
608 fade.addListener(new AnimatorListenerAdapter() {
610 public void onAnimationEnd(Animator animation) {
611 removeView(mNotificationItemView);
612 mNotificationItemView = null;
613 if (getItemCount() == 0) {
619 removeNotification.play(fade);
620 final long arrowScaleDuration = getResources().getInteger(
621 R.integer.config_deepShortcutArrowOpenDuration);
622 Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
623 hideArrow.setStartDelay(0);
624 Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
625 showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
626 removeNotification.playSequentially(hideArrow, showArrow);
627 removeNotification.start();
630 mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
631 badgeInfo.getNotificationKeys()));
635 protected void onWidgetsBound() {
636 if (mShortcutsItemView != null) {
637 mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
641 private ObjectAnimator createArrowScaleAnim(float scale) {
642 return LauncherAnimUtils.ofPropertyValuesHolder(
643 mArrow, new PropertyListBuilder().scale(scale).build());
647 * Animates the height of the notification item and the translationY of other items accordingly.
649 public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
650 if (mReduceHeightAnimatorSet != null) {
651 mReduceHeightAnimatorSet.cancel();
653 final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
654 mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet();
655 mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval(heightToRemove));
656 PropertyResetListener<View, Float> resetTranslationYListener
657 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
658 for (int i = 0; i < getItemCount(); i++) {
659 final PopupItemView itemView = getItemViewAt(i);
660 if (!mIsAboveIcon && itemView == mNotificationItemView) {
661 // The notification view is already in the right place when container is below icon.
664 ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
665 itemView.getTranslationY() + translateYBy).setDuration(duration);
666 translateItem.addListener(resetTranslationYListener);
667 mReduceHeightAnimatorSet.play(translateItem);
669 mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() {
671 public void onAnimationEnd(Animator animation) {
673 // All the items, including the notification item, translated down, but the
674 // container itself did not. This means the items would jump back to their
675 // original translation unless we update the container's translationY here.
676 setTranslationY(getTranslationY() + translateYBy);
678 mReduceHeightAnimatorSet = null;
681 return mReduceHeightAnimatorSet;
685 public boolean supportsAppInfoDropTarget() {
690 public boolean supportsDeleteDropTarget() {
695 public float getIntrinsicIconScaleFactor() {
700 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
704 mLauncher.showWorkspace(true);
705 mLauncher.getDropTargetBar().onDragEnd();
710 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
711 // Either the original icon or one of the shortcuts was dragged.
712 // Hide the container, but don't remove it yet because that interferes with touch events.
713 mDeferContainerRemoval = true;
718 public void onDragEnd() {
720 if (mOpenCloseAnimator != null) {
721 // Close animation is running.
722 mDeferContainerRemoval = false;
724 // Close animation is not running.
725 if (mDeferContainerRemoval) {
733 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
734 target.itemType = ItemType.DEEPSHORTCUT;
735 targetParent.containerType = ContainerType.DEEPSHORTCUTS;
739 protected void handleClose(boolean animate) {
747 protected void animateClose() {
751 if (mOpenCloseAnimator != null) {
752 mOpenCloseAnimator.cancel();
756 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
757 final int itemCount = getItemCount();
758 int numOpenShortcuts = 0;
759 for (int i = 0; i < itemCount; i++) {
760 if (getItemViewAt(i).isOpenOrOpening()) {
764 final long duration = getResources().getInteger(
765 R.integer.config_deepShortcutCloseDuration);
766 final long arrowScaleDuration = getResources().getInteger(
767 R.integer.config_deepShortcutArrowOpenDuration);
768 final long stagger = getResources().getInteger(
769 R.integer.config_deepShortcutCloseStagger);
770 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
772 int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
773 for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
774 final PopupItemView view = getItemViewAt(i);
776 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
777 int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
778 : numOpenShortcuts - i - 1;
779 anim.setStartDelay(stagger * animationIndex);
781 Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
782 // Don't start fading until the arrow is gone.
783 fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
784 fadeAnim.setDuration(duration - arrowScaleDuration);
785 fadeAnim.setInterpolator(fadeInterpolator);
786 shortcutAnims.play(fadeAnim);
787 anim.addListener(new AnimatorListenerAdapter() {
789 public void onAnimationEnd(Animator animation) {
790 view.setVisibility(INVISIBLE);
793 shortcutAnims.play(anim);
795 Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
796 arrowAnim.setStartDelay(0);
797 shortcutAnims.play(arrowAnim);
799 shortcutAnims.addListener(new AnimatorListenerAdapter() {
801 public void onAnimationEnd(Animator animation) {
802 mOpenCloseAnimator = null;
803 if (mDeferContainerRemoval) {
804 setVisibility(INVISIBLE);
810 mOpenCloseAnimator = shortcutAnims;
811 shortcutAnims.start();
812 mOriginalIcon.forceHideBadge(false);
816 * Closes the folder without animation.
818 protected void closeComplete() {
819 if (mOpenCloseAnimator != null) {
820 mOpenCloseAnimator.cancel();
821 mOpenCloseAnimator = null;
824 mDeferContainerRemoval = false;
825 boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
826 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
827 mOriginalIcon.setTextVisibility(!isInHotseat);
828 mOriginalIcon.forceHideBadge(false);
829 mLauncher.getDragController().removeDragListener(this);
830 mLauncher.getDragLayer().removeView(this);
834 protected boolean isOfType(int type) {
835 return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
839 * Returns a DeepShortcutsContainer which is already open or null
841 public static PopupContainerWithArrow getOpen(Launcher launcher) {
842 return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
846 public int getLogContainerType() {
847 return ContainerType.DEEPSHORTCUTS;