IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> ViewPager2滑动冲突解决方案 -> 正文阅读

[移动开发]ViewPager2滑动冲突解决方案

简介

滑动冲突,简单来说就是两个可滑动的组件嵌套在一起,其中一个组件拦截了滑动事件,导致另一个组件无法滑动或难以滑动的情况。

常见的情况有ViewPager2嵌套ViewPager2,ViewPager2嵌套RecyclerView。

官方解决方案

谷歌官方提供了一种解决同方向滑动冲突的方案 ——NestedScrollableHost.kt

将这个布局放在ViewPager2和RecyclerView之间即可解决滑动冲突,类似这样:

<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <com.miui.gallery.widget.tsd.NestedScrollableHost
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            
        </androidx.recyclerview.widget.RecyclerView>
        
    </com.miui.gallery.widget.tsd.NestedScrollableHost>
    
</androidx.viewpager2.widget.ViewPager2>

简单分析一下NestedScrollableHost是如何处理滑动冲突的:

  1. 获取父布局ViewPager2,ViewPager2可以不是其直接父布局。
  2. 获取子布局RecyclerView,RecyclerView必须是其第一个子View。
  3. 处理滑动事件,判断滑动方向:
    • 不是RecyclerView的滑动方向,不做任何处理,Return。
    • 是RecyclerView的滑动方向,判断RecyclerView能否继续滑动,若可以,则禁止ViewPager2拦截此事件;反之,允许ViewPager2拦截此事件。

原理十分简单,无非就是:如果RecyclerView仍能处理事件,则由它处理,反之由ViewPager2处理。

从解决方案来看,该方案提供了一种不错的思路,但并不完全适用于各种开发情景,对于我所做的需求而言,仍存在以下两个痛点

  1. RecyclerView必须是NestedScrollableHost的第一个子View。
  2. 只解决同方向的滑动冲突。

那么,就针对这两个问题,对NestedScrollableHost进行一个简单的改造吧。

完善解决方案

提供遍历子View的方法

针对痛点1,我希望NestedScrollableHost无论下层布局如何,都能找到其中的RecyclerView,那么仅需提供一个遍历子View的方法即可。

public static View getChildRecyclerView(View view) {
        ArrayList<View> unvisited = new ArrayList<>();
        unvisited.add(view);

        while (!unvisited.isEmpty()) {
            View child = unvisited.remove(0);
            if (child instanceof RecyclerView) {
                return child;
            }
            if (!(child instanceof ViewGroup)) {
                continue;
            }
            ViewGroup viewGroup = (ViewGroup) child;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                unvisited.add(viewGroup.getChildAt(i));
            }
        }
        return null;
    }

针对不同滑动方向的处理

针对痛点2,需要为NestedScrollableHost额外添加一条属性,以此属性来判断当前的ViewPager2与RecyclerView的滑动方向是否一致,并在代码中修改相关的逻辑。

<declare-styleable name="NestedScrollableHost">
    <attr name="sameDirectionWithParent" format="boolean" />
</declare-styleable>
<com.mone.NestedScrollableHost xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:sameDirectionWithParent="false">
    
</com.mone.NestedScrollableHost>

完整代码

虽然进行了一些调整,但整体的思路不变:如果RecyclerView仍能处理事件,则由它处理,反之由ViewPager2处理。

class NestedScrollableHost : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        val a = context.obtainStyledAttributes(attrs, R.styleable.NestedScrollableHost)
        isChildHasSameDirection =
            a.getBoolean(R.styleable.NestedScrollableHost_sameDirectionWithParent, false)
        a.recycle()
    }

    private var isChildHasSameDirection = true
    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = ViewUtils.getChildRecyclerView(this)

    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        val orientation = parentViewPager?.orientation ?: return

        // Early return if child can't scroll in its direction
        val childOrientation = if (isChildHasSameDirection) orientation else orientation xor 1
        if (!canChildScroll(childOrientation, -1f) && !canChildScroll(childOrientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDx > scaledDy)) {
                    // Gesture direction is same, query child if movement in that direction is possible
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        // Child can scroll, disallow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        // Child cannot scroll, allow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                } else {
                    // Gesture direction is different, allow all parents to intercept
                    parent.requestDisallowInterceptTouchEvent(true)
                }
            }
        }
    }
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 21:02:21  更:2022-03-21 21:07:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 19:45:15-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码