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
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 39a7d09..39cc0b8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -124,6 +124,7 @@
<dimen name="taskbar_size">48dp</dimen>
<dimen name="taskbar_icon_size">32dp</dimen>
<dimen name="taskbar_icon_touch_size">48dp</dimen>
+ <dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
<!-- Note that this applies to both sides of all icons, so visible space is double this. -->
<dimen name="taskbar_icon_spacing">14dp</dimen>
</resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index f91bfb7..7608645 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.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 @@
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 @@
createTaskbarVisibilityControllerCallbacks());
mHotseatController = new TaskbarHotseatController(mLauncher,
createTaskbarHotseatControllerCallbacks());
+ mDragController = new TaskbarDragController(mLauncher);
}
private TaskbarVisibilityControllerCallbacks createTaskbarVisibilityControllerCallbacks() {
@@ -100,6 +103,11 @@
public View.OnClickListener getItemOnClickListener() {
return ItemClickHandler.INSTANCE;
}
+
+ @Override
+ public View.OnLongClickListener getItemOnLongClickListener() {
+ return mDragController::startDragOnLongClick;
+ }
};
}
@@ -227,6 +235,18 @@
}
/**
+ * @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.
*/
public boolean isViewInTaskbar(View v) {
@@ -254,6 +274,7 @@
*/
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 0000000..2318ff9
--- /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 adcfaec..c98f09c 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.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 @@
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 @@
private boolean mDelegateTargeted;
private View mDelegateView;
+ private boolean mIsDraggingItem;
+
public TaskbarView(@NonNull Context context) {
this(context, null);
}
@@ -135,9 +139,12 @@
(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 @@
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 @@
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 5f6e59f..ce14197 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -153,6 +153,13 @@
return deviceState.isInDeferredGestureRegion(ev);
}
+ /**
+ * @return Whether the gesture in progress should be cancelled.
+ */
+ public boolean shouldCancelCurrentGesture() {
+ return false;
+ }
+
public abstract void onExitOverview(RotationTouchHelper deviceState,
Runnable exitRunnable);
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index deb86ec..8b0d782 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -26,6 +26,7 @@
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.Log;
+import android.view.MotionEvent;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -311,4 +312,22 @@
boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
taskbarController.setIsImeVisible(isImeVisible);
}
+
+ @Override
+ public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return super.deferStartingActivity(deviceState, ev);
+ }
+ return taskbarController.isEventOverAnyTaskbarItem(ev);
+ }
+
+ @Override
+ public boolean shouldCancelCurrentGesture() {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return super.shouldCancelCurrentGesture();
+ }
+ return taskbarController.isDraggingItem();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a98fc1c..8ebea33 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -514,9 +514,14 @@
}
}
- boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
+ boolean cancelGesture = mGestureState.getActivityInterface() != null
+ && mGestureState.getActivityInterface().shouldCancelCurrentGesture();
+ boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL || cancelGesture)
&& mConsumer != null
&& !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
+ if (cancelGesture) {
+ event.setAction(ACTION_CANCEL);
+ }
mUncheckedConsumer.onMotionEvent(event);
if (cleanUpConsumer) {