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
    }

}
相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui