DrawerLayout 滑动冲突

异常现象:

DrawerLayout实现侧滑菜单功能,当侧滑菜单出现和侧滑菜单滑动方向一直的子空间 ,就会弧线子控件滑动失效的情况.如侧滑 +子控件水平滑动 就会出现子控件滑动不生效.

一、代码逻辑分析

核心思路:

  1. 事件拦截控制

    重写 onInterceptTouchEvent,在手指按下时(ACTION_DOWN)判断触摸点是否在需要保护的子控件范围内。若在范围内,则禁止 DrawerLayout 拦截事件,让子控件优先处理。

  2. 子控件范围判断

    • 通过 limitIds 集合存储需要保护的子控件 ID(如横向 RecyclerView 的 ID)。
    • 使用 isTouchPointInView 方法判断触摸点是否在目标子控件的区域内。
  3. ** 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 中增加对应的滑动状态判断逻辑。

四、注意事项

  1. 性能影响

    遍历 limitIds 和计算触摸点范围可能带来轻微性能开销,建议避免添加过多需要保护的 View。

  2. 滑动冲突优先级

    若子控件同时支持横向和纵向滑动,需根据业务需求调整判断逻辑(如优先处理横向滑动)。

  3. 测试场景

    • 测试 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
    }

}
相关推荐
dog shit2 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
科技道人3 小时前
Android15 launcher3
android·launcher3·android15·hotseat
CYRUS_STUDIO7 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向
Shujie_L9 小时前
【Android基础回顾】四:ServiceManager
android
Think Spatial 空间思维10 小时前
【实施指南】Android客户端HTTPS双向认证实施指南
android·网络协议·https·ssl
louisgeek11 小时前
Git 使用 SSH 连接
android
二流小码农11 小时前
鸿蒙开发:实现一个标题栏吸顶
android·ios·harmonyos
八月林城12 小时前
echarts在uniapp中使用安卓真机运行时无法显示的问题
android·uni-app·echarts
雨白12 小时前
搞懂 Fragment 的生命周期
android
casual_clover12 小时前
Android 之 kotlin语言学习笔记三(Kotlin-Java 互操作)
android·java·kotlin