diff --git a/res/drawable/bg_white_pill.xml b/res/drawable/bg_white_pill.xml new file mode 100644 index 0000000000..29c3145cc0 --- /dev/null +++ b/res/drawable/bg_white_pill.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/deep_shortcut.xml b/res/layout/deep_shortcut.xml new file mode 100644 index 0000000000..b879235649 --- /dev/null +++ b/res/layout/deep_shortcut.xml @@ -0,0 +1,20 @@ + + + + diff --git a/res/layout/deep_shortcuts_container.xml b/res/layout/deep_shortcuts_container.xml new file mode 100644 index 0000000000..0441995f1c --- /dev/null +++ b/res/layout/deep_shortcuts_container.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 02c6c704d5..c7dd3753b1 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -154,4 +154,14 @@ 8dp 2dp + + 9dp + 180dp + 4dp + 6dp + + 35dp + 36dp + diff --git a/res/values/styles.xml b/res/values/styles.xml index 0bfd0a041b..a1ba0b0b5a 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -70,6 +70,21 @@ false + + - diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 1762ca4bd5..ca60d5cbdb 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; @@ -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 diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java index 483c62249e..dde733cd16 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 f54a2d47ad..2a94e55c04 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 97af37c2eb..84c29dcfa4 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 getShortcutIdsForItem(ItemInfo info) { + if (!DeepShortcutManager.supportsShortcuts(info)) { + return Collections.EMPTY_LIST; + } + ComponentName component = info.getTargetComponent(); + List 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 8b42debcf4..cedbe74c74 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 65fc94c0ee..9366c420fd 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 e67c9df040..c3da491243 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 ca2556e525..6540a23dd5 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 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 0000000000..7997d1e2e9 --- /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 0000000000..008b2653f5 --- /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 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>() { + public List doInBackground(Void ... args) { + List shortcuts = mDeepShortcutsManager + .queryForAllAppShortcuts(activity, ids, user); + List 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 shortcuts) { + for (int i = 0; i < shortcuts.size(); i++) { + DeepShortcutView iconAndText = (DeepShortcutView) getChildAt(i); + ShortcutInfo launcherShortcutInfo = shortcuts.get(i); + iconAndText.applyFromShortcutInfo(launcherShortcutInfo, + LauncherAppState.getInstance().getIconCache()); + iconAndText.setOnClickListener(DeepShortcutsContainer.this); + iconAndText.setOnLongClickListener(DeepShortcutsContainer.this); + iconAndText.setOnTouchListener(DeepShortcutsContainer.this); + int viewId = mLauncher.getViewIdForItem(originalInfo); + iconAndText.setId(viewId); + } + } + }.execute(); + } + + // TODO: update this animation + private void animateOpen(BubbleTextView originalIcon) { + orientAboutIcon(originalIcon); + + setVisibility(View.VISIBLE); + int rx = (int) Math.max(Math.max(getMeasuredWidth() - getPivotX(), 0), getPivotX()); + int ry = (int) Math.max(Math.max(getMeasuredHeight() - getPivotY(), 0), getPivotY()); + float radius = (float) Math.hypot(rx, ry); + Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(), + (int) getPivotY(), 0, radius); + reveal.setDuration(getResources().getInteger(R.integer.config_materialFolderExpandDuration)); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + reveal.start(); + } + + /** + * Orients this container above or below the given icon, aligning with the left or right. + * + * These are the preferred orientations, in order: + * - Above and left-aligned + * - Above and right-aligned + * - Below and left-aligned + * - Below and right-aligned + * + * So we always align left if there is enough horizontal space + * and align above if there is enough vertical space. + * + * TODO: draw pointer based on orientation. + */ + private void orientAboutIcon(BubbleTextView icon) { + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect); + // Align left and above by default. + int x = mTempRect.left + icon.getPaddingLeft(); + int y = mTempRect.top - height; + Rect insets = dragLayer.getInsets(); + + mIsLeftAligned = x + width < dragLayer.getRight() - insets.right; + if (!mIsLeftAligned) { + x = mTempRect.right - width - icon.getPaddingRight(); + } + + mIsAboveIcon = mTempRect.top - height > dragLayer.getTop() + insets.top; + if (!mIsAboveIcon) { + y = mTempRect.bottom; + } + + setPivotX(width / 2); + setPivotY(height / 2); + + // Insets are added later, so subtract them now. + y -= insets.top; + + setX(x); + setY(y); + } + + private void deferDrag(BubbleTextView originalIcon) { + mDeferredDragIcon = originalIcon; + showDragView(originalIcon); + } + + public BubbleTextView getDeferredDragIcon() { + return mDeferredDragIcon; + } + + private void showDragView(BubbleTextView originalIcon) { + // TODO: implement support for Drawable DragViews so we don't have to create a bitmap here. + Bitmap b = Utilities.createIconBitmap(originalIcon.getIcon(), mLauncher); + float scale = mLauncher.getDragLayer().getLocationInDragLayer(originalIcon, mTempXY); + int dragLayerX = Math.round(mTempXY[0] - (b.getWidth() - scale * originalIcon.getWidth()) / 2); + int dragLayerY = Math.round(mTempXY[1] - (b.getHeight() - scale * b.getHeight()) / 2 + - Workspace.DRAG_BITMAP_PADDING / 2) + originalIcon.getPaddingTop(); + int motionDownX = mLauncher.getDragController().getMotionDown().x; + int motionDownY = mLauncher.getDragController().getMotionDown().y; + final int registrationX = motionDownX - dragLayerX; + final int registrationY = motionDownY - dragLayerY; + + float scaleDps = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_drag_view_scale); + mDragView = new DragView(mLauncher, b, registrationX, registrationY, + 0, 0, b.getWidth(), b.getHeight(), 1f, scaleDps); + mLastX = mLastY = mDistanceDragged = 0; + mDragView.show(motionDownX, motionDownY); + } + + public boolean onForwardedEvent(MotionEvent ev, int activePointerId, MotionEvent touchDownEvent) { + mTouchDown = new Point((int) touchDownEvent.getX(), (int) touchDownEvent.getY()); + mActivePointerId = activePointerId; + return dispatchTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mDeferredDragIcon == null) { + return false; + } + + + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex < 0) { + return false; + } + final float x = ev.getX(activePointerIndex); + final float y = ev.getY(activePointerIndex); + + + int action = ev.getAction(); + // The event was in this container's coordinate system before this, + // but will be in DragLayer's coordinate system from now on. + Utilities.translateEventCoordinates(this, mLauncher.getDragLayer(), ev); + final int dragLayerX = (int) ev.getX(); + final int dragLayerY = (int) ev.getY(); + int childCount = getChildCount(); + if (action == MotionEvent.ACTION_MOVE) { + if (mLastX != 0 || mLastY != 0) { + mDistanceDragged += Math.hypot(mLastX - x, mLastY - y); + } + mLastX = x; + mLastY = y; + + boolean containerContainsTouch = x >= 0 && y >= 0 && x < getWidth() && y < getHeight(); + if (shouldStartDeferredDrag((int) x, (int) y, containerContainsTouch)) { + mDeferredDragIcon.getParent().requestDisallowInterceptTouchEvent(false); + mDeferredDragIcon.setVisibility(VISIBLE); + mDeferredDragIcon.getOnLongClickListener().onLongClick(mDeferredDragIcon); + mLauncher.getDragLayer().removeView(this); + mLauncher.getDragController().onTouchEvent(ev); + cleanupDeferredDrag(); + return true; + } else { + // Determine whether touch is over a shortcut. + boolean hoveringOverShortcut = false; + for (int i = 0; i < childCount; i++) { + DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i); + if (shortcut.updateHoverState(containerContainsTouch, hoveringOverShortcut, y)) { + hoveringOverShortcut = true; + } + } + + if (!hoveringOverShortcut && mDistanceDragged > mDragDeadzone) { + // After dragging further than a small deadzone, + // have the drag view follow the user's finger. + mDragView.setVisibility(VISIBLE); + mDragView.move(dragLayerX, dragLayerY); + mDeferredDragIcon.setVisibility(INVISIBLE); + } else if (hoveringOverShortcut) { + // Jump drag view back to original place on grid, + // so user doesn't think they are still dragging. + // TODO: can we improve this interaction? maybe with a ghost icon or similar? + mDragView.setVisibility(INVISIBLE); + mDeferredDragIcon.setVisibility(VISIBLE); + } + } + } else if (action == MotionEvent.ACTION_UP) { + mDeferredDragIcon.setVisibility(VISIBLE); + cleanupDeferredDrag(); + // Launch a shortcut if user was hovering over it. + for (int i = 0; i < childCount; i++) { + DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i); + if (shortcut.isHoveringOver()) { + shortcut.performClick(); + break; + } + } + } + return true; + } + + /** + * Determines whether the deferred drag should be started based on touch coordinates + * relative to the original icon and the shortcuts container. + * + * Current behavior: + * - Compute distance from original touch down to closest container edge. + * - Compute distance from latest touch (given x and y) and compare to original distance; + * if the new distance is larger than a threshold, the deferred drag should start. + * - Never defer the drag if this container contains the touch. + * + * @param x the x touch coordinate relative to this container + * @param y the y touch coordinate relative to this container + */ + private boolean shouldStartDeferredDrag(int x, int y, boolean containerContainsTouch) { + Point closestEdge = new Point(mTouchDown.x, mIsAboveIcon ? getMeasuredHeight() : 0); + double distToEdge = Math.hypot(mTouchDown.x - closestEdge.x, mTouchDown.y - closestEdge.y); + double newDistToEdge = Math.hypot(x - closestEdge.x, y - closestEdge.y); + return !containerContainsTouch && (newDistToEdge - distToEdge > mStartDragThreshold); + } + + public void cleanupDeferredDrag() { + if (mDragView != null) { + mDragView.remove(); + } + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + // Touched a shortcut, update where it was touched so we can drag from there on long click. + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public void onClick(View view) { + // Clicked on a shortcut. + mLauncher.onClick(view); + ((DragLayer) getParent()).removeView(this); + } + + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + + // Long clicked on a shortcut. + // TODO remove this hack; it required because DragLayer isn't intercepting touch, so + // the controller is not updated from what it was previously. + mLauncher.getDragLayer().setController(mLauncher.getDragController()); + mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false); + ((DragLayer) getParent()).removeView(this); + return false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + @Override + public boolean supportsDeleteDropTarget() { + return true; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return (float) getResources().getDimensionPixelSize(R.dimen.deep_shortcut_icon_size) + / mLauncher.getDeviceProfile().iconSizePx; + } + + @Override + public void onFlingToDeleteCompleted() { + // Don't care; ignore. + } + + @Override + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { + if (!success) { + d.dragView.remove(); + mLauncher.showWorkspace(true); + mLauncher.getDropTargetBar().onDragEnd(); + } + } + + @Override + public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) { + target.itemType = LauncherLogProto.SHORTCUT; // TODO: change to DYNAMIC_SHORTCUT + target.gridX = info.cellX; + target.gridY = info.cellY; + target.pageIndex = 0; + targetParent.containerType = LauncherLogProto.FOLDER; // TODO: change to DYNAMIC_SHORTCUTS + } +} diff --git a/src/com/android/launcher3/shortcuts/ShortcutCache.java b/src/com/android/launcher3/shortcuts/ShortcutCache.java index fc118a86eb..d4db96d313 100644 --- a/src/com/android/launcher3/shortcuts/ShortcutCache.java +++ b/src/com/android/launcher3/shortcuts/ShortcutCache.java @@ -56,6 +56,7 @@ public class ShortcutCache { for (ShortcutInfoCompat shortcut : shortcuts) { ShortcutKey key = ShortcutKey.fromInfo(shortcut); mCachedShortcuts.remove(key); + mPinnedShortcuts.remove(key); } } diff --git a/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java new file mode 100644 index 0000000000..956623e2c9 --- /dev/null +++ b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java @@ -0,0 +1,289 @@ +package com.android.launcher3.shortcuts; + +import android.content.Context; +import android.os.SystemClock; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.dragndrop.DragLayer; + +import java.util.List; + +/** + * A {@link android.view.View.OnTouchListener} that creates a {@link DeepShortcutsContainer} and + * forwards touch events to it. This listener should be put on any icon that supports shortcuts. + */ +public class ShortcutsContainerListener implements View.OnTouchListener, + View.OnAttachStateChangeListener { + + /** Scaled touch slop, used for detecting movement outside bounds. */ + private final float mScaledTouchSlop; + + /** Timeout before disallowing intercept on the source's parent. */ + private final int mTapTimeout; + + /** Timeout before accepting a long-press to start forwarding. */ + private final int mLongPressTimeout; + + /** Source view from which events are forwarded. */ + private final BubbleTextView mSrcIcon; + + /** Runnable used to prevent conflicts with scrolling parents. */ + private Runnable mDisallowIntercept; + + /** Runnable used to trigger forwarding on long-press. */ + private Runnable mTriggerLongPress; + + /** Whether this listener is currently forwarding touch events. */ + private boolean mForwarding; + + /** The id of the first pointer down in the current event stream. */ + private int mActivePointerId; + + private Launcher mLauncher; + private DragLayer mDragLayer; + private MotionEvent mTouchDownEvent; + + public ShortcutsContainerListener(BubbleTextView icon) { + mSrcIcon = icon; + mScaledTouchSlop = ViewConfiguration.get(icon.getContext()).getScaledTouchSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + + mLongPressTimeout = CheckLongPressHelper.DEFAULT_LONG_PRESS_TIMEOUT; + + icon.addOnAttachStateChangeListener(this); + + mLauncher = Launcher.getLauncher(mSrcIcon.getContext()); + + mDragLayer = mLauncher.getDragLayer(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mLauncher.getShortcutIdsForItem((ItemInfo) v.getTag()).isEmpty()) { + // There are no shortcuts associated with this item, so return to normal touch handling. + return false; + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mTouchDownEvent = MotionEvent.obtainNoHistory(event); + } + + final boolean wasForwarding = mForwarding; + final boolean forwarding; + if (wasForwarding) { + forwarding = onTouchForwarded(event) || !onForwardingStopped(); + } else { + forwarding = onTouchObserved(event) && onForwardingStarted(); + + if (forwarding) { + // Make sure we cancel any ongoing source event stream. + final long now = SystemClock.uptimeMillis(); + final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, + 0.0f, 0.0f, 0); + mSrcIcon.onTouchEvent(e); + e.recycle(); + } + } + + mForwarding = forwarding; + return forwarding || wasForwarding; + } + + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + mForwarding = false; + mActivePointerId = MotionEvent.INVALID_POINTER_ID; + + if (mDisallowIntercept != null) { + mSrcIcon.removeCallbacks(mDisallowIntercept); + } + } + + /** + * Called when forwarding would like to start. + *

+ * This is when we populate the shortcuts container and add it to the DragLayer. + * + * @return true to start forwarding, false otherwise + */ + protected boolean onForwardingStarted() { + List ids = mLauncher.getShortcutIdsForItem((ItemInfo) mSrcIcon.getTag()); + if (!ids.isEmpty()) { + // There are shortcuts associated with the app, so defer its drag. + LayoutInflater layoutInflater = (LayoutInflater) mLauncher.getSystemService + (Context.LAYOUT_INFLATER_SERVICE); + final DeepShortcutsContainer deepShortcutsContainer = (DeepShortcutsContainer) + layoutInflater.inflate(R.layout.deep_shortcuts_container, mDragLayer, false); + deepShortcutsContainer.setVisibility(View.INVISIBLE); + mDragLayer.addView(deepShortcutsContainer); + deepShortcutsContainer.populateAndShow(mSrcIcon, ids); + mSrcIcon.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + return true; + } + return false; + } + + /** + * Called when forwarding would like to stop. + * + * @return true to stop forwarding, false otherwise + */ + protected boolean onForwardingStopped() { + return true; + } + + /** + * Observes motion events and determines when to start forwarding. + * + * @param srcEvent motion event in source view coordinates + * @return true to start forwarding motion events, false otherwise + */ + private boolean onTouchObserved(MotionEvent srcEvent) { + final View src = mSrcIcon; + if (!src.isEnabled()) { + return false; + } + + final int actionMasked = srcEvent.getActionMasked(); + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = srcEvent.getPointerId(0); + + if (mDisallowIntercept == null) { + mDisallowIntercept = new DisallowIntercept(); + } + src.postDelayed(mDisallowIntercept, mTapTimeout); + + if (mTriggerLongPress == null) { + mTriggerLongPress = new TriggerLongPress(); + } + src.postDelayed(mTriggerLongPress, mLongPressTimeout); + break; + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + final float x = srcEvent.getX(activePointerIndex); + final float y = srcEvent.getY(activePointerIndex); + + // Has the pointer moved outside of the view? + if (!Utilities.pointInView(src, x, y, mScaledTouchSlop)) { + clearCallbacks(); + + return false; + } + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + clearCallbacks(); + break; + } + + return false; + } + + private void clearCallbacks() { + if (mTriggerLongPress != null) { + mSrcIcon.removeCallbacks(mTriggerLongPress); + } + + if (mDisallowIntercept != null) { + mSrcIcon.removeCallbacks(mDisallowIntercept); + } + } + + private void onLongPress() { + clearCallbacks(); + + final View src = mSrcIcon; + if (!src.isEnabled() || mLauncher.getShortcutIdsForItem((ItemInfo) src.getTag()).isEmpty()) { + // Ignore long-press if the view is disabled or doesn't have shortcuts. + return; + } + + if (!onForwardingStarted()) { + return; + } + + // Don't let the parent intercept our events. + src.getParent().requestDisallowInterceptTouchEvent(true); + + // Make sure we cancel any ongoing source event stream. + final long now = SystemClock.uptimeMillis(); + final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + src.onTouchEvent(e); + e.recycle(); + + mForwarding = true; + } + + /** + * Handles forwarded motion events and determines when to stop + * forwarding. + * + * @param srcEvent motion event in source view coordinates + * @return true to continue forwarding motion events, false to cancel + */ + private boolean onTouchForwarded(MotionEvent srcEvent) { + final View src = mSrcIcon; + + final DeepShortcutsContainer dst = (DeepShortcutsContainer) + mDragLayer.findViewById(R.id.deep_shortcuts_container); + if (dst == null) { + return false; + } + + // Convert event to destination-local coordinates. + final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); + Utilities.translateEventCoordinates(src, dst, dstEvent); + + // Convert touch down event to destination-local coordinates. + // TODO: only create this once, or just store the x and y. + final MotionEvent touchDownEvent = MotionEvent.obtainNoHistory(mTouchDownEvent); + Utilities.translateEventCoordinates(src, dst, touchDownEvent); + + // Forward converted event to destination view, then recycle it. + // TODO: don't create objects in onForwardedEvent. + final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId, touchDownEvent); + dstEvent.recycle(); + touchDownEvent.recycle(); + + // Always cancel forwarding when the touch stream ends. + final int action = srcEvent.getActionMasked(); + final boolean keepForwarding = action != MotionEvent.ACTION_UP + && action != MotionEvent.ACTION_CANCEL; + + return handled && keepForwarding; + } + + private class DisallowIntercept implements Runnable { + @Override + public void run() { + final ViewParent parent = mSrcIcon.getParent(); + parent.requestDisallowInterceptTouchEvent(true); + } + } + + private class TriggerLongPress implements Runnable { + @Override + public void run() { + onLongPress(); + } + } +}