- 示例工程
- 教程
【免费下载链接】cw-omnibus
Source code to omnibus edition of _The Busy Coder's Guide to Android Development_
本文基于开源仓库 cw-omnibus(The Busy Coder's Guide to Android Development全量示例源码)中的 DragDrop/Nougat 示例工程,完整还原该工程在 Android 7.0(API 24)上暴露的拖放(Drag-and-Drop)事件派发缺陷:系统不再把拖放事件派发给视图层级中所有注册了监听器的容器。通过逐一剖析
MainActivity、BugActivity、InclusiveActivity三个入口以及DropTarget包装器源码,你将掌握缺陷的触发条件、肉眼可验证的复现步骤,以及一套不依赖系统回调、可在 Nougat 及更高版本上恢复旧有行为的通用修复方案。
缺陷背景:Android 7.0 对拖放事件派发的行为变更
示例工程的 README.markdown 开宗明义:这是一个用于演示Android 7.0 上拖放处理缺陷的示例应用,对应《The Busy Coder's Guide to Android Development》一书的拖放章节。
在 Android 7.0(API 24)之前,拖放过程中,系统会把DragEvent派发给当前窗口视图层级中所有注册了View.OnDragListener的视图——无论拖拽指针是否真正悬停在它们上方。这带来一个隐含契约:父容器即使不直接处于拖拽点正下方,也能感知ACTION_DRAG_STARTED、ACTION_DRAG_ENTERED、ACTION_DRAG_EXITED等事件,从而做出进入高亮、退出复原等 UI 反馈。
从 Android 7.0 开始,系统仅把拖放事件派发给拖拽点正下方、位于视图栈顶层的那一个视图。因此,当拖拽指针停留在某个子视图上方时,其父容器上注册的OnDragListener将收不到任何事件——这正是本示例所称的“Android 7.0 上的拖放缺陷”。示例工程同时提供了裸复现(BugActivity)与修复方案(InclusiveActivity+DropTarget),用一份代码把问题、现象和补丁完整呈现出来。
工程全景:三种演示场景与构建配置
工程位于 DragDrop/Nougat,共包含四个 Java 类与一个辅助类,职责划分如下:
| 文件 | 角色 | 对应布局 | 说明 |
|---|---|---|---|
| AbstractDragDropDemoActivity.java | 抽象基类 | main.xml | 拖放发起、事件处理、菜单、状态保存的统一实现 |
| MainActivity.java | 基准演示 | simple.xml | 只有单个拖放目标,行为正确 |
| BugActivity.java | 缺陷复现 | bug.xml | 在嵌套容器上注册监听器,暴露 Nougat 缺陷 |
| InclusiveActivity.java | 修复演示 | bug.xml | 通过DropTarget包装器恢复全部容器的反馈 |
| DropTarget.java | 修复核心 | — | 手动沿父链向上补发拖放事件 |
三个 Activity 通过 AndroidManifest.xml 注册,其中MainActivity是启动入口,另两个通过 actions.xml 的溢出菜单切换;每个 Activity 在菜单中隐藏自己的入口项。
构建配置方面,app/build.gradle 声明compileSdkVersion 24、minSdkVersion 23、targetSdkVersion 24,并依赖 Picasso 2.5.2(图片加载)与com.commonsware.cwac:provider:0.4.2(以content://URI 暴露 assets 资源)。整体采用 Gradle 4.4 wrapper(见 gradle-wrapper.properties)与 AGP 3.1.4。需要特别说明的是:缺陷现象只在 Android 7.0(API 24)及以上的设备或模拟器上才能观察到,在低版本设备上BugActivity与InclusiveActivity的表现将完全相同。
复现步骤:如何在设备上亲眼验证缺陷
- 将工程导入 Android Studio,或使用 Gradle wrapper 执行构建,安装到一台 API 24+ 的设备/模拟器。
- 进入
BugActivity(菜单 “Bug”)。界面由两层带内边距的嵌套容器与一个铺满内层容器的缩略图组成,各容器背景色分别为橙色(外层)、蓝色(内层)、紫色(图片)。 - 长按顶部图片发起拖拽,将其拖入下方嵌套容器区域。
- 观察现象:在 Android 7.0 上,无论拖拽指针如何移动,外层与内层容器都不会变为“就绪绿色”;而在 Android 6.0 及以下,拖拽路径上的容器会依次变绿、移出后复原。
- 返回
InclusiveActivity(菜单 “Inclusive”)重复同样的拖拽,所有容器都会正确出现绿色高亮——缺陷已被修复。
绿色高亮色值定义在 colors.xml:drop_target_ready=#64dd17,各容器常态色分别为outer_normal=#ffa000、inner_normal=#00b0ff、image_normal=#d500f9。
发起拖放:startDragAndDrop、ClipData 与自定义拖影
拖放的起点在 AbstractDragDropDemoActivity.java 的onLongClick():图片加载完成后注册OnLongClickListener,用户长按即发起拖放。
关键代码(onLongClick,第 102-120 行):
Uri uri=PROVIDER .buildUpon() .appendEncodedPath(StreamProvider.getUriPrefix(AUTHORITY)) .appendEncodedPath("assets/FreedomTower-Morning.jpg") .build(); ClipData clip=ClipData.newRawUri(getString(R.string.msg_photo), uri); View.DragShadowBuilder shadow=new ThumbDragShadow(); if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) { iv.startDragAndDrop(clip, shadow, Boolean.TRUE, 0); } else { iv.startDrag(clip, shadow, Boolean.TRUE, 0); }这里体现了三个要点:
- 拖放载荷:
ClipData.newRawUri()构造携带text/uri-listMIME 类型的 URI 载荷,载荷指向一个content://URI。该 URI 由 Manifest 中注册的StreamProvider提供(AndroidManifest.xml 中authorities="${applicationId}.provider"),其路径映射配置在 provider.xml,通过<asset name="assets" />把assets/FreedomTower-Morning.jpg(即工程 assets 下的真实图片资源)暴露给拖放接收方。 - 版本分支:Android 7.0 新增
View.startDragAndDrop(),支持跨 Activity、跨应用的拖放,而旧 APIstartDrag()在 N 上已被标记废弃;示例用Build.VERSION.SDK_INT做运行时兼容分支。第三个参数Boolean.TRUE是随拖拽携带的localState,用于拖放双方在同一个进程内传递对象。 - 自定义拖影:内部类
ThumbDragShadow(第 225-237 行)继承View.DragShadowBuilder,把阴影缩小到原图 1/8(iv.getWidth()/8、iv.getHeight()/8),触点居中,并把图片本身绘制成阴影外观。
接收拖放:onDrag 回调与容器高亮反馈
基类同时实现了View.OnDragListener,其 onDrag()(第 180-223 行) 是接收端的事件处理器:
ACTION_DRAG_STARTED:校验localState非空且 ClipDescription 包含MIME_TYPE_TEXT_URILIST,不满足则返回false拒绝接收本次拖放;ACTION_DRAG_ENTERED:把容器背景置为就绪绿色;ACTION_DRAG_EXITED/ACTION_DRAG_ENDED:恢复原色;ACTION_DROP:若目标视图是ImageView,在 API 24+ 上先调用requestDragAndDropPermissions(event),再从event.getClipData().getItemAt(0)取出 URI,交由showThumbnail()用 Picasso 渲染为缩略图。
其中 requestDragAndDropPermissions(第 209-211 行) 是 Nougat 引入的权限 API:当拖放载荷来自其他应用的内容 URI 时,接收方必须在丢弃事件回调内请求临时读取权限,否则无法读取该 URI——这正是工程姊妹示例 DragDrop/Permissions(跨应用拖放)的核心知识点,在本示例中以一行代码先行埋下伏笔。
缺陷复现:BugActivity 的裸监听器
BugActivity.java 是缺陷的最小复现单元。它沿用 bug.xml 的嵌套结构——外层FrameLayout outer_container(48dp 内边距)包裹内层FrameLayout inner_container(48dp 内边距),内层再铺满ImageView thumbnail_large——并在三个视图上逐一注册监听器:
image=(ImageView)findViewById(R.id.thumbnail_large); image.setOnDragListener(this); findViewById(R.id.outer_container).setOnDragListener(this); findViewById(R.id.inner_container).setOnDragListener(this);这套写法在 Android 6.0 及以下运行完全正常:拖拽指针经过哪一层,哪一层就触发ACTION_DRAG_ENTERED并变绿。但在 Android 7.0 上,事件只派发给拖拽点正下方最顶层的视图——当指针停留在铺满内层的ImageView上方时,两个容器收不到任何事件,高亮反馈就此失效。这就是 README 所指的缺陷本体。
修复方案:DropTarget 包装器的父链补发机制
InclusiveActivity.java 换用 DropTarget.java 注册监听器:
new DropTarget() .on(image, findViewById(R.id.outer_container), findViewById(R.id.inner_container)) .to(this);DropTarget是一个可链式调用的OnDragListener包装器,其核心逻辑在 onDrag()(第 41-60 行):
if (Build.VERSION.SDK_INT<Build.VERSION_CODES.N) { return(listener.onDrag(view, dragEvent)); } boolean result=listener.onDrag(view, dragEvent); ViewParent parent=view.getParent(); while (parent!=null && parent instanceof View) { View parentView=(View)parent; if (views.contains(parentView)) { listener.onDrag(parentView, dragEvent); } parent=parentView.getParent(); }其设计意图非常清晰:
- 低版本直通:在 Android 6.0 及以下,系统本身就会把事件派发给所有注册视图,因此直接透传给真正的监听器,保持旧行为零开销;
- Nougat 及以上补发:先让最顶层视图按正常流程处理事件(返回值
result以此为准),随后沿getParent()链向上遍历,凡是被.on(...)注册过的祖先视图,都手动补发一次相同的DragEvent,从而把“系统只通知一个视图”收缩为“通知整条注册链上的所有视图”。
从源码结构可以推断,views.contains(parentView)的注册表设计让补发范围精确限定在用户显式注册的容器集合内,避免误伤层级中无关的父视图;result保留顶层视图的处理结论,则保证了事件消费语义(如拒绝拖放)不被祖先的返回值覆盖。这一包装器用约 60 行代码,把 Android 7.0 被系统削掉的“广播式派发”能力在应用层完整找回,是理解事件派发机制与“面向系统行为变更做兼容层”的极佳范本。
布局与横竖屏适配
接收区由 main.xml 定义:上方为拖放源图片(asset,纵向布局中占一半高度),下方为FrameLayout drop_frame,在onCreate()中通过getDropFrameContentId()动态填充三种场景各自的布局(第 70-73 行)。横屏时 layout-land/main.xml 改为左右并排(orientation="horizontal"、等宽layout_weight=1)。
值得注意的是,bug.xml采用 48dp 的内边距刻意制造“指针悬停在子视图上方、却越过父容器”的典型情形——父容器的可接收区域被其子视图完整覆盖,这正是 Nougat 行为变更后最容易踩坑的布局形态,也使缺陷在真机上极易被触发。
总结
DragDrop/Nougat是一个“小而完整”的缺陷驱动型示例:它以最简布局复现 Android 7.0 拖放事件派发从“全体广播”到“单点投递”的行为变更,并给出了一套可复用的工程化解法(DropTarget沿父链补发)。对于正在适配 API 24+ 的应用而言,它提示了一个容易被忽略的兼容点——凡依赖父容器收到ACTION_DRAG_ENTERED/EXITED来驱动 UI 反馈的拖放实现,都必须自行处理父链派发,而不能假设系统仍会通知所有注册视图。仓库中同属该主题的姊妹示例 DragDrop/Simple(应用内基础拖放)、DragDrop/Legacy 与 DragDrop/Permissions(Android 7.0+ 跨应用拖放)可与本文对照阅读,构成从基础 API 到权限模型再到兼容性修复的完整知识链条。
- 示例工程
- 教程
【免费下载链接】cw-omnibus
Source code to omnibus edition of _The Busy Coder's Guide to Android Development_
相关推荐
cw-omnibus 跨应用拖放实战:基于 Android 7.0+ 的 Drag-and-Drop 双端实现解析
cw omnibus 跨应用拖放实战:基于 Android 7.0+ 的 Drag and Drop 双端实现解析 本篇文章围绕开源仓库 cw omnibus
示例工程教程Android 三栏联动界面(Three-Pane UI)实现剖析:基于 cw-omnibus 的 ThreePane 动画示例
Android 三栏联动界面(Three Pane UI)实现剖析:基于 cw omnibus 的 ThreePane 动画示例 本文以 cw omnibus
示例工程教程深入 Android 7.0 的 AlarmManager.OnAlarmListener 回调式闹钟:cw-omnibus Listener 示例全解
深入 Android 7.0 的 AlarmManager.OnAlarmListener 回调式闹钟:cw omnibus Listener 示例全解 cw
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考