
异常现象:
DrawerLayout实现侧滑菜单功能,当侧滑菜单出现和侧滑菜单滑动方向一直的子空间 ,就会弧线子控件滑动失效的情况.如侧滑 +子控件水平滑动 就会出现子控件滑动不生效.
一、代码逻辑分析
核心思路:
-
事件拦截控制 :
重写
onInterceptTouchEvent
,在手指按下时(ACTION_DOWN
)判断触摸点是否在需要保护的子控件范围内。若在范围内,则禁止DrawerLayout
拦截事件,让子控件优先处理。 -
子控件范围判断:
- 通过
limitIds
集合存储需要保护的子控件 ID(如横向 RecyclerView 的 ID)。 - 使用
isTouchPointInView
方法判断触摸点是否在目标子控件的区域内。
- 通过
-
** RecyclerView 滑动状态处理 **:
针对 RecyclerView,额外判断其是否 "可以向左滑动"(
canScrollHorizontally(-1)
)。若无法滑动(已滑到最左侧),则允许DrawerLayout
拦截事件,触发侧滑菜单;若可以滑动,则禁止拦截,保证 RecyclerView 正常滚动。
二、优化建议
1. 参数校验与空安全
-
非空判断 :在
isTouchPointInView
中,view
可能为null
(如 ID 未找到),需增加防御性校验。 -
可见性判断 :除了
VISIBLE
,还需考虑INVISIBLE
(占据空间但不可见,仍可能响应触摸事件)。
优化后代码:
kotlin
kotlin
fun isTouchPointInView(view: View?, x: Int, y: Int): Boolean {
if (view == null) return false
if (view.visibility != View.VISIBLE && view.visibility != View.INVISIBLE) {
return false
}
val location = IntArray(2)
view.getLocationOnScreen(location)
val left = location[0]
val top = location[1]
val right = left + view.width // 使用 view.width 替代 measuredWidth(已布局完成)
val bottom = top + view.height
return y in top..bottom && x in left..right
}
2. 滑动方向兼容性
-
横向滑动方向判断 :当前仅判断了向左滑动(
canScrollHorizontally(-1)
),但 RecyclerView 可能向右滑动(canScrollHorizontally(1)
)。应改为判断 "是否仍可滑动"(无论方向)。 -
通用化处理 :对于非 RecyclerView 的子控件(如
HorizontalScrollView
),需统一处理滑动状态。
优化后逻辑:
kotlin
kotlin
private fun isLimitIntercept(x: Int, y: Int): Boolean {
limitIds.forEach { id ->
val view = findViewById<View>(id) ?: return@forEach // 确保 View 存在
if (isTouchPointInView(view, x, y)) {
// 判断 View 是否支持横向滑动(通用逻辑)
val canScrollHorizontally = when (view) {
is RecyclerView -> view.canScrollHorizontally(-1) || view.canScrollHorizontally(1)
is HorizontalScrollView -> view.canScrollHorizontally(-1) || view.canScrollHorizontally(1)
else -> false // 其他控件默认禁止 DrawerLayout 拦截
}
return canScrollHorizontally // 可滑动时禁止拦截,否则允许
}
}
return false
}
3. 事件分发的完整性
- 当前仅在
ACTION_DOWN
中处理拦截逻辑,但事件序列(ACTION_MOVE
,ACTION_UP
)可能仍受父类拦截影响。建议在onInterceptTouchEvent
中对所有事件类型进行统一处理。
优化后代码:
kotlin
kotlin
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
val x = event.rawX.toInt()
val y = event.rawY.toInt()
// 无论事件类型,只要在保护 View 内且可滑动,就禁止拦截
if (isLimitIntercept(x, y)) {
return false // 不拦截,让子 View 处理
}
return super.onInterceptTouchEvent(event)
}
三、使用示例与扩展
1. 布局文件配置
xml
ini
<com.example.MoreLiveDrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 主内容区(包含横向 RecyclerView) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/horizontal_rv"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:orientation="horizontal" />
</LinearLayout>
<!-- 侧滑菜单 -->
<LinearLayout
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical" />
</com.example.MoreLiveDrawerLayout>
2. 代码中设置禁止拦截的 View
kotlin
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var drawerLayout: MoreLiveDrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer_layout)
drawerLayout.addLimitIntercept(R.id.horizontal_rv) // 添加 RecyclerView 的 ID
}
}
3. 扩展支持其他滑动控件
- 若子控件为
HorizontalScrollView
,无需额外代码,通用逻辑已覆盖。 - 若为自定义滑动控件,需在
isLimitIntercept
中增加对应的滑动状态判断逻辑。
四、注意事项
-
性能影响 :
遍历
limitIds
和计算触摸点范围可能带来轻微性能开销,建议避免添加过多需要保护的 View。 -
滑动冲突优先级 :
若子控件同时支持横向和纵向滑动,需根据业务需求调整判断逻辑(如优先处理横向滑动)。
-
测试场景:
-
测试 RecyclerView 滑到最左 / 右端时,侧滑菜单是否正常触发。
-
测试快速滑动时事件分发是否流畅,避免卡顿。
-
通过以上优化,代码将更健壮地处理各类滑动冲突场景,确保侧滑菜单与子控件的交互体验。如果需要进一步扩展功能(如支持纵向滑动子控件),可调整滑动方向的判断逻辑。
核心代码
kotlin
/**
*
*@Author: wkq
*
*@Time: 2025/1/21 11:07
*
*@Desc: 通过 xy 判断点击位置是否在 禁止拦截的View内 在范围内 禁止拦截 不在范围内 父类方法处理
*/
class MoreLiveDrawerLayout : DrawerLayout {
//禁止拦截的ID
val limitIds = HashSet<Int>()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
)
/**
*
* 重写事件拦截方法
* @param event MotionEvent
* @return Boolean
*/
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if (event!!.action == MotionEvent.ACTION_DOWN) {
//方向确定设置为false
val x = event.rawX.toInt()
val y = event.rawY.toInt()
//通过x,y 判断是否禁止拦截 禁止拦截 返回false
if (isLimitIntercept(x, y)) return false
}
return super.onInterceptTouchEvent(event)
}
/**
* 判断 是否禁止拦截
* @param x Int
* @param y Int
* @return Boolean
*/
private fun isLimitIntercept(x: Int, y: Int): Boolean {
limitIds.forEach {
if (isTouchPointInView(findViewById<View>(it), x, y)) {
if (findViewById<View>(it) is RecyclerView) {
// 是否 可以向左滑动
val isHorizontally = (findViewById<View>(it) as RecyclerView).canScrollHorizontally(-1)
if (!isHorizontally) {
return false
} else {
//当前触摸的位置 是否属于此View
return true
}
} else {
return true
}
}
}
return false
}
/**
* 添加禁止拦截的id
* @param id Int
*/
fun addLimitIntercept(id: Int) {
limitIds.add(id)
}
/**
* 点击区域 是否在当前 View 的区域内
* @param view View?
* @param x Int
* @param y Int
* @return Boolean
*/
fun isTouchPointInView(view: View?, x: Int, y: Int): Boolean {
if (view == null || VISIBLE != view.visibility) {
return false
}
val location = IntArray(2)
view.getLocationOnScreen(location)
val left = location[0]
val top = location[1]
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
if (y >= top && y <= bottom && x >= left && x <= right) {
return true
}
return false
}
}