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 static com.android.launcher3.Utilities.squaredHypot; 20 import static com.android.launcher3.Utilities.squaredTouchSlop; 21 import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO; 22 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS; 23 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS; 24 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 25 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; 26 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target; 27 28 import android.animation.AnimatorSet; 29 import android.animation.LayoutTransition; 30 import android.annotation.TargetApi; 31 import android.content.Context; 32 import android.graphics.Point; 33 import android.graphics.PointF; 34 import android.graphics.Rect; 35 import android.os.Build; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.Pair; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.ImageView; 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.DropTarget.DragObject; 51 import com.android.launcher3.ItemInfo; 52 import com.android.launcher3.ItemInfoWithIcon; 53 import com.android.launcher3.Launcher; 54 import com.android.launcher3.LauncherModel; 55 import com.android.launcher3.R; 56 import com.android.launcher3.Utilities; 57 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; 58 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; 59 import com.android.launcher3.dot.DotInfo; 60 import com.android.launcher3.dragndrop.DragController; 61 import com.android.launcher3.dragndrop.DragOptions; 62 import com.android.launcher3.dragndrop.DragView; 63 import com.android.launcher3.logging.LoggerUtils; 64 import com.android.launcher3.notification.NotificationInfo; 65 import com.android.launcher3.notification.NotificationItemView; 66 import com.android.launcher3.notification.NotificationKeyData; 67 import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener; 68 import com.android.launcher3.shortcuts.DeepShortcutManager; 69 import com.android.launcher3.shortcuts.DeepShortcutView; 70 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; 71 import com.android.launcher3.testing.TestProtocol; 72 import com.android.launcher3.touch.ItemClickHandler; 73 import com.android.launcher3.touch.ItemLongClickListener; 74 import com.android.launcher3.util.PackageUserKey; 75 import com.android.launcher3.views.BaseDragLayer; 76 77 import java.util.ArrayList; 78 import java.util.List; 79 import java.util.Map; 80 import java.util.function.Predicate; 81 82 /** 83 * A container for shortcuts to deep links and notifications associated with an app. 84 */ 85 public class PopupContainerWithArrow extends ArrowPopup implements DragSource, 86 DragController.DragListener, View.OnLongClickListener, 87 View.OnTouchListener, PopupDataChangeListener { 88 89 private final List<DeepShortcutView> mShortcuts = new ArrayList<>(); 90 private final PointF mInterceptTouchDown = new PointF(); 91 protected final Point mIconLastTouchPos = new Point(); 92 93 private final int mStartDragThreshold; 94 private final LauncherAccessibilityDelegate mAccessibilityDelegate; 95 96 private BubbleTextView mOriginalIcon; 97 private NotificationItemView mNotificationItemView; 98 private int mNumNotifications; 99 100 private ViewGroup mSystemShortcutContainer; 101 PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr)102 public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) { 103 super(context, attrs, defStyleAttr); 104 mStartDragThreshold = getResources().getDimensionPixelSize( 105 R.dimen.deep_shortcuts_start_drag_threshold); 106 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); 107 } 108 PopupContainerWithArrow(Context context, AttributeSet attrs)109 public PopupContainerWithArrow(Context context, AttributeSet attrs) { 110 this(context, attrs, 0); 111 } 112 PopupContainerWithArrow(Context context)113 public PopupContainerWithArrow(Context context) { 114 this(context, null, 0); 115 } 116 getAccessibilityDelegate()117 public LauncherAccessibilityDelegate getAccessibilityDelegate() { 118 return mAccessibilityDelegate; 119 } 120 121 @Override onAttachedToWindow()122 protected void onAttachedToWindow() { 123 super.onAttachedToWindow(); 124 mLauncher.getPopupDataProvider().setChangeListener(this); 125 } 126 127 @Override onDetachedFromWindow()128 protected void onDetachedFromWindow() { 129 super.onDetachedFromWindow(); 130 mLauncher.getPopupDataProvider().setChangeListener(null); 131 } 132 133 @Override onInterceptTouchEvent(MotionEvent ev)134 public boolean onInterceptTouchEvent(MotionEvent ev) { 135 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 136 mInterceptTouchDown.set(ev.getX(), ev.getY()); 137 } 138 if (mNotificationItemView != null 139 && mNotificationItemView.onInterceptTouchEvent(ev)) { 140 return true; 141 } 142 // Stop sending touch events to deep shortcut views if user moved beyond touch slop. 143 return squaredHypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY()) 144 > squaredTouchSlop(getContext()); 145 } 146 147 @Override onTouchEvent(MotionEvent ev)148 public boolean onTouchEvent(MotionEvent ev) { 149 if (mNotificationItemView != null) { 150 return mNotificationItemView.onTouchEvent(ev) || super.onTouchEvent(ev); 151 } 152 return super.onTouchEvent(ev); 153 } 154 155 @Override isOfType(int type)156 protected boolean isOfType(int type) { 157 return (type & TYPE_ACTION_POPUP) != 0; 158 } 159 160 @Override logActionCommand(int command)161 public void logActionCommand(int command) { 162 mLauncher.getUserEventDispatcher().logActionCommand( 163 command, mOriginalIcon, getLogContainerType()); 164 } 165 166 @Override getLogContainerType()167 public int getLogContainerType() { 168 return ContainerType.DEEPSHORTCUTS; 169 } 170 getItemClickListener()171 public OnClickListener getItemClickListener() { 172 return ItemClickHandler.INSTANCE; 173 } 174 175 @Override onControllerInterceptTouchEvent(MotionEvent ev)176 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 177 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 178 BaseDragLayer dl = getPopupContainer(); 179 if (!dl.isEventOverView(this, ev)) { 180 mLauncher.getUserEventDispatcher().logActionTapOutside( 181 LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS)); 182 close(true); 183 184 // We let touches on the original icon go through so that users can launch 185 // the app with one tap if they don't find a shortcut they want. 186 return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev); 187 } 188 } 189 return false; 190 } 191 192 /** 193 * Shows the notifications and deep shortcuts associated with {@param icon}. 194 * @return the container if shown or null. 195 */ showForIcon(BubbleTextView icon)196 public static PopupContainerWithArrow showForIcon(BubbleTextView icon) { 197 Launcher launcher = Launcher.getLauncher(icon.getContext()); 198 if (getOpen(launcher) != null) { 199 // There is already an items container open, so don't open this one. 200 icon.clearFocus(); 201 return null; 202 } 203 ItemInfo itemInfo = (ItemInfo) icon.getTag(); 204 if (!DeepShortcutManager.supportsShortcuts(itemInfo)) { 205 return null; 206 } 207 208 final PopupContainerWithArrow container = 209 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( 210 R.layout.popup_container, launcher.getDragLayer(), false); 211 container.populateAndShow(icon, itemInfo, SystemShortcutFactory.INSTANCE.get(launcher)); 212 return container; 213 } 214 215 @Override onInflationComplete(boolean isReversed)216 protected void onInflationComplete(boolean isReversed) { 217 if (isReversed && mNotificationItemView != null) { 218 mNotificationItemView.inverseGutterMargin(); 219 } 220 221 // Update dividers 222 int count = getChildCount(); 223 DeepShortcutView lastView = null; 224 for (int i = 0; i < count; i++) { 225 View view = getChildAt(i); 226 if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { 227 if (lastView != null) { 228 lastView.setDividerVisibility(VISIBLE); 229 } 230 lastView = (DeepShortcutView) view; 231 lastView.setDividerVisibility(INVISIBLE); 232 } 233 } 234 } 235 populateAndShow( BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory)236 protected void populateAndShow( 237 BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory) { 238 PopupDataProvider popupDataProvider = mLauncher.getPopupDataProvider(); 239 populateAndShow(icon, 240 popupDataProvider.getShortcutCountForItem(item), 241 popupDataProvider.getNotificationKeysForItem(item), 242 factory.getEnabledShortcuts(mLauncher, item)); 243 } 244 getSystemShortcutContainerForTesting()245 public ViewGroup getSystemShortcutContainerForTesting() { 246 return mSystemShortcutContainer; 247 } 248 249 @TargetApi(Build.VERSION_CODES.P) populateAndShow(final BubbleTextView originalIcon, int shortcutCount, final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts)250 protected void populateAndShow(final BubbleTextView originalIcon, int shortcutCount, 251 final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) { 252 mNumNotifications = notificationKeys.size(); 253 mOriginalIcon = originalIcon; 254 255 // Add views 256 if (mNumNotifications > 0) { 257 // Add notification entries 258 View.inflate(getContext(), R.layout.notification_content, this); 259 mNotificationItemView = new NotificationItemView(this); 260 if (mNumNotifications == 1) { 261 mNotificationItemView.removeFooter(); 262 } 263 updateNotificationHeader(); 264 } 265 int viewsToFlip = getChildCount(); 266 mSystemShortcutContainer = this; 267 268 if (shortcutCount > 0) { 269 if (mNotificationItemView != null) { 270 mNotificationItemView.addGutter(); 271 } 272 273 for (int i = shortcutCount; i > 0; i--) { 274 mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this)); 275 } 276 updateHiddenShortcuts(); 277 278 if (!systemShortcuts.isEmpty()) { 279 mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this); 280 for (SystemShortcut shortcut : systemShortcuts) { 281 initializeSystemShortcut( 282 R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut); 283 } 284 } 285 } else if (!systemShortcuts.isEmpty()) { 286 if (mNotificationItemView != null) { 287 mNotificationItemView.addGutter(); 288 } 289 290 for (SystemShortcut shortcut : systemShortcuts) { 291 initializeSystemShortcut(R.layout.system_shortcut, this, shortcut); 292 } 293 } 294 295 reorderAndShow(viewsToFlip); 296 297 ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); 298 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 299 setAccessibilityPaneTitle(getTitleForAccessibility()); 300 } 301 302 mLauncher.getDragController().addDragListener(this); 303 mOriginalIcon.forceHideDot(true); 304 305 // All views are added. Animate layout from now on. 306 setLayoutTransition(new LayoutTransition()); 307 308 // Load the shortcuts on a background thread and update the container as it animates. 309 final Looper workerLooper = LauncherModel.getWorkerLooper(); 310 new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable( 311 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()), 312 this, mShortcuts, notificationKeys)); 313 } 314 getTitleForAccessibility()315 private String getTitleForAccessibility() { 316 return getContext().getString(mNumNotifications == 0 ? 317 R.string.action_deep_shortcut : 318 R.string.shortcuts_menu_with_notifications_description); 319 } 320 321 @Override getAccessibilityTarget()322 protected Pair<View, String> getAccessibilityTarget() { 323 return Pair.create(this, ""); 324 } 325 326 @Override getTargetObjectLocation(Rect outPos)327 protected void getTargetObjectLocation(Rect outPos) { 328 getPopupContainer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos); 329 outPos.top += mOriginalIcon.getPaddingTop(); 330 outPos.left += mOriginalIcon.getPaddingLeft(); 331 outPos.right -= mOriginalIcon.getPaddingRight(); 332 outPos.bottom = outPos.top + (mOriginalIcon.getIcon() != null 333 ? mOriginalIcon.getIcon().getBounds().height() 334 : mOriginalIcon.getHeight()); 335 } 336 applyNotificationInfos(List<NotificationInfo> notificationInfos)337 public void applyNotificationInfos(List<NotificationInfo> notificationInfos) { 338 mNotificationItemView.applyNotificationInfos(notificationInfos); 339 } 340 updateHiddenShortcuts()341 private void updateHiddenShortcuts() { 342 int allowedCount = mNotificationItemView != null 343 ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; 344 int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height); 345 int itemHeight = mNotificationItemView != null ? 346 getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height) 347 : originalHeight; 348 float iconScale = ((float) itemHeight) / originalHeight; 349 350 int total = mShortcuts.size(); 351 for (int i = 0; i < total; i++) { 352 DeepShortcutView view = mShortcuts.get(i); 353 view.setVisibility(i >= allowedCount ? GONE : VISIBLE); 354 view.getLayoutParams().height = itemHeight; 355 view.getIconView().setScaleX(iconScale); 356 view.getIconView().setScaleY(iconScale); 357 } 358 } 359 updateDividers()360 private void updateDividers() { 361 int count = getChildCount(); 362 DeepShortcutView lastView = null; 363 for (int i = 0; i < count; i++) { 364 View view = getChildAt(i); 365 if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { 366 if (lastView != null) { 367 lastView.setDividerVisibility(VISIBLE); 368 } 369 lastView = (DeepShortcutView) view; 370 lastView.setDividerVisibility(INVISIBLE); 371 } 372 } 373 } 374 375 @Override onWidgetsBound()376 public void onWidgetsBound() { 377 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); 378 SystemShortcut widgetInfo = new SystemShortcut.Widgets(); 379 View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo); 380 View widgetsView = null; 381 int count = mSystemShortcutContainer.getChildCount(); 382 for (int i = 0; i < count; i++) { 383 View systemShortcutView = mSystemShortcutContainer.getChildAt(i); 384 if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { 385 widgetsView = systemShortcutView; 386 break; 387 } 388 } 389 390 if (onClickListener != null && widgetsView == null) { 391 // We didn't have any widgets cached but now there are some, so enable the shortcut. 392 if (mSystemShortcutContainer != this) { 393 initializeSystemShortcut( 394 R.layout.system_shortcut_icon_only, mSystemShortcutContainer, widgetInfo); 395 } else { 396 // If using the expanded system shortcut (as opposed to just the icon), we need to 397 // reopen the container to ensure measurements etc. all work out. While this could 398 // be quite janky, in practice the user would typically see a small flicker as the 399 // animation restarts partway through, and this is a very rare edge case anyway. 400 close(false); 401 PopupContainerWithArrow.showForIcon(mOriginalIcon); 402 } 403 } else if (onClickListener == null && widgetsView != null) { 404 // No widgets exist, but we previously added the shortcut so remove it. 405 if (mSystemShortcutContainer != this) { 406 mSystemShortcutContainer.removeView(widgetsView); 407 } else { 408 close(false); 409 PopupContainerWithArrow.showForIcon(mOriginalIcon); 410 } 411 } 412 } 413 initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info)414 private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) { 415 View view = inflateAndAdd( 416 resId, container, getInsertIndexForSystemShortcut(container, info)); 417 if (view instanceof DeepShortcutView) { 418 // Expanded system shortcut, with both icon and text shown on white background. 419 final DeepShortcutView shortcutView = (DeepShortcutView) view; 420 info.setIconAndLabelFor(shortcutView.getIconView(), shortcutView.getBubbleText()); 421 } else if (view instanceof ImageView) { 422 // Only the system shortcut icon shows on a gray background header. 423 info.setIconAndContentDescriptionFor((ImageView) view); 424 } 425 view.setTag(info); 426 view.setOnClickListener(info.getOnClickListener(mLauncher, 427 (ItemInfo) mOriginalIcon.getTag())); 428 } 429 430 /** 431 * Returns an index for inserting a shortcut into a container. 432 */ getInsertIndexForSystemShortcut(ViewGroup container, SystemShortcut shortcut)433 private int getInsertIndexForSystemShortcut(ViewGroup container, SystemShortcut shortcut) { 434 final View separator = container.findViewById(R.id.separator); 435 436 return separator != null && shortcut.isLeftGroup() ? 437 container.indexOfChild(separator) : 438 container.getChildCount(); 439 } 440 441 /** 442 * Determines when the deferred drag should be started. 443 * 444 * Current behavior: 445 * - Start the drag if the touch passes a certain distance from the original touch down. 446 */ createPreDragCondition()447 public DragOptions.PreDragCondition createPreDragCondition() { 448 return new DragOptions.PreDragCondition() { 449 450 @Override 451 public boolean shouldStartDrag(double distanceDragged) { 452 if (TestProtocol.sDebugTracing) { 453 Log.d(TestProtocol.NO_DRAG_TAG, 454 "createPreDragCondition().shouldStartDrag " + distanceDragged + ", " 455 + mStartDragThreshold); 456 } 457 return distanceDragged > mStartDragThreshold; 458 } 459 460 @Override 461 public void onPreDragStart(DropTarget.DragObject dragObject) { 462 if (mIsAboveIcon) { 463 // Hide only the icon, keep the text visible. 464 mOriginalIcon.setIconVisible(false); 465 mOriginalIcon.setVisibility(VISIBLE); 466 } else { 467 // Hide both the icon and text. 468 mOriginalIcon.setVisibility(INVISIBLE); 469 } 470 } 471 472 @Override 473 public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { 474 mOriginalIcon.setIconVisible(true); 475 if (dragStarted) { 476 // Make sure we keep the original icon hidden while it is being dragged. 477 mOriginalIcon.setVisibility(INVISIBLE); 478 } else { 479 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon); 480 if (!mIsAboveIcon) { 481 // Show the icon but keep the text hidden. 482 mOriginalIcon.setVisibility(VISIBLE); 483 mOriginalIcon.setTextVisibility(false); 484 } 485 } 486 } 487 }; 488 } 489 490 /** 491 * Updates the notification header if the original icon's dot updated. 492 */ 493 @Override 494 public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) { 495 ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); 496 PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo); 497 if (updatedDots.test(packageUser)) { 498 updateNotificationHeader(); 499 } 500 } 501 502 private void updateNotificationHeader() { 503 ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag(); 504 DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo); 505 if (mNotificationItemView != null && dotInfo != null) { 506 mNotificationItemView.updateHeader( 507 dotInfo.getNotificationCount(), itemInfo.iconColor); 508 } 509 } 510 511 @Override 512 public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { 513 if (mNotificationItemView == null) { 514 return; 515 } 516 ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); 517 DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo)); 518 if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) { 519 // No more notifications, remove the notification views and expand all shortcuts. 520 mNotificationItemView.removeAllViews(); 521 mNotificationItemView = null; 522 updateHiddenShortcuts(); 523 updateDividers(); 524 } else { 525 mNotificationItemView.trimNotifications( 526 NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys())); 527 } 528 } 529 530 @Override 531 public void onDropCompleted(View target, DragObject d, boolean success) { } 532 533 @Override 534 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 535 // Either the original icon or one of the shortcuts was dragged. 536 // Hide the container, but don't remove it yet because that interferes with touch events. 537 mDeferContainerRemoval = true; 538 animateClose(); 539 } 540 541 @Override 542 public void onDragEnd() { 543 if (!mIsOpen) { 544 if (mOpenCloseAnimator != null) { 545 // Close animation is running. 546 mDeferContainerRemoval = false; 547 } else { 548 // Close animation is not running. 549 if (mDeferContainerRemoval) { 550 closeComplete(); 551 } 552 } 553 } 554 } 555 556 @Override 557 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { 558 if (info == NOTIFICATION_ITEM_INFO) { 559 target.itemType = ItemType.NOTIFICATION; 560 } else { 561 target.itemType = ItemType.DEEPSHORTCUT; 562 target.rank = info.rank; 563 } 564 targetParent.containerType = ContainerType.DEEPSHORTCUTS; 565 } 566 567 @Override 568 protected void onCreateCloseAnimation(AnimatorSet anim) { 569 // Animate original icon's text back in. 570 anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */)); 571 mOriginalIcon.forceHideDot(false); 572 } 573 574 @Override 575 protected void closeComplete() { 576 super.closeComplete(); 577 mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible()); 578 mOriginalIcon.forceHideDot(false); 579 } 580 581 @Override 582 public boolean onTouch(View v, MotionEvent ev) { 583 // Touched a shortcut, update where it was touched so we can drag from there on long click. 584 switch (ev.getAction()) { 585 case MotionEvent.ACTION_DOWN: 586 case MotionEvent.ACTION_MOVE: 587 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); 588 break; 589 } 590 return false; 591 } 592 593 @Override 594 public boolean onLongClick(View v) { 595 if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; 596 // Return early if not the correct view 597 if (!(v.getParent() instanceof DeepShortcutView)) return false; 598 599 // Long clicked on a shortcut. 600 DeepShortcutView sv = (DeepShortcutView) v.getParent(); 601 sv.setWillDrawIcon(false); 602 603 // Move the icon to align with the center-top of the touch point 604 Point iconShift = new Point(); 605 iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; 606 iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; 607 608 DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), 609 this, sv.getFinalInfo(), 610 new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions()); 611 dv.animateShift(-iconShift.x, -iconShift.y); 612 613 // TODO: support dragging from within folder without having to close it 614 AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); 615 return false; 616 } 617 618 /** 619 * Returns a PopupContainerWithArrow which is already open or null 620 */ 621 public static PopupContainerWithArrow getOpen(Launcher launcher) { 622 return getOpenView(launcher, TYPE_ACTION_POPUP); 623 } 624 } 625