Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e747278e authored by Tony Wickham's avatar Tony Wickham
Browse files

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
parent b11e4d51
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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>
+21 −0
Original line number Diff line number Diff line
@@ -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();
    }

    /**
+133 −0
Original line number Diff line number Diff line
/*
 * 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;
        };
    }
}
+67 −19
Original line number Diff line number Diff line
@@ -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;
            View delegateView = findDelegateView(x, y);
            if (delegateView != null) {
                mDelegateTargeted = true;
                mDelegateView = delegateView;
                mDelegateSlopBounds.set(mTempDelegateBounds);
                mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
                    break;
                }
            }
        }

@@ -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);
    }
+7 −0
Original line number Diff line number Diff line
@@ -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);

Loading