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