Viewpager2+Fragment实现无限循环滑动

在一次需求中需要实现播放卡片无限滚动的效果,现有代码使用的是Viewpager2+Fragment的框架,但官方的Viewpager2并没有提供无限滑动的API,所以这里摸索了一套方案记录如下。

在动手coding之前在网上调研了相关效果的实现方案,大致有两种思路:一种是在FragmentStateAdapter#getItemCount()中返回Integer.MAX_VALUE通过不断填充内容使得滑不动尽头达到无限滑动的效果;另一种则是在列表前后各加一个元素,再配合自动切换实现无限滑动。需求实现中采用的是第二种思路。

改造数据源

前后各加一个元素的具体实现,是在构造Adapter数据的时候分别将数据列表的最后一个元素和第一个元素拷贝添加至数据列表的头部和尾部。即数据列表的内容由[A, B, C] 变为 [C, A, B, C, A]。

Viewpager2自动切换

注册Viewpager2页面状态改变的监听器,在回调中判断如果当前处于第一张卡片(即C`处)则自动切换到倒数第二张卡片(即C处),如果当前当前处于最后一张卡片则自动切换到第二张,通过这样一种方式可以让用户无限滑动卡片。

相关核心代码如下:

|

1
// 改造数据源,原数据为[RED, YELLOW, BLUE]dataList = arrayListOf<Int>(Color.BLUE, Color.RED, Color.YELLOW, Color.BLUE, Color.RED)// 在viewpager2的页面状态监听器中实现自动切换viewPager2?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {    override fun onPageScrolled(        position: Int,        positionOffset: Float,        positionOffsetPixels: Int    ) {        super.onPageScrolled(position, positionOffset, positionOffsetPixels)                if (positionOffset != 0f) return                // itemCount > 1要支持循环滑动        if (itemCount > 1) {            if (position == 0) {                viewPager?.setCurrentItem(itemCount - 2, false)            } else if (position == itemCount - 1) {                viewPager?.setCurrentItem(1, false)            }        }    }        })

|

该示例代码在Demo中运行是能达到预期效果的,但是当在主工程上实现时发现[A, B, C] to [C, A, B, C, A]这个对数据源的改造出现了水土不服,原因是Adapter中有一个通过songID获取当前索引的方法:

|

1
fun findPosition(songId: String): Int {    return dataList.indexOfFirst { it.songId == songId }    }

|

所以存在一个问题,即在 [C, A, B, C, A]中,当想获取C的位置时返回的却是C`的索引。这个方法在原有代码中被多处调用,改造这个方法也存在一定风险,本着不动现有逻辑的原则,唯有探索其他方法了。

之所以这一种方案会出现水土不服,是因为改造数据源时对数据元素进行了深拷贝,导致执行findPostion可能出错。能不能避开通过深拷贝的方式来适配当前的findPosition逻辑呢?答案是肯定的。

优化后的数据源

既然通过深拷贝元素对象不可行,那干脆不进行对象的拷贝了。在数据列表前后各加一个元素这个方案优化后的数据源实现方式是在数据列表前后各加一个元素类型的空对象,即只创建元素对象,不对其进行使用。

|

1
if (list.size > 1) {    // 构造循环列表 [A, B, C] to [obj, A, B, C, obj]    dataList.add(PlaySongInfo())    dataList.addAll(list)    dataList.add(PlaySongInfo())} else {    dataList.addAll(list)}

|

由于改造后的数据源有前后各有一个仅作填充使用的对象,所以在使用列表数据的时候需要做一下index的转换,以下是使用数据源创建fragment的示例代码:

|

1
override fun createFragment(position: Int): Fragment {    val dataSize = dataList.size    // 卡片大于1时支持循环滑动    val realPosition = if (dataSize > 1) {        if (position == 0) {            dataSize - 2        } else if (position == dataSize - 1) {            1        } else {            position        }    } else {        position    }      val songInfo = dataList[realPosition]      return TargetFragment.newInstance(songInfo)    }

|

至此,优化后的数据源再配合上Viewpager2状态监听器中的自动切换逻辑,播放卡片无限滑动的效果就实现了。