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 就跟随手指有移动了。