summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorTony Wickham <twickham@google.com>2016-04-28 17:39:03 -0700
committerTony Wickham <twickham@google.com>2016-06-29 17:48:46 -0700
commit1bce7fd342875be8f7c1f82c8cf21d0199c8d544 (patch)
tree44871413914606a046e9f4dcf24d3ee3bd1a8756 /src
parent3ccedd234acb1534190c596351cf293e8ef4727a (diff)
downloadandroid_packages_apps_Trebuchet-1bce7fd342875be8f7c1f82c8cf21d0199c8d544.tar.gz
android_packages_apps_Trebuchet-1bce7fd342875be8f7c1f82c8cf21d0199c8d544.tar.bz2
android_packages_apps_Trebuchet-1bce7fd342875be8f7c1f82c8cf21d0199c8d544.zip
Long-press on an app to reveal its shortcuts.
- Add ShortcutsContainerListener to icons on workspace, folders, and all apps. This handles long-press and forwards following touches to the DeepShortcutsContainer that is created. - Drag over shortcut before lifting finger to launch it. - Shortcuts are rendered in pill-shaped DeepShortcutViews, which are inside DeepShortcutContainer on DragLayer. - The shortcut container orients above or below the icon, and left or right-aligns with it. Biases for above + left-align. - Long press a DeepShortcutPill to drag and pin it to the workspace. Bug: 28980830 Change-Id: I08658d13ae51fe53064644e8d8f7b42f150fdd7d
Diffstat (limited to 'src')
-rw-r--r--src/com/android/launcher3/BubbleTextView.java11
-rw-r--r--src/com/android/launcher3/CheckLongPressHelper.java4
-rw-r--r--src/com/android/launcher3/ItemInfo.java5
-rw-r--r--src/com/android/launcher3/Launcher.java14
-rw-r--r--src/com/android/launcher3/Utilities.java34
-rw-r--r--src/com/android/launcher3/Workspace.java7
-rw-r--r--src/com/android/launcher3/allapps/AllAppsContainerView.java29
-rw-r--r--src/com/android/launcher3/allapps/AllAppsGridAdapter.java25
-rw-r--r--src/com/android/launcher3/dragndrop/DragController.java10
-rw-r--r--src/com/android/launcher3/dragndrop/DragDriver.java12
-rw-r--r--src/com/android/launcher3/dragndrop/DragLayer.java46
-rw-r--r--src/com/android/launcher3/dragndrop/DragView.java13
-rw-r--r--src/com/android/launcher3/folder/Folder.java3
-rw-r--r--src/com/android/launcher3/folder/FolderPagedView.java6
-rw-r--r--src/com/android/launcher3/shortcuts/DeepShortcutManager.java7
-rw-r--r--src/com/android/launcher3/shortcuts/DeepShortcutView.java113
-rw-r--r--src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java409
-rw-r--r--src/com/android/launcher3/shortcuts/ShortcutCache.java1
-rw-r--r--src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java289
19 files changed, 966 insertions, 72 deletions
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 1762ca4bd..ca60d5cbd 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -67,6 +67,7 @@ public class BubbleTextView extends TextView
private final Launcher mLauncher;
private Drawable mIcon;
private final Drawable mBackground;
+ private OnLongClickListener mOnLongClickListener;
private final CheckLongPressHelper mLongPressHelper;
private final HolographicOutlineHelper mOutlineHelper;
private final StylusEventHelper mStylusEventHelper;
@@ -271,6 +272,16 @@ public class BubbleTextView extends TextView
}
@Override
+ public void setOnLongClickListener(OnLongClickListener l) {
+ super.setOnLongClickListener(l);
+ mOnLongClickListener = l;
+ }
+
+ public OnLongClickListener getOnLongClickListener() {
+ return mOnLongClickListener;
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent event) {
// Call the superclass onTouchEvent first, because sometimes it changes the state to
// isPressed() on an ACTION_UP
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index 483c62249..dde733cd1 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -22,10 +22,12 @@ import com.android.launcher3.util.Thunk;
public class CheckLongPressHelper {
+ public static final int DEFAULT_LONG_PRESS_TIMEOUT = 300;
+
@Thunk View mView;
@Thunk View.OnLongClickListener mListener;
@Thunk boolean mHasPerformedLongPress;
- private int mLongPressTimeout = 300;
+ private int mLongPressTimeout = DEFAULT_LONG_PRESS_TIMEOUT;
private CheckForLongPress mPendingCheckForLongPress;
class CheckForLongPress implements Runnable {
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index f54a2d47a..2a94e55c0 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -16,6 +16,7 @@
package com.android.launcher3;
+import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -137,6 +138,10 @@ public class ItemInfo {
return null;
}
+ public ComponentName getTargetComponent() {
+ return getIntent() == null ? null : getIntent().getComponent();
+ }
+
public void writeToValues(ContentValues values) {
values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
values.put(LauncherSettings.Favorites.CONTAINER, container);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 97af37c2e..84c29dcfa 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -59,7 +59,6 @@ import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
@@ -113,11 +112,10 @@ import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.logging.LoggerUtils;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.pageindicators.PageIndicator;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageManagerHelper;
@@ -136,7 +134,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
/**
* Default launcher application.
@@ -4062,6 +4059,15 @@ public class Launcher extends Activity
if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
}
+ public List<String> getShortcutIdsForItem(ItemInfo info) {
+ if (!DeepShortcutManager.supportsShortcuts(info)) {
+ return Collections.EMPTY_LIST;
+ }
+ ComponentName component = info.getTargetComponent();
+ List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
+ return ids == null ? Collections.EMPTY_LIST : ids;
+ }
+
/**
* A package was updated.
*
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 8b42debcf..cedbe74c7 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -47,7 +47,6 @@ import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.PowerManager;
-import android.support.v4.os.BuildCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@@ -57,7 +56,9 @@ import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.util.TypedValue;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewParent;
import android.widget.Toast;
import com.android.launcher3.compat.UserHandleCompat;
@@ -68,8 +69,10 @@ import com.android.launcher3.util.IconNormalizer;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -425,6 +428,30 @@ public final class Utilities {
localY < (v.getHeight() + slop);
}
+ /** Translates MotionEvents from src's coordinate system to dst's. */
+ public static void translateEventCoordinates(View src, View dst, MotionEvent dstEvent) {
+ toGlobalMotionEvent(src, dstEvent);
+ toLocalMotionEvent(dst, dstEvent);
+ }
+
+ /**
+ * Emulates View.toGlobalMotionEvent(). This implementation does not handle transformations
+ * (scaleX, scaleY, etc).
+ */
+ private static void toGlobalMotionEvent(View view, MotionEvent event) {
+ view.getLocationOnScreen(sLoc0);
+ event.offsetLocation(sLoc0[0], sLoc0[1]);
+ }
+
+ /**
+ * Emulates View.toLocalMotionEvent(). This implementation does not handle transformations
+ * (scaleX, scaleY, etc).
+ */
+ private static void toLocalMotionEvent(View view, MotionEvent event) {
+ view.getLocationOnScreen(sLoc0);
+ event.offsetLocation(-sLoc0[0], -sLoc0[1]);
+ }
+
public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) {
v0.getLocationInWindow(sLoc0);
v1.getLocationInWindow(sLoc1);
@@ -819,6 +846,11 @@ public final class Utilities {
return true;
}
+ /** Returns whether the collection is null or empty. */
+ public static boolean isEmpty(Collection c) {
+ return c == null || c.isEmpty();
+ }
+
/**
* An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
* This allows the badging to be done based on the action bitmap size rather than
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 65fc94c0e..9366c420f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -77,7 +77,8 @@ import com.android.launcher3.dragndrop.SpringLoadedDragController;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.LongArrayMap;
@@ -1094,6 +1095,10 @@ public class Workspace extends PagedView
if (!(child instanceof Folder)) {
child.setHapticFeedbackEnabled(false);
child.setOnLongClickListener(mLongClickListener);
+ if (child instanceof BubbleTextView && DeepShortcutManager.supportsShortcuts(info)) {
+ // TODO: only add this listener if the item has shortcuts associated with it.
+ child.setOnTouchListener(new ShortcutsContainerListener((BubbleTextView) child));
+ }
}
if (child instanceof DropTarget) {
mDragController.addDropTarget((DropTarget) child);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index e67c9df04..c3da49124 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -18,17 +18,13 @@ package com.android.launcher3.allapps;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.method.TextKeyListener;
import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -45,14 +41,14 @@ import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.Folder;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherTransitionable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.Folder;
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.util.ComponentKey;
@@ -133,8 +129,7 @@ final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlg
* The all apps view container.
*/
public class AllAppsContainerView extends BaseContainerView implements DragSource,
- LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener,
- AllAppsSearchBarController.Callbacks {
+ LauncherTransitionable, View.OnLongClickListener, AllAppsSearchBarController.Callbacks {
private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
private static final int MAX_NUM_MERGES_PHONE = 2;
@@ -163,8 +158,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
private int mRecyclerViewTopBottomPadding;
// This coordinate is relative to this container view
private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
- // This coordinate is relative to its parent
- private final Point mIconLastTouchPos = new Point();
public AllAppsContainerView(Context context) {
this(context, null);
@@ -181,7 +174,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mLauncher = Launcher.getLauncher(context);
mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
mApps = new AlphabeticalAppsList(context);
- mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this);
+ mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
mApps.setAdapter(mAdapter);
mLayoutManager = mAdapter.getLayoutManager();
mItemDecoration = mAdapter.getItemDecoration();
@@ -529,18 +522,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
return handleTouchEvent(ev);
}
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouch(View v, MotionEvent ev) {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_MOVE:
- mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
- break;
- }
- return false;
- }
-
@Override
public boolean onLongClick(View v) {
// Return early if this is not initiated from a touch
@@ -553,7 +534,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
if (!mLauncher.isDraggingEnabled()) return false;
// Start the drag
- mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
+ mLauncher.getWorkspace().beginDragShared(v, this, false);
// Enter spring loaded mode
mLauncher.enterSpringLoadedDragMode();
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index ca2556e52..6540a23dd 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -17,17 +17,13 @@ package com.android.launcher3.allapps;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.net.Uri;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
@@ -38,13 +34,14 @@ import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView;
+
import com.android.launcher3.AppInfo;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Thunk;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
import java.util.HashMap;
import java.util.List;
@@ -331,7 +328,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
private final GridLayoutManager mGridLayoutMgr;
private final GridSpanSizer mGridSizer;
private final GridItemDecoration mItemDecoration;
- private final View.OnTouchListener mTouchListener;
private final View.OnClickListener mIconClickListener;
private final View.OnLongClickListener mIconLongClickListener;
@@ -357,8 +353,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
private Intent mMarketSearchIntent;
public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps,
- View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
- View.OnLongClickListener iconLongClickListener) {
+ View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
Resources res = launcher.getResources();
mLauncher = launcher;
mApps = apps;
@@ -368,7 +363,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
mItemDecoration = new GridItemDecoration();
mLayoutInflater = LayoutInflater.from(launcher);
- mTouchListener = touchListener;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
@@ -454,7 +448,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
viewType == ICON_VIEW_TYPE ? R.layout.all_apps_icon :
R.layout.all_apps_prediction_bar_icon, parent, false);
- icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
@@ -490,6 +483,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
+ if (DeepShortcutManager.supportsShortcuts(info)) {
+ // TODO: only add this listener if the item has shortcuts associated with it.
+ icon.setOnTouchListener(new ShortcutsContainerListener(icon));
+ }
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
@@ -497,6 +494,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
+ if (DeepShortcutManager.supportsShortcuts(info)) {
+ // TODO: only add this listener if the item has shortcuts associated with it.
+ icon.setOnTouchListener(new ShortcutsContainerListener(icon));
+ }
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index af5ff587a..dc93bca72 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -45,6 +45,7 @@ import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
@@ -247,9 +248,12 @@ public class DragController implements DragDriver.EventListener, TouchController
mDragObject = new DropTarget.DragObject();
+ final Resources res = mLauncher.getResources();
+ final float scaleDps = FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND ?
+ res.getDimensionPixelSize(R.dimen.dragViewScale) : 0f;
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
registrationY, 0, 0, b.getWidth(), b.getHeight(),
- initialDragViewScale);
+ initialDragViewScale, scaleDps);
mDragObject.dragComplete = false;
if (mIsAccessibleDrag) {
@@ -284,6 +288,10 @@ public class DragController implements DragDriver.EventListener, TouchController
return dragView;
}
+ public Point getMotionDown() {
+ return new Point(mMotionDownX, mMotionDownY);
+ }
+
/**
* Call this from a drag source view like this:
*
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 7ad45f905..2164708ba 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -16,12 +16,6 @@
package com.android.launcher3.dragndrop;
-import com.android.launcher3.AnotherWindowDropTarget;
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-
import android.content.ClipData;
import android.content.Intent;
import android.graphics.Canvas;
@@ -30,6 +24,12 @@ import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.View;
+import com.android.launcher3.AnotherWindowDropTarget;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+
/**
* Base class for driving a drag/drop operation.
*/
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8aed6d834..aebb1fd10 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -43,23 +43,22 @@ import android.widget.TextView;
import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.CellLayout;
+import com.android.launcher3.DropTargetBar;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetHostView;
import com.android.launcher3.PinchToOverviewListener;
import com.android.launcher3.R;
-import com.android.launcher3.DropTargetBar;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.shortcuts.DeepShortcutsContainer;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
@@ -193,22 +192,23 @@ public class DragLayer extends InsettableFrameLayout {
}
public boolean isEventOverHotseat(MotionEvent ev) {
- getDescendantRectRelativeToSelf(mLauncher.getHotseat(), mHitRect);
- return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+ return isEventOverView(mLauncher.getHotseat(), ev);
}
private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
- getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
- return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+ return isEventOverView(folder.getEditTextRegion(), ev);
}
private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
- getDescendantRectRelativeToSelf(folder, mHitRect);
- return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+ return isEventOverView(folder, ev);
}
private boolean isEventOverDropTargetBar(MotionEvent ev) {
- getDescendantRectRelativeToSelf(mLauncher.getDropTargetBar(), mHitRect);
+ return isEventOverView(mLauncher.getDropTargetBar(), ev);
+ }
+
+ private boolean isEventOverView(View view, MotionEvent ev) {
+ getDescendantRectRelativeToSelf(view, mHitRect);
return mHitRect.contains((int) ev.getX(), (int) ev.getY());
}
@@ -251,6 +251,25 @@ public class DragLayer extends InsettableFrameLayout {
}
}
}
+
+ // Remove the shortcuts container when touching outside of it.
+ DeepShortcutsContainer deepShortcutsContainer = (DeepShortcutsContainer)
+ findViewById(R.id.deep_shortcuts_container);
+ if (deepShortcutsContainer != null) {
+ if (!isEventOverView(deepShortcutsContainer, ev)) {
+ if (isInAccessibleDrag()) {
+ // Do not close the container if in drag and drop.
+ if (!isEventOverDropTargetBar(ev)) {
+ return true;
+ }
+ } else {
+ removeView(deepShortcutsContainer);
+ // We let touches on the original icon go through so that users can launch
+ // the app with one tap if they don't find a shortcut they want.
+ return !isEventOverView(deepShortcutsContainer.getDeferredDragIcon(), ev);
+ }
+ }
+ }
return false;
}
@@ -258,7 +277,6 @@ public class DragLayer extends InsettableFrameLayout {
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
-
if (action == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, true)) {
return true;
@@ -275,6 +293,7 @@ public class DragLayer extends InsettableFrameLayout {
mActiveController = mDragController;
return true;
}
+
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && mAllAppsController.onInterceptTouchEvent(ev)) {
mActiveController = mAllAppsController;
return true;
@@ -395,7 +414,6 @@ public class DragLayer extends InsettableFrameLayout {
int x = (int) ev.getX();
int y = (int) ev.getY();
-
if (action == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, false)) {
return true;
@@ -526,6 +544,10 @@ public class DragLayer extends InsettableFrameLayout {
return new LayoutParams(p);
}
+ public void setController(TouchController controller) {
+ mActiveController = controller;
+ }
+
public static class LayoutParams extends InsettableFrameLayout.LayoutParams {
public int x, y;
public boolean customPosition = false;
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index b1df41b9d..a5644ad73 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -84,16 +84,13 @@ public class DragView extends View {
* @param registrationY The y coordinate of the registration point.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
- public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
- int left, int top, int width, int height, final float initialScale) {
+ public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, int left,
+ int top, int width, int height, final float initialScale, final float finalScaleDps) {
super(launcher);
mDragLayer = launcher.getDragLayer();
mDragController = launcher.getDragController();
- final Resources res = getResources();
- final float scaleDps = !FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND ? 0f
- : res.getDimensionPixelSize(R.dimen.dragViewScale);
- final float scale = (width + scaleDps) / width;
+ final float scale = (width + finalScaleDps) / width;
// Set the initial scale to avoid any jumps
setScaleX(initialScale);
@@ -349,12 +346,12 @@ public class DragView extends View {
* @param touchX the x coordinate the user touched in DragLayer coordinates
* @param touchY the y coordinate the user touched in DragLayer coordinates
*/
- void move(int touchX, int touchY) {
+ public void move(int touchX, int touchY) {
setTranslationX(touchX - mRegistrationX);
setTranslationY(touchY - mRegistrationY);
}
- void remove() {
+ public void remove() {
if (getParent() != null) {
mDragLayer.removeView(DragView.this);
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 2035f9960..e94e02f91 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -25,7 +25,6 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
@@ -295,7 +294,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
return false;
}
- mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
+ mLauncher.getWorkspace().beginDragShared(v, this, accessible);
mCurrentDragInfo = item;
mEmptyCellRank = item.rank;
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index c6fc4cb63..82c79a9ec 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -46,6 +46,8 @@ import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.pageindicators.PageIndicator;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
import com.android.launcher3.util.Thunk;
import java.util.ArrayList;
@@ -234,6 +236,10 @@ public class FolderPagedView extends PagedView {
textView.applyFromShortcutInfo(item, mIconCache);
textView.setOnClickListener(mFolder);
textView.setOnLongClickListener(mFolder);
+ if (DeepShortcutManager.supportsShortcuts(item)) {
+ // TODO: only add this listener if the item has shortcuts associated with it.
+ textView.setOnTouchListener(new ShortcutsContainerListener(textView));
+ }
textView.setOnFocusChangeListener(mFocusIndicatorHelper);
textView.setOnKeyListener(mKeyListener);
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
index 97c384d8a..0d5102fe3 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -27,6 +27,8 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Utilities;
import com.android.launcher3.compat.UserHandleCompat;
@@ -54,6 +56,11 @@ public class DeepShortcutManager {
mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
}
+ public static boolean supportsShortcuts(ItemInfo info) {
+ return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+ || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+ }
+
public void onShortcutsChanged(List<ShortcutInfoCompat> shortcuts) {
// mShortcutCache.removeShortcuts(shortcuts);
}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
new file mode 100644
index 000000000..7997d1e2e
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shortcuts;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.util.AttributeSet;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+
+/**
+ * A {@link BubbleTextView} that represents a deep shortcut within an app.
+ */
+public class DeepShortcutView extends BubbleTextView {
+
+ private static final float HOVER_SCALE = 1.1f;
+ // The direction this view should translate when animating the hover state.
+ // This allows hovered shortcuts to "push" other shortcuts away.
+ @IntDef({DIRECTION_UP, DIRECTION_NONE, DIRECTION_DOWN})
+ public @interface TranslationDirection {}
+
+ public static final int DIRECTION_UP = -1;
+ public static final int DIRECTION_NONE = 0;
+ public static final int DIRECTION_DOWN = 1;
+ @TranslationDirection
+ private int mTranslationDirection = DIRECTION_NONE;
+
+ private int mSpacing;
+ private int mTop;
+ private boolean mIsHoveringOver = false;
+
+ public DeepShortcutView(Context context) {
+ this(context, null, 0);
+ }
+
+ public DeepShortcutView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DeepShortcutView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mSpacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+ }
+
+ public int getSpacing() {
+ return mSpacing;
+ }
+
+ /**
+ * Updates the state of this view based on touches over the container before user lifts finger.
+ *
+ * @param containerContainsTouch whether the {@link DeepShortcutsContainer} this shortcut
+ * is inside contains the current touch
+ * @param isBelowHoveredShortcut whether a sibling shortcut before this one in the
+ * view hierarchy is being hovered over
+ * @param touchY the y coordinate of the touch, relative to the {@link DeepShortcutsContainer}
+ * this shortcut is inside
+ * @return whether this shortcut is being hovered over
+ */
+ public boolean updateHoverState(boolean containerContainsTouch, boolean isBelowHoveredShortcut,
+ float touchY) {
+ if (!containerContainsTouch) {
+ mIsHoveringOver = false;
+ mTranslationDirection = DIRECTION_NONE;
+ } else if (isBelowHoveredShortcut) {
+ mIsHoveringOver = false;
+ mTranslationDirection = DIRECTION_DOWN;
+ } else {
+ // Include space around the view when determining hover state to avoid gaps.
+ mTop = (int) (getY() - getTranslationY());
+ mIsHoveringOver = (touchY >= mTop - mSpacing / 2)
+ && (touchY < mTop + getHeight() + mSpacing / 2);
+ mTranslationDirection = mIsHoveringOver ? DIRECTION_NONE : DIRECTION_UP;
+ }
+ animateHoverState();
+ return mIsHoveringOver;
+ }
+
+ /**
+ * If this shortcut is being hovered over, we scale it up. If another shortcut is being hovered
+ * over, we translate this one away from it to account for its increased size.
+ *
+ * TODO: apply motion spec here
+ */
+ private void animateHoverState() {
+ float scale = mIsHoveringOver ? HOVER_SCALE : 1f;
+ setScaleX(scale);
+ setScaleY(scale);
+
+ float translation = (HOVER_SCALE - 1f) * getHeight();
+ setTranslationY(translation * mTranslationDirection);
+ }
+
+ public boolean isHoveringOver() {
+ return mIsHoveringOver;
+ }
+}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
new file mode 100644
index 000000000..008b2653f
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -0,0 +1,409 @@
+package com.android.launcher3.shortcuts;
+
+import android.animation.Animator;
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LogDecelerateInterpolator;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadCircularReveal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A container for shortcuts to deep links within apps.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DeepShortcutsContainer extends LinearLayout implements View.OnClickListener,
+ View.OnLongClickListener, View.OnTouchListener, DragSource,
+ UserEventDispatcher.LaunchSourceProvider, TouchController {
+ private static final String TAG = "ShortcutsContainer";
+
+ private Launcher mLauncher;
+ private DeepShortcutManager mDeepShortcutsManager;
+ private final int mDragDeadzone;
+ private final int mStartDragThreshold;
+ private BubbleTextView mDeferredDragIcon;
+ private int mActivePointerId;
+ private Point mTouchDown = null;
+ private DragView mDragView;
+ private float mLastX, mLastY;
+ private float mDistanceDragged = 0;
+ private final Rect mTempRect = new Rect();
+ private final int[] mTempXY = new int[2];
+ private Point mIconLastTouchPos = new Point();
+ private boolean mIsLeftAligned;
+ private boolean mIsAboveIcon;
+
+ public DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = (Launcher) context;
+ mDeepShortcutsManager = LauncherAppState.getInstance().getShortcutManager();
+
+ mDragDeadzone = ViewConfiguration.get(context).getScaledTouchSlop();
+ mStartDragThreshold = getResources().getDimensionPixelSize(
+ R.dimen.deep_shortcuts_start_drag_threshold);
+ }
+
+ public DeepShortcutsContainer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DeepShortcutsContainer(Context context) {
+ this(context, null, 0);
+ }
+
+ public void populateAndShow(final BubbleTextView originalIcon, final List<String> ids) {
+ // Add dummy views first, and populate with real shortcut info when ready.
+ for (int i = 0; i < ids.size(); i++) {
+ final DeepShortcutView shortcut = (DeepShortcutView)
+ mLauncher.getLayoutInflater().inflate(R.layout.deep_shortcut, this, false);
+ if (i < ids.size() - 1) {
+ ((LayoutParams) shortcut.getLayoutParams()).bottomMargin = shortcut.getSpacing();
+ }
+ addView(shortcut);
+ }
+
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ animateOpen(originalIcon);
+
+ deferDrag(originalIcon);
+
+ // Load the shortcuts on a background thread and update the container as it animates.
+ final ItemInfo originalInfo = (ItemInfo) originalIcon.getTag();
+ final UserHandleCompat user = originalInfo.user;
+ final ComponentName activity = originalInfo.getTargetComponent();
+ new AsyncTask<Void, Void, List<ShortcutInfo>>() {
+ public List<ShortcutInfo> doInBackground(Void ... args) {
+ List<ShortcutInfoCompat> shortcuts = mDeepShortcutsManager
+ .queryForAllAppShortcuts(activity, ids, user);
+ List<ShortcutInfo> shortcutInfos = new ArrayList<>(shortcuts.size());
+ for (ShortcutInfoCompat shortcut : shortcuts) {
+ shortcutInfos.add(ShortcutInfo.fromDeepShortcutInfo(shortcut, mLauncher));
+ }
+ return shortcutInfos;
+ }
+
+ // TODO: implement onProgressUpdate() to load shortcuts one at a time.
+
+ @Override
+ protected void onPostExecute(List<ShortcutInfo> 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 fc118a86e..d4db96d31 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 000000000..956623e2c
--- /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.
+ * <p>
+ * 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<String> 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();
+ }
+ }
+}