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.shortcuts; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.TimeInterpolator; 23 import android.annotation.TargetApi; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.graphics.Bitmap; 28 import android.graphics.Color; 29 import android.graphics.Point; 30 import android.graphics.Rect; 31 import android.graphics.drawable.ShapeDrawable; 32 import android.os.Build; 33 import android.os.Handler; 34 import android.os.Looper; 35 import android.util.AttributeSet; 36 import android.view.Gravity; 37 import android.view.LayoutInflater; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.accessibility.AccessibilityEvent; 41 import android.view.animation.DecelerateInterpolator; 42 import android.widget.LinearLayout; 43 44 import com.android.launcher3.BubbleTextView; 45 import com.android.launcher3.DragSource; 46 import com.android.launcher3.DropTarget; 47 import com.android.launcher3.IconCache; 48 import com.android.launcher3.ItemInfo; 49 import com.android.launcher3.Launcher; 50 import com.android.launcher3.LauncherAnimUtils; 51 import com.android.launcher3.LauncherAppState; 52 import com.android.launcher3.LauncherModel; 53 import com.android.launcher3.LauncherSettings; 54 import com.android.launcher3.LauncherViewPropertyAnimator; 55 import com.android.launcher3.LogAccelerateInterpolator; 56 import com.android.launcher3.R; 57 import com.android.launcher3.ShortcutInfo; 58 import com.android.launcher3.Utilities; 59 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; 60 import com.android.launcher3.compat.UserHandleCompat; 61 import com.android.launcher3.dragndrop.DragController; 62 import com.android.launcher3.dragndrop.DragLayer; 63 import com.android.launcher3.dragndrop.DragOptions; 64 import com.android.launcher3.dragndrop.DragView; 65 import com.android.launcher3.graphics.TriangleShape; 66 import com.android.launcher3.userevent.nano.LauncherLogProto; 67 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 68 69 import java.util.Collections; 70 import java.util.List; 71 72 /** 73 * A container for shortcuts to deep links within apps. 74 */ 75 @TargetApi(Build.VERSION_CODES.N) 76 public class DeepShortcutsContainer extends LinearLayout implements View.OnLongClickListener, 77 View.OnTouchListener, DragSource, DragController.DragListener { 78 private static final String TAG = "ShortcutsContainer"; 79 80 private final Point mIconShift = new Point(); 81 82 private final Launcher mLauncher; 83 private final DeepShortcutManager mDeepShortcutsManager; 84 private final int mStartDragThreshold; 85 private final ShortcutMenuAccessibilityDelegate mAccessibilityDelegate; 86 private final boolean mIsRtl; 87 88 private BubbleTextView mDeferredDragIcon; 89 private final Rect mTempRect = new Rect(); 90 private Point mIconLastTouchPos = new Point(); 91 private boolean mIsLeftAligned; 92 private boolean mIsAboveIcon; 93 private View mArrow; 94 95 private Animator mOpenCloseAnimator; 96 private boolean mDeferContainerRemoval; 97 private boolean mIsOpen; 98 99 public DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr) { 100 super(context, attrs, defStyleAttr); 101 mLauncher = Launcher.getLauncher(context); 102 mDeepShortcutsManager = LauncherAppState.getInstance().getShortcutManager(); 103 104 mStartDragThreshold = getResources().getDimensionPixelSize( 105 R.dimen.deep_shortcuts_start_drag_threshold); 106 mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); 107 mIsRtl = Utilities.isRtl(getResources()); 108 } 109 110 public DeepShortcutsContainer(Context context, AttributeSet attrs) { 111 this(context, attrs, 0); 112 } 113 114 public DeepShortcutsContainer(Context context) { 115 this(context, null, 0); 116 } 117 118 public void populateAndShow(final BubbleTextView originalIcon, final List<String> ids) { 119 final Resources resources = getResources(); 120 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width); 121 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height); 122 final int arrowHorizontalOffset = resources.getDimensionPixelSize( 123 R.dimen.deep_shortcuts_arrow_horizontal_offset); 124 final int arrowVerticalOffset = resources.getDimensionPixelSize( 125 R.dimen.deep_shortcuts_arrow_vertical_offset); 126 127 // Add dummy views first, and populate with real shortcut info when ready. 128 final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); 129 final LayoutInflater inflater = mLauncher.getLayoutInflater(); 130 int numShortcuts = Math.min(ids.size(), ShortcutFilter.MAX_SHORTCUTS); 131 for (int i = 0; i < numShortcuts; i++) { 132 final DeepShortcutView shortcut = 133 (DeepShortcutView) inflater.inflate(R.layout.deep_shortcut, this, false); 134 if (i < numShortcuts - 1) { 135 ((LayoutParams) shortcut.getLayoutParams()).bottomMargin = spacing; 136 } 137 shortcut.getBubbleText().setAccessibilityDelegate(mAccessibilityDelegate); 138 addView(shortcut); 139 } 140 setContentDescription(getContext().getString(R.string.shortcuts_menu_description, 141 numShortcuts, originalIcon.getContentDescription().toString())); 142 143 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 144 orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); 145 146 // Add the arrow. 147 mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight); 148 mArrow.setPivotX(arrowWidth / 2); 149 mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight); 150 151 animateOpen(); 152 153 deferDrag(originalIcon); 154 155 // Load the shortcuts on a background thread and update the container as it animates. 156 final Looper workerLooper = LauncherModel.getWorkerLooper(); 157 final Handler uiHandler = new Handler(Looper.getMainLooper()); 158 final ItemInfo originalInfo = (ItemInfo) originalIcon.getTag(); 159 final UserHandleCompat user = originalInfo.user; 160 final ComponentName activity = originalInfo.getTargetComponent(); 161 new Handler(workerLooper).postAtFrontOfQueue(new Runnable() { 162 @Override 163 public void run() { 164 final List<ShortcutInfoCompat> shortcuts = ShortcutFilter.sortAndFilterShortcuts( 165 mDeepShortcutsManager.queryForShortcutsContainer(activity, ids, user)); 166 // We want the lowest rank to be closest to the user's finger. 167 if (mIsAboveIcon) { 168 Collections.reverse(shortcuts); 169 } 170 for (int i = 0; i < shortcuts.size(); i++) { 171 final ShortcutInfoCompat shortcut = shortcuts.get(i); 172 uiHandler.post(new UpdateShortcutChild( 173 i, new UnbadgedShortcutInfo(shortcut, mLauncher))); 174 } 175 } 176 }); 177 } 178 179 /** Updates the child of this container at the given index based on the given shortcut info. */ 180 private class UpdateShortcutChild implements Runnable { 181 private int mShortcutChildIndex; 182 private UnbadgedShortcutInfo mShortcutChildInfo; 183 184 public UpdateShortcutChild(int shortcutChildIndex, UnbadgedShortcutInfo shortcutChildInfo) { 185 mShortcutChildIndex = shortcutChildIndex; 186 mShortcutChildInfo = shortcutChildInfo; 187 } 188 189 @Override 190 public void run() { 191 getShortcutAt(mShortcutChildIndex) 192 .applyShortcutInfo(mShortcutChildInfo, DeepShortcutsContainer.this); 193 } 194 } 195 196 private DeepShortcutView getShortcutAt(int index) { 197 if (!mIsAboveIcon) { 198 // Opening down, so arrow is the first view. 199 index++; 200 } 201 return (DeepShortcutView) getChildAt(index); 202 } 203 204 private int getShortcutCount() { 205 // All children except the arrow are shortcuts. 206 return getChildCount() - 1; 207 } 208 209 private void animateOpen() { 210 setVisibility(View.VISIBLE); 211 mIsOpen = true; 212 213 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet(); 214 final int shortcutCount = getShortcutCount(); 215 216 final long duration = getResources().getInteger( 217 R.integer.config_deepShortcutOpenDuration); 218 final long arrowScaleDuration = getResources().getInteger( 219 R.integer.config_deepShortcutArrowOpenDuration); 220 final long arrowScaleDelay = duration - arrowScaleDuration; 221 final long stagger = getResources().getInteger( 222 R.integer.config_deepShortcutOpenStagger); 223 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0); 224 225 // Animate shortcuts 226 DecelerateInterpolator interpolator = new DecelerateInterpolator(); 227 for (int i = 0; i < shortcutCount; i++) { 228 final DeepShortcutView deepShortcutView = getShortcutAt(i); 229 deepShortcutView.setVisibility(INVISIBLE); 230 deepShortcutView.setAlpha(0); 231 232 Animator anim = deepShortcutView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned); 233 anim.addListener(new AnimatorListenerAdapter() { 234 @Override 235 public void onAnimationStart(Animator animation) { 236 deepShortcutView.setVisibility(VISIBLE); 237 } 238 }); 239 anim.setDuration(duration); 240 int animationIndex = mIsAboveIcon ? shortcutCount - i - 1 : i; 241 anim.setStartDelay(stagger * animationIndex); 242 anim.setInterpolator(interpolator); 243 shortcutAnims.play(anim); 244 245 Animator fadeAnim = new LauncherViewPropertyAnimator(deepShortcutView).alpha(1); 246 fadeAnim.setInterpolator(fadeInterpolator); 247 // We want the shortcut to be fully opaque before the arrow starts animating. 248 fadeAnim.setDuration(arrowScaleDelay); 249 shortcutAnims.play(fadeAnim); 250 } 251 shortcutAnims.addListener(new AnimatorListenerAdapter() { 252 @Override 253 public void onAnimationEnd(Animator animation) { 254 mOpenCloseAnimator = null; 255 Utilities.sendCustomAccessibilityEvent( 256 DeepShortcutsContainer.this, 257 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 258 getContext().getString(R.string.action_deep_shortcut)); 259 } 260 }); 261 262 // Animate the arrow 263 mArrow.setScaleX(0); 264 mArrow.setScaleY(0); 265 Animator arrowScale = new LauncherViewPropertyAnimator(mArrow).scaleX(1).scaleY(1); 266 arrowScale.setStartDelay(arrowScaleDelay); 267 arrowScale.setDuration(arrowScaleDuration); 268 shortcutAnims.play(arrowScale); 269 270 mOpenCloseAnimator = shortcutAnims; 271 shortcutAnims.start(); 272 } 273 274 /** 275 * Orients this container above or below the given icon, aligning with the left or right. 276 * 277 * These are the preferred orientations, in order (RTL prefers right-aligned over left): 278 * - Above and left-aligned 279 * - Above and right-aligned 280 * - Below and left-aligned 281 * - Below and right-aligned 282 * 283 * So we always align left if there is enough horizontal space 284 * and align above if there is enough vertical space. 285 */ 286 private void orientAboutIcon(BubbleTextView icon, int arrowHeight) { 287 int width = getMeasuredWidth(); 288 int height = getMeasuredHeight() + arrowHeight; 289 290 DragLayer dragLayer = mLauncher.getDragLayer(); 291 dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect); 292 Rect insets = dragLayer.getInsets(); 293 294 // Align left (right in RTL) if there is room. 295 int leftAlignedX = mTempRect.left + icon.getPaddingLeft(); 296 int rightAlignedX = mTempRect.right - width - icon.getPaddingRight(); 297 int x = leftAlignedX; 298 boolean canBeLeftAligned = leftAlignedX + width < dragLayer.getRight() - insets.right; 299 boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left; 300 if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) { 301 x = rightAlignedX; 302 } 303 mIsLeftAligned = x == leftAlignedX; 304 if (mIsRtl) { 305 x -= dragLayer.getWidth() - width; 306 } 307 308 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon. 309 int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight(); 310 iconWidth *= icon.getScaleX(); 311 Resources resources = getResources(); 312 int xOffset; 313 if (isAlignedWithStart()) { 314 // Aligning with the shortcut icon. 315 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size); 316 int shortcutPaddingStart = resources.getDimensionPixelSize( 317 R.dimen.deep_shortcut_padding_start); 318 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart; 319 } else { 320 // Aligning with the drag handle. 321 int shortcutDragHandleWidth = resources.getDimensionPixelSize( 322 R.dimen.deep_shortcut_drag_handle_size); 323 int shortcutPaddingEnd = resources.getDimensionPixelSize( 324 R.dimen.deep_shortcut_padding_end); 325 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd; 326 } 327 x += mIsLeftAligned ? xOffset : -xOffset; 328 329 // Open above icon if there is room. 330 int iconHeight = icon.getIcon().getBounds().height(); 331 int y = mTempRect.top + icon.getPaddingTop() - height; 332 mIsAboveIcon = y > dragLayer.getTop() + insets.top; 333 if (!mIsAboveIcon) { 334 y = mTempRect.top + icon.getPaddingTop() + iconHeight; 335 } 336 337 // Insets are added later, so subtract them now. 338 y -= insets.top; 339 340 setX(x); 341 setY(y); 342 } 343 344 private boolean isAlignedWithStart() { 345 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl; 346 } 347 348 /** 349 * Adds an arrow view pointing at the original icon. 350 * @param horizontalOffset the horizontal offset of the arrow, so that it 351 * points at the center of the original icon 352 */ 353 private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) { 354 LinearLayout.LayoutParams layoutParams = new LayoutParams(width, height); 355 if (mIsLeftAligned) { 356 layoutParams.gravity = Gravity.LEFT; 357 layoutParams.leftMargin = horizontalOffset; 358 } else { 359 layoutParams.gravity = Gravity.RIGHT; 360 layoutParams.rightMargin = horizontalOffset; 361 } 362 if (mIsAboveIcon) { 363 layoutParams.topMargin = verticalOffset; 364 } else { 365 layoutParams.bottomMargin = verticalOffset; 366 } 367 368 View arrowView = new View(getContext()); 369 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( 370 width, height, !mIsAboveIcon)); 371 arrowDrawable.getPaint().setColor(Color.WHITE); 372 arrowView.setBackground(arrowDrawable); 373 arrowView.setElevation(getElevation()); 374 addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams); 375 return arrowView; 376 } 377 378 private void deferDrag(BubbleTextView originalIcon) { 379 mDeferredDragIcon = originalIcon; 380 mLauncher.getDragController().addDragListener(this); 381 } 382 383 public BubbleTextView getDeferredDragIcon() { 384 return mDeferredDragIcon; 385 } 386 387 /** 388 * Determines when the deferred drag should be started. 389 * 390 * Current behavior: 391 * - Start the drag if the touch passes a certain distance from the original touch down. 392 */ 393 public DragOptions.DeferDragCondition createDeferDragCondition(final Runnable onDragStart) { 394 return new DragOptions.DeferDragCondition() { 395 @Override 396 public boolean shouldStartDeferredDrag(double distanceDragged) { 397 return distanceDragged > mStartDragThreshold; 398 } 399 400 @Override 401 public void onDeferredDragStart() { 402 mDeferredDragIcon.setVisibility(INVISIBLE); 403 } 404 405 @Override 406 public void onDropBeforeDeferredDrag() { 407 mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mDeferredDragIcon); 408 if (!mIsAboveIcon) { 409 mDeferredDragIcon.setTextVisibility(false); 410 } 411 } 412 413 @Override 414 public void onDragStart() { 415 if (onDragStart != null) { 416 onDragStart.run(); 417 } 418 } 419 }; 420 } 421 422 @Override 423 public boolean onTouch(View v, MotionEvent ev) { 424 // Touched a shortcut, update where it was touched so we can drag from there on long click. 425 switch (ev.getAction()) { 426 case MotionEvent.ACTION_DOWN: 427 case MotionEvent.ACTION_MOVE: 428 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); 429 break; 430 } 431 return false; 432 } 433 434 public boolean onLongClick(View v) { 435 // Return early if this is not initiated from a touch or not the correct view 436 if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false; 437 // Return if global dragging is not enabled 438 if (!mLauncher.isDraggingEnabled()) return false; 439 440 // Long clicked on a shortcut. 441 mDeferContainerRemoval = true; 442 DeepShortcutView sv = (DeepShortcutView) v.getParent(); 443 sv.setWillDrawIcon(false); 444 445 // Move the icon to align with the center-top of the touch point 446 mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; 447 mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; 448 449 DragView dv = mLauncher.getWorkspace().beginDragShared( 450 sv.getBubbleText(), this, sv.getFinalInfo(), 451 new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions()); 452 dv.animateShift(-mIconShift.x, -mIconShift.y); 453 454 // TODO: support dragging from within folder without having to close it 455 mLauncher.closeFolder(); 456 return false; 457 } 458 459 @Override 460 public boolean supportsFlingToDelete() { 461 return true; 462 } 463 464 @Override 465 public boolean supportsAppInfoDropTarget() { 466 return true; 467 } 468 469 @Override 470 public boolean supportsDeleteDropTarget() { 471 return false; 472 } 473 474 @Override 475 public float getIntrinsicIconScaleFactor() { 476 return 1f; 477 } 478 479 @Override 480 public void onFlingToDeleteCompleted() { 481 // Don't care; ignore. 482 } 483 484 @Override 485 public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, 486 boolean success) { 487 if (!success) { 488 d.dragView.remove(); 489 mLauncher.showWorkspace(true); 490 mLauncher.getDropTargetBar().onDragEnd(); 491 } 492 } 493 494 @Override 495 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 496 // Either the original icon or one of the shortcuts was dragged. 497 // Hide the container, but don't remove it yet because that interferes with touch events. 498 animateClose(); 499 } 500 501 @Override 502 public void onDragEnd() { 503 if (!mIsOpen) { 504 if (mOpenCloseAnimator != null) { 505 // Close animation is running. 506 mDeferContainerRemoval = false; 507 } else { 508 // Close animation is not running. 509 if (mDeferContainerRemoval) { 510 close(); 511 } 512 } 513 } 514 mDeferredDragIcon.setVisibility(VISIBLE); 515 } 516 517 @Override 518 public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) { 519 target.itemType = LauncherLogProto.DEEPSHORTCUT; 520 // TODO: add target.rank 521 targetParent.containerType = LauncherLogProto.DEEPSHORTCUTS; 522 } 523 524 public void animateClose() { 525 if (!mIsOpen) { 526 return; 527 } 528 if (mOpenCloseAnimator != null) { 529 mOpenCloseAnimator.cancel(); 530 } 531 mIsOpen = false; 532 533 final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet(); 534 final int shortcutCount = getShortcutCount(); 535 int numOpenShortcuts = 0; 536 for (int i = 0; i < shortcutCount; i++) { 537 if (getShortcutAt(i).isOpenOrOpening()) { 538 numOpenShortcuts++; 539 } 540 } 541 final long duration = getResources().getInteger( 542 R.integer.config_deepShortcutCloseDuration); 543 final long arrowScaleDuration = getResources().getInteger( 544 R.integer.config_deepShortcutArrowOpenDuration); 545 final long stagger = getResources().getInteger( 546 R.integer.config_deepShortcutCloseStagger); 547 final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0); 548 549 int firstOpenShortcutIndex = mIsAboveIcon ? shortcutCount - numOpenShortcuts : 0; 550 for (int i = firstOpenShortcutIndex; i < firstOpenShortcutIndex + numOpenShortcuts; i++) { 551 final DeepShortcutView view = getShortcutAt(i); 552 Animator anim; 553 if (view.willDrawIcon()) { 554 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration); 555 int animationIndex = mIsAboveIcon ? i - firstOpenShortcutIndex 556 : numOpenShortcuts - i - 1; 557 anim.setStartDelay(stagger * animationIndex); 558 559 Animator fadeAnim = new LauncherViewPropertyAnimator(view).alpha(0); 560 // Don't start fading until the arrow is gone. 561 fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration); 562 fadeAnim.setDuration(duration - arrowScaleDuration); 563 fadeAnim.setInterpolator(fadeInterpolator); 564 shortcutAnims.play(fadeAnim); 565 } else { 566 // The view is being dragged. Animate it such that it collapses with the drag view 567 anim = view.collapseToIcon(); 568 anim.setDuration(DragView.VIEW_ZOOM_DURATION); 569 570 // Scale and translate the view to follow the drag view. 571 Point iconCenter = view.getIconCenter(); 572 view.setPivotX(iconCenter.x); 573 view.setPivotY(iconCenter.y); 574 575 float scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / view.getHeight(); 576 LauncherViewPropertyAnimator anim2 = new LauncherViewPropertyAnimator(view) 577 .scaleX(scale) 578 .scaleY(scale) 579 .translationX(mIconShift.x) 580 .translationY(mIconShift.y); 581 anim2.setDuration(DragView.VIEW_ZOOM_DURATION); 582 shortcutAnims.play(anim2); 583 } 584 anim.addListener(new AnimatorListenerAdapter() { 585 @Override 586 public void onAnimationEnd(Animator animation) { 587 view.setVisibility(INVISIBLE); 588 } 589 }); 590 shortcutAnims.play(anim); 591 } 592 Animator arrowAnim = new LauncherViewPropertyAnimator(mArrow) 593 .scaleX(0).scaleY(0).setDuration(arrowScaleDuration); 594 arrowAnim.setStartDelay(0); 595 shortcutAnims.play(arrowAnim); 596 597 shortcutAnims.addListener(new AnimatorListenerAdapter() { 598 @Override 599 public void onAnimationEnd(Animator animation) { 600 mOpenCloseAnimator = null; 601 if (mDeferContainerRemoval) { 602 setVisibility(INVISIBLE); 603 } else { 604 close(); 605 } 606 } 607 }); 608 mOpenCloseAnimator = shortcutAnims; 609 shortcutAnims.start(); 610 } 611 612 /** 613 * Closes the folder without animation. 614 */ 615 public void close() { 616 if (mOpenCloseAnimator != null) { 617 mOpenCloseAnimator.cancel(); 618 mOpenCloseAnimator = null; 619 } 620 mIsOpen = false; 621 mDeferContainerRemoval = false; 622 boolean isInHotseat = ((ItemInfo) mDeferredDragIcon.getTag()).container 623 == LauncherSettings.Favorites.CONTAINER_HOTSEAT; 624 mDeferredDragIcon.setTextVisibility(!isInHotseat); 625 mLauncher.getDragController().removeDragListener(this); 626 mLauncher.getDragLayer().removeView(this); 627 } 628 629 public boolean isOpen() { 630 return mIsOpen; 631 } 632 633 /** 634 * Shows the shortcuts container for {@param icon} 635 * @return the container if shown or null. 636 */ 637 public static DeepShortcutsContainer showForIcon(BubbleTextView icon) { 638 Launcher launcher = Launcher.getLauncher(icon.getContext()); 639 if (launcher.getOpenShortcutsContainer() != null) { 640 // There is already a shortcuts container open, so don't open this one. 641 icon.clearFocus(); 642 return null; 643 } 644 List<String> ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag()); 645 if (!ids.isEmpty()) { 646 // There are shortcuts associated with the app, so defer its drag. 647 final DeepShortcutsContainer container = 648 (DeepShortcutsContainer) launcher.getLayoutInflater().inflate( 649 R.layout.deep_shortcuts_container, launcher.getDragLayer(), false); 650 container.setVisibility(View.INVISIBLE); 651 launcher.getDragLayer().addView(container); 652 container.populateAndShow(icon, ids); 653 return container; 654 } 655 return null; 656 } 657 658 /** 659 * Extension of {@link ShortcutInfo} which does not badge the icons. 660 */ 661 static class UnbadgedShortcutInfo extends ShortcutInfo { 662 public final ShortcutInfoCompat mDetail; 663 664 public UnbadgedShortcutInfo(ShortcutInfoCompat shortcutInfo, Context context) { 665 super(shortcutInfo, context); 666 mDetail = shortcutInfo; 667 } 668 669 @Override 670 protected Bitmap getBadgedIcon(Bitmap unbadgedBitmap, ShortcutInfoCompat shortcutInfo, 671 IconCache cache, Context context) { 672 return unbadgedBitmap; 673 } 674 } 675 } 676