shortcuts) {
+ for (int i = 0; i < shortcuts.size(); i++) {
+ DeepShortcutView iconAndText = (DeepShortcutView) getChildAt(i);
+ ShortcutInfo launcherShortcutInfo = shortcuts.get(i);
+ iconAndText.applyFromShortcutInfo(launcherShortcutInfo,
+ LauncherAppState.getInstance().getIconCache());
+ iconAndText.setOnClickListener(DeepShortcutsContainer.this);
+ iconAndText.setOnLongClickListener(DeepShortcutsContainer.this);
+ iconAndText.setOnTouchListener(DeepShortcutsContainer.this);
+ int viewId = mLauncher.getViewIdForItem(originalInfo);
+ iconAndText.setId(viewId);
+ }
+ }
+ }.execute();
+ }
+
+ // TODO: update this animation
+ private void animateOpen(BubbleTextView originalIcon) {
+ orientAboutIcon(originalIcon);
+
+ setVisibility(View.VISIBLE);
+ int rx = (int) Math.max(Math.max(getMeasuredWidth() - getPivotX(), 0), getPivotX());
+ int ry = (int) Math.max(Math.max(getMeasuredHeight() - getPivotY(), 0), getPivotY());
+ float radius = (float) Math.hypot(rx, ry);
+ Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
+ (int) getPivotY(), 0, radius);
+ reveal.setDuration(getResources().getInteger(R.integer.config_materialFolderExpandDuration));
+ reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
+ reveal.start();
+ }
+
+ /**
+ * Orients this container above or below the given icon, aligning with the left or right.
+ *
+ * These are the preferred orientations, in order:
+ * - Above and left-aligned
+ * - Above and right-aligned
+ * - Below and left-aligned
+ * - Below and right-aligned
+ *
+ * So we always align left if there is enough horizontal space
+ * and align above if there is enough vertical space.
+ *
+ * TODO: draw pointer based on orientation.
+ */
+ private void orientAboutIcon(BubbleTextView icon) {
+ int width = getMeasuredWidth();
+ int height = getMeasuredHeight();
+
+ DragLayer dragLayer = mLauncher.getDragLayer();
+ dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
+ // Align left and above by default.
+ int x = mTempRect.left + icon.getPaddingLeft();
+ int y = mTempRect.top - height;
+ Rect insets = dragLayer.getInsets();
+
+ mIsLeftAligned = x + width < dragLayer.getRight() - insets.right;
+ if (!mIsLeftAligned) {
+ x = mTempRect.right - width - icon.getPaddingRight();
+ }
+
+ mIsAboveIcon = mTempRect.top - height > dragLayer.getTop() + insets.top;
+ if (!mIsAboveIcon) {
+ y = mTempRect.bottom;
+ }
+
+ setPivotX(width / 2);
+ setPivotY(height / 2);
+
+ // Insets are added later, so subtract them now.
+ y -= insets.top;
+
+ setX(x);
+ setY(y);
+ }
+
+ private void deferDrag(BubbleTextView originalIcon) {
+ mDeferredDragIcon = originalIcon;
+ showDragView(originalIcon);
+ }
+
+ public BubbleTextView getDeferredDragIcon() {
+ return mDeferredDragIcon;
+ }
+
+ private void showDragView(BubbleTextView originalIcon) {
+ // TODO: implement support for Drawable DragViews so we don't have to create a bitmap here.
+ Bitmap b = Utilities.createIconBitmap(originalIcon.getIcon(), mLauncher);
+ float scale = mLauncher.getDragLayer().getLocationInDragLayer(originalIcon, mTempXY);
+ int dragLayerX = Math.round(mTempXY[0] - (b.getWidth() - scale * originalIcon.getWidth()) / 2);
+ int dragLayerY = Math.round(mTempXY[1] - (b.getHeight() - scale * b.getHeight()) / 2
+ - Workspace.DRAG_BITMAP_PADDING / 2) + originalIcon.getPaddingTop();
+ int motionDownX = mLauncher.getDragController().getMotionDown().x;
+ int motionDownY = mLauncher.getDragController().getMotionDown().y;
+ final int registrationX = motionDownX - dragLayerX;
+ final int registrationY = motionDownY - dragLayerY;
+
+ float scaleDps = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_drag_view_scale);
+ mDragView = new DragView(mLauncher, b, registrationX, registrationY,
+ 0, 0, b.getWidth(), b.getHeight(), 1f, scaleDps);
+ mLastX = mLastY = mDistanceDragged = 0;
+ mDragView.show(motionDownX, motionDownY);
+ }
+
+ public boolean onForwardedEvent(MotionEvent ev, int activePointerId, MotionEvent touchDownEvent) {
+ mTouchDown = new Point((int) touchDownEvent.getX(), (int) touchDownEvent.getY());
+ mActivePointerId = activePointerId;
+ return dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mDeferredDragIcon == null) {
+ return false;
+ }
+
+
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (activePointerIndex < 0) {
+ return false;
+ }
+ final float x = ev.getX(activePointerIndex);
+ final float y = ev.getY(activePointerIndex);
+
+
+ int action = ev.getAction();
+ // The event was in this container's coordinate system before this,
+ // but will be in DragLayer's coordinate system from now on.
+ Utilities.translateEventCoordinates(this, mLauncher.getDragLayer(), ev);
+ final int dragLayerX = (int) ev.getX();
+ final int dragLayerY = (int) ev.getY();
+ int childCount = getChildCount();
+ if (action == MotionEvent.ACTION_MOVE) {
+ if (mLastX != 0 || mLastY != 0) {
+ mDistanceDragged += Math.hypot(mLastX - x, mLastY - y);
+ }
+ mLastX = x;
+ mLastY = y;
+
+ boolean containerContainsTouch = x >= 0 && y >= 0 && x < getWidth() && y < getHeight();
+ if (shouldStartDeferredDrag((int) x, (int) y, containerContainsTouch)) {
+ mDeferredDragIcon.getParent().requestDisallowInterceptTouchEvent(false);
+ mDeferredDragIcon.setVisibility(VISIBLE);
+ mDeferredDragIcon.getOnLongClickListener().onLongClick(mDeferredDragIcon);
+ mLauncher.getDragLayer().removeView(this);
+ mLauncher.getDragController().onTouchEvent(ev);
+ cleanupDeferredDrag();
+ return true;
+ } else {
+ // Determine whether touch is over a shortcut.
+ boolean hoveringOverShortcut = false;
+ for (int i = 0; i < childCount; i++) {
+ DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i);
+ if (shortcut.updateHoverState(containerContainsTouch, hoveringOverShortcut, y)) {
+ hoveringOverShortcut = true;
+ }
+ }
+
+ if (!hoveringOverShortcut && mDistanceDragged > mDragDeadzone) {
+ // After dragging further than a small deadzone,
+ // have the drag view follow the user's finger.
+ mDragView.setVisibility(VISIBLE);
+ mDragView.move(dragLayerX, dragLayerY);
+ mDeferredDragIcon.setVisibility(INVISIBLE);
+ } else if (hoveringOverShortcut) {
+ // Jump drag view back to original place on grid,
+ // so user doesn't think they are still dragging.
+ // TODO: can we improve this interaction? maybe with a ghost icon or similar?
+ mDragView.setVisibility(INVISIBLE);
+ mDeferredDragIcon.setVisibility(VISIBLE);
+ }
+ }
+ } else if (action == MotionEvent.ACTION_UP) {
+ mDeferredDragIcon.setVisibility(VISIBLE);
+ cleanupDeferredDrag();
+ // Launch a shortcut if user was hovering over it.
+ for (int i = 0; i < childCount; i++) {
+ DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i);
+ if (shortcut.isHoveringOver()) {
+ shortcut.performClick();
+ break;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether the deferred drag should be started based on touch coordinates
+ * relative to the original icon and the shortcuts container.
+ *
+ * Current behavior:
+ * - Compute distance from original touch down to closest container edge.
+ * - Compute distance from latest touch (given x and y) and compare to original distance;
+ * if the new distance is larger than a threshold, the deferred drag should start.
+ * - Never defer the drag if this container contains the touch.
+ *
+ * @param x the x touch coordinate relative to this container
+ * @param y the y touch coordinate relative to this container
+ */
+ private boolean shouldStartDeferredDrag(int x, int y, boolean containerContainsTouch) {
+ Point closestEdge = new Point(mTouchDown.x, mIsAboveIcon ? getMeasuredHeight() : 0);
+ double distToEdge = Math.hypot(mTouchDown.x - closestEdge.x, mTouchDown.y - closestEdge.y);
+ double newDistToEdge = Math.hypot(x - closestEdge.x, y - closestEdge.y);
+ return !containerContainsTouch && (newDistToEdge - distToEdge > mStartDragThreshold);
+ }
+
+ public void cleanupDeferredDrag() {
+ if (mDragView != null) {
+ mDragView.remove();
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ // Touched a shortcut, update where it was touched so we can drag from there on long click.
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public void onClick(View view) {
+ // Clicked on a shortcut.
+ mLauncher.onClick(view);
+ ((DragLayer) getParent()).removeView(this);
+ }
+
+ public boolean onLongClick(View v) {
+ // Return early if this is not initiated from a touch
+ if (!v.isInTouchMode()) return false;
+ // Return if global dragging is not enabled
+ if (!mLauncher.isDraggingEnabled()) return false;
+
+ // Long clicked on a shortcut.
+ // TODO remove this hack; it required because DragLayer isn't intercepting touch, so
+ // the controller is not updated from what it was previously.
+ mLauncher.getDragLayer().setController(mLauncher.getDragController());
+ mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
+ ((DragLayer) getParent()).removeView(this);
+ return false;
+ }
+
+ @Override
+ public boolean supportsFlingToDelete() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsAppInfoDropTarget() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsDeleteDropTarget() {
+ return true;
+ }
+
+ @Override
+ public float getIntrinsicIconScaleFactor() {
+ return (float) getResources().getDimensionPixelSize(R.dimen.deep_shortcut_icon_size)
+ / mLauncher.getDeviceProfile().iconSizePx;
+ }
+
+ @Override
+ public void onFlingToDeleteCompleted() {
+ // Don't care; ignore.
+ }
+
+ @Override
+ public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+ boolean success) {
+ if (!success) {
+ d.dragView.remove();
+ mLauncher.showWorkspace(true);
+ mLauncher.getDropTargetBar().onDragEnd();
+ }
+ }
+
+ @Override
+ public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) {
+ target.itemType = LauncherLogProto.SHORTCUT; // TODO: change to DYNAMIC_SHORTCUT
+ target.gridX = info.cellX;
+ target.gridY = info.cellY;
+ target.pageIndex = 0;
+ targetParent.containerType = LauncherLogProto.FOLDER; // TODO: change to DYNAMIC_SHORTCUTS
+ }
+}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutCache.java b/src/com/android/launcher3/shortcuts/ShortcutCache.java
index fc118a86eb..d4db96d313 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutCache.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutCache.java
@@ -56,6 +56,7 @@ public class ShortcutCache {
for (ShortcutInfoCompat shortcut : shortcuts) {
ShortcutKey key = ShortcutKey.fromInfo(shortcut);
mCachedShortcuts.remove(key);
+ mPinnedShortcuts.remove(key);
}
}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
new file mode 100644
index 0000000000..956623e2c9
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
@@ -0,0 +1,289 @@
+package com.android.launcher3.shortcuts;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+
+import java.util.List;
+
+/**
+ * A {@link android.view.View.OnTouchListener} that creates a {@link DeepShortcutsContainer} and
+ * forwards touch events to it. This listener should be put on any icon that supports shortcuts.
+ */
+public class ShortcutsContainerListener implements View.OnTouchListener,
+ View.OnAttachStateChangeListener {
+
+ /** Scaled touch slop, used for detecting movement outside bounds. */
+ private final float mScaledTouchSlop;
+
+ /** Timeout before disallowing intercept on the source's parent. */
+ private final int mTapTimeout;
+
+ /** Timeout before accepting a long-press to start forwarding. */
+ private final int mLongPressTimeout;
+
+ /** Source view from which events are forwarded. */
+ private final BubbleTextView mSrcIcon;
+
+ /** Runnable used to prevent conflicts with scrolling parents. */
+ private Runnable mDisallowIntercept;
+
+ /** Runnable used to trigger forwarding on long-press. */
+ private Runnable mTriggerLongPress;
+
+ /** Whether this listener is currently forwarding touch events. */
+ private boolean mForwarding;
+
+ /** The id of the first pointer down in the current event stream. */
+ private int mActivePointerId;
+
+ private Launcher mLauncher;
+ private DragLayer mDragLayer;
+ private MotionEvent mTouchDownEvent;
+
+ public ShortcutsContainerListener(BubbleTextView icon) {
+ mSrcIcon = icon;
+ mScaledTouchSlop = ViewConfiguration.get(icon.getContext()).getScaledTouchSlop();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+
+ mLongPressTimeout = CheckLongPressHelper.DEFAULT_LONG_PRESS_TIMEOUT;
+
+ icon.addOnAttachStateChangeListener(this);
+
+ mLauncher = Launcher.getLauncher(mSrcIcon.getContext());
+
+ mDragLayer = mLauncher.getDragLayer();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mLauncher.getShortcutIdsForItem((ItemInfo) v.getTag()).isEmpty()) {
+ // There are no shortcuts associated with this item, so return to normal touch handling.
+ return false;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchDownEvent = MotionEvent.obtainNoHistory(event);
+ }
+
+ final boolean wasForwarding = mForwarding;
+ final boolean forwarding;
+ if (wasForwarding) {
+ forwarding = onTouchForwarded(event) || !onForwardingStopped();
+ } else {
+ forwarding = onTouchObserved(event) && onForwardingStarted();
+
+ if (forwarding) {
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
+ 0.0f, 0.0f, 0);
+ mSrcIcon.onTouchEvent(e);
+ e.recycle();
+ }
+ }
+
+ mForwarding = forwarding;
+ return forwarding || wasForwarding;
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mForwarding = false;
+ mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+ if (mDisallowIntercept != null) {
+ mSrcIcon.removeCallbacks(mDisallowIntercept);
+ }
+ }
+
+ /**
+ * Called when forwarding would like to start.
+ *
+ * This is when we populate the shortcuts container and add it to the DragLayer.
+ *
+ * @return true to start forwarding, false otherwise
+ */
+ protected boolean onForwardingStarted() {
+ List ids = mLauncher.getShortcutIdsForItem((ItemInfo) mSrcIcon.getTag());
+ if (!ids.isEmpty()) {
+ // There are shortcuts associated with the app, so defer its drag.
+ LayoutInflater layoutInflater = (LayoutInflater) mLauncher.getSystemService
+ (Context.LAYOUT_INFLATER_SERVICE);
+ final DeepShortcutsContainer deepShortcutsContainer = (DeepShortcutsContainer)
+ layoutInflater.inflate(R.layout.deep_shortcuts_container, mDragLayer, false);
+ deepShortcutsContainer.setVisibility(View.INVISIBLE);
+ mDragLayer.addView(deepShortcutsContainer);
+ deepShortcutsContainer.populateAndShow(mSrcIcon, ids);
+ mSrcIcon.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called when forwarding would like to stop.
+ *
+ * @return true to stop forwarding, false otherwise
+ */
+ protected boolean onForwardingStopped() {
+ return true;
+ }
+
+ /**
+ * Observes motion events and determines when to start forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to start forwarding motion events, false otherwise
+ */
+ private boolean onTouchObserved(MotionEvent srcEvent) {
+ final View src = mSrcIcon;
+ if (!src.isEnabled()) {
+ return false;
+ }
+
+ final int actionMasked = srcEvent.getActionMasked();
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ mActivePointerId = srcEvent.getPointerId(0);
+
+ if (mDisallowIntercept == null) {
+ mDisallowIntercept = new DisallowIntercept();
+ }
+ src.postDelayed(mDisallowIntercept, mTapTimeout);
+
+ if (mTriggerLongPress == null) {
+ mTriggerLongPress = new TriggerLongPress();
+ }
+ src.postDelayed(mTriggerLongPress, mLongPressTimeout);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+ if (activePointerIndex >= 0) {
+ final float x = srcEvent.getX(activePointerIndex);
+ final float y = srcEvent.getY(activePointerIndex);
+
+ // Has the pointer moved outside of the view?
+ if (!Utilities.pointInView(src, x, y, mScaledTouchSlop)) {
+ clearCallbacks();
+
+ return false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ clearCallbacks();
+ break;
+ }
+
+ return false;
+ }
+
+ private void clearCallbacks() {
+ if (mTriggerLongPress != null) {
+ mSrcIcon.removeCallbacks(mTriggerLongPress);
+ }
+
+ if (mDisallowIntercept != null) {
+ mSrcIcon.removeCallbacks(mDisallowIntercept);
+ }
+ }
+
+ private void onLongPress() {
+ clearCallbacks();
+
+ final View src = mSrcIcon;
+ if (!src.isEnabled() || mLauncher.getShortcutIdsForItem((ItemInfo) src.getTag()).isEmpty()) {
+ // Ignore long-press if the view is disabled or doesn't have shortcuts.
+ return;
+ }
+
+ if (!onForwardingStarted()) {
+ return;
+ }
+
+ // Don't let the parent intercept our events.
+ src.getParent().requestDisallowInterceptTouchEvent(true);
+
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ src.onTouchEvent(e);
+ e.recycle();
+
+ mForwarding = true;
+ }
+
+ /**
+ * Handles forwarded motion events and determines when to stop
+ * forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to continue forwarding motion events, false to cancel
+ */
+ private boolean onTouchForwarded(MotionEvent srcEvent) {
+ final View src = mSrcIcon;
+
+ final DeepShortcutsContainer dst = (DeepShortcutsContainer)
+ mDragLayer.findViewById(R.id.deep_shortcuts_container);
+ if (dst == null) {
+ return false;
+ }
+
+ // Convert event to destination-local coordinates.
+ final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+ Utilities.translateEventCoordinates(src, dst, dstEvent);
+
+ // Convert touch down event to destination-local coordinates.
+ // TODO: only create this once, or just store the x and y.
+ final MotionEvent touchDownEvent = MotionEvent.obtainNoHistory(mTouchDownEvent);
+ Utilities.translateEventCoordinates(src, dst, touchDownEvent);
+
+ // Forward converted event to destination view, then recycle it.
+ // TODO: don't create objects in onForwardedEvent.
+ final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId, touchDownEvent);
+ dstEvent.recycle();
+ touchDownEvent.recycle();
+
+ // Always cancel forwarding when the touch stream ends.
+ final int action = srcEvent.getActionMasked();
+ final boolean keepForwarding = action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL;
+
+ return handled && keepForwarding;
+ }
+
+ private class DisallowIntercept implements Runnable {
+ @Override
+ public void run() {
+ final ViewParent parent = mSrcIcon.getParent();
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ private class TriggerLongPress implements Runnable {
+ @Override
+ public void run() {
+ onLongPress();
+ }
+ }
+}