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

  1. Generate drag events, encapsulate the event and data into DragState objects and save them as WMS global variables
  2. Notify all Windows that the drag event has started
  3. 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
  4. Subsequent events are called back according to the same process as ACTION_DRAG_LOCATION