2026/10/15 21:12:48

Android 7.0(Nougat)拖放事件派发缺陷的复现与修复:基于 cw-omnibus 仓库 DragDrop/Nougat 示例源码剖析

Android 7.0(Nougat)拖放事件派发缺陷的复现与修复:基于 cw-omnibus 仓库 DragDrop/Nougat 示例源码剖析 示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载本文基于开源仓库 cw-omnibusThe Busy Coders Guide to Android Development全量示例源码中的 DragDrop/Nougat 示例工程完整还原该工程在 Android 7.0API 24上暴露的拖放Drag-and-Drop事件派发缺陷系统不再把拖放事件派发给视图层级中所有注册了监听器的容器。通过逐一剖析MainActivity、BugActivity、InclusiveActivity三个入口以及DropTarget包装器源码你将掌握缺陷的触发条件、肉眼可验证的复现步骤以及一套不依赖系统回调、可在 Nougat 及更高版本上恢复旧有行为的通用修复方案。缺陷背景Android 7.0 对拖放事件派发的行为变更示例工程的 README.markdown 开宗明义这是一个用于演示Android 7.0 上拖放处理缺陷的示例应用对应《The Busy Coders Guide to Android Development》一书的拖放章节。在 Android 7.0API 24之前拖放过程中系统会把DragEvent派发给当前窗口视图层级中所有注册了View.OnDragListener的视图——无论拖拽指针是否真正悬停在它们上方。这带来一个隐含契约父容器即使不直接处于拖拽点正下方也能感知ACTION_DRAG_STARTED、ACTION_DRAG_ENTERED、ACTION_DRAG_EXITED等事件从而做出进入高亮、退出复原等 UI 反馈。从 Android 7.0 开始系统仅把拖放事件派发给拖拽点正下方、位于视图栈顶层的那一个视图。因此当拖拽指针停留在某个子视图上方时其父容器上注册的OnDragListener将收不到任何事件——这正是本示例所称的“Android 7.0 上的拖放缺陷”。示例工程同时提供了裸复现BugActivity与修复方案InclusiveActivityDropTarget用一份代码把问题、现象和补丁完整呈现出来。工程全景三种演示场景与构建配置工程位于 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.0API 24及以上的设备或模拟器上才能观察到在低版本设备上BugActivity与InclusiveActivity的表现将完全相同。复现步骤如何在设备上亲眼验证缺陷将工程导入 Android Studio或使用 Gradle wrapper 执行构建安装到一台 API 24 的设备/模拟器。进入BugActivity菜单 “Bug”。界面由两层带内边距的嵌套容器与一个铺满内层容器的缩略图组成各容器背景色分别为橙色外层、蓝色内层、紫色图片。长按顶部图片发起拖拽将其拖入下方嵌套容器区域。观察现象在 Android 7.0 上无论拖拽指针如何移动外层与内层容器都不会变为“就绪绿色”而在 Android 6.0 及以下拖拽路径上的容器会依次变绿、移出后复原。返回InclusiveActivity菜单 “Inclusive”重复同样的拖拽所有容器都会正确出现绿色高亮——缺陷已被修复。绿色高亮色值定义在 colors.xmldrop_target_ready#64dd17各容器常态色分别为outer_normal#ffa000、inner_normal#00b0ff、image_normal#d500f9。发起拖放startDragAndDrop、ClipData 与自定义拖影拖放的起点在 AbstractDragDropDemoActivity.java 的onLongClick()图片加载完成后注册OnLongClickListener用户长按即发起拖放。关键代码onLongClick第 102-120 行Uri uriPROVIDER .buildUpon() .appendEncodedPath(StreamProvider.getUriPrefix(AUTHORITY)) .appendEncodedPath(assets/FreedomTower-Morning.jpg) .build(); ClipData clipClipData.newRawUri(getString(R.string.msg_photo), uri); View.DragShadowBuilder shadownew ThumbDragShadow(); if (Build.VERSION.SDK_INTBuild.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 nameassets /把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/8iv.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_container48dp 内边距包裹内层FrameLayout inner_container48dp 内边距内层再铺满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_INTBuild.VERSION_CODES.N) { return(listener.onDrag(view, dragEvent)); } boolean resultlistener.onDrag(view, dragEvent); ViewParent parentview.getParent(); while (parent!null parent instanceof View) { View parentView(View)parent; if (views.contains(parentView)) { listener.onDrag(parentView, dragEvent); } parentparentView.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 改为左右并排orientationhorizontal、等宽layout_weight1。值得注意的是bug.xml采用 48dp 的内边距刻意制造“指针悬停在子视图上方、却越过父容器”的典型情形——父容器的可接收区域被其子视图完整覆盖这正是 Nougat 行为变更后最容易踩坑的布局形态也使缺陷在真机上极易被触发。总结DragDrop/Nougat是一个“小而完整”的缺陷驱动型示例它以最简布局复现 Android 7.0 拖放事件派发从“全体广播”到“单点投递”的行为变更并给出了一套可复用的工程化解法DropTarget沿父链补发。对于正在适配 API 24 的应用而言它提示了一个容易被忽略的兼容点——凡依赖父容器收到ACTION_DRAG_ENTERED/EXITED来驱动 UI 反馈的拖放实现都必须自行处理父链派发而不能假设系统仍会通知所有注册视图。仓库中同属该主题的姊妹示例 DragDrop/Simple应用内基础拖放、DragDrop/Legacy 与 DragDrop/PermissionsAndroid 7.0 跨应用拖放可与本文对照阅读构成从基础 API 到权限模型再到兼容性修复的完整知识链条。赞分享示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载相关推荐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示例工程教程上一篇ViTPose/MMPose 数据前处理流水线Data Pipeline完全指南从原理到自定义扩展下一篇Reflex 文档站工程指南基于 reflex_docs 包的项目结构、开发命令与页面白名单机制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考