前言
公司最近有个平板项目需要做一个拖拽item到指定位置播放视频的效果,由于想偷懒,加上项目特殊性只需要兼容特定几个型号的设备于是决定直接使用Drag and drop API。
这个API提供view的拖拽操作,而且支持通过拖拽事件传递数据,最重要的是按照官方文档的说法,它能够在开启了Multi-Window mode的情况下在两个app之间传递拖拽事件(实际上经测试isInMultiWindowMode = false时也能够在不同的app之间拖拽)。
使用
使用起来非常简单,发送端调用View.startDragAndDrop
,接收端View.setOnDragListener
,下面来测试一下两个APP之间的拖拽,主要流程是从接收端app调转到发送端activity,长按按钮触发拖拽后结束掉发送端activity回到接收端activity响应拖拽事件。
-
接收端app activity
val root = findViewById
(R.id.root) val btn = findViewById
-
发送端app activity
val btn = findViewById
经测试这种情况下两个app间传递数据也毫无问题,要注意的是这个API要求系统>7.0。
源码浅析
让我们来简单追一下源码看是怎么处理的,手头只有8.0的源码,就按这个来。
View.startDragAndDrop
public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) {
......
if (data != null) {
// flags包含View.DRAG_FLAG_GLOBAL就做一下切换进程的预处理,主要是针对data中包含的intent处理
data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
}
......
// 注释1
mAttachInfo.mDragSurface = new Surface();
// 注释2
mAttachInfo.mDragToken = mAttachInfo.mSession.prepareDrag(mAttachInfo.mWindow, flags, shadowSize.x, shadowSize.y, mAttachInfo.mDragSurface);
if (mAttachInfo.mDragToken != null) {
Canvas canvas = mAttachInfo.mDragSurface.lockCanvas(null);
try {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
shadowBuilder.onDrawShadow(canvas);
} finally {
mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
}
final ViewRootImpl root = getViewRootImpl();
root.setLocalDragState(myLocalState);
root.getLastTouchPoint(shadowSize);
// 注释3
okay = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, mAttachInfo.mDragToken,
root.getLastTouchSource(), shadowSize.x, shadowSize.y,
shadowTouchPoint.x, shadowTouchPoint.y, data);
if (ViewDebug.DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "performDrag returned " + okay);
}
注释1处创建一个surface用于显示拖拽图标
注释2处做一些开始拖拽之前的准备动作, mAttachInfo.mSession是一个Session的AIDL远程代理,用于和WindowManagerService进行IPC通信,prepareDrag最后调用到了WindowManagerService.prepareDragSurface
,来看下代码
Session.prepareDrag
public IBinder prepareDrag(IWindow window, int flags, int width, int height, Surface outSurface) {
return mService.prepareDragSurface(window, mSurfaceSession, flags,
width, height, outSurface);
}
WindowManagerService.prepareDragSurface
IBinder prepareDragSurface(IWindow window, SurfaceSession session, int flags, int width, int height, Surface outSurface) {
final DisplayContent displayContent = getDefaultDisplayContentLocked();
final Display display = displayContent.getDisplay();
SurfaceControl surface = new SurfaceControl(session, "drag surface", width, height, PixelFormat.TRANSLUCENT, SurfaceControl.HIDDEN);
surface.setLayerStack(display.getLayerStack());
float alpha = 1;
if ((flags & View.DRAG_FLAG_OPAQUE) == 0) {
alpha = DRAG_SHADOW_ALPHA_TRANSPARENT;
}
surface.setAlpha(alpha);
if (SHOW_TRANSACTIONS) Slog.i(TAG_WM, " DRAG "
+ surface + ": CREATE");
outSurface.copyFrom(surface);
// window是客户端窗口的代理
final IBinder winBinder = window.asBinder();
token = new Binder();
mDragState = new DragState(this, token, surface, flags, winBinder);
mDragState.mPid = callerPid;
mDragState.mUid = callerUid;
mDragState.mOriginalAlpha = alpha;
token = mDragState.mToken = new Binder();
// 5 second timeout for this window to actually begin the drag
mH.removeMessages(H.DRAG_START_TIMEOUT, winBinder);
Message msg = mH.obtainMessage(H.DRAG_START_TIMEOUT, winBinder);
mH.sendMessageDelayed(msg, 5000);
}
这里主要做了两件事,一是对传入的显示拖拽图标surface初始化,以便拖拽时显示图标,另一件事是将此次事件封装成一个DragState对象作为WMS的全局变量保存,然后设置拖拽事件5秒超时。
现在回到View.startDragAndDrop中的注释3处,mAttachInfo.mSession.performDrag
调用了Session.performDrag
Session.performDrag
public boolean performDrag(IWindow window, IBinder dragToken, int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
// 将发送端的ClipData保存在WMS的DragState中
mService.mDragState.mData = data;
// 注释1
mService.mDragState.broadcastDragStartedLw(touchX, touchY);
......
// 注释2
mService.mDragState.notifyLocationLw(touchX, touchY);
}
注释1处调用到DragState.broadcastDragStartedLw
DragState.broadcastDragStartedLw
void broadcastDragStartedLw(final float touchX, final float touchY) {
......
mDisplayContent.forAllWindows(w -> {
// 回调拖拽开始事件
sendDragStartedLw(w, touchX, touchY, mDataDescription);
}
}
private void sendDragStartedLw(WindowState newWin, float touchX, float touchY, ClipDescription desc) {
if (mDragInProgress && isValidDropTarget(newWin)) {
DragEvent event = obtainDragEvent(newWin, DragEvent.ACTION_DRAG_STARTED, touchX, touchY, null, desc, null, null, false);
try {
// 注释1
newWin.mClient.dispatchDragEvent(event);
mNotifiedWindows.add(newWin);
} catch (RemoteException e) {
Slog.w(TAG_WM, "Unable to drag-start window " + newWin);
} finally {
if (Process.myPid() != newWin.mSession.mPid) {
event.recycle();
}
}
}
}
这部分代码主要是遍历所有窗口,回调他们的dispatchDragEvent
方法(个人理解是此时还无法确定目标窗口,所以回调所有窗口)。
注释1处mClient是IWindow对象,他代表客户端的窗口在WMS中的基于AIDL的IPC代理,对应客户端实现是ViewRootImpl
中的内部类W,所以最后调用到了ViewRootImpl.W.dispatchDragEvent
ViewRootImpl.W.dispatchDragEvent
W.dispatchDragEvent
public void dispatchDragEvent(DragEvent event) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
// 直接给ViewRootImpl.dispatchDragEvent处理
viewAncestor.dispatchDragEvent(event);
}
}
ViewRootImpl.dispatchDragEvent
public void dispatchDragEvent(DragEvent event) {
final int what;
if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
what = MSG_DISPATCH_DRAG_LOCATION_EVENT;
mHandler.removeMessages(what);
} else {
// 注释1
what = MSG_DISPATCH_DRAG_EVENT;
}
Message msg = mHandler.obtainMessage(what, event);
mHandler.sendMessage(msg);
}
这里通过handler调用到了ViewRootImpl.handleDragEvent
ViewRootImpl.handleDragEvent
private void handleDragEvent(DragEvent event) {
if (what == DragEvent.ACTION_DRAG_EXITED) {
......
} else {
boolean result = mView.dispatchDragEvent(event);
}
}
mView实际上就是DecorView;所以这里最后就像分发触摸事件一样将拖拽事件层层分发下去,回调给设置了监听拖拽的view,后续流程省略。
接下来重新回过头看Session.performDrag中的注释2,广播了拖拽开始事件后调用mService.mDragState.notifyLocationLw
回调拖动坐标
DragState.notifyLocationLw
void notifyLocationLw(float x, float y) {
// 注释1
WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
......
if ((touchedWin != mTargetWindow) && (mTargetWindow != null)) {
DragEvent evt = obtainDragEvent(mTargetWindow, DragEvent.ACTION_DRAG_EXITED,
0, 0, null, null, null, null, false);
// 注释2
mTargetWindow.mClient.dispatchDragEvent(evt);
if (myPid != mTargetWindow.mSession.mPid) {
evt.recycle();
}
}
if (touchedWin != null) {
DragEvent evt = obtainDragEvent(touchedWin, DragEvent.ACTION_DRAG_LOCATION,
x, y, null, null, null, null, false);
// 注释3
touchedWin.mClient.dispatchDragEvent(evt);
// 注释4
if (myPid != touchedWin.mSession.mPid) {
evt.recycle();
}
}
}
- 注释1处获取当前拖动到的位置的最上层Window
- 注释2处的代码只有在拖拽动作经过的窗口改变了之后才会执行,mTargetWindow 记录的是拖拽事件上次经过的窗口,所以先回调它的ACTION_DRAG_EXITED事件
- 注释3处回调当前坐标最上层Window的ACTION_DRAG_LOCATION事件
- 注释4处判断两个窗口是否在同一进程,不在的话要主动回收事件释放内存
自此整个拖动事件从产生到目标窗口接收大概流程已经走完
总结
因为要支持跨窗口拖动,拖动事件通过IPC调用交给了WMS中转,同时创建独立窗口的Surface用于显示拖动图标,基本流程如下
- 产生拖动事件,将事件以及数据封装成DragState对象保存为WMS全局变量
- 通知所有窗口拖动事件开始
- 对当前坐标点最上层Window进行一次拖动事件坐标改变的通知,Window通过ViewRootImpl找到设置了拖动监听的View进行回调
- 后续事件依照ACTION_DRAG_LOCATION同样流程回调