diff options
author | Tony Wickham <twickham@google.com> | 2016-04-28 17:39:03 -0700 |
---|---|---|
committer | Tony Wickham <twickham@google.com> | 2016-06-29 17:48:46 -0700 |
commit | 1bce7fd342875be8f7c1f82c8cf21d0199c8d544 (patch) | |
tree | 44871413914606a046e9f4dcf24d3ee3bd1a8756 /src/com/android/launcher3 | |
parent | 3ccedd234acb1534190c596351cf293e8ef4727a (diff) | |
download | packages_apps_Trebuchet-1bce7fd342875be8f7c1f82c8cf21d0199c8d544.tar.gz packages_apps_Trebuchet-1bce7fd342875be8f7c1f82c8cf21d0199c8d544.tar.bz2 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/com/android/launcher3')
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(); + } + } +} |