preface
Recently, the company had a tablet project that required the effect of dragging and dropping an item to a specified position to play a video. Due to laziness and the particularity of the project, it only needed to be compatible with certain types of devices, so it decided to directly use the Drag and Drop API.
This API provides drag-and-drop operations for views, and supports passing data through drag-and-drop events, and most importantly, according to the official documentation, It can pass drag events between two apps when multi-window Mode is turned on (in fact, it can drag between different apps when isInMultiWindowMode = false is tested).
use
It is very simple to use. The sender calls view. startDragAndDrop and the receiver view. setOnDragListener. Let’s test the drag between the two apps. Long press the button to trigger the drag. End the sending activity and return the receiving activity to respond to the drag event.
-
App activity on the receiving end
val root = findViewById<View>(R.id.root) val btn = findViewById<Button>(R.id.button) btn.setOnClickListener { // Implicitly jump to sender app startActivity(Intent("com.lyj.drag.send")) } root.setOnDragListener { v, event -> // clipData can only be received when dragEvent. ACTION_DROP if (event.action == DragEvent.ACTION_DROP) { val data = event.clipData val id = Process.myPid() Log.e("test"."TargetActivity process id:$idThe data:${data.getItemAt(0).text}")}true } Copy the code
-
Sending app Activity
val btn = findViewById<Button>(R.id.btn) btn.setOnLongClickListener { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { val id = Process.myPid() Log.e("test"."SendActivity process id:$id") // Data to be passed val data = ClipData.newPlainText("test"."message") // The second argument is an object to build the drag icon DRAG_FLAG_GLOBAL: the receiver is only allowed to access ClipData of the text and intent types btn.startDragAndDrop(data, View.DragShadowBuilder(btn), null, View.DRAG_FLAG_GLOBAL) // Switch from the receiving app to the activity, drag and drop to end the current activity and return to the receiving activity finish() } true } Copy the code
In this case, there was no problem transferring data between the two apps. Note that this API requires a system >7.0.
Source analyses
Let’s take a quick look at the source code to see how it works. We only have 8.0 source code at hand, so here we go.
View.startDragAndDrop
public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) {...if(data ! =null) {
DRAG_FLAG_GLOBAL = view. DRAG_FLAG_GLOBAL = view. DRAG_FLAG_GLOBAL = view. DRAG_FLAG_GLOBAL = view. DRAG_FLAG_GLOBAL = view. DRAG_FLAG_GLOBALdata.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) ! =0); }.../ / comment 1
mAttachInfo.mDragSurface = new Surface();
/ / comment 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);
/ / comment 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);
}
Copy the code
Create a Surface in comment 1 to display the drag icon
Mattachinfo. mSession is a Session AIDL remote proxy for IPC communication with WindowManagerService. PrepareDrag last call to WindowManagerService prepareDragSurface, look at the code
Session.prepareDrag
public IBinder prepareDrag(IWindow window, int flags, int width, int height, Surface outSurface) {
return mService.prepareDragSurface(window, mSurfaceSession, flags,
width, height, outSurface);
}
Copy the code
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 is the proxy for the client 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);
}
Copy the code
Two main things are done here. One is to initialize the surface, which is passed in to display the drag icon, so that the icon is displayed when dragged, and the other is to encapsulate the event as a DragState object and save it as a global variable of WMS, and then set the drag event to a 5 second timeout.
Now back to the View. The comments in the startDragAndDrop 3, mAttachInfo. MSession. PerformDrag invoked the Session. PerformDrag
Session.performDrag
public boolean performDrag(IWindow window, IBinder dragToken, int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
// Save the sending ClipData in the DragState of the WMS
mService.mDragState.mData = data;
/ / comment 1mService.mDragState.broadcastDragStartedLw(touchX, touchY); ./ / comment 2
mService.mDragState.notifyLocationLw(touchX, touchY);
}
Copy the code
Note 1 call the DragState broadcastDragStartedLw
DragState.broadcastDragStartedLw
void broadcastDragStartedLw(final float touchX, final float touchY) {... mDisplayContent.forAllWindows(w -> {// Callback drags the start eventsendDragStartedLw(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 {
/ / comment 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(); }}}}Copy the code
This part of the code basically iterates through all the Windows, calling back their dispatchDragEvent method.
Note 1 mClient is IWindow object, he is on behalf of the client window based on AIDL IPC agent in WMS, corresponding client implementation is the inner class ViewRootImpl W, so the last call to ViewRootImpl. W.d ispatchDragEvent
ViewRootImpl.W.dispatchDragEvent
W.dispatchDragEvent
public void dispatchDragEvent(DragEvent event) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if(viewAncestor ! =null) {
/ / directly to ViewRootImpl dispatchDragEvent processingviewAncestor.dispatchDragEvent(event); }}Copy the code
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 {
/ / comment 1
what = MSG_DISPATCH_DRAG_EVENT;
}
Message msg = mHandler.obtainMessage(what, event);
mHandler.sendMessage(msg);
}
Copy the code
Here by handler calls to ViewRootImpl handleDragEvent
ViewRootImpl.handleDragEvent
private void handleDragEvent(DragEvent event) {
if (what == DragEvent.ACTION_DRAG_EXITED) {
......
} else {
booleanresult = mView.dispatchDragEvent(event); }}Copy the code
MView is really just a DecorView; So we end up dispatching drag events as if they were touch events, calling back to the view that was set up to listen for drag, and the rest of the process is omitted.
Looking back again next Session. The comments in the performDrag 2, broadcasting the drag start event after call mService. MDragState. NotifyLocationLw callback drag coordinates
DragState.notifyLocationLw
void notifyLocationLw(float x, float y) {
/ / comment 1WindowState 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);
/ / comment 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);
/ / comment 3
touchedWin.mClient.dispatchDragEvent(evt);
/ / comment 4
if(myPid ! = touchedWin.mSession.mPid) { evt.recycle(); }}}Copy the code
- Comment 1 gets the uppermost Window of the current drag location
- The code in comment 2 will only execute if the window that the drag action went through has changed. MTargetWindow records the window that the drag event last went through, so it first calls back its ACTION_DRAG_EXITED event
- Comment 3 calls back the ACTION_DRAG_LOCATION event of the Window at the top level of the current coordinate
- Note 4 to determine whether the two Windows are in the same process, if not, to actively reclaim the event to release memory
The entire drag event has probably gone through the process from generation to receiving in the target window
conclusion
To support drag across Windows, drag events are handed to WMS through IPC calls, and Surface creates independent Windows for displaying drag ICONS. The basic process is as follows
- Generate drag events, encapsulate the event and data into DragState objects and save them as WMS global variables
- Notify all Windows that the drag event has started
- The Window is notified of a drag event coordinate change at the top level of the current coordinate point. The Window finds the View with the drag monitor set through ViewRootImpl for callback
- Subsequent events are called back according to the same process as ACTION_DRAG_LOCATION