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.Color;
29 import android.graphics.PointF;
30 import android.graphics.Rect;
31 import android.graphics.drawable.ShapeDrawable;
32 import android.os.Build;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.util.AttributeSet;
36 import android.view.Gravity;
37 import android.view.LayoutInflater;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.ViewConfiguration;
41 import android.view.accessibility.AccessibilityEvent;
42 import android.view.animation.DecelerateInterpolator;
43 import android.widget.FrameLayout;
45 import com.android.launcher3.AbstractFloatingView;
46 import com.android.launcher3.BubbleTextView;
47 import com.android.launcher3.DragSource;
48 import com.android.launcher3.DropTarget;
49 import com.android.launcher3.FastBitmapDrawable;
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;
75 import java.util.Collections;
76 import java.util.List;
79 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
80 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
81 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
84 * A container for shortcuts to deep links within apps.
86 @TargetApi(Build.VERSION_CODES.N)
87 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
88 DragController.DragListener {
90 protected final Launcher mLauncher;
91 private final int mStartDragThreshold;
92 private LauncherAccessibilityDelegate mAccessibilityDelegate;
93 private final boolean mIsRtl;
95 public ShortcutsItemView mShortcutsItemView;
96 private NotificationItemView mNotificationItemView;
98 protected BubbleTextView mOriginalIcon;
99 private final Rect mTempRect = new Rect();
100 private PointF mInterceptTouchDown = new PointF();
101 private boolean mIsLeftAligned;
102 protected boolean mIsAboveIcon;
105 protected Animator mOpenCloseAnimator;
106 private boolean mDeferContainerRemoval;
108 public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
109 super(context, attrs, defStyleAttr);
110 mLauncher = Launcher.getLauncher(context);
112 mStartDragThreshold = getResources().getDimensionPixelSize(
113 R.dimen.deep_shortcuts_start_drag_threshold);
114 // TODO: make sure the delegate works for all items, not just shortcuts.
115 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
116 mIsRtl = Utilities.isRtl(getResources());
119 public PopupContainerWithArrow(Context context, AttributeSet attrs) {
120 this(context, attrs, 0);
123 public PopupContainerWithArrow(Context context) {
124 this(context, null, 0);
127 public LauncherAccessibilityDelegate getAccessibilityDelegate() {
128 return mAccessibilityDelegate;
132 * Shows the notifications and deep shortcuts associated with {@param icon}.
133 * @return the container if shown or null.
135 public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
136 Launcher launcher = Launcher.getLauncher(icon.getContext());
137 if (getOpen(launcher) != null) {
138 // There is already an items container open, so don't open this one.
142 ItemInfo itemInfo = (ItemInfo) icon.getTag();
143 if (!DeepShortcutManager.supportsShortcuts(itemInfo)) {
147 List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
148 List<NotificationKeyData> notificationKeys = launcher.getPopupDataProvider()
149 .getNotificationKeysForItem(itemInfo);
151 final PopupContainerWithArrow container =
152 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
153 R.layout.popup_container, launcher.getDragLayer(), false);
154 container.setVisibility(View.INVISIBLE);
155 launcher.getDragLayer().addView(container);
156 container.populateAndShow(icon, shortcutIds, notificationKeys);
160 public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
161 final List<NotificationKeyData> notificationKeys) {
162 final Resources resources = getResources();
163 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
164 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
165 final int arrowHorizontalOffset = resources.getDimensionPixelSize(
166 R.dimen.popup_arrow_horizontal_offset);
167 final int arrowVerticalOffset = resources.getDimensionPixelSize(
168 R.dimen.popup_arrow_vertical_offset);
170 // Add dummy views first, and populate with real info when ready.
171 PopupPopulator.Item[] itemsToPopulate = PopupPopulator
172 .getItemsToPopulate(shortcutIds, notificationKeys);
173 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
175 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
176 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
178 boolean reverseOrder = mIsAboveIcon;
181 mNotificationItemView = null;
182 mShortcutsItemView = null;
183 itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
184 addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
186 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
187 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
190 ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
191 List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
192 ? Collections.EMPTY_LIST
193 : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
194 List<View> systemShortcutViews = mShortcutsItemView == null
195 ? Collections.EMPTY_LIST
196 : mShortcutsItemView.getSystemShortcutViews(reverseOrder || true);
197 if (mNotificationItemView != null) {
198 BadgeInfo badgeInfo = mLauncher.getPopupDataProvider()
199 .getBadgeInfoForItem(originalItemInfo);
200 updateNotificationHeader(badgeInfo, originalIcon);
204 mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
205 mArrow.setPivotX(arrowWidth / 2);
206 mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
210 mOriginalIcon = originalIcon;
212 mLauncher.getDragController().addDragListener(this);
214 // Load the shortcuts on a background thread and update the container as it animates.
215 final Looper workerLooper = LauncherModel.getWorkerLooper();
216 new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
217 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
218 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
219 systemShortcutViews));
222 private void addDummyViews(BubbleTextView originalIcon,
223 PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
224 final Resources res = getResources();
225 final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
226 final LayoutInflater inflater = mLauncher.getLayoutInflater();
228 int numItems = itemTypesToPopulate.length;
229 for (int i = 0; i < numItems; i++) {
230 PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
231 PopupPopulator.Item nextItemTypeToPopulate =
232 i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
233 final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
235 if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
236 mNotificationItemView = (NotificationItemView) item;
237 int footerHeight = notificationFooterHasIcons ?
238 res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
239 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
242 boolean shouldAddBottomMargin = nextItemTypeToPopulate != null
243 && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
245 item.setAccessibilityDelegate(mAccessibilityDelegate);
246 if (itemTypeToPopulate.isShortcut) {
247 if (mShortcutsItemView == null) {
248 mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
249 R.layout.shortcuts_item, this, false);
250 addView(mShortcutsItemView);
252 mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
253 if (shouldAddBottomMargin) {
254 ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
258 if (shouldAddBottomMargin) {
259 ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
263 // TODO: update this, since not all items are shortcuts
264 setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
265 numItems, originalIcon.getContentDescription().toString()));
268 protected PopupItemView getItemViewAt(int index) {
270 // Opening down, so arrow is the first view.
273 return (PopupItemView) getChildAt(index);
276 protected int getItemCount() {
277 // All children except the arrow are items.
278 return getChildCount() - 1;
281 private void animateOpen() {
282 setVisibility(View.VISIBLE);
285 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
286 final int itemCount = getItemCount();
288 final long duration = getResources().getInteger(
289 R.integer.config_deepShortcutOpenDuration);
290 final long arrowScaleDuration = getResources().getInteger(
291 R.integer.config_deepShortcutArrowOpenDuration);
292 final long arrowScaleDelay = duration - arrowScaleDuration;
293 final long stagger = getResources().getInteger(
294 R.integer.config_deepShortcutOpenStagger);
295 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
298 DecelerateInterpolator interpolator = new DecelerateInterpolator();
299 for (int i = 0; i < itemCount; i++) {
300 final PopupItemView popupItemView = getItemViewAt(i);
301 popupItemView.setVisibility(INVISIBLE);
302 popupItemView.setAlpha(0);
304 Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
305 anim.addListener(new AnimatorListenerAdapter() {
307 public void onAnimationStart(Animator animation) {
308 popupItemView.setVisibility(VISIBLE);
311 anim.setDuration(duration);
312 int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
313 anim.setStartDelay(stagger * animationIndex);
314 anim.setInterpolator(interpolator);
315 shortcutAnims.play(anim);
317 Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
318 fadeAnim.setInterpolator(fadeInterpolator);
319 // We want the shortcut to be fully opaque before the arrow starts animating.
320 fadeAnim.setDuration(arrowScaleDelay);
321 shortcutAnims.play(fadeAnim);
323 shortcutAnims.addListener(new AnimatorListenerAdapter() {
325 public void onAnimationEnd(Animator animation) {
326 mOpenCloseAnimator = null;
327 Utilities.sendCustomAccessibilityEvent(
328 PopupContainerWithArrow.this,
329 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
330 getContext().getString(R.string.action_deep_shortcut));
337 Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
338 arrowScale.setStartDelay(arrowScaleDelay);
339 shortcutAnims.play(arrowScale);
341 mOpenCloseAnimator = shortcutAnims;
342 shortcutAnims.start();
346 * Orients this container above or below the given icon, aligning with the left or right.
348 * These are the preferred orientations, in order (RTL prefers right-aligned over left):
349 * - Above and left-aligned
350 * - Above and right-aligned
351 * - Below and left-aligned
352 * - Below and right-aligned
354 * So we always align left if there is enough horizontal space
355 * and align above if there is enough vertical space.
357 private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
358 int width = getMeasuredWidth();
359 int height = getMeasuredHeight() + arrowHeight;
361 DragLayer dragLayer = mLauncher.getDragLayer();
362 dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
363 Rect insets = dragLayer.getInsets();
365 // Align left (right in RTL) if there is room.
366 int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
367 int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
368 int x = leftAlignedX;
369 boolean canBeLeftAligned = leftAlignedX + width + insets.left
370 < dragLayer.getRight() - insets.right;
371 boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
372 if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
375 mIsLeftAligned = x == leftAlignedX;
377 x -= dragLayer.getWidth() - width;
380 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
381 int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
382 iconWidth *= icon.getScaleX();
383 Resources resources = getResources();
385 if (isAlignedWithStart()) {
386 // Aligning with the shortcut icon.
387 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
388 int shortcutPaddingStart = resources.getDimensionPixelSize(
389 R.dimen.popup_padding_start);
390 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
392 // Aligning with the drag handle.
393 int shortcutDragHandleWidth = resources.getDimensionPixelSize(
394 R.dimen.deep_shortcut_drag_handle_size);
395 int shortcutPaddingEnd = resources.getDimensionPixelSize(
396 R.dimen.popup_padding_end);
397 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
399 x += mIsLeftAligned ? xOffset : -xOffset;
401 // Open above icon if there is room.
402 int iconHeight = icon.getIcon().getBounds().height();
403 int y = mTempRect.top + icon.getPaddingTop() - height;
404 mIsAboveIcon = y > dragLayer.getTop() + insets.top;
406 y = mTempRect.top + icon.getPaddingTop() + iconHeight;
409 // Insets are added later, so subtract them now.
417 if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
418 // The container is opening off the screen, so just center it in the drag layer instead.
419 ((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
420 // Put the container next to the icon, preferring the right side in ltr (left in rtl).
421 int rightSide = leftAlignedX + iconWidth - insets.left;
422 int leftSide = rightAlignedX - iconWidth - insets.left;
424 if (rightSide + width < dragLayer.getRight()) {
426 mIsLeftAligned = true;
429 mIsLeftAligned = false;
432 if (leftSide > dragLayer.getLeft()) {
434 mIsLeftAligned = false;
437 mIsLeftAligned = true;
443 if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
444 // If we are still off screen, center horizontally too.
445 ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
448 int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
449 if (!Gravity.isHorizontal(gravity)) {
452 if (!Gravity.isVertical(gravity)) {
457 private boolean isAlignedWithStart() {
458 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
462 * Adds an arrow view pointing at the original icon.
463 * @param horizontalOffset the horizontal offset of the arrow, so that it
464 * points at the center of the original icon
466 private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
467 LayoutParams layoutParams = new LayoutParams(width, height);
468 if (mIsLeftAligned) {
469 layoutParams.gravity = Gravity.LEFT;
470 layoutParams.leftMargin = horizontalOffset;
472 layoutParams.gravity = Gravity.RIGHT;
473 layoutParams.rightMargin = horizontalOffset;
476 layoutParams.topMargin = verticalOffset;
478 layoutParams.bottomMargin = verticalOffset;
481 View arrowView = new View(getContext());
482 if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
483 // This is only true if there wasn't room for the container next to the icon,
484 // so we centered it instead. In that case we don't want to show the arrow.
485 arrowView.setVisibility(INVISIBLE);
487 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
488 width, height, !mIsAboveIcon));
489 arrowDrawable.getPaint().setColor(Color.WHITE);
490 arrowView.setBackground(arrowDrawable);
491 arrowView.setElevation(getElevation());
493 addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
498 public View getExtendedTouchView() {
499 return mOriginalIcon;
503 * Determines when the deferred drag should be started.
506 * - Start the drag if the touch passes a certain distance from the original touch down.
508 public DragOptions.PreDragCondition createPreDragCondition() {
509 return new DragOptions.PreDragCondition() {
511 public boolean shouldStartDrag(double distanceDragged) {
512 return distanceDragged > mStartDragThreshold;
516 public void onPreDragStart(DropTarget.DragObject dragObject) {
517 mOriginalIcon.setVisibility(INVISIBLE);
521 public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
523 mOriginalIcon.setVisibility(VISIBLE);
524 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
526 mOriginalIcon.setTextVisibility(false);
534 public boolean onInterceptTouchEvent(MotionEvent ev) {
535 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
536 mInterceptTouchDown.set(ev.getX(), ev.getY());
539 // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
540 return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
541 > ViewConfiguration.get(getContext()).getScaledTouchSlop();
545 * Updates the notification header to reflect the badge info. Since this can be called
546 * for any badge info (not necessarily the one associated with this app), we first
547 * check that the ItemInfo matches the one of this popup.
549 public void updateNotificationHeader(BadgeInfo badgeInfo, ItemInfo originalItemInfo) {
550 if (originalItemInfo != mOriginalIcon.getTag()) {
553 updateNotificationHeader(badgeInfo, mOriginalIcon);
556 private void updateNotificationHeader(BadgeInfo badgeInfo, BubbleTextView originalIcon) {
557 if (mNotificationItemView != null && badgeInfo != null) {
558 IconPalette palette = originalIcon.getIcon() instanceof FastBitmapDrawable
559 ? ((FastBitmapDrawable) originalIcon.getIcon()).getIconPalette()
561 mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette);
565 public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
566 if (mNotificationItemView == null) {
569 ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
570 BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
571 if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
572 AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
573 final int duration = getResources().getInteger(
574 R.integer.config_removeNotificationViewDuration);
575 final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
576 removeNotification.play(reduceNotificationViewHeight(
577 mNotificationItemView.getHeight() + spacing, duration));
578 final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
579 : mNotificationItemView;
580 if (removeMarginView != null) {
581 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
582 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
584 public void onAnimationUpdate(ValueAnimator valueAnimator) {
585 ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
586 = (int) (spacing * (float) valueAnimator.getAnimatedValue());
589 removeNotification.play(removeMargin);
591 Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
592 .setDuration(duration);
593 fade.addListener(new AnimatorListenerAdapter() {
595 public void onAnimationEnd(Animator animation) {
596 removeView(mNotificationItemView);
597 mNotificationItemView = null;
598 if (getItemCount() == 0) {
604 removeNotification.play(fade);
605 final long arrowScaleDuration = getResources().getInteger(
606 R.integer.config_deepShortcutArrowOpenDuration);
607 Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
608 hideArrow.setStartDelay(0);
609 Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
610 showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
611 removeNotification.playSequentially(hideArrow, showArrow);
612 removeNotification.start();
615 mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
616 badgeInfo.getNotificationKeys()));
620 protected void onWidgetsBound() {
624 public boolean enableWidgets() {
625 return mShortcutsItemView != null && mShortcutsItemView.enableWidgets(
626 (ItemInfo) mOriginalIcon.getTag());
629 private ObjectAnimator createArrowScaleAnim(float scale) {
630 return LauncherAnimUtils.ofPropertyValuesHolder(
631 mArrow, new PropertyListBuilder().scale(scale).build());
635 * Animates the height of the notification item and the translationY of other items accordingly.
637 public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
638 final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
639 AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet();
640 animatorSet.play(mNotificationItemView.animateHeightRemoval(heightToRemove));
641 PropertyResetListener<View, Float> resetTranslationYListener
642 = new PropertyResetListener<>(TRANSLATION_Y, 0f);
643 for (int i = 0; i < getItemCount(); i++) {
644 final PopupItemView itemView = getItemViewAt(i);
645 if (!mIsAboveIcon && itemView == mNotificationItemView) {
646 // The notification view is already in the right place when container is below icon.
649 ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
650 itemView.getTranslationY() + translateYBy).setDuration(duration);
651 translateItem.addListener(resetTranslationYListener);
652 animatorSet.play(translateItem);
655 // All the items, including the notification item, translated down, but the
656 // container itself did not. This means the items would jump back to their
657 // original translation unless we update the container's translationY here.
658 animatorSet.addListener(new AnimatorListenerAdapter() {
660 public void onAnimationEnd(Animator animation) {
661 setTranslationY(getTranslationY() + translateYBy);
669 public boolean supportsAppInfoDropTarget() {
674 public boolean supportsDeleteDropTarget() {
679 public float getIntrinsicIconScaleFactor() {
684 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
688 mLauncher.showWorkspace(true);
689 mLauncher.getDropTargetBar().onDragEnd();
694 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
695 // Either the original icon or one of the shortcuts was dragged.
696 // Hide the container, but don't remove it yet because that interferes with touch events.
697 mDeferContainerRemoval = true;
702 public void onDragEnd() {
704 if (mOpenCloseAnimator != null) {
705 // Close animation is running.
706 mDeferContainerRemoval = false;
708 // Close animation is not running.
709 if (mDeferContainerRemoval) {
717 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
718 target.itemType = ItemType.DEEPSHORTCUT;
719 targetParent.containerType = ContainerType.DEEPSHORTCUTS;
723 protected void handleClose(boolean animate) {
731 protected void animateClose() {
735 if (mOpenCloseAnimator != null) {
736 mOpenCloseAnimator.cancel();
740 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
741 final int itemCount = getItemCount();
742 int numOpenShortcuts = 0;
743 for (int i = 0; i < itemCount; i++) {
744 if (getItemViewAt(i).isOpenOrOpening()) {
748 final long duration = getResources().getInteger(
749 R.integer.config_deepShortcutCloseDuration);
750 final long arrowScaleDuration = getResources().getInteger(
751 R.integer.config_deepShortcutArrowOpenDuration);
752 final long stagger = getResources().getInteger(
753 R.integer.config_deepShortcutCloseStagger);
754 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
756 int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
757 for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
758 final PopupItemView view = getItemViewAt(i);
760 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
761 int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
762 : numOpenShortcuts - i - 1;
763 anim.setStartDelay(stagger * animationIndex);
765 Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
766 // Don't start fading until the arrow is gone.
767 fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
768 fadeAnim.setDuration(duration - arrowScaleDuration);
769 fadeAnim.setInterpolator(fadeInterpolator);
770 shortcutAnims.play(fadeAnim);
771 anim.addListener(new AnimatorListenerAdapter() {
773 public void onAnimationEnd(Animator animation) {
774 view.setVisibility(INVISIBLE);
777 shortcutAnims.play(anim);
779 Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
780 arrowAnim.setStartDelay(0);
781 shortcutAnims.play(arrowAnim);
783 shortcutAnims.addListener(new AnimatorListenerAdapter() {
785 public void onAnimationEnd(Animator animation) {
786 mOpenCloseAnimator = null;
787 if (mDeferContainerRemoval) {
788 setVisibility(INVISIBLE);
794 mOpenCloseAnimator = shortcutAnims;
795 shortcutAnims.start();
799 * Closes the folder without animation.
801 protected void closeComplete() {
802 if (mOpenCloseAnimator != null) {
803 mOpenCloseAnimator.cancel();
804 mOpenCloseAnimator = null;
807 mDeferContainerRemoval = false;
808 boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
809 == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
810 mOriginalIcon.setTextVisibility(!isInHotseat);
811 mLauncher.getDragController().removeDragListener(this);
812 mLauncher.getDragLayer().removeView(this);
816 protected boolean isOfType(int type) {
817 return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
821 * Returns a DeepShortcutsContainer which is already open or null
823 public static PopupContainerWithArrow getOpen(Launcher launcher) {
824 return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
828 public int getLogContainerType() {
829 return ContainerType.DEEPSHORTCUTS;