一、背景
- scwang90/SmartRefreshLayout 官网,当前版本 2.0.3
- 官方 demo 下载地址
- 遇到的问题:
官方 demo.gif当 RecyclerView 上方有位置固定的控件存在时,从该控件开始触摸触发下拉刷新,刷新完毕后,RecyclerView 会自动滚动一段距离,造成视觉上的抖动。而且是每触发一次刷新,就会移动一段距离,感觉还是不太爽的。
二、复现问题
- 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'
- 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)
}
}
}
- 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>
- 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>
- 效果
-
经典刷新头
经典刷新头.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:彻底解决
- 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
}
}
- MyRefreshContentWrapper.kt
class MyRefreshContentWrapper(view: View) : RefreshContentWrapper(view) {
override fun scrollContentWhenFinished(spinner: Int): ValueAnimator.AnimatorUpdateListener? {
return null
}
}
- 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)
}
}
}
- 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>
- 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- 效果
-
经典刷新头
经典刷新头.gif -
谷歌刷新头
谷歌刷新头.gif
六、回归原生
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
- 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>
- content_pull_refresh.xml
同上
- 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)
}
}
}
-
效果
回归原生.gif -
说明
可以看到,原生 GooglePullRefreshActivity 就已经支持将 RecyclerView 包裹在容器里了,如这里用到的 LinearLayout,同时在容器里添加了其它的控件。而且,也支持在 This is banner 的位置触发刷新,并且刷新后不会出现 RecyclerView 还会莫名滚动的问题 -
局限
这样的局限是只能使用默认下下拉刷新头
七、最后
鼓掌.png八、补充
通过查看 SmartRefreshLayout 源码,发现其提供了 SmartRefreshLayout#setEnableScrollContentWhenRefreshed(boolean) 方法可以用于控制是否在刷新完成后滚动内容。
网友评论