From e747278ee8e81bcb23715d471876fa6a861422c2 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Fri, 22 Jan 2021 18:45:04 -0800 Subject: [PATCH] Support drag and drop from Taskbar - Long clicking a BubbleTextView in Taskbar will start a system drag and drop operation, setting the original view invisible meanwhile. - Defer gesture navigation when starting over a Taskbar item, and cancel any started gesture if a Taskbar drag and drop starts. Bug: 171917176 Change-Id: If5049071fbf1755f545ee937daa4edabd869f00d --- quickstep/res/values/dimens.xml | 1 + .../launcher3/taskbar/TaskbarController.java | 21 +++ .../taskbar/TaskbarDragController.java | 133 ++++++++++++++++++ .../launcher3/taskbar/TaskbarView.java | 86 ++++++++--- .../quickstep/BaseActivityInterface.java | 7 + .../quickstep/LauncherActivityInterface.java | 19 +++ .../quickstep/TouchInteractionService.java | 7 +- 7 files changed, 254 insertions(+), 20 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 39a7d09649..39cc0b891c 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -124,6 +124,7 @@ 48dp 32dp 48dp + 54dp 14dp diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java index f91bfb78fc..7608645a63 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java @@ -26,6 +26,7 @@ import android.animation.Animator; import android.graphics.PixelFormat; import android.graphics.Point; import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; @@ -59,6 +60,7 @@ public class TaskbarController { private final TaskbarStateHandler mTaskbarStateHandler; private final TaskbarVisibilityController mTaskbarVisibilityController; private final TaskbarHotseatController mHotseatController; + private final TaskbarDragController mDragController; // Initialized in init(). private WindowManager.LayoutParams mWindowLayoutParams; @@ -77,6 +79,7 @@ public class TaskbarController { createTaskbarVisibilityControllerCallbacks()); mHotseatController = new TaskbarHotseatController(mLauncher, createTaskbarHotseatControllerCallbacks()); + mDragController = new TaskbarDragController(mLauncher); } private TaskbarVisibilityControllerCallbacks createTaskbarVisibilityControllerCallbacks() { @@ -100,6 +103,11 @@ public class TaskbarController { public View.OnClickListener getItemOnClickListener() { return ItemClickHandler.INSTANCE; } + + @Override + public View.OnLongClickListener getItemOnLongClickListener() { + return mDragController::startDragOnLongClick; + } }; } @@ -226,6 +234,18 @@ public class TaskbarController { mHotseatController.onHotseatUpdated(); } + /** + * @param ev MotionEvent in screen coordinates. + * @return Whether any Taskbar item could handle the given MotionEvent if given the chance. + */ + public boolean isEventOverAnyTaskbarItem(MotionEvent ev) { + return mTaskbarView.isEventOverAnyItem(ev); + } + + public boolean isDraggingItem() { + return mTaskbarView.isDraggingItem(); + } + /** * @return Whether the given View is in the same window as Taskbar. */ @@ -254,6 +274,7 @@ public class TaskbarController { */ protected interface TaskbarViewCallbacks { View.OnClickListener getItemOnClickListener(); + View.OnLongClickListener getItemOnLongClickListener(); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java new file mode 100644 index 0000000000..2318ff96b6 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2021 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.taskbar; + +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Point; +import android.view.DragEvent; +import android.view.View; + +import com.android.launcher3.BaseQuickstepLauncher; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.R; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.systemui.shared.system.ClipDescriptionCompat; +import com.android.systemui.shared.system.LauncherAppsCompat; + +/** + * Handles long click on Taskbar items to start a system drag and drop operation. + */ +public class TaskbarDragController { + + private final BaseQuickstepLauncher mLauncher; + private final int mDragIconSize; + + public TaskbarDragController(BaseQuickstepLauncher launcher) { + mLauncher = launcher; + Resources resources = mLauncher.getResources(); + mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size); + } + + /** + * Attempts to start a system drag and drop operation for the given View, using its tag to + * generate the ClipDescription and Intent. + * @return Whether {@link View#startDragAndDrop} started successfully. + */ + protected boolean startDragOnLongClick(View view) { + if (!(view instanceof BubbleTextView)) { + return false; + } + + BubbleTextView btv = (BubbleTextView) view; + + View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) { + @Override + public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { + shadowSize.set(mDragIconSize, mDragIconSize); + // TODO: should be based on last touch point on the icon. + shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y / 2); + } + + @Override + public void onDrawShadow(Canvas canvas) { + canvas.save(); + float scale = (float) mDragIconSize / btv.getIconSize(); + canvas.scale(scale, scale); + btv.getIcon().draw(canvas); + canvas.restore(); + } + }; + + Object tag = view.getTag(); + ClipDescription clipDescription = null; + Intent intent = null; + if (tag instanceof WorkspaceItemInfo) { + WorkspaceItemInfo item = (WorkspaceItemInfo) tag; + LauncherApps launcherApps = mLauncher.getSystemService(LauncherApps.class); + clipDescription = new ClipDescription(item.title, + new String[] { + item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT + ? ClipDescriptionCompat.MIMETYPE_APPLICATION_SHORTCUT + : ClipDescriptionCompat.MIMETYPE_APPLICATION_ACTIVITY + }); + intent = new Intent(); + if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage()); + intent.putExtra(Intent.EXTRA_SHORTCUT_ID, item.getDeepShortcutId()); + } else { + intent.putExtra(ClipDescriptionCompat.EXTRA_PENDING_INTENT, + LauncherAppsCompat.getMainActivityLaunchIntent(launcherApps, + item.getIntent().getComponent(), null, item.user)); + } + intent.putExtra(Intent.EXTRA_USER, item.user); + } + + if (clipDescription != null && intent != null) { + ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent)); + view.setOnDragListener(getDraggedViewDragListener()); + return view.startDragAndDrop(clipData, shadowBuilder, null /* localState */, + View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE); + } + return false; + } + + /** + * Hide the original Taskbar item while it is being dragged. + */ + private View.OnDragListener getDraggedViewDragListener() { + return (view, dragEvent) -> { + switch (dragEvent.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + view.setVisibility(INVISIBLE); + return true; + case DragEvent.ACTION_DRAG_ENDED: + view.setVisibility(VISIBLE); + view.setOnDragListener(null); + return true; + } + return false; + }; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index adcfaec102..c98f09ca0f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -20,6 +20,7 @@ import android.content.res.Resources; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.util.AttributeSet; +import android.view.DragEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -46,6 +47,7 @@ public class TaskbarView extends LinearLayout { private final int mTouchSlop; private final RectF mTempDelegateBounds = new RectF(); private final RectF mDelegateSlopBounds = new RectF(); + private final int[] mTempOutLocation = new int[2]; // Initialized in init(). private int mHotseatStartIndex; @@ -57,6 +59,8 @@ public class TaskbarView extends LinearLayout { private boolean mDelegateTargeted; private View mDelegateView; + private boolean mIsDraggingItem; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -135,9 +139,12 @@ public class TaskbarView extends LinearLayout { (WorkspaceItemInfo) hotseatItemInfo); hotseatView.setVisibility(VISIBLE); hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener()); + hotseatView.setOnLongClickListener( + mControllerCallbacks.getItemOnLongClickListener()); } else { hotseatView.setVisibility(GONE); hotseatView.setOnClickListener(null); + hotseatView.setOnLongClickListener(null); } } } @@ -157,25 +164,12 @@ public class TaskbarView extends LinearLayout { final float x = event.getX(); final float y = event.getY(); if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (!child.isShown() || !child.isClickable()) { - continue; - } - int childCenterX = child.getLeft() + child.getWidth() / 2; - int childCenterY = child.getTop() + child.getHeight() / 2; - mTempDelegateBounds.set( - childCenterX - mIconTouchSize / 2f, - childCenterY - mIconTouchSize / 2f, - childCenterX + mIconTouchSize / 2f, - childCenterY + mIconTouchSize / 2f); - mDelegateTargeted = mTempDelegateBounds.contains(x, y); - if (mDelegateTargeted) { - mDelegateView = child; - mDelegateSlopBounds.set(mTempDelegateBounds); - mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop); - break; - } + View delegateView = findDelegateView(x, y); + if (delegateView != null) { + mDelegateTargeted = true; + mDelegateView = delegateView; + mDelegateSlopBounds.set(mTempDelegateBounds); + mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop); } } @@ -210,6 +204,60 @@ public class TaskbarView extends LinearLayout { return handled; } + /** + * Return an item whose touch bounds contain the given coordinates, + * or null if no such item exists. + * + * Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view. + */ + private @Nullable View findDelegateView(float x, float y) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (!child.isShown() || !child.isClickable()) { + continue; + } + int childCenterX = child.getLeft() + child.getWidth() / 2; + int childCenterY = child.getTop() + child.getHeight() / 2; + mTempDelegateBounds.set( + childCenterX - mIconTouchSize / 2f, + childCenterY - mIconTouchSize / 2f, + childCenterX + mIconTouchSize / 2f, + childCenterY + mIconTouchSize / 2f); + if (mTempDelegateBounds.contains(x, y)) { + return child; + } + } + return null; + } + + /** + * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's + * touch bounds. + */ + public boolean isEventOverAnyItem(MotionEvent ev) { + getLocationOnScreen(mTempOutLocation); + float xInOurCoordinates = ev.getX() - mTempOutLocation[0]; + float yInOurCoorindates = ev.getY() - mTempOutLocation[1]; + return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null; + } + + @Override + public boolean onDragEvent(DragEvent event) { + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + mIsDraggingItem = true; + return true; + case DragEvent.ACTION_DRAG_ENDED: + mIsDraggingItem = false; + break; + } + return super.onDragEvent(event); + } + + public boolean isDraggingItem() { + return mIsDraggingItem; + } + private View inflate(@LayoutRes int layoutResId) { return LayoutInflater.from(getContext()).inflate(layoutResId, this, false); } diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 5f6e59fa11..ce14197c2e 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -153,6 +153,13 @@ public abstract class BaseActivityInterface