Android 拖拽功能研究 —— startDragAndDrop

startDragAndDrop

最近一直在研究拖拽功能, 想要实现分屏状态下,左右应用的拖拽切换。

android 提供了两种用于实现view拖拽的API。
  1. ViewDragHelper (需要自定义ViewGroup)
  2. startDrag / startDragAndDrop (配合 setOnDragListener / onDragEvent)
使用场景:
  1. ViewDragHelper 适用于 “View本身的拖拽”。
  2. startDrag / startDragAndDrop 适用于 “View携带的数据的拖拽”。
不同点:
  1. startDrag / startDragAndDrop 产生的拖拽效果,是拖拽一个半透明的view。该view的绘制内容可以自定义绘制。一般默认和被拖拽view相同。该view位于最顶层。
  2. startDrag / startDragAndDrop 可以在拖拽时携带数据。该数据可以跨进程传输。
  3. 使用startDrag / startDragAndDrop时,要响应(监听)该view拖拽事件的view都要设置setOnDragListener。
  4. ViewDragHelper 拖拽的是ViewGroup的内容,针对直接子View。
  5. ViewDragHelper 拖拽不携带数据。

因为我这里需要实现携带数据的拖拽,所以需要使用 View#startDragAndDrop

// 首先看看注释怎么说:
1. 拖放操作,调用此方法时,会传递一个{@link android.view.View.DragShadowBuilder} 对象给系统,系统会去调用 {@link DragShadowBuilder#onProvideShadowMetrics(Point, Point)} 来获取拖动阴影的度量,然后再调用 {@link DragShadowBuilder#onDrawShadow(Canvas)}来绘制拖动阴影本身。

2. 一旦下系统有了拖动阴影,会开始拖动操作通过 发送 拖拽事件给你的应用程序中目前可见的所有 View 对象。

3. 这里通过调用 View 的 {@link android.view.View.OnDragListener#onDrag(View,DragEvent) onDrag()} 或者调用 {@link android.view.View#onDragEvent(DragEvent) onDragEvent()} 方法。都是通过 {@link android.view.DragEvent} 对象的{@link android.view.DragEvent#getAction()} {@link android.view.DragEvent#ACTION_DRAG_STARTED},可以使用在任何的附加的视图对象上。

这里看看参数:

startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags)
// 1\. ClipData  将该数据对象转换为拖动操作
// 2\. DragShadowBuilder 构造 拖动阴影
// 3\. myLocalState  包含本地的拖动操作数据,当传递拖拽事件给相同 activity 中的 views , 该对象可以通过 @link android.view.DragEvent#getLocalState() 使用。其他activity的 view 不能访问这个数据。是一种轻量型的机制, 从拖动view 和 target view 之间发送信息。 通过 flag 来区分 是拷贝操作还是移动操作。
// 4\. flags  控制拖拽操作类型的标志位 DRAG_FLAG_GLOBAL   DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION ...

   public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,
27338              Object myLocalState, int flags) {
27339          if (ViewDebug.DEBUG_DRAG) {
27340              Log.d(VIEW_LOG_TAG, "startDragAndDrop: data=" + data + " flags=" + flags);
27341          }
27342          if (mAttachInfo == null) {
27343              Log.w(VIEW_LOG_TAG, "startDragAndDrop called on a detached view.");
27344              return false;
27345          }
27346          if (!mAttachInfo.mViewRootImpl.mSurface.isValid()) {
27347              Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface.");
27348              return false;
27349          }
27350  
27351          if (data != null) {
                 // 1\. 离开应用程序,准备工作
27352              data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
27353          }
27354  
27355          Rect bounds = new Rect();
               // 2\. 获取屏幕上的边界大小
27356          getBoundsOnScreen(bounds, true);
27357  
27358          Point lastTouchPoint = new Point();
27359          mAttachInfo.mViewRootImpl.getLastTouchPoint(lastTouchPoint); // 最后的触摸点
              // 3\. 获取 ViewRoot 
27360          final ViewRootImpl root = mAttachInfo.mViewRootImpl;
27361  
27362          // Skip surface logic since shadows and animation are not required during the a11y drag
               // 4\. AccessibilityManager 是系统级的事件分派服务
27363          final boolean a11yEnabled = AccessibilityManager.getInstance(mContext).isEnabled();
27364          if (a11yEnabled && (flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) != 0) {
27365              try {
27366                  IBinder token = mAttachInfo.mSession.performDrag(
27367                          mAttachInfo.mWindow, flags, null,
27368                          mAttachInfo.mViewRootImpl.getLastTouchSource(),
27369                          0f, 0f, 0f, 0f, data);
27370                  if (ViewDebug.DEBUG_DRAG) {
27371                      Log.d(VIEW_LOG_TAG, "startDragAndDrop via a11y action returned " + token);
27372                  }
27373                  if (token != null) {
27374                      root.setLocalDragState(myLocalState);
27375                      mAttachInfo.mDragToken = token;
27376                      mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
27377                      setAccessibilityDragStarted(true); // 开始拖拽
27378                  }
27379                  return token != null;
27380              } catch (Exception e) {
27381                  Log.e(VIEW_LOG_TAG, "Unable to initiate a11y drag", e);
27382                  return false;
27383              }
27384          }
27385  
27386          Point shadowSize = new Point();
27387          Point shadowTouchPoint = new Point();
27388          shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
27389         // 5\. 这里处理一些 shadowSize 为负数或者0 的情况
27390          if ((shadowSize.x < 0) || (shadowSize.y < 0)
27391                  || (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
27392              throw new IllegalStateException("Drag shadow dimensions must not be negative");
27393          }
27394  
27395          // Create 1x1 surface when zero surface size is specified because SurfaceControl.Builder
27396          // does not accept zero size surface.
27397          if (shadowSize.x == 0  || shadowSize.y == 0) {
27398              if (!sAcceptZeroSizeDragShadow) {
27399                  throw new IllegalStateException("Drag shadow dimensions must be positive");
27400              }
                  // 为 0 时就置为 1 * 1
27401              shadowSize.x = 1;
27402              shadowSize.y = 1;
27403          }
27404  
27405          if (ViewDebug.DEBUG_DRAG) {
27406              Log.d(VIEW_LOG_TAG, "drag shadow: width=" + shadowSize.x + " height=" + shadowSize.y
27407                      + " shadowX=" + shadowTouchPoint.x + " shadowY=" + shadowTouchPoint.y);
27408          }
27409         // 6\. 建立 surfaceSession 
27410          final SurfaceSession session = new SurfaceSession();
27411          final SurfaceControl surfaceControl = new SurfaceControl.Builder(session)
27412                  .setName("drag surface")
27413                  .setParent(root.getSurfaceControl())
27414                  .setBufferSize(shadowSize.x, shadowSize.y)
27415                  .setFormat(PixelFormat.TRANSLUCENT)
27416                  .setCallsite("View.startDragAndDrop")
27417                  .build();
27418          final Surface surface = new Surface();
27419          surface.copyFrom(surfaceControl);
27420          IBinder token = null;
27421          try {
                 //7\. 真正的拖拽操作
27422              final Canvas canvas = isHardwareAccelerated() // 是否硬件加速?
27423                      ? surface.lockHardwareCanvas()
27424                      : surface.lockCanvas(null);
27425              try {
27426                  canvas.drawColor(0, PorterDuff.Mode.CLEAR);
27427                  shadowBuilder.onDrawShadow(canvas);
27428              } finally {
27429                  surface.unlockCanvasAndPost(canvas);
27430              }
27431  
27432              token = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, flags, surfaceControl,
27433                      root.getLastTouchSource(), lastTouchPoint.x, lastTouchPoint.y,
27434                      shadowTouchPoint.x, shadowTouchPoint.y, data);
27435              if (ViewDebug.DEBUG_DRAG) {
27436                  Log.d(VIEW_LOG_TAG, "performDrag returned " + token);
27437              }
27438              if (token != null) {
27439                  if (mAttachInfo.mDragSurface != null) {
27440                      mAttachInfo.mDragSurface.release();
27441                  }
27442                  mAttachInfo.mDragSurface = surface;
27443                  mAttachInfo.mDragToken = token;
27444                  // Cache the local state object for delivery with DragEvents
27445                  root.setLocalDragState(myLocalState);
27446                  if (a11yEnabled) {
27447                      // Set for AccessibilityEvents
27448                      mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
27449                  }
27450              }
27451              return token != null;
27452          } catch (Exception e) {
27453              Log.e(VIEW_LOG_TAG, "Unable to initiate drag", e);
27454              return false;
27455          } finally {
27456              if (token == null) {
27457                  surface.destroy();
27458              }
27459              session.kill();
27460          }
27461      }

2.实现

使用这个方法,最关键的是 ClipData(剪贴板) 的构造

2.1 ClipData

Clip Object 的三种形式:

  1. Text
  2. URI 解析数据资源
  3. Intent 支持复制应用快捷方式

注意: 剪贴板每次仅仅支持一个 clip 对象。

3. setOnDragListener()

接收拖放事件的View我们暂且称之为目标View,目标View调用setOnDragListener(),并实现其中的方法onDrag()后可以接收拖放事件的回调。

这里设置监听:

4.View.DragShadowBuilder

在拖放操作进行的时候,需要显示正在拖动的图片,View.DragShadowBuilder类提供了可以传入View的构造方法,这个View是被拖放的View,我们将通过DragShadowBuilder创建的拖动图片称为拖动阴影,这个将作为参数传入startDragAndDrop()或startDrag()方法中,如若有需要的话,还可以继承View.DragShadowBuilder类去实现自定义的效果。

参考文献:

  1. Android 拖拽理解: www.jianshu.com/p/dc90a8543…
  2. Android为View添加拖放效果:zhuanlan.zhihu.com/p/468692495
  3. clipdata : blog.csdn.net/jjj11223344…

作者:饭盒君
链接:https://juejin.cn/post/7176131749020188731

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容