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 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东、美团首页效果Tab页滚动到顶部时自动吸附 -> 正文阅读

[移动开发]Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东、美团首页效果Tab页滚动到顶部时自动吸附

先上效果图(TabLayout滚动到顶部时自动吸附):

先看下布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".nestedscroll.NestedScrollActivity">

    <com.zs.test.nestedscroll.ZSNestedScrollView
        android:id="@+id/activity_nested_scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

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

            <View
                android:id="@+id/activity_nested_view"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="@drawable/home_top_banner1" />

            <LinearLayout
                android:id="@+id/activity_nested_ll"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <com.google.android.material.tabs.TabLayout
                    android:id="@+id/activity_nested_tb"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ff7f00"
                    app:tabSelectedTextColor="#0000ff"
                    app:tabTextColor="#00ff00" />

                <androidx.viewpager.widget.ViewPager
                    android:id="@+id/activity_nested_viewpager"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />

            </LinearLayout>
        </LinearLayout>
    </com.zs.test.nestedscroll.ZSNestedScrollView>

</LinearLayout>

其中?com.zs.test.nestedscroll.ZSNestedScrollView 是实现此效果的关键所在,其他都是常规的布局代码

package com.zs.test.nestedscroll

import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import com.zs.test.util.log

@RequiresApi(Build.VERSION_CODES.M)
class ZSNestedScrollView : NestedScrollView {
    /**
     * 顶部的view  id = activity_nested_view
     */
    private var topView: View? = null

    /**
     * 包裹TabLayout+RecyclerView 的 LinearLayout id = activity_nested_ll
     */
    private var contentView: ViewGroup? = null

    /**
     * 处理惯性滑动的工具类
     */
    private var mFlingHelper: FlingHelper? = null

    /**
     * 记录当前自身已经滑动的距离
     */
    var totalDy = 0

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

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

    constructor(context: Context) : super(context) {
        init()
    }

    /**
     * 必须的构造函数,系统会通过反射来调用此构造方法完成view的创建
     */
    constructor(context: Context, attr: AttributeSet) : super(context, attr) {
        init()
    }

    constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
        context,
        attr,
        defZStyle
    ) {
        init()
    }

    private fun init() {
        mFlingHelper = FlingHelper(context)
        //添加滚动监听 v就是当前NestedScrollLayout
        setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
            if (isRecyclerViewStartFling) {
                totalDy = 0
                isRecyclerViewStartFling = false
            }
            //scrollY 是 当前向上滑动了多少 0 就是一点没滑动 就是在顶部状态
            if (scrollY == 0) {
                log("TOP SCROLL")
                // refreshLayout.setEnabled(true);
            }
            //v.measuredHeight() 就是屏幕高度

            if (scrollY == topView!!.measuredHeight) {
                log("BOTTOM SCROLL v.getMeasuredHeight() = " + v.measuredHeight)
                //滑动到底部以后 还有惯性让子类接着来滑动
                dispatchChildFling()
            }
            //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
            totalDy += scrollY - oldScrollY
        }
    }

    private fun dispatchChildFling() {
        if (velocityY != 0) {
            //将惯性的加速度转换为具体的距离
            val splineFlingDistance = mFlingHelper!!.getSplineFlingDistance(velocityY)
            //举例解释:假设用力滑动一下 能滑动100个单位的距离,totalDy是外层ZSNestedScrollView已经滑动的距离
            // 假设是50 那么还有50 咋办呢 ,要让子布局(RecycleView)来滑动剩下的50
            if (splineFlingDistance > totalDy) {
                childFling(
                    mFlingHelper!!.getVelocityByDistance(
                        splineFlingDistance - java.lang.Double.valueOf(
                            totalDy.toDouble()
                        )
                    )
                )
            }
        }
        //重置变量
        totalDy = 0
        velocityY = 0
    }

    private fun childFling(velY: Int) {
        if (contentView != null) {
            val childRecyclerView: RecyclerView? = getChildRecyclerView(contentView!!)
            childRecyclerView?.fling(0, velY)
        }
    }

    private fun getChildRecyclerView(viewGroup: ViewGroup): RecyclerView? {
        for (i in 0 until viewGroup.childCount) {
            val view = viewGroup.getChildAt(i)
            if (view is RecyclerView && view.javaClass == RecyclerView::class.java) {
                return viewGroup.getChildAt(i) as RecyclerView
            } else if (viewGroup.getChildAt(i) is ViewGroup) {
                val childRecyclerView: ViewGroup? =
                    getChildRecyclerView(viewGroup.getChildAt(i) as ViewGroup)
                if (childRecyclerView is RecyclerView) {
                    return childRecyclerView
                }
            }
            continue
        }
        return null
    }

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

    /**
     * view 加载完成后执行
     */
    override fun onFinishInflate() {
        super.onFinishInflate()
        topView = (getChildAt(0) as ViewGroup).getChildAt(0)
        contentView = (getChildAt(0) as ViewGroup).getChildAt(1) as ViewGroup
    }

    /**
     * 参数	解释
     * target	触发嵌套滑动的 view
     * dx	表示 view 本次 x 方向的滚动的总距离,单位:像素
     * dy	表示 view 本次 y 方向的滚动的总距离,单位:像素
     * consumed	输出:表示父布局消费的水平和垂直距离。
     * type	触发滑动事件的类型:其值有
     * ViewCompat. TYPE_TOUCH
     * ViewCompat. TYPE_NON_TOUCH
     *
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
//        log(getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight())
//        log( "dx="+ dx + " dy=" + dy)
        // 如果能继续向上滑动,就滑动
        val canScroll = dy > 0 && scrollY < topView!!.measuredHeight
        if (canScroll) {
            scrollBy(0, dy)
            consumed[1] = dy
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val lp = contentView!!.layoutParams
        // getMeasuredHeight() 得到的就是屏幕高度 当前ZSNestedScrollView高度是MatchParent
        log("onMeasure getMeasuredHeight() = $measuredHeight")
        lp.height = measuredHeight
        // 调整contentView的高度为屏幕高度,这样ZSNestedScrollView总高度就是屏幕高度+topView的高度
        // 因此往上滑动 滑完topView后,TabLayout就卡在顶部了,因为ZSNestedScrollView滑不动了啊,就这么高
        // 接着在滑就是其内部的RecyclerView去滑动了
        contentView!!.layoutParams = lp
    }

}

关键逻辑都加了注释,这里就简单总结下实现思路吧:

第一步:在RecyclerView区域滚动时 要实现能够整体滚动,而不是单独的RecyclerView自身在滚动,这个我们借助系统的力量就可以了,我们写一个ZSNestedScrollView 继承NestedScrollView即可。

第二步:实现TabLayout滚动到顶部时自动吸附在顶部保持不动,这里我们做一个巧妙的的设置:我们把TabLayout + ViewPager的总高度设置为屏幕的高度,这样ZSNestedScrollView总高度就是屏幕高度+topView的高度 ,因此往上滑动 滑完topView后,TabLayout就会呆在顶部了不动了,此时在让RecyclerView内部滑动就可以了,如下图所示:

?第三步:第二步高度固定后,在RecyclerView区域滚动时,将不能再整体滚动,于是我们要手动处理,当在RecyclerView区域往上滑时,主动判断父布局ZSNestedScrollView是否可以滑动,是的话,让父类滑动:

?这里dy表示view本次y方式的股东的总距离,单位像素,scrollY是ZSNestedScrollView Y轴上已经滑动的距离,小于topView的高度说明还有继续往上滑的空间

第四步:处理惯性滑动,用户在RecyclerView区域用力往上一滑的时候,TabLayout吸附到顶部以后,应该有继续滑动的效果来,这样更顺畅,体验更好,京东、美图都是这样实现的。

(1)对ZSNestedScrollView 滚动进行监听,滚动到底部时也就是TabLayout卡在顶部的时候,进行惯性处理

?(2)如果速度不为0(有惯性需要处理)并且ZSNestedScrollView没有将惯性处理完,则子类自己处理

(3)子类找到RecyclerView,调用其fling方法

?上面涉及到一个工具类FlingHelper 代码如下:

package com.zs.test.nestedscroll

import android.content.Context
import com.zs.test.nestedscroll.FlingHelper
import android.view.ViewConfiguration

class FlingHelper(context: Context) {
    private fun getSplineDeceleration(i: Int): Double {
        return Math.log(
            (0.35f * Math.abs(i)
                .toFloat() / (mFlingFriction * mPhysicalCoeff)).toDouble()
        )
    }

    private fun getSplineDecelerationByDistance(d: Double): Double {
        return (DECELERATION_RATE.toDouble() - 1.0) * Math.log(d / (mFlingFriction * mPhysicalCoeff).toDouble()) / DECELERATION_RATE.toDouble()
    }

    fun getSplineFlingDistance(i: Int): Double {
        return Math.exp(getSplineDeceleration(i) * (DECELERATION_RATE.toDouble() / (DECELERATION_RATE.toDouble() - 1.0))) * (mFlingFriction * mPhysicalCoeff).toDouble()
    }

    fun getVelocityByDistance(d: Double): Int {
        return Math.abs((Math.exp(getSplineDecelerationByDistance(d)) * mFlingFriction.toDouble() * mPhysicalCoeff.toDouble() / 0.3499999940395355).toInt())
    }

    companion object {
        private val DECELERATION_RATE = (Math.log(0.78) / Math.log(0.9)).toFloat()
        private val mFlingFriction = ViewConfiguration.getScrollFriction()
        private var mPhysicalCoeff: Float = 0.0f
    }

    init {
        mPhysicalCoeff = context.resources.displayMetrics.density * 160.0f * 386.0878f * 0.84f
    }
}

?以上就是所有核心代码和逻辑。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-02-07 13:50:15  更:2022-02-07 13:50:26 
 
开发: 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 14:45:58-

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