Kotlin实现ScrollView和RecyclerView的嵌套滚

此篇文章给出在Android上用Kotlin实现ScrollView和RecyclerView的嵌套滚动。

首先看一下实现后的效果:

我们需要了解的是Android已为我们实现了ScrollView的嵌套类NestedScrollView和RecyclerView的嵌套。NestedScrollView实现了NestedScrollingParent3和NestedScrollingChild3这两个支持嵌套的接口;RecyclerView实现了NestedScrollingChild2, NestedScrollingChild3这两个支持嵌套的接口。为了实现以上效果,简单的方式就是继承以上类来自定义实现。

实际上只需要自定义NestedScrollView就行了,原理涉及到事件分发,分发原理本文不涉及。

NestedScrollViewFather类的代码:

class NestedScrollViewFather : NestedScrollView {

    private val TAG = "NestedScrollViewFather"

    private val DEBUG = false

    constructor(context: Context) :super(context) {
        init()
    }
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        init()
    }

    private lateinit var topView: View

    private lateinit var contentView: ViewGroup

    private lateinit var mFlingHelper: FlingHelper

    private var totalDy = 0

    /**
     * 用于判断RecyclerView是否在fling
     */
    private var isStartingFling = false

    /**
     * 记录当前滑动的y轴加速度
     */
    private var velocityY = 0

    private fun init() {
        mFlingHelper = FlingHelper(context)
        setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
            if (isStartingFling) {
                totalDy = 0
                isStartingFling = false
            }
            if(0 == scrollY) {
                if (DEBUG) {
                    Log.d(TAG, "TOP SCROLL")
                }
            }
            if (scrollY == (getChildAt(0).measuredHeight - v.measuredHeight)) {
                if (DEBUG) {
                    Log.d(TAG, "BOTTOM SCROLL")
                }
                dispatchChildFling()
            }
            //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
            totalDy += scrollY - oldScrollY
        }
    }

    private fun dispatchChildFling() {
        if (0 != velocityY) {
            val splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY)
            if (splineFlingDistance > totalDy) {
                childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - totalDy))
            }
        }
        totalDy = 0
        velocityY = 0
    }

    private fun childFling(velY: Int) {
        getChildRecyclerView(contentView)?.fling(0, velY)
    }

    override fun fling(velocityY: Int) {
        super.fling(velocityY)
        if (velocityY <= 0) {
            this.velocityY = 0
        } else {
            isStartingFling = true
            this.velocityY = velocityY
        }
    }

    override fun onFinishInflate() {
        super.onFinishInflate()
        topView = (getChildAt(0) as ViewGroup).getChildAt(0)
        contentView = (getChildAt(0) as ViewGroup).getChildAt(1) as ViewGroup
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val lp = contentView.layoutParams!!.also {
            it.height = measuredHeight
        }
        if (DEBUG) {
            Log.e(TAG, "lp.height=${lp.height}")
        }
        contentView.layoutParams = lp
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        val toHideTop = dy > 0 && scrollY < topView.height
     val toShowTop = dy < 0 && scrollY >=0 && !target.canScrollVertically(-1)
if (hideTop || toShowTop) {
       val balance = topView.height - scrollY
       if (balance < dy) {
         scrollBy(0, balance)
         consumed[1] = balance
       } else {
scrollBy(0, dy)
consumed[1] = dy
}
}
}

companion object {
fun getChildRecyclerView(viewGroup: ViewGroup) : RecyclerView? {
for (i in 0 until viewGroup.childCount) {
val view = viewGroup.getChildAt(i)
if (view is NestLogRecyclerView) {
return view
} else if (view is ViewGroup) {
return getChildRecyclerView(view)
}
}
return null
}
}
}

列出速度与距离换算的工具类:

class FlingHelper constructor(context: Context) {

    companion object {
        private val DECELERATION_RATE = ln(.78) / ln(.9)
        private val mFlingFriction = ViewConfiguration.getScrollFriction()
    }

    private var mPhysicalCoeff = context.resources.displayMetrics.density * 160 * 386.0878f * .84f

    private fun getSplineDeceleration(i: Int): Double {
        return ln((.35 * abs(i)) / (mFlingFriction * mPhysicalCoeff))
    }

    private fun getSplineDecelerationByDistance(d: Double) : Double {
        return (DECELERATION_RATE - 1) * ln((d / (mFlingFriction * mPhysicalCoeff))) / DECELERATION_RATE
    }

    /**
     * 速度 转换成 距离
     */
    fun getSplineFlingDistance(i: Int): Double {
        return exp(getSplineDeceleration(i) * (DECELERATION_RATE / (DECELERATION_RATE - 1)) * mFlingFriction * mPhysicalCoeff)
    }

    /**
     * 距离 转换成 速度
     */
    fun getVelocityByDistance(d: Double): Int {
        return abs(exp(getSplineDecelerationByDistance(d)) * mFlingFriction * mPhysicalCoeff / .3499999940395355).toInt()
    }
}

配套的XML布局文件代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/conflict_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.tikeda.binley.conflictdemo.nested.NestedScrollViewFather
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="60dp"
                    android:textSize="24sp"
                    android:textColor="@color/black"
                    android:text="这是第一个"
                    android:gravity="center"/>

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="60dp"
                    android:textSize="24sp"
                    android:textColor="@color/black"
                    android:text="这是第二个"
                    android:gravity="center"/>

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="60dp"
                    android:textSize="24sp"
                    android:textColor="@color/black"
                    android:text="这是第三个"
                    android:gravity="center"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="46dp"
                    android:orientation="horizontal"
                    android:background="@color/purple_200">

                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:text="表1"
                        android:textColor="@color/black"
                        android:gravity="center"
                        android:layout_weight="1"/>
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:text="表2"
                        android:textColor="@color/black"
                        android:gravity="center"
                        android:layout_weight="1"/>
                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:text="表3"
                        android:textColor="@color/black"
                        android:gravity="center"
                        android:layout_weight="1"/>
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/rv_demo"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
            </LinearLayout>
        </LinearLayout>
    </com.tikeda.binley.conflictdemo.nested.NestedScrollViewFather>
</LinearLayout>

NestedScrollViewFather由于内部写死了获取topView和contentView的规则,所以仅针对给出的XML代码有效。

在gitee可以获取到完整的项目代码:https://gitee.com/binley/nested-demo

posted @ 2022-10-22 01:41  swalka`x  阅读(334)  评论(0编辑  收藏  举报