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
This commit is contained in:
Tony Wickham
2021-01-22 18:45:04 -08:00
parent b11e4d517d
commit e747278ee8
7 changed files with 254 additions and 20 deletions

View File

@@ -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>

View File

@@ -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();
}
/**

View File

@@ -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;
};
}
}

View File

@@ -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);
}

View File

@@ -153,6 +153,13 @@ public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_T
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);

View File

@@ -26,6 +26,7 @@ import android.content.Context;
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 @@ public final class LauncherActivityInterface extends
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();
}
}

View File

@@ -514,9 +514,14 @@ public class TouchInteractionService extends Service implements PluginListener<O
}
}
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) {