mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 15:26:58 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user