美文网首页
Android 解决 SmartRefreshLayout 刷新

Android 解决 SmartRefreshLayout 刷新

作者: 雁过留声_泪落无痕 | 来源:发表于2021-09-28 15:00 被阅读0次

    一、背景

    1. scwang90/SmartRefreshLayout 官网,当前版本 2.0.3
    2. 官方 demo 下载地址
    3. 遇到的问题:

    当 RecyclerView 上方有位置固定的控件存在时,从该控件开始触摸触发下拉刷新,刷新完毕后,RecyclerView 会自动滚动一段距离,造成视觉上的抖动。而且是每触发一次刷新,就会移动一段距离,感觉还是不太爽的。

    官方 demo.gif

    二、复现问题

    1. build.gradle
    // 万能适配器
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.2'
    
    // 下拉刷新
    implementation  'com.scwang.smart:refresh-layout-kernel:2.0.3'
    // 谷歌刷新头
    implementation  'com.scwang.smart:refresh-header-material:2.0.3'
    // 经典刷新头
    implementation  'com.scwang.smart:refresh-header-classics:2.0.3'
    
    1. OfficialPullRefreshActivity.kt
    class OfficialPullRefreshActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_official_pull_refresh)
    
            val list = ArrayList<String>()
            for (i in 0..25) {
                val char = 'a' + i
                list.add(char.toString())
            }
    
            val smartRefreshLayout = findViewById<SmartRefreshLayout>(R.id.refresh_layout)
            smartRefreshLayout.setOnRefreshListener {
                it.finishRefresh(2000)
            }
    
            val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
            recyclerView.adapter = MyAdapter(list).apply {
                setOnItemClickListener { adapter, view, position ->
                    ToastUtils.showShort("$position clicked.")
                }
            }
            recyclerView.layoutManager = LinearLayoutManager(this)
        }
    
        private class MyAdapter(list: MutableList<String>) :
            BaseQuickAdapter<String, BaseViewHolder>(android.R.layout.simple_list_item_1, list) {
            override fun convert(holder: BaseViewHolder, item: String) {
                holder.setText(android.R.id.text1, item)
            }
        }
    
    }
    
    1. activity_official_pull_refresh.xml
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.scwang.smart.refresh.layout.SmartRefreshLayout
            android:id="@+id/refresh_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <com.scwang.smart.refresh.header.ClassicsHeader
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
    
            <include layout="@layout/content_pull_refresh" />
        </com.scwang.smart.refresh.layout.SmartRefreshLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    1. content_pull_refresh.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/recycler_view_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="120dp"
            android:background="#7fff0000"
            android:gravity="center"
            android:text="This is banner"
            android:textSize="40dp" />
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:listitem="@android:layout/simple_list_item_1" />
    </LinearLayout>
    
    1. 效果
    • 经典刷新头


      经典刷新头.gif
    • 谷歌刷新头


      谷歌刷新头.gif

    可以看到,该问题确实是存在的。

    三、解决1:简单解决

    每次刷新后主动滚动到顶部位置,否则每下拉刷新一次会向上滚动一点,直到滚动到顶部不能再滚动

    val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
    recyclerView.adapter = MyAdapter(list).apply {
        setOnItemClickListener { adapter, view, position ->
            ToastUtils.showShort("$position clicked.")
        }
    
        recyclerView.post {
            recyclerView.scrollToPosition(0)
        }
    }
    recyclerView.layoutManager = LinearLayoutManager(this)
    

    四、解决2:不允许从固定控件的位置触摸时触发刷新

    用到了官方提供的 setScrollBoundaryDecider 接口,用于指定一个自定义边界来界定是否触发刷新

    smartRefreshLayout.setScrollBoundaryDecider(object : SimpleBoundaryDecider() {
        override fun canRefresh(content: View): Boolean {
            return !recyclerView.canScrollVertically(-1)
        }
    
        override fun canLoadMore(content: View): Boolean {
            return super.canLoadMore(content)
        }
    })
    

    效果:


    2.gif

    可以看到,如果 RecyclerView 还没有滚动到顶部(第 0 个 item 没有完全显示出来)则不允许从 This is banner 的位置开始触发刷新,只能先把 RecyclerView 完全滚动到顶部后才能触发下拉刷新。

    五、解决3:彻底解决

    1. MySmartRefreshLayout.kt
    class MySmartRefreshLayout : SmartRefreshLayout {
    
        constructor (context: Context) : this(context, null)
    
        constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    
        override fun setRefreshContent(content: View): RefreshLayout {
            super.setRefreshContent(content)
            mRefreshContent = MyRefreshContentWrapper(content)
    
            return this
        }
    
    }
    
    1. MyRefreshContentWrapper.kt
    class MyRefreshContentWrapper(view: View) : RefreshContentWrapper(view) {
    
        override fun scrollContentWhenFinished(spinner: Int): ValueAnimator.AnimatorUpdateListener? {
            return null
        }
    
    }
    
    1. MyPullRefreshActivity.kt
    class MyPullRefreshActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_my_pull_refresh)
    
            val list = ArrayList<String>()
            for (i in 0..25) {
                val char = 'a' + i
                list.add(char.toString())
            }
    
            val smartRefreshLayout = findViewById<SmartRefreshLayout>(R.id.refresh_layout)
            smartRefreshLayout.setOnRefreshListener {
                it.finishRefresh(2000)
            }
    
            layoutInflater.inflate(R.layout.content_pull_refresh, null).also {
                smartRefreshLayout.setRefreshContent(it)
            }
    
            val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
            recyclerView.adapter = MyAdapter(list).apply {
                setOnItemClickListener { adapter, view, position ->
                    ToastUtils.showShort("$position clicked.")
                }
            }
            recyclerView.layoutManager = LinearLayoutManager(this)
        }
    
        private class MyAdapter(list: MutableList<String>) :
            BaseQuickAdapter<String, BaseViewHolder>(android.R.layout.simple_list_item_1, list) {
            override fun convert(holder: BaseViewHolder, item: String) {
                holder.setText(android.R.id.text1, item)
            }
        }
    
    }
    
    1. activity_my_pull_refresh.xml
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <top.gangshanghua.xiaobo.helloworld.ui.refresh.MySmartRefreshLayout
            android:id="@+id/refresh_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <com.scwang.smart.refresh.header.ClassicsHeader
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
    
            <!-- 注意这里没有 include-->
            <!-- <include layout="@layout/content_pull_refresh" /> -->
    
        </top.gangshanghua.xiaobo.helloworld.ui.refresh.MySmartRefreshLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    1. content_pull_refresh.xml
    同上
    

    通过 MySmartRefreshLayout#setRefreshContent(View) 接口来传递真正的 content。先调用父类的 super.setRefreshContent(content),父类的该方法里会给 mRefreshContent 赋值,默认是 RefreshContentWrapper 的实例,所以在 MySmartRefreshLayout 中在对 mRefreshContent 进行重新赋值为 MyRefreshContentWrapper 的实例,达到偷梁换柱的效果!

    所以会运行到 MyRefreshContentWrapper#scrollContentWhenFinished(int) 来处理结束刷新后 content 的滑动问题,在我们这种情况下是不需要滑动 content 的,所以直接返回 null 即可。如果不返回 null 会在 RefreshContentWrapper#onAnimationUpdate(ValueAnimator) 调用 RecyclerView#scrollBy(int, int) 完成真正的滚动。

    RefreshContentWrapper#onAnimationUpdate(ValueAnimator).png
    1. 效果
    • 经典刷新头


      经典刷新头.gif
    • 谷歌刷新头


      谷歌刷新头.gif

    六、回归原生

    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"

    1. activity_google_pull_refresh.xml
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/swipe_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <include layout="@layout/content_pull_refresh" />
    
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    1. content_pull_refresh.xml
    同上
    
    1. GooglePullRefreshActivity.kt
    class GooglePullRefreshActivity : AppCompatActivity() {
    
        private lateinit var mRecyclerView: RecyclerView
        private lateinit var mSwipeRefreshLayout: SwipeRefreshLayout
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_google_pull_refresh)
    
            mRecyclerView = findViewById(R.id.recycler_view)
            mRecyclerView.layoutManager = LinearLayoutManager(this)
    
            mSwipeRefreshLayout = findViewById(R.id.swipe_refresh)
            mSwipeRefreshLayout.apply {
                setOnRefreshListener {
                    postDelayed(2000L) {
                        isRefreshing = false
                    }
                }
            }
    
            initData()
        }
    
        private fun initData() {
            val list = ArrayList<String>()
            for (i in 0..25) {
                val char = 'a' + i
                list.add(char.toString())
            }
    
            mRecyclerView.adapter = MyAdapter(list)
        }
    
        private class MyAdapter(list: MutableList<String>) :
            BaseQuickAdapter<String, BaseViewHolder>(android.R.layout.simple_list_item_1, list) {
            override fun convert(holder: BaseViewHolder, item: String) {
                holder.setText(android.R.id.text1, item)
            }
        }
    
    }
    
    1. 效果


      回归原生.gif
    2. 说明
      可以看到,原生 GooglePullRefreshActivity 就已经支持将 RecyclerView 包裹在容器里了,如这里用到的 LinearLayout,同时在容器里添加了其它的控件。而且,也支持在 This is banner 的位置触发刷新,并且刷新后不会出现 RecyclerView 还会莫名滚动的问题

    3. 局限
      这样的局限是只能使用默认下下拉刷新头

    七、最后

    鼓掌.png

    八、补充

    通过查看 SmartRefreshLayout 源码,发现其提供了 SmartRefreshLayout#setEnableScrollContentWhenRefreshed(boolean) 方法可以用于控制是否在刷新完成后滚动内容。

    相关文章

      网友评论

          本文标题:Android 解决 SmartRefreshLayout 刷新

          本文链接:https://www.haomeiwen.com/subject/pnxrnltx.html