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.Outline; 30 import android.graphics.Paint; 31 import android.graphics.Point; 32 import android.graphics.PointF; 33 import android.graphics.Rect; 34 import android.graphics.drawable.ShapeDrawable; 35 import android.os.Build; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.support.annotation.IntDef; 39 import android.util.AttributeSet; 40 import android.view.Gravity; 41 import android.view.LayoutInflater; 42 import android.view.MotionEvent; 43 import android.view.View; 44 import android.view.ViewConfiguration; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.animation.AccelerateDecelerateInterpolator; 47 48 import com.android.launcher3.AbstractFloatingView; 49 import com.android.launcher3.BubbleTextView; 50 import com.android.launcher3.DragSource; 51 import com.android.launcher3.DropTarget; 52 import com.android.launcher3.ItemInfo; 53 import com.android.launcher3.Launcher; 54 import com.android.launcher3.LauncherAnimUtils; 55 import com.android.launcher3.LauncherModel; 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.anim.RoundedRectRevealOutlineProvider; 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; 75 import com.android.launcher3.util.Themes; 76 77 import java.lang.annotation.Retention; 78 import java.lang.annotation.RetentionPolicy; 79 import java.util.Collections; 80 import java.util.List; 81 import java.util.Map; 82 import java.util.Set; 83 84 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS; 85 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 86 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; 87 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target; 88 89 /** 90 * A container for shortcuts to deep links within apps. 91 */ 92 @TargetApi(Build.VERSION_CODES.N) 93 public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource, 94 DragController.DragListener { 95 96 public static final int ROUNDED_TOP_CORNERS = 1 << 0; 97 public static final int ROUNDED_BOTTOM_CORNERS = 1 << 1; 98 99 @IntDef(flag = true, value = { 100 ROUNDED_TOP_CORNERS, 101 ROUNDED_BOTTOM_CORNERS 102 }) 103 @Retention(RetentionPolicy.SOURCE) 104 public @interface RoundedCornerFlags {} 105 106 protected final Launcher mLauncher; 107 private final int mStartDragThreshold; 108 private LauncherAccessibilityDelegate mAccessibilityDelegate; 109 private final boolean mIsRtl; 110 111 public ShortcutsItemView mShortcutsItemView; 112 private NotificationItemView mNotificationItemView; 113 114 protected BubbleTextView mOriginalIcon; 115 private final Rect mTempRect = new Rect(); 116 private PointF mInterceptTouchDown = new PointF(); 117 private boolean mIsLeftAligned; 118 protected boolean mIsAboveIcon; 119 private View mArrow; 120 private int mGravity; 121 122 protected Animator mOpenCloseAnimator; 123 private boolean mDeferContainerRemoval; 124 private AnimatorSet mReduceHeightAnimatorSet; 125 private final Rect mStartRect = new Rect(); 126 private final Rect mEndRect = new Rect(); 127 PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr)128 public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) { 129 super(context, attrs, defStyleAttr); 130 mLauncher = Launcher.getLauncher(context); 131 132 mStartDragThreshold = getResources().getDimensionPixelSize( 133 R.dimen.deep_shortcuts_start_drag_threshold); 134 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); 135 mIsRtl = Utilities.isRtl(getResources()); 136 } 137 PopupContainerWithArrow(Context context, AttributeSet attrs)138 public PopupContainerWithArrow(Context context, AttributeSet attrs) { 139 this(context, attrs, 0); 140 } 141 PopupContainerWithArrow(Context context)142 public PopupContainerWithArrow(Context context) { 143 this(context, null, 0); 144 } 145 getAccessibilityDelegate()146 public LauncherAccessibilityDelegate getAccessibilityDelegate() { 147 return mAccessibilityDelegate; 148 } 149 150 /** 151 * Shows the notifications and deep shortcuts associated with {@param icon}. 152 * @return the container if shown or null. 153 */ showForIcon(BubbleTextView icon)154 public static PopupContainerWithArrow showForIcon(BubbleTextView icon) { 155 Launcher launcher = Launcher.getLauncher(icon.getContext()); 156 if (getOpen(launcher) != null) { 157 // There is already an items container open, so don't open this one. 158 icon.clearFocus(); 159 return null; 160 } 161 ItemInfo itemInfo = (ItemInfo) icon.getTag(); 162 if (!DeepShortcutManager.supportsShortcuts(itemInfo)) { 163 return null; 164 } 165 166 PopupDataProvider popupDataProvider = launcher.getPopupDataProvider(); 167 List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo); 168 List<NotificationKeyData> notificationKeys = popupDataProvider 169 .getNotificationKeysForItem(itemInfo); 170 List<SystemShortcut> systemShortcuts = popupDataProvider 171 .getEnabledSystemShortcutsForItem(itemInfo); 172 173 final PopupContainerWithArrow container = 174 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( 175 R.layout.popup_container, launcher.getDragLayer(), false); 176 container.setVisibility(View.INVISIBLE); 177 launcher.getDragLayer().addView(container); 178 container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts); 179 return container; 180 } 181 populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts)182 public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, 183 final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) { 184 final Resources resources = getResources(); 185 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); 186 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); 187 final int arrowVerticalOffset = resources.getDimensionPixelSize( 188 R.dimen.popup_arrow_vertical_offset); 189 190 mOriginalIcon = originalIcon; 191 192 // Add dummy views first, and populate with real info when ready. 193 PopupPopulator.Item[] itemsToPopulate = PopupPopulator 194 .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts); 195 addDummyViews(itemsToPopulate, notificationKeys.size()); 196 197 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 198 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); 199 200 boolean reverseOrder = mIsAboveIcon; 201 if (reverseOrder) { 202 removeAllViews(); 203 mNotificationItemView = null; 204 mShortcutsItemView = null; 205 itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate); 206 addDummyViews(itemsToPopulate, notificationKeys.size()); 207 208 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 209 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); 210 } 211 212 ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); 213 List<DeepShortcutView> shortcutViews = mShortcutsItemView == null 214 ? Collections.EMPTY_LIST 215 : mShortcutsItemView.getDeepShortcutViews(reverseOrder); 216 List<View> systemShortcutViews = mShortcutsItemView == null 217 ? Collections.EMPTY_LIST 218 : mShortcutsItemView.getSystemShortcutViews(reverseOrder); 219 if (mNotificationItemView != null) { 220 updateNotificationHeader(); 221 } 222 223 int numShortcuts = shortcutViews.size() + systemShortcutViews.size(); 224 int numNotifications = notificationKeys.size(); 225 if (numNotifications == 0) { 226 setContentDescription(getContext().getString(R.string.shortcuts_menu_description, 227 numShortcuts, originalIcon.getContentDescription().toString())); 228 } else { 229 setContentDescription(getContext().getString( 230 R.string.shortcuts_menu_with_notifications_description, numShortcuts, 231 numNotifications, originalIcon.getContentDescription().toString())); 232 } 233 234 // Add the arrow. 235 final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ? 236 R.dimen.popup_arrow_horizontal_offset_start : 237 R.dimen.popup_arrow_horizontal_offset_end); 238 mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight); 239 mArrow.setPivotX(arrowWidth / 2); 240 mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight); 241 242 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 243 animateOpen(); 244 245 mLauncher.getDragController().addDragListener(this); 246 mOriginalIcon.forceHideBadge(true); 247 248 // Load the shortcuts on a background thread and update the container as it animates. 249 final Looper workerLooper = LauncherModel.getWorkerLooper(); 250 new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable( 251 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()), 252 this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView, 253 systemShortcuts, systemShortcutViews)); 254 } 255 addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications)256 private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) { 257 final Resources res = getResources(); 258 final LayoutInflater inflater = mLauncher.getLayoutInflater(); 259 260 int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS; 261 int numItems = itemTypesToPopulate.length; 262 for (int i = 0; i < numItems; i++) { 263 PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i]; 264 PopupPopulator.Item prevItemTypeToPopulate = 265 i > 0 ? itemTypesToPopulate[i - 1] : null; 266 PopupPopulator.Item nextItemTypeToPopulate = 267 i < numItems - 1 ? itemTypesToPopulate[i + 1] : null; 268 final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false); 269 270 boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null 271 && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut; 272 boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null 273 && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut; 274 275 if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) { 276 mNotificationItemView = (NotificationItemView) item; 277 boolean notificationFooterHasIcons = numNotifications > 1; 278 int footerHeight = res.getDimensionPixelSize( 279 notificationFooterHasIcons ? R.dimen.notification_footer_height 280 : R.dimen.notification_empty_footer_height); 281 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight; 282 if (notificationFooterHasIcons) { 283 mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE); 284 } 285 286 int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS; 287 if (shouldUnroundTopCorners) { 288 roundedCorners &= ~ROUNDED_TOP_CORNERS; 289 mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE); 290 } 291 if (shouldUnroundBottomCorners) { 292 roundedCorners &= ~ROUNDED_BOTTOM_CORNERS; 293 mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE); 294 } 295 int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary); 296 mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners); 297 298 mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate); 299 } else if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) { 300 item.setAccessibilityDelegate(mAccessibilityDelegate); 301 } 302 303 if (itemTypeToPopulate.isShortcut) { 304 if (mShortcutsItemView == null) { 305 mShortcutsItemView = (ShortcutsItemView) inflater.inflate( 306 R.layout.shortcuts_item, this, false); 307 addView(mShortcutsItemView); 308 if (shouldUnroundTopCorners) { 309 shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS; 310 } 311 } 312 if (itemTypeToPopulate != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON 313 && numNotifications > 0) { 314 int prevHeight = item.getLayoutParams().height; 315 // Condense shortcuts height when there are notifications. 316 item.getLayoutParams().height = res.getDimensionPixelSize( 317 R.dimen.bg_popup_item_condensed_height); 318 if (item instanceof DeepShortcutView) { 319 float iconScale = (float) item.getLayoutParams().height / prevHeight; 320 ((DeepShortcutView) item).getIconView().setScaleX(iconScale); 321 ((DeepShortcutView) item).getIconView().setScaleY(iconScale); 322 } 323 } 324 mShortcutsItemView.addShortcutView(item, itemTypeToPopulate); 325 if (shouldUnroundBottomCorners) { 326 shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS; 327 } 328 } else { 329 addView(item); 330 } 331 } 332 int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary); 333 mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners); 334 if (numNotifications > 0) { 335 mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS); 336 } 337 } 338 getItemViewAt(int index)339 protected PopupItemView getItemViewAt(int index) { 340 if (!mIsAboveIcon) { 341 // Opening down, so arrow is the first view. 342 index++; 343 } 344 return (PopupItemView) getChildAt(index); 345 } 346 getItemCount()347 protected int getItemCount() { 348 // All children except the arrow are items. 349 return getChildCount() - 1; 350 } 351 animateOpen()352 private void animateOpen() { 353 setVisibility(View.VISIBLE); 354 mIsOpen = true; 355 356 final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet(); 357 final Resources res = getResources(); 358 final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration); 359 final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); 360 361 // Rectangular reveal. 362 int itemsTotalHeight = 0; 363 for (int i = 0; i < getItemCount(); i++) { 364 itemsTotalHeight += getItemViewAt(i).getMeasuredHeight(); 365 } 366 Point startPoint = computeAnimStartPoint(itemsTotalHeight); 367 int top = mIsAboveIcon ? getPaddingTop() : startPoint.y; 368 float radius = getItemViewAt(0).getBackgroundRadius(); 369 mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y); 370 mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight); 371 final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider 372 (radius, radius, mStartRect, mEndRect).createRevealAnimator(this, false); 373 revealAnim.setDuration(revealDuration); 374 revealAnim.setInterpolator(revealInterpolator); 375 376 Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1); 377 fadeIn.setDuration(revealDuration); 378 fadeIn.setInterpolator(revealInterpolator); 379 openAnim.play(fadeIn); 380 381 // Animate the arrow. 382 mArrow.setScaleX(0); 383 mArrow.setScaleY(0); 384 Animator arrowScale = createArrowScaleAnim(1).setDuration(res.getInteger( 385 R.integer.config_popupArrowOpenDuration)); 386 387 openAnim.addListener(new AnimatorListenerAdapter() { 388 @Override 389 public void onAnimationEnd(Animator animation) { 390 mOpenCloseAnimator = null; 391 Utilities.sendCustomAccessibilityEvent( 392 PopupContainerWithArrow.this, 393 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 394 getContext().getString(R.string.action_deep_shortcut)); 395 } 396 }); 397 398 mOpenCloseAnimator = openAnim; 399 openAnim.playSequentially(revealAnim, arrowScale); 400 openAnim.start(); 401 } 402 403 @Override onLayout(boolean changed, int l, int t, int r, int b)404 protected void onLayout(boolean changed, int l, int t, int r, int b) { 405 super.onLayout(changed, l, t, r, b); 406 enforceContainedWithinScreen(l, r); 407 408 } 409 enforceContainedWithinScreen(int left, int right)410 private void enforceContainedWithinScreen(int left, int right) { 411 DragLayer dragLayer = mLauncher.getDragLayer(); 412 if (getTranslationX() + left < 0 || 413 getTranslationX() + right > dragLayer.getWidth()) { 414 // If we are still off screen, center horizontally too. 415 mGravity |= Gravity.CENTER_HORIZONTAL; 416 } 417 418 if (Gravity.isHorizontal(mGravity)) { 419 setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2); 420 } 421 if (Gravity.isVertical(mGravity)) { 422 setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2); 423 } 424 } 425 426 /** 427 * Returns the point at which the center of the arrow merges with the first popup item. 428 */ computeAnimStartPoint(int itemsTotalHeight)429 private Point computeAnimStartPoint(int itemsTotalHeight) { 430 int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ? 431 R.dimen.popup_arrow_horizontal_center_start: 432 R.dimen.popup_arrow_horizontal_center_end); 433 if (!mIsLeftAligned) { 434 arrowCenterX = getMeasuredWidth() - arrowCenterX; 435 } 436 int arrowHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() 437 - itemsTotalHeight; 438 // The y-coordinate of edge between the arrow and the first popup item. 439 int arrowEdge = getPaddingTop() + (mIsAboveIcon ? itemsTotalHeight : arrowHeight); 440 return new Point(arrowCenterX, arrowEdge); 441 } 442 443 /** 444 * Orients this container above or below the given icon, aligning with the left or right. 445 * 446 * These are the preferred orientations, in order (RTL prefers right-aligned over left): 447 * - Above and left-aligned 448 * - Above and right-aligned 449 * - Below and left-aligned 450 * - Below and right-aligned 451 * 452 * So we always align left if there is enough horizontal space 453 * and align above if there is enough vertical space. 454 */ orientAboutIcon(BubbleTextView icon, int arrowHeight)455 private void orientAboutIcon(BubbleTextView icon, int arrowHeight) { 456 int width = getMeasuredWidth(); 457 int height = getMeasuredHeight() + arrowHeight; 458 459 DragLayer dragLayer = mLauncher.getDragLayer(); 460 dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect); 461 Rect insets = dragLayer.getInsets(); 462 463 // Align left (right in RTL) if there is room. 464 int leftAlignedX = mTempRect.left + icon.getPaddingLeft(); 465 int rightAlignedX = mTempRect.right - width - icon.getPaddingRight(); 466 int x = leftAlignedX; 467 boolean canBeLeftAligned = leftAlignedX + width + insets.left 468 < dragLayer.getRight() - insets.right; 469 boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left; 470 if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) { 471 x = rightAlignedX; 472 } 473 mIsLeftAligned = x == leftAlignedX; 474 if (mIsRtl) { 475 x -= dragLayer.getWidth() - width; 476 } 477 478 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon. 479 int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight(); 480 iconWidth *= icon.getScaleX(); 481 Resources resources = getResources(); 482 int xOffset; 483 if (isAlignedWithStart()) { 484 // Aligning with the shortcut icon. 485 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size); 486 int shortcutPaddingStart = resources.getDimensionPixelSize( 487 R.dimen.popup_padding_start); 488 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart; 489 } else { 490 // Aligning with the drag handle. 491 int shortcutDragHandleWidth = resources.getDimensionPixelSize( 492 R.dimen.deep_shortcut_drag_handle_size); 493 int shortcutPaddingEnd = resources.getDimensionPixelSize( 494 R.dimen.popup_padding_end); 495 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd; 496 } 497 x += mIsLeftAligned ? xOffset : -xOffset; 498 499 // Open above icon if there is room. 500 int iconHeight = icon.getIcon() != null 501 ? icon.getIcon().getBounds().height() 502 : icon.getHeight(); 503 int y = mTempRect.top + icon.getPaddingTop() - height; 504 mIsAboveIcon = y > dragLayer.getTop() + insets.top; 505 if (!mIsAboveIcon) { 506 y = mTempRect.top + icon.getPaddingTop() + iconHeight; 507 } 508 509 // Insets are added later, so subtract them now. 510 if (mIsRtl) { 511 x += insets.right; 512 } else { 513 x -= insets.left; 514 } 515 y -= insets.top; 516 517 mGravity = 0; 518 if (y + height > dragLayer.getBottom() - insets.bottom) { 519 // The container is opening off the screen, so just center it in the drag layer instead. 520 mGravity = Gravity.CENTER_VERTICAL; 521 // Put the container next to the icon, preferring the right side in ltr (left in rtl). 522 int rightSide = leftAlignedX + iconWidth - insets.left; 523 int leftSide = rightAlignedX - iconWidth - insets.left; 524 if (!mIsRtl) { 525 if (rightSide + width < dragLayer.getRight()) { 526 x = rightSide; 527 mIsLeftAligned = true; 528 } else { 529 x = leftSide; 530 mIsLeftAligned = false; 531 } 532 } else { 533 if (leftSide > dragLayer.getLeft()) { 534 x = leftSide; 535 mIsLeftAligned = false; 536 } else { 537 x = rightSide; 538 mIsLeftAligned = true; 539 } 540 } 541 mIsAboveIcon = true; 542 } 543 544 setX(x); 545 setY(y); 546 } 547 isAlignedWithStart()548 private boolean isAlignedWithStart() { 549 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl; 550 } 551 552 /** 553 * Adds an arrow view pointing at the original icon. 554 * @param horizontalOffset the horizontal offset of the arrow, so that it 555 * points at the center of the original icon 556 */ addArrowView(int horizontalOffset, int verticalOffset, int width, int height)557 private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) { 558 LayoutParams layoutParams = new LayoutParams(width, height); 559 if (mIsLeftAligned) { 560 layoutParams.gravity = Gravity.LEFT; 561 layoutParams.leftMargin = horizontalOffset; 562 } else { 563 layoutParams.gravity = Gravity.RIGHT; 564 layoutParams.rightMargin = horizontalOffset; 565 } 566 if (mIsAboveIcon) { 567 layoutParams.topMargin = verticalOffset; 568 } else { 569 layoutParams.bottomMargin = verticalOffset; 570 } 571 572 View arrowView = new View(getContext()); 573 if (Gravity.isVertical(mGravity)) { 574 // This is only true if there wasn't room for the container next to the icon, 575 // so we centered it instead. In that case we don't want to show the arrow. 576 arrowView.setVisibility(INVISIBLE); 577 } else { 578 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( 579 width, height, !mIsAboveIcon)); 580 Paint arrowPaint = arrowDrawable.getPaint(); 581 // Note that we have to use getChildAt() instead of getItemViewAt(), 582 // since the latter expects the arrow which hasn't been added yet. 583 PopupItemView itemAttachedToArrow = (PopupItemView) 584 (getChildAt(mIsAboveIcon ? getChildCount() - 1 : 0)); 585 arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary)); 586 // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable. 587 int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius); 588 arrowPaint.setPathEffect(new CornerPathEffect(radius)); 589 arrowView.setBackground(arrowDrawable); 590 arrowView.setElevation(getElevation()); 591 } 592 addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams); 593 return arrowView; 594 } 595 596 @Override getExtendedTouchView()597 public View getExtendedTouchView() { 598 return mOriginalIcon; 599 } 600 601 /** 602 * Determines when the deferred drag should be started. 603 * 604 * Current behavior: 605 * - Start the drag if the touch passes a certain distance from the original touch down. 606 */ createPreDragCondition()607 public DragOptions.PreDragCondition createPreDragCondition() { 608 return new DragOptions.PreDragCondition() { 609 610 @Override 611 public boolean shouldStartDrag(double distanceDragged) { 612 return distanceDragged > mStartDragThreshold; 613 } 614 615 @Override 616 public void onPreDragStart(DropTarget.DragObject dragObject) { 617 if (mIsAboveIcon) { 618 // Hide only the icon, keep the text visible. 619 mOriginalIcon.setIconVisible(false); 620 mOriginalIcon.setVisibility(VISIBLE); 621 } else { 622 // Hide both the icon and text. 623 mOriginalIcon.setVisibility(INVISIBLE); 624 } 625 } 626 627 @Override 628 public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { 629 mOriginalIcon.setIconVisible(true); 630 if (dragStarted) { 631 // Make sure we keep the original icon hidden while it is being dragged. 632 mOriginalIcon.setVisibility(INVISIBLE); 633 } else { 634 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon); 635 if (!mIsAboveIcon) { 636 // Show the icon but keep the text hidden. 637 mOriginalIcon.setVisibility(VISIBLE); 638 mOriginalIcon.setTextVisibility(false); 639 } 640 } 641 } 642 }; 643 } 644 645 @Override 646 public boolean onInterceptTouchEvent(MotionEvent ev) { 647 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 648 mInterceptTouchDown.set(ev.getX(), ev.getY()); 649 return false; 650 } 651 // Stop sending touch events to deep shortcut views if user moved beyond touch slop. 652 return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY()) 653 > ViewConfiguration.get(getContext()).getScaledTouchSlop(); 654 } 655 656 /** 657 * Updates the notification header if the original icon's badge updated. 658 */ 659 public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) { 660 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); 661 PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo); 662 if (updatedBadges.contains(packageUser)) { 663 updateNotificationHeader(); 664 } 665 } 666 667 private void updateNotificationHeader() { 668 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); 669 BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo); 670 if (mNotificationItemView != null && badgeInfo != null) { 671 IconPalette palette = mOriginalIcon.getBadgePalette(); 672 mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette); 673 } 674 } 675 676 public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) { 677 if (mNotificationItemView == null) { 678 return; 679 } 680 ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); 681 BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo)); 682 if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) { 683 // There are no more notifications, so create an animation to remove 684 // the notifications view and expand the shortcuts view (if possible). 685 AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet(); 686 int hiddenShortcutsHeight = 0; 687 if (mShortcutsItemView != null) { 688 hiddenShortcutsHeight = mShortcutsItemView.getHiddenShortcutsHeight(); 689 int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary); 690 // With notifications gone, all corners of shortcuts item should be rounded. 691 mShortcutsItemView.setBackgroundWithCorners(backgroundColor, 692 ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS); 693 removeNotification.play(mShortcutsItemView.showAllShortcuts(mIsAboveIcon)); 694 } 695 final int duration = getResources().getInteger( 696 R.integer.config_removeNotificationViewDuration); 697 removeNotification.play(adjustItemHeights(mNotificationItemView.getHeightMinusFooter(), 698 hiddenShortcutsHeight, duration)); 699 Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0) 700 .setDuration(duration); 701 fade.addListener(new AnimatorListenerAdapter() { 702 @Override 703 public void onAnimationEnd(Animator animation) { 704 removeView(mNotificationItemView); 705 mNotificationItemView = null; 706 if (getItemCount() == 0) { 707 close(false); 708 } 709 } 710 }); 711 removeNotification.play(fade); 712 final long arrowScaleDuration = getResources().getInteger( 713 R.integer.config_popupArrowOpenDuration); 714 Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration); 715 hideArrow.setStartDelay(0); 716 Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration); 717 showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5)); 718 removeNotification.playSequentially(hideArrow, showArrow); 719 removeNotification.start(); 720 return; 721 } 722 mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly( 723 badgeInfo.getNotificationKeys())); 724 } 725 726 @Override 727 protected void onWidgetsBound() { 728 if (mShortcutsItemView != null) { 729 mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon); 730 } 731 } 732 733 private ObjectAnimator createArrowScaleAnim(float scale) { 734 return LauncherAnimUtils.ofPropertyValuesHolder( 735 mArrow, new PropertyListBuilder().scale(scale).build()); 736 } 737 738 public Animator reduceNotificationViewHeight(int heightToRemove, int duration) { 739 return adjustItemHeights(heightToRemove, 0, duration); 740 } 741 742 /** 743 * Animates the height of the notification item and the translationY of other items accordingly. 744 */ 745 public Animator adjustItemHeights(int notificationHeightToRemove, int shortcutHeightToAdd, 746 int duration) { 747 if (mReduceHeightAnimatorSet != null) { 748 mReduceHeightAnimatorSet.cancel(); 749 } 750 final int translateYBy = mIsAboveIcon ? notificationHeightToRemove - shortcutHeightToAdd 751 : -notificationHeightToRemove; 752 mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet(); 753 boolean removingNotification = 754 notificationHeightToRemove == mNotificationItemView.getHeightMinusFooter(); 755 boolean shouldRemoveNotificationHeightFromTop = mIsAboveIcon && removingNotification; 756 mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval( 757 notificationHeightToRemove, shouldRemoveNotificationHeightFromTop)); 758 PropertyResetListener<View, Float> resetTranslationYListener 759 = new PropertyResetListener<>(TRANSLATION_Y, 0f); 760 boolean itemIsAfterShortcuts = false; 761 for (int i = 0; i < getItemCount(); i++) { 762 final PopupItemView itemView = getItemViewAt(i); 763 if (itemIsAfterShortcuts) { 764 // Every item after the shortcuts item needs to adjust for the new height. 765 itemView.setTranslationY(itemView.getTranslationY() - shortcutHeightToAdd); 766 } 767 if (itemView == mNotificationItemView && (!mIsAboveIcon || removingNotification)) { 768 // The notification view is already in the right place. 769 continue; 770 } 771 ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y, 772 itemView.getTranslationY() + translateYBy).setDuration(duration); 773 translateItem.addListener(resetTranslationYListener); 774 mReduceHeightAnimatorSet.play(translateItem); 775 if (itemView == mShortcutsItemView) { 776 itemIsAfterShortcuts = true; 777 } 778 } 779 if (mIsAboveIcon) { 780 // We also need to adjust the arrow position to account for the new shortcuts height. 781 mArrow.setTranslationY(mArrow.getTranslationY() - shortcutHeightToAdd); 782 } 783 mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() { 784 @Override 785 public void onAnimationEnd(Animator animation) { 786 if (mIsAboveIcon) { 787 // All the items, including the notification item, translated down, but the 788 // container itself did not. This means the items would jump back to their 789 // original translation unless we update the container's translationY here. 790 setTranslationY(getTranslationY() + translateYBy); 791 mArrow.setTranslationY(0); 792 } 793 mReduceHeightAnimatorSet = null; 794 } 795 }); 796 return mReduceHeightAnimatorSet; 797 } 798 799 @Override 800 public boolean supportsAppInfoDropTarget() { 801 return true; 802 } 803 804 @Override 805 public boolean supportsDeleteDropTarget() { 806 return false; 807 } 808 809 @Override 810 public float getIntrinsicIconScaleFactor() { 811 return 1f; 812 } 813 814 @Override 815 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, 816 boolean success) { 817 if (!success) { 818 d.dragView.remove(); 819 mLauncher.showWorkspace(true); 820 mLauncher.getDropTargetBar().onDragEnd(); 821 } 822 } 823 824 @Override 825 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 826 // Either the original icon or one of the shortcuts was dragged. 827 // Hide the container, but don't remove it yet because that interferes with touch events. 828 mDeferContainerRemoval = true; 829 animateClose(); 830 } 831 832 @Override 833 public void onDragEnd() { 834 if (!mIsOpen) { 835 if (mOpenCloseAnimator != null) { 836 // Close animation is running. 837 mDeferContainerRemoval = false; 838 } else { 839 // Close animation is not running. 840 if (mDeferContainerRemoval) { 841 closeComplete(); 842 } 843 } 844 } 845 } 846 847 @Override 848 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { 849 target.itemType = ItemType.DEEPSHORTCUT; 850 targetParent.containerType = ContainerType.DEEPSHORTCUTS; 851 } 852 853 @Override 854 protected void handleClose(boolean animate) { 855 if (animate) { 856 animateClose(); 857 } else { 858 closeComplete(); 859 } 860 } 861 862 protected void animateClose() { 863 if (!mIsOpen) { 864 return; 865 } 866 mEndRect.setEmpty(); 867 if (mOpenCloseAnimator != null) { 868 Outline outline = new Outline(); 869 getOutlineProvider().getOutline(this, outline); 870 outline.getRect(mEndRect); 871 mOpenCloseAnimator.cancel(); 872 } 873 mIsOpen = false; 874 875 final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet(); 876 final Resources res = getResources(); 877 final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration); 878 final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); 879 880 // Rectangular reveal (reversed). 881 int itemsTotalHeight = 0; 882 for (int i = 0; i < getItemCount(); i++) { 883 itemsTotalHeight += getItemViewAt(i).getMeasuredHeight(); 884 } 885 Point startPoint = computeAnimStartPoint(itemsTotalHeight); 886 int top = mIsAboveIcon ? getPaddingTop() : startPoint.y; 887 float radius = getItemViewAt(0).getBackgroundRadius(); 888 mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y); 889 if (mEndRect.isEmpty()) { 890 mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight); 891 } 892 final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider( 893 radius, radius, mStartRect, mEndRect).createRevealAnimator(this, true); 894 revealAnim.setDuration(revealDuration); 895 revealAnim.setInterpolator(revealInterpolator); 896 closeAnim.play(revealAnim); 897 898 Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0); 899 fadeOut.setDuration(revealDuration); 900 fadeOut.setInterpolator(revealInterpolator); 901 closeAnim.play(fadeOut); 902 903 // Animate original icon's text back in. 904 Animator fadeText = mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */); 905 fadeText.setDuration(revealDuration); 906 closeAnim.play(fadeText); 907 908 closeAnim.addListener(new AnimatorListenerAdapter() { 909 @Override 910 public void onAnimationEnd(Animator animation) { 911 mOpenCloseAnimator = null; 912 if (mDeferContainerRemoval) { 913 setVisibility(INVISIBLE); 914 } else { 915 closeComplete(); 916 } 917 } 918 }); 919 mOpenCloseAnimator = closeAnim; 920 closeAnim.start(); 921 mOriginalIcon.forceHideBadge(false); 922 } 923 924 /** 925 * Closes the folder without animation. 926 */ 927 protected void closeComplete() { 928 if (mOpenCloseAnimator != null) { 929 mOpenCloseAnimator.cancel(); 930 mOpenCloseAnimator = null; 931 } 932 mIsOpen = false; 933 mDeferContainerRemoval = false; 934 mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible()); 935 mOriginalIcon.forceHideBadge(false); 936 mLauncher.getDragController().removeDragListener(this); 937 mLauncher.getDragLayer().removeView(this); 938 } 939 940 @Override 941 protected boolean isOfType(int type) { 942 return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0; 943 } 944 945 /** 946 * Returns a DeepShortcutsContainer which is already open or null 947 */ 948 public static PopupContainerWithArrow getOpen(Launcher launcher) { 949 return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW); 950 } 951 952 @Override 953 public int getLogContainerType() { 954 return ContainerType.DEEPSHORTCUTS; 955 } 956 } 957