Android 解决 SmartRefreshLayout 刷新

2021-09-28  本文已影响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. 效果

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

三、解决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. 效果

六、回归原生

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) 方法可以用于控制是否在刷新完成后滚动内容。

上一篇 下一篇

猜你喜欢

热点阅读