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
This commit is contained in:
Tony Wickham
2016-04-28 17:39:03 -07:00
parent 3ccedd234a
commit 1bce7fd342
24 changed files with 1058 additions and 73 deletions
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/quantum_panel_bg_color" />
<size android:height="48dp" />
<corners android:radius="24dp" />
</shape>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.shortcuts.DeepShortcutView
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Icon.DeepShortcut"
android:focusable="true"/>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.shortcuts.DeepShortcutsContainer
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/deep_shortcuts_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="@dimen/deep_shortcuts_elevation"
android:orientation="vertical">
</com.android.launcher3.shortcuts.DeepShortcutsContainer>
+10
View File
@@ -154,4 +154,14 @@
<dimen name="pending_widget_min_padding">8dp</dimen>
<dimen name="pending_widget_elevation">2dp</dimen>
<!-- Deep shortcuts -->
<dimen name="deep_shortcuts_elevation">9dp</dimen>
<dimen name="deep_shortcuts_width">180dp</dimen>
<dimen name="deep_shortcuts_spacing">4dp</dimen>
<dimen name="deep_shortcuts_drag_view_scale">6dp</dimen>
<!-- an icon with shortcuts must be dragged this much distance away from the nearest edge
of the shortcut container before the container is removed. -->
<dimen name="deep_shortcuts_start_drag_threshold">35dp</dimen>
<dimen name="deep_shortcut_icon_size">36dp</dimen>
</resources>
+15 -1
View File
@@ -70,6 +70,21 @@
<item name="customShadows">false</item>
</style>
<style name="Icon.DeepShortcut">
<item name="android:background">@drawable/bg_white_pill</item>
<item name="android:gravity">start|center_vertical</item>
<item name="android:minWidth">@dimen/deep_shortcuts_width</item>
<item name="android:maxWidth">@dimen/deep_shortcuts_width</item>
<item name="android:elevation">@dimen/deep_shortcuts_elevation</item>
<item name="android:paddingLeft">7dp</item>
<item name="android:drawablePadding">12dp</item>
<item name="android:textColor">@color/quantum_panel_text_color</item>
<item name="android:shadowRadius">0</item>
<item name="customShadows">false</item>
<item name="layoutHorizontal">true</item>
<item name="iconSizeOverride">@dimen/deep_shortcut_icon_size</item>
</style>
<!-- Drop targets -->
<style name="DropTargetButtonBase">
<item name="android:drawablePadding">7.5dp</item>
@@ -99,5 +114,4 @@
<item name="indicatorSize">4dp</item>
<item name="ringOutset">4dp</item>
</style>
</resources>
@@ -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;
@@ -270,6 +271,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
@@ -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 {
+5
View File
@@ -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);
+10 -4
View File
@@ -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.
*
+33 -1
View File
@@ -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
+6 -1
View File
@@ -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);
@@ -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();
@@ -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;
}
@@ -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:
*
@@ -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.
*/
@@ -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;
@@ -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);
}
+1 -2
View File
@@ -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;
@@ -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);
@@ -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);
}
@@ -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;
}
}
@@ -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
}
}
@@ -56,6 +56,7 @@ public class ShortcutCache {
for (ShortcutInfoCompat shortcut : shortcuts) {
ShortcutKey key = ShortcutKey.fromInfo(shortcut);
mCachedShortcuts.remove(key);
mPinnedShortcuts.remove(key);
}
}
@@ -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();
}
}
}