Launcher拖拽框架
桌面应用 icon 的拖拽框架
前置文章
- 《Launcher的启动过程 》
- 《 Launcher界面结构 》
前言
在手机桌面,我们经常会把一个应用的图标从菜单里面,拉到桌面。或者把一个应用的图标移到自己更加喜欢的位置。这个过程,我们叫它拖拽。拖拽能够让用户方便的把应用放到用户可记得易操作的位置,从而能够让用户快捷的打开高频使用的应用。同时,拖拽也可以让用户能够布置自己的桌面,能够把应用进行分类的存放。因此,Launcher拖拽让用户可自定义桌面。本文所使用代码和任何展示均是使用Launcher3。拖拽演示如下图:
Launcher 拖拽框架
在Launcher的拖拽过程中,代码出现的对应的关键字便是Drag和Drop,Drag表示用户拖拽开始,Drop表示用户拖拽完成。
拖拽开始
在 Launcher 中触发拖拽,是通过长按应用的图标的方式,拖拽开始后,用户便可把应用的图标拖到喜欢的位置。我们以从 Workspace 长按应用图标为例(即上图中的拖拽演示),看看触发拖拽过程。如果读者对 workspace 的概念不是很清楚,可以先阅读文章《 Launcher界面结构 》。
下面我们就从长按的回调方法开始
public boolean onLongClick(View v) {
......
CellLayout.CellInfo longClickCellInfo = null;
if (v.getTag() instanceof ItemInfo) {
ItemInfo info = (ItemInfo) v.getTag();
longClickCellInfo = new CellLayout.CellInfo(v, info);
itemUnderLongClick = longClickCellInfo.cell;
resetAddInfo();
}
......
if (!(itemUnderLongClick instanceof Folder || isAllAppsButton)) {
// User long pressed on an item
mWorkspace.startDrag(longClickCellInfo);
}
}
}
return true;
}
(代码1)
这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/Launcher.java中。
长按事件传递过来的 Vie w便是我们长按的应用图标,在 Launcher 中,v的实质便是 BubbleTextView,文章中《 Launcher界面结构 》有相关描述。在 onLongClick() 中,首先是从 v 中获取到 tag,tag 实质是一个 ItemInfo,当然,如果我们从 workspace 中拖拽一个图标,这个 tag 的真面目是 ItemInfo 的子类 ShortcutInfo。ShortcutInfo 或者说 ItemInfo 有什么作用呢?在 Launcher 中我们看到的每一个应用的图标,代码中就是抽象成BubbleTextView,一个 BubbleTextView 就是一个 item,每个 item 都会拥有自己的 ItemInfo 实例,这个 ItemInfo 实例记录了每个 item 的信息,如这个 item 在 Workspace 中的具体位置, item 的标题和描述,item 的拥有者等等。还有一点很重要的,记录了 item 的单击触发的意图,也即抽象成 Android 中的 Intent 实例。也就是说,当我们点击一个应用的图标后,打开哪个应用,便由 ItemInfo 中的 Intent 变量来决定。
在onLongClick()中,把 v 即 BubbleTextView 封装到 CellLayout.CellInfo 的实例 longClickCellInfo 中,longClickCellInfo.cell 即是 BubbleTextView。然后 longClickCellInfo 作为参数调用 startDrag() 方法。
public void startDrag(CellLayout.CellInfo cellInfo) {
startDrag(cellInfo, false);
}
@Override
public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) {
View child = cellInfo.cell;
......
child.setVisibility(INVISIBLE);
CellLayout layout = (CellLayout) child.getParent().getParent();
layout.prepareChildForDrag(child);
beginDragShared(child, this, accessible);
}
(代码2)这个两个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/Workspace.java中。
上述方法中 cellInfo.cell 即 BubbleTextView 实例赋值给 child,即我们点击的应用图标,参数 accessible 是辅助功能所用,本文不赘述这个。然后把 child 的可见性设置成不可见,layout.prepareChildForDrag(child) 把child的位置信息记录下来,以防 child 还要回到这个位置。接着调用 beginDragShared(),这里注意第二个参数 this,实际类型是 DragSource,DragSource 的接口定义如下
public interface DragSource {
boolean supportsFlingToDelete();
boolean supportsAppInfoDropTarget();
boolean supportsDeleteDropTarget();
float getIntrinsicIconScaleFactor();
void onFlingToDeleteCompleted();
void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success);
}
(代码3)这个接口定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragSource.java中。
DragSource 表示本次拖拽的应用图标来自哪里,在 beginDragShared() 中传入的参数是 this,也即本次拖拽来自 Workspace。DragSource 定义了一些方法,目的两个,第一,告诉拖拽框架,来自我这个 DragSource 拖拽的 item 所能支持的某些功能或者某些属性。如,是否允许拖拽删除;第二,让拖拽框架告诉我这个 DragSource 拖拽的一些“情报”,如拖拽结束。
注意:这里论述到 DragSource,这里抛出拖拽框架中两个核心问题:
第一:拖拽的应用图标来自哪里,对拖拽框架而言,你从哪里来? 第二:拖拽的应用图标放到哪里,对拖拽框架而言,你到哪里去?(后文会现身)
在拖拽框架中,也就围绕着两个核心问题进行应用图标的拖拽。
回到beginDragShared()这个方法
public void beginDragShared(View child, DragSource source, boolean accessible) {
beginDragShared(child, new Point(), source, accessible);
}
public void beginDragShared(View child, Point relativeTouchPos, DragSource source,
boolean accessible) {
......
final Bitmap b = createDragBitmap(child, padding);
......
DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible);
dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
}
(代码4)这个两个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/Workspace.java中。
在第二个beginDragShared()方法中多了一个参数,Point 类型的 relativeTouchPos。我们先看 mDragController.startDrag() 返回一个 DragView,DragView 是 View 的子类。如下图,当我们按下应用的图标后,就好像图标被放大。
在(代码2)中,我们知道,代码 child.setVisibility(INVISIBLE) 把长按的应用 icon 设置成不可见,那么上图中这个被略放大的 icon 和被长按的 icon 长得一样的到底又是什么东西。它就是上面代码中的 DragView。换句话说,我们拖拽时拖动的 icon 并不是原来的 icon,而是一个原 icon 的“亲兄弟” DragView。
回到(代码4),beginDragShared()中,当我们手指触摸手机屏幕的地方和 DragView 形成的的地方有偏移量,此时可以通过参数 relativeTouchPos 把 DragView 移到手指触摸的地方。当然,在我们的图片中,长按的地方,DragView 形成的地方也就是我们手指触摸的地方,因此,本文中的代码传入的是一个没有使用价值的 Point 实例(x, y = 0)(其实设置 y 的属性是不会生效的,在startDrag()方法中不考虑 y 属性)。再回到代码中,首先调用了 createDragBitmap() 方法,把 child 中的 icon 取出来,作为 startDrag() 中的第一个参数,详情如下:
第一参数,也就是 DragView 的 icon,
第二、第三个参数 dragLayerX, dragLayerY 是 int 类型,从 relativeTouchPos 化身而来,也就是x, y。
第六个参数 dragAction 是 DragController.DRAG_ACTION_MOVE。
第七个参数 dragVisualizeOffset 用于描绘 DragView 拖动时在位置上的类似投影的视图所使用的偏移参数。
第八个参数 dragRect 记录了 icon 到 BubbleTextView 边框的距离,用于后面给 View 计算位置。
第九个参数 scale 是对 DragView 的缩放。
在这个方法中的参数也是够多的,这里调用 startDrag() 的是 mDragController,mDragController 是 DragController 的实例,DragController 是整个拖拽框架中的控制中心,在拖拽中两个核心问题你从哪里来,你到哪里去的指挥者。DragController 在 Launcher 启动 Laucher activity 的时候,在 onCreate() 方法中实例化。从长按应用 icon,到调用 startDrag() 方法时,表示拖拽真正进入到拖拽框架,拖拽也就从 startDrag() 真正开始。
在(代码4)中,我们知道真正拖拽移动的是一个 DragView,因此,这个 DragView 在拖拽框架中充当一个事件执行的代表,它不是真正的应用 icon,也没有任何可利用的数据价值,它是一个应用 icon(BubbleTextView) 的“代理者”,在拖拽框架中你到哪里去的执行代表。
继续往下跟踪代码
public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
float initialDragViewScale, boolean accessible) {
......
for (DragListener listener : mListeners) {
listener.onDragStart(source, dragInfo, dragAction);
}
......
mDragObject = new DropTarget.DragObject();
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
......
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
......
dragView.show(mMotionDownX, mMotionDownY);
handleMoveEvent(mMotionDownX, mMotionDownY);
return dragView;
}
(代码5)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
首先循环遍历 mListeners 把 DragListener 取出来,DragListener 顾名思义,拖拽监听者。拖拽开始时,通过回调 onDragStart() 通知监听者拖拽已经开始,在 Launcher3 中,注册拖拽监听者的有 Folder.java 、Launcher activity 、SearchDropTarget.java、 InfoDropTarget.java、 DeleteDropTarget.java、 UninstallDropTarget.java 和 WidgetHostViewLoader.java 等。对这些不熟悉的读者,可以先阅读文章 《 Launcher界面结构 》。
随后 new 一个 DropTarget.DragObject,DropTarget.DragObject 是整个拖拽框架中最有“实权”的实例对象,它包含了拖拽的视图代表 DragView,包含被拖拽的应用 icon(BubbleTextView) 数据 ItemInfo,包含拖拽的源头 DragSource。在整个拖拽过程中,接受控制中心 DragController 的执行指令,是指令的首要执行者,DragObject 会伴随整个拖拽过程,对 DragView、icon(BubbleTextView)、DragSource 有绝对“控制权”,如控制 DragView 的显示、消失和位置等。
继续往下便是 DragView 的初始化,传入的参数基本都是上文中有说明的变量或类型,本文不再赘述 DragView 初始化的详细过程。随后调用 dragView.show(mMotionDownX, mMotionDownY) 把 DragView 显示到屏幕上,这里应该改为 mDragObject.dragView.show(),这样更能充分体现 DragObject 的“实权作用”。显示 DragView 之后,直接调用 handleMoveEvent() 方法,这个方法很重要,它是整个拖拽过程实现的基本条件,因此,这个方法在拖拽过程中会被调用非常多次数,也就是处理我们拖拽的时候的手指移动事件。
到此,拖拽的开始和准别工作到这里已经彻底完成,下面将进入真正的拖拽过程。
拖拽过程
在上一章节中,我们知道,我们拖拽的视图是一个 DragView,而不是应用 icon(BubbleTextView) 的本身,为什么?为什么不直接拖拽 icon,而是找一个化身 DragView?我们知道,一个子 View 是不能越界到它的父控件的外面,因此,icon(BubbleTextView) 它的父控件的范围可能就好小,但我们的 icon(BubbleTextView) 是要能拖到界面上得任何一个位置的。所以,Launcher 有一个覆盖整个 Laucher 界面的 DragLayer,而 DragView 便是依附在 DragLayer 作为它的直接子 View,便能在整个 Launcher 界面上移动。
因为 DragLayer 在拖拽框架中控制拖拽范围的,因此,实质我们手指在手机屏幕上的移动范围便是 DragLayer,换句话说,我们手指移到哪里,需要受 DragLayer 的限制,也就因此,我们手指的触摸事件应由 DragLayer 来接收,并传达给拖拽的控制中心 DragControlller, 即 handleMoveEvent()。
public boolean onTouchEvent(MotionEvent ev) {
......
return mDragController.onTouchEvent(ev);
}
(代码6)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragLayer.java中。
public boolean onTouchEvent(MotionEvent ev) {
......
case MotionEvent.ACTION_MOVE:
handleMoveEvent(dragLayerX, dragLayerY);
break;
......
return true;
}
(代码7)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
上面的代码是触摸事件的接收和传递,最终传递到 DragControlller 的 handleMoveEvent() 方法。如下:
private void handleMoveEvent(int x, int y) {
mDragObject.dragView.move(x, y);
// Drop on someone?
final int[] coordinates = mCoordinatesTemp;
DropTarget dropTarget = findDropTarget(x, y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
checkTouchMove(dropTarget);
// Check if we are hovering over the scroll areas
mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
mLastTouch[0] = x;
mLastTouch[1] = y;
checkScrollState(x, y);
}
(代码8)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
handleMoveEvent() 的比较简洁,没有过多的计算等代码,不需要做任何的代码“省略”。首先是通过 DragObject 控制移动 DragView 到手指的位置。接着,注意,请注意,调用了 findDropTarget(x, y, coordinates) 方法,传入了 int 类型的 x, y 和 int[] 类型的 coordinates, 返回一个 DropTarget 的实例 dropTarget,drop 放下, target 目标,放下目标,即 DragView 放下的目标,即 icon(BubbleTextView) 移到的新的位置。因此,DropTarget 是拖拽框架中两个核心问题中的第二个,你到哪里去的真实身份现身,它就是 DropTarget。我们进入 findDropTarget() 这个方法瞧瞧
private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
final Rect r = mRectTemp;
final ArrayList<DropTarget> dropTargets = mDropTargets;
final int count = dropTargets.size();
for (int i=count-1; i>=0; i--) {
DropTarget target = dropTargets.get(i);
if (!target.isDropEnabled())
continue;
//获取target 在 draglayer 中的位置
target.getHitRectRelativeToDragLayer(r);
mDragObject.x = x;
mDragObject.y = y;
if (r.contains(x, y)) {
dropCoordinates[0] = x;
dropCoordinates[1] = y;
return target;
}
}
}
(代码9)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
在这里,有一个 mDropTargets 的全局变量,保存了所有的 Target,这些 Target 在 Laucher 启动的时候就通过方法 addDropTarget(DropTarget target) 添加到 DrageController 中来。包括 Workspace、Folder、InfoDropTarget 等等。在这么多 Target 中,如何判断当前是移到哪个 Target 呢?通过 target 的实例调用 getHitRectRelativeToDragLayer(r) 方法,获取到 target 所处的位置 r,通过 r.contains(x, y) 判断, x, y 是否包含在 target 内,如果是,则手指移到了该 target,同时把 x,y 保存到 target 实例中,返回该 target 对象。
回到(代码8)handleMoveEvent() 方法,获取到当前手指移到得 targe 实例后,作为参数传递给 checkTouchMove(dropTarget),如下:
private void checkTouchMove(DropTarget dropTarget) {
if (dropTarget != null) {
if (mLastDropTarget != dropTarget) {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
dropTarget.onDragEnter(mDragObject);
}
dropTarget.onDragOver(mDragObject);
} else {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
}
mLastDropTarget = dropTarget;
}
(代码10)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
如果前后两次的 target 实例不一致,说明 target 发生变化,通过调用 onDragExit() 方法通知上一次的 target 拖拽已经移走,然后通过 onDragEnter() 方法通知当前 target,拖拽已经移动进来。同时,通过 onDragOver() 通知 target 拖拽已经移到你的上面,准确的说,是 DragView 移到了 target 的上面。在这里,每个方法的参数都是 mDragObject 实例,在上文中我们知道,mDragObject 在拖拽中是最有“实权”的“人物”,拥有视图的化身 DragView,保存有 icon(BubbleTextView)的数据,持有拖拽的来源 DragSource,同时,mDragObject 到达了 target,也可以操作 target 对象实例本身,因此,在 target 可以发生一切事情。
拖拽结束
当我们的手指离开屏幕,标志着一次拖拽结束,代码如下:
public boolean onTouchEvent(MotionEvent ev) {
......
case MotionEvent.ACTION_UP:
// Ensure that we've processed a move event at the current pointer location.
handleMoveEvent(dragLayerX, dragLayerY);
mHandler.removeCallbacks(mScrollRunnable);
if (mDragging) {
// 是否 甩 到移除
PointF vec = isFlingingToDelete(mDragObject.dragSource);
if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) {
vec = null;
}
if (vec != null) {
// 执行 移除
dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
} else {
drop(dragLayerX, dragLayerY);
}
}
endDrag();
break;
......
}
(代码11)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
再次调用 handleMoveEvent() 把 DragView 精确到手指离开的位置,首先是判断是否有甩到移除的手势,如果是,移除相关数据和操作,如果不是,执行 drop() 方法。我们先来看 endDrag() 方法,在回头看这个 drop() 方法。
private void endDrag() {
if (mDragging) {
mDragging = false;
......
if (!isDeferred) {
mDragObject.dragView.remove();
}
mDragObject.dragView = null;
}
// Only end the drag if we are not deferred
if (!isDeferred) {
for (DragListener listener : new ArrayList<>(mListeners)) {
listener.onDragEnd();
}
}
}
}
(代码12)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
如果不是 Deferred,立即执行把 DragView 从 DragLayer 中移除,然后通知监听者 onDragEnd(),拖拽结束。好,这里没有什么特别的,执行了一些结束的动作,我们回到前面的 drop() 方法
private void drop(float x, float y) {
final int[] coordinates = mCoordinatesTemp;
final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
boolean accepted = false;
if (dropTarget != null) {
mDragObject.dragComplete = true;
dropTarget.onDragExit(mDragObject);
if (dropTarget.acceptDrop(mDragObject)) {
dropTarget.onDrop(mDragObject);
accepted = true;
}
}
mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);
}
(代码13)这个方法定义在文件packages/apps/Launcher3/src/com/android/launcher3/DragController.java中。
再次获取手指离开屏幕时的 DropTarget,把 DragObject 的标志为 dragComplete 置为 true,通过 onDragExit() 方法通知 DragTarget 拖拽退出。到这里 DragTarget 还没有做拖拽退出的事情,应用 icon 还不可以就此“安家”。而是先调用 target 的 acceptDrop() 方法征求 target 是否可以接受本次拖拽,也就是是否可以接受该应用 icon(BubbleTextView)。target 如果可以接受该拖拽,接着调用 target 的 onDrop() 方法,表示本次拖拽的真正结束。以本次演习的例子来看,我们是要给 icon 寻找一个新的家,那么当 Workspace 接受这个 icon的时候,调用 animateViewIntoPosition() 为应用 icon 重新按一个“家”。当然,icon(BubbleTextView)还是原来的 icon,但是,有时候我们是从 Workspace 的外部拖拽一个 icon 到 Workspace 的,这时,在 onDrop() 方法中,会走 onDropExternal() 这个分支从而会形成一个新的 icon(BubbleTextView)。最后,调用 onDropCompleted() 通知拖拽的起源地,拖拽完成,并传送拖拽结果。
拖拽控制框架图
以从 Workspace 拖拽一个 icon(BubbleTextView)到另外一个位置为例
拖拽时序图
总结
在 Launcher 的拖拽框架中,由 DragController 担当指挥中心,用 DragSource 抽象拖拽的来源,用 DragView 描绘拖拽视图,设置 DragListener 通知拖拽的开始和结束,整个拖拽过程中,由 DragObject 执行拖拽事务,与 DragSource 相对的 DropTarget 描述拖拽的目的地,DragSource、 DropTarget 代表“你从哪里来,你到哪里去”,是拖拽框架中核心问题的抽象。当长按应用 icon 触发后,DragLayer 把触摸事件拦截,再传递给控制中心 DragControlller 处理。拖拽结束后,在 DropTarget 的 onDrop() 方法中处理拖拽的结束的事务。