Launcher3 如何实现长按后可拖动?

Launcher3 如何实现长按后可拖动?

桌面页 Workspace 上添加的各种控件可以通过长按触发其拖动功能,即控件跟随手指移动,松开后将控件摆放到新的位置。

实现该功能需以下几步。

  • 准备工作
  • 监听长按事件
  • 创建 DragView
  • 显示 DragView
  • 拦截 Move 事件
  • 移动 DragView

准备工作

  • 创建拖动控制器 LauncherDragController
  • 绑定拖动控制器与拖动层视图 DragLayer
    • 事件的分发与拦截
  • 绑定拖动控制器与桌面页 Workspace
    • 监听长按事件。

DragLayer 与 Workspace 的层级关系如下:

XML 复制代码
<com.android.launcher3.dragndrop.DragLayer>
    <com.android.launcher3.Workspace />
</com.android.launcher3.dragndrop.DragLayer>

在 Launcher#onCreate 中做准备工作。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

@Override
@TargetApi(Build.VERSION_CODES.S)
protected void onCreate(Bundle savedInstanceState) {
    // 创建拖动控制器
    initDragController();
    // 设置视图
    setupViews();
}

创建拖动控制器,这里使用的是 LauncherDragController。

com.android.launcher3.Launcher#initDragController

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

protected void initDragController() {
    mDragController = new LauncherDragController(this);
}

绑定 DragLayer 与 Workspace。

com.android.launcher3.Launcher#setupViews

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

protected void setupViews() {
    // 绑定拖动控制器与拖动层视图 DragLayer
    mDragLayer.setup(mDragController, mWorkspace);

    // 绑定拖动控制器与桌面页 Workspace
    mWorkspace.setup(mDragController);
}

先看如何绑定 DragController 与 DragLayer。

com.android.launcher3.dragndrop.DragLayer#setup

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragLayer.java

public void setup(DragController dragController, Workspace<?> workspace) {
    // 绑定拖动控制器与拖动层视图 DragLayer
    mDragController = dragController;
    recreateControllers();
}

@Override
public void recreateControllers() {
    // 初始化 触摸控制器 集合。
    mControllers = mContainer.createTouchControllers();
}

mContainer.createTouchControllers() 这里调用的是 Launcher 的 createTouchControllers

com.android.launcher3.Launcher#createTouchControllers

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

public TouchController[] createTouchControllers() {
    // 两种触摸控制器,其中一种是拖动控制器。
    return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
}

public DragController getDragController() {
    // 获取通过 initDragController 函数创建好的拖动控制器。
    return mDragController;
}

小结:DragLayer 里的 mDragController 属性就是 Launcher 里创建的 LauncherDragController。

再看如何绑定 DragController 与 Workspace。

com.android.launcher3.Workspace#setup

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Workspace.java

void setup(DragController dragController) {
    // 绑定拖动控制器与桌面页 Workspace
    mDragController = dragController;
}

这里直接赋值,将 Launcher 里创建的 LauncherDragController,赋值到 Workspace 里的 mDragController 属性。

监听长按事件

  • 向桌面页 Workspace 上添加控件。
  • 添加控件时为其设置长按事件监听器。
  • 在长按事件的回调中触发开启拖拽功能

向桌面页 Workspace 上添加控件。

Workspace 里可添加的控件有如下几种:

  • 快捷图标 shortcut
  • 微件的宿主控件 AppWidgetHostView
  • 文件夹图标 FolderIcon
  • 底部的固定快捷栏 Hotseat
  • 应用微件 AppWidget,即小组件。

添加快捷图标 shortcut

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

protected void completeAddShortcut(Intent data, int container, int screenId, int cellX,
    int cellY, PendingRequestArgs args) {
    mWorkspace.addInScreen(view, info);
}

添加微件的宿主控件 AppWidgetHostView

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

@Thunk
void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
    @Nullable AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo,
    boolean showPendingWidget, boolean updateWidgetSize,
    @Nullable Bitmap widgetPreviewBitmap) {
    mWorkspace.addInScreen(hostView, launcherInfo);
}

添加文件夹图标 FolderIcon

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

public FolderIcon addFolder(CellLayout layout, int container, final int screenId, int cellX,
    int cellY) {
    mWorkspace.addInScreen(newFolder, folderInfo);
}

添加进底部的固定快捷栏 Hotseat

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

public void bindInflatedItems(
    List<Pair<ItemInfo, View>> shortcuts, @Nullable AnimatorSet boundAnim) {
    workspace.addInScreenFromBind(view, item);
}

添加应用微件 AppWidget,即小组件

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

public void bindAppWidget(LauncherAppWidgetInfo item) {
    mWorkspace.addInScreen(view, item);
}

添加控件时为其设置长按事件监听器。

Workspace 实现了 WorkspaceLayoutManager 接口。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Workspace.java

public class Workspace<T extends View & PageIndicator> extends PagedView<T>
        implements DropTarget, DragSource, View.OnTouchListener, CellLayoutContainer,
        DragController.DragListener, Insettable, StateHandler<LauncherState>,
        WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks {
}

addInScreen 和 addInScreenFromBind 最终都会调用到有七个参数的 addInScreen 方法。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/WorkspaceLayoutManager.java

public interface WorkspaceLayoutManager {
    default void addInScreenFromBind(View child, ItemInfo info) {
        addInScreen(child, info.container, presenterPos.screenId, x, y, info.spanX, info.spanY);
    }

    default void addInScreen(View child, ItemInfo info) {
        addInScreen(child, info.container,
                presenterPos.screenId, presenterPos.cellX, presenterPos.cellY,
                info.spanX, info.spanY);
    }

    default void addInScreen(View child, int container, int screenId, int x, int y,
            int spanX, int spanY) {
        // 设置长按监听。
        child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
    }

    default View.OnLongClickListener getWorkspaceChildOnLongClickListener() {
        // 具体长按事件来自 ItemLongClickListener
        return ItemLongClickListener.INSTANCE_WORKSPACE;
    }
}

在长按事件的回调中触发开启拖拽功能

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/touch/ItemLongClickListener.java

public class ItemLongClickListener {

    // 每个添加进桌面页 Workspace 的控件都设置了该长按事件监听器。
    public static final OnLongClickListener INSTANCE_WORKSPACE =
            ItemLongClickListener::onWorkspaceItemLongClick;

    private static boolean onWorkspaceItemLongClick(View v) {
        // 开启拖拽功能。
        beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
        return true;
    }

    public static void beginDrag(View v, Launcher launcher, ItemInfo info,
            DragOptions dragOptions) {
        if (info.container >= 0) {
            Folder folder = Folder.getOpen(launcher);
            if (folder != null) {
                if (!folder.getIconsInReadingOrder().contains(v)) {
                    folder.close(true);
                } else {
                    // 文件夹展开时,处理文件夹内的拖拽。
                    folder.startDrag(v, dragOptions);
                    return;
                }
            }
        }

        CellInfo longClickCellInfo = new CellInfo(v, info,
                launcher.getCellPosMapper().mapModelToPresenter(info));
        // 桌面页 Workspace 开启拖拽。
        launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
    }
}

创建 DragView

长按后触发在桌面页中开启拖拽。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/Workspace.java

public void startDrag(CellInfo cellInfo, DragOptions options) {
    beginDragShared(child, this, options);
}

public void beginDragShared(View child, DragSource source, DragOptions options) {
    beginDragShared(child, null, source, (ItemInfo) dragObject,
            new DragPreviewProvider(child), options);
}

public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
    ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
    // 最终回到拖动控制器中开启当前被长按选中的控件的拖拽。【此处为伪代码】
    mDragController.startDrag();
}

即 一开始 Launcher#initDragController 创建的 LauncherDragController。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/LauncherDragController.java

public class LauncherDragController extends DragController<Launcher> {
    @Override
    protected DragView startDrag(
            @Nullable Drawable drawable,
            @Nullable View view,
            DraggableView originalView,
            int dragLayerX,
            int dragLayerY,
            DragSource source,
            ItemInfo dragInfo,
            Rect dragRegion,
            float initialDragViewScale,
            float dragViewScaleOnDrop,
            DragOptions options) {
        // 创建一个可以跟随手指移动进行拖拽的视图。【此处为伪代码】
        final DragView dragView = new LauncherDragView();
        // 创建主屏上处理实际拖拽事件的驱动程序。
        mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
        // 显示根据长按区域创建的可拖拽视图。
        dragView.show(mLastTouch.x, mLastTouch.y);
    }
}

显示 DragView

在 DragLayer 显示根据长按区域创建的可拖拽视图。

com.android.launcher3.dragndrop.DragView#show

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragView.java

public void show(int touchX, int touchY) {
    // 在 DragLayer 显示。
    mDragLayer.addView(this);
}

小结:

长按桌面页上面的控件后,根据所在区域显示一个新的 DragView 在 DragLayer 的最上层。

拦截 Move 事件

  • DragLayer 分发触摸事件
  • DragLayer 拦截触摸事件
  • DragLayer 处理触摸事件

下面分析手指移动后,最上层的 View 即 DragView 是如何跟随移动的?

DragLayer 分发触摸事件。

com.android.launcher3.dragndrop.DragLayer#dispatchTouchEvent

DragLayer 继承自 BaseDragLayer,事件的分发与拦截主要由 BaseDragLayer 处理。

com.android.launcher3.views.BaseDragLayer#dispatchTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/views/BaseDragLayer.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case ACTION_DOWN: {
            if ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0) {
                // 第一次的 Down 事件 是不会进来的。
                // TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS 代表 Down 事件已经分发到子 View。
                // Cancel the previous touch
                int action = ev.getAction();
                // 第二次的 Down 事件,会被主动丢弃。即在进行拖动第一个控件时,不允许继续通过长按触发另一个控件的拖动。
                ev.setAction(ACTION_CANCEL);
                super.dispatchTouchEvent(ev);
                ev.setAction(action);
            }
            // 0001 | 1000 = 1001
            // mTouchDispatchState | 1001
            mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW
                    | TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;

            // 判断触摸事件是否位于系统手势区域内。
            if (isEventWithinSystemGestureRegion(ev)) {
                // ~0010 = 1101
                // mTouchDispatchState & 1101
                // 即清除 TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION 标记。
                // 便于后续获取控制器。
                mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
            } else {
                // mTouchDispatchState | 0010
                mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
            }
            break;
        }
        case ACTION_CANCEL:
        case ACTION_UP:
            // 即清除 TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION 标记。
            mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
            // 即清除 TOUCH_DISPATCHING_FROM_VIEW 标记。
            mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW;
            // 即清除 TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS 标记。
            mTouchDispatchState &= ~TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
            break;
    }
    // 尝试拦截后续触摸事件。
    super.dispatchTouchEvent(ev);

    // We want to get all events so that mTouchDispatchSource is maintained properly
    return true;
}

上述分发过程主要功能有:

  • 在未收到 ACTION_UP 事件前,不会处理第二个 ACTION_DOWN 事件。
  • 触发 ACTION_DOWN 事件的位置发生在以下区域将不会尝试拦截后续的事件。
    • 系统返回手势区域,即屏幕侧滑返回区域。
    • 系统顶部状态栏区域。
    • 系统底部导航栏区域。

DragLayer 拦截触摸事件

android.view.ViewGroup#dispatchTouchEvent

ViewGroup 分发触摸事件过程中会触发 ViewGroup 的 onInterceptTouchEvent

android.view.ViewGroup#onInterceptTouchEvent

BaseDragLayer 重写了该方法。

尝试拦截触摸事件。

com.android.launcher3.views.BaseDragLayer#onInterceptTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/views/BaseDragLayer.java

public abstract class BaseDragLayer<T extends Context & ActivityContext>
        extends InsettableFrameLayout {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 拦截触摸事件。
        return findActiveController(ev);
    }

    protected boolean findActiveController(MotionEvent ev) {
        mActiveController = null;
        // 收到 Down 事件的后续事件,如 Move 事件后,才允许拦截事件。
        if (canFindActiveController()) {
            // 找一个触摸控制器来处理该事件。
            mActiveController = findControllerToHandleTouch(ev);
        }
        // 允许查找并且找到一个可用的控制器后,就拦截事件。
        return mActiveController != null;
    }

    private TouchController findControllerToHandleTouch(MotionEvent ev) {
        for (TouchController controller : mControllers) {
            // 找到控制器,并判断控制器是否拦截该事件。
            if (controller.onControllerInterceptTouchEvent(ev)) {
                return controller;
            }
        }
        return null;
    }

    /**
     * VIEW_GESTURE_REGION 来自视图手势区域。
     * PROXY 来自代理。
     *
     * @return 触摸事件不是视图区域的并且没有设置代理就可以去搜索触摸控制器。
     */
    protected boolean canFindActiveController() {
        // Only look for controllers if we are not dispatching from gesture area and proxy is
        // not active
        // 0010 | 0100 = 0110
        // mTouchDispatchState & 0110
        // 要求等于零,即不是来自视图手势区域 也不是来自代理
        return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
                | TOUCH_DISPATCHING_FROM_PROXY)) == 0;
    }
}

什么情况下会成功拦截事件?

主要看控制器,是否支持拦截。

使用到的控制器 LauncherDragController 继承自 DragController。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/LauncherDragController.java

public class LauncherDragController extends DragController<Launcher> { }

com.android.launcher3.dragndrop.DragController#onControllerInterceptTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragController.java

public abstract class DragController<T extends ActivityContext>
        implements DragDriver.EventListener, TouchController {

    @Override
    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        // 控制器要看驱动是否支持拦截该事件。
        return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
    }
}

回顾对桌面页控件的长按事件过程,得知拖拽驱动是在 LauncherDragController#startDrag 中创建的。

即通过长按开启拖拽后拖拽驱动才会尝试拦截后续事件。

分析 DragDriver.create 方法发现创建的拖拽驱动是 InternalDragDriver。

com.android.launcher3.dragndrop.DragDriver#create

Java 复制代码
public static DragDriver create(DragController dragController, DragOptions options,
        Consumer<MotionEvent> sec) {
    if (options.simulatedDndStartPoint != null) {
        if  (options.isAccessibleDrag) {
            return null;
        }
        return new SystemDragDriver(dragController, sec);
    } else {
        return new InternalDragDriver(dragController, sec);
    }
}

InternalDragDriver 如何拦截事件

com.android.launcher3.dragndrop.DragDriver.InternalDragDriver#onInterceptTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragDriver.java

public boolean onInterceptTouchEvent(MotionEvent ev) {
    mSecondaryEventConsumer.accept(ev);
    final int action = ev.getAction();

    switch (action) {
        case MotionEvent.ACTION_UP:
            // 手抬起时结束拖拽。
            mEventListener.onDriverDragEnd(mDragController.getX(ev),
                    mDragController.getY(ev));
            break;
        case MotionEvent.ACTION_CANCEL:
            mEventListener.onDriverDragCancel();
            break;
    }
    // 这里将后续事件全部拦截。
    return true;
}

小结:长按后开启拖拽功能,最终由 InternalDragDriver 决定是否拦截事件。

DragLayer 处理触摸事件

拦截到事件后执行 BaseDragLayer#onTouchEvent

com.android.launcher3.views.BaseDragLayer#onTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/views/BaseDragLayer.java

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 交由控制器处理后续事件。
    return mActiveController.onControllerTouchEvent(ev);
}

com.android.launcher3.dragndrop.DragController#onControllerTouchEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragController.java

@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
    return mDragDriver != null && mDragDriver.onTouchEvent(ev);
}

com.android.launcher3.dragndrop.DragDriver.InternalDragDriver#onTouchEvent

BaseDragLayer 将事件拦截后,传递到此处。 BaseDragLayer 不会将 Down 事件传递到此处的。

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragDriver.java

@Override
public boolean onTouchEvent(MotionEvent ev) {
    mSecondaryEventConsumer.accept(ev);
    final int action = ev.getAction();

    switch (action) {
        case MotionEvent.ACTION_MOVE:
            // 移动事件。
            mEventListener.onDriverDragMove(mDragController.getX(ev),
                    mDragController.getY(ev));
            break;
        case MotionEvent.ACTION_UP:
            mEventListener.onDriverDragMove(mDragController.getX(ev),
                    mDragController.getY(ev));
            // 手抬起时结束拖拽。
            mEventListener.onDriverDragEnd(mDragController.getX(ev),
                    mDragController.getY(ev));
            break;
        case MotionEvent.ACTION_CANCEL:
            mEventListener.onDriverDragCancel();
            break;
    }

    return true;
}

com.android.launcher3.dragndrop.DragController#onDriverDragMove

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragController.java

@Override
public void onDriverDragMove(float x, float y) {
    // 触摸事件不能超出拖动层 DragLayer 边界。
    Point dragLayerPos = getClampedDragLayerPos(x, y);
    handleMoveEvent(dragLayerPos.x, dragLayerPos.y);
}

移动 DragView

com.android.launcher3.dragndrop.DragController#handleMoveEvent

Java 复制代码
// packages/apps/Launcher3/src/com/android/launcher3/dragndrop/DragController.java

protected void handleMoveEvent(int x, int y) {
    // 跟随 Move 事件,移动 dragView 的位置。
    mDragObject.dragView.move(x, y);
}

至此 DragView 就跟随手指有移动了。

相关推荐
百锦再2 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子3 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师3 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
江上清风山间明月6 小时前
Android 系统超级实用的分析调试命令
android·内存·调试·dumpsys
百锦再6 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
用户693717500138410 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
SHEN_ZIYUAN10 小时前
Android 主线程性能优化实战:从 90% 降至 13%
android·cpu优化
曹绍华10 小时前
android 线程loop
android·java·开发语言
雨白10 小时前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack
介一安全10 小时前
【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包
android·okhttp·网络安全·frida