Android拖拽RecyclerView项到CardView报ClassCastException问题
问题结论
该拖拽交互需求完全可实现,抛出的类型转换异常属于View落点判定的逻辑错误,和Android官方拖拽框架能力无关。
异常根因
运行时抛出androidx.recyclerview.widget.RecyclerView cannot be cast to androidx.cardview.widget.CardView的直接原因是:拖拽事件回调中获取触摸位置对应View时,返回的是当前坐标下Z轴最高的顶层子View。当拖拽释放点落在CardView布局范围内、但坐标刚好覆盖CardView内部嵌套的RecyclerView区域时,拿到的View实例是RecyclerView而非外层CardView,未做类型校验直接强转就会触发该异常。
此前实现TextView拖拽到ConstraintLayout未出现同类问题,是因为当时目标ConstraintLayout内部没有其他可响应触摸的子控件,落点下的顶层View始终是ConstraintLayout本身,不存在强转类型不匹配的情况。
正确实现方案
- 替换直接强转落点View的逻辑,实现沿View树向上递归查找目标CardView的工具方法,从当前获取到的落点View开始,不断向上遍历父View,直到匹配到目标CardView实例或遍历到根节点终止:
// 递归查找目标CardView,避免直接强转触发类型转换异常 private fun findAcceptableCardView(current: View?): CardView? { var tempView = current while (tempView != null) { if (tempView is CardView) { // 可通过tag、id进一步校验是否为允许接收拖拽的目标CardView,避免匹配到布局内其他无关CardView return tempView } val parent = tempView.parent tempView = if (parent is View) parent else null } return null }
- 在拖拽监听的所有回调(尤其是
ACTION_DROP释放逻辑分支)中,先通过上述工具方法获取合法的CardView实例,判空通过后再执行放置业务逻辑,禁止直接强转:
override fun onDrag(view: View, event: DragEvent): Boolean { when(event.action) { DragEvent.ACTION_DROP -> { // 获取当前拖拽坐标下的顶层View val topView = (view as ViewGroup).findTopChildUnder(event.x.toInt(), event.y.toInt()) val targetCard = findAcceptableCardView(topView) if (targetCard != null) { // 拖拽携带的数据从event.localState中取出,执行放置后的UI更新、数据同步逻辑 val dragData = event.localState as Statement handleDropSuccess(targetCard, dragData) } else { // 落点不在合法接收区域,执行拖拽回弹、无效提示逻辑 } return true } // ACTION_DRAG_ENTERED、ACTION_DRAG_LOCATION等其他分支的目标判定逻辑保持一致,不要直接强转 } return true }
- 若存在子控件拦截拖拽事件的情况,可直接给页面根布局设置全局OnDragListener,统一处理全页面的拖拽事件分发与落点判定,无需给每个子控件单独设置监听,从根源上避免事件拦截导致的目标查找错误。
避坑提示
不要为了规避强转错误将CardView内部的RecyclerView设置为clickable=false、focusable=false,该操作会破坏RecyclerView本身的列表滚动、条目点击、长按等基础交互,沿View树向上查找目标的方案无侵入、不影响原有控件功能,是官方推荐的落点目标判定方式。
内容的提问来源于stack exchange,提问作者Mohamed Rimshad
相关产品推荐
相关产品推荐

