行业知识
【Android】ViewPager2+TabLayout实现无限轮播图
文章目录
- 需求分析
- ViewPager2
- 如何使用
- 如何无限循环
- 如何自动滑动
- TabLayout实现指示器
- 封装成组件
需求分析
- 需要无限循环向右轮播,间隔为3S
- 支持手动向左或向右滑动
- 支持点击跳转目标页面
- 支持关闭
ViewPager2
ViewPager2是ViewPager的升级版本并且在功能和性能上有所提升,ViewPager2主要用在APP中进行页面切换。
ViewPager2本身支持左右滑动,且兼容嵌套滑动容器,这两点为我们的轮播图减少了不少工作量。
如何使用
ViewPager2内部实现依靠的是RecyclerView,所以它的使用方法和RecyclerView是一模一样的。都需要定义Adapter、ViewHolder。
上面的xml代码,定义了我们轮播图的骨架,由于关闭按钮比较简单,这里省略了。
上面的代码定义了每一个轮播图的Item布局,在我们这个需求中只需要展示图片即可。
// PagerViewHolder.ktclass PagerViewHolder( private val bind: LayoutBannerItemBinding, private val onClickListener:((bean: BannerData)->Unit)?=null) : RecyclerView.ViewHolder(bind.root) { /** * @param bean BannerData 渲染每个轮播图Item的数据源 */ fun render(bean: BannerData) { // todo 替换图片加载 bind.imageView.setBackgroundColor(Color.BLUE) bind.imageView.setOnClickListener { onClickListener?.invoke(bean) } } }// BannerAdapter.ktclass BannerAdapter(val list: MutableList) : RecyclerView.Adapter() { private var onClickListener:((bannerData: BannerData)->Unit)?=null fun setOnClickListener(onClickListener:((bannerData: BannerData)->Unit)?=null){ this.onClickListener=onClickListener } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerViewHolder { return PagerViewHolder( LayoutBannerItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ){ onClickListener?.invoke(it) } } override fun getItemCount() = list.size override fun onBindViewHolder(holder: PagerViewHolder, position: Int) { holder.render(list[position]) }}如何无限循环
到这里ViewPager虽然可以正常显示了,但是在使用的时候我们会发现,虽然可以左右滑,但是滑到最后一张的时候,不能继续左滑,或者在第一张的时候,不可以右滑。
解决这个问题,需要在数据源的首位插入2张图片,详细见下图:
当我们滑动到最右边的3时,也就是理论上的最后一张,此时继续左滑就可以滑动到最右边的1,营造一种进入循环的假象。此时需要调用viewPager的setCurrentItem手动把当前的Item移动到左边的1,如下图所示:
同样的道理,如果当前处于最左边的1时,此时向右滑,会滑动到最左边的3,这时候需要再次调用setCurrentItem移动到最右边的3
对上面的理论进行代码实践:
//BannerAdapter.ktclass BannerAdapter { // ... /** * @param data List : 更新的全量数据 * 图片大于一张时,需要在首尾插入图片达到无限轮播的目的 */ fun updateAllList(data: List) { list.clear() if (data.isNotEmpty()) { if (data.size > 1) { list.add(data[data.size - 1]) } list.addAll(data) if (data.size > 1) { list.add(data[0]) } } notifyDataSetChanged() } // ...}首先是为Adapter新增一个更新数据的方法,这个方法当图片大于一张的时候,会为首尾插入需要的图片。下一步就是要监听viewPager的页面变化,这里是ViewPager2.OnPageChangeCallback
val pageChangeCallback = object : ViewPager2.OnPageChangeCallback(){ override fun onPageSelected(position: Int) { super.onPageSelected(position) // todo 当前ViewPager切换时,同时计算tab应该如何切换 } override fun onPageScrollStateChanged(state: Int) { super.onPageScrollStateChanged(state) // 当滑动到最右边时,需要移动到index1的位置 val total = (binding.viewPager.adapter?.itemCount ?: 0) if (state == ViewPager2.SCROLL_STATE_IDLE && total > 1 && binding.viewPager.currentItem == total - 1) { binding.viewPager.setCurrentItem(1, false) } // 当滑动到最左边的时候,需要移动到index = total-2的位置 if (state == ViewPager2.SCROLL_STATE_IDLE && total > 1 && binding.viewPager.currentItem == 0) { binding.viewPager.setCurrentItem(total - 2, false) } }}在onPageScrollStateChanged处理每次滑动完以后的事件,并移动到相应的位置。其中SCROLL_STATE_IDLE是等page稳定以后在执行,这样移动page会非常无感。
如何自动滑动
想让轮播图自己动起来比较简单,只需要启动定时器或者Handler就可以达到这个目的,这部分的代码放到后面一起展示。
TabLayout实现指示器
TabLayout主要是用于实现选项卡式布局,经常与ViewPager搭配使用,其中TabLayout还提供setupWithViewPager(ViewPager)方法用于自动与ViewPager绑定。
我们在layout_banner.xml文件中已经定义了TabLayout在布局中的代码了。接下来就是如何绑定,如果正常使用ViewPager只需要调用setupWithViewPager就可以自动绑定了,但是由于我们要达到无限循环的效果,在ViewPager首尾添加了重复数据,所以我们如果直接用setupWithViewPager那么TabLayout就会多两个Tab。
// 初始化下方圆点指示器binding.indicatorLayout.removeAllTabs()for (i in 0 until data.size) { val tab = binding.indicatorLayout.newTab() // 禁用tab点击事件 tab.view.isEnabled = false binding.indicatorLayout.addTab(tab)}val pageChangeCallback = object : ViewPager2.OnPageChangeCallback(){ private var lastPage=-1 override fun onPageSelected(position: Int) { super.onPageSelected(position) // 当前ViewPager切换时,同时计算tab应该如何切换 val total = (binding.viewPager.adapter?.itemCount ?: 0) val curPosition = if (total > 1 && position >= 1 && position private var binding: LayoutBannerBinding private var bannerAdapter:BannerAdapter // 每个页面之间的延迟时间(毫秒) private val DELAY_TIME_MS: Long = 3000 private val viewPagerHandler = Handler(Looper.getMainLooper()) private var viewPagerRunnable:Runnable private var listener:((currentPage:Int)-Unit)?=null init { binding=LayoutBannerBinding.inflate(LayoutInflater.from(context),this) bannerAdapter= BannerAdapter(mutableListOf()) binding.viewPager.adapter=bannerAdapter val pageChangeCallback = object : ViewPager2.OnPageChangeCallback(){ override fun onPageSelected(position: Int) { // 参上 } override fun onPageScrollStateChanged(state: Int) { // 参上 } } binding.viewPager.registerOnPageChangeCallback(pageChangeCallback) viewPagerRunnable = object : Runnable { override fun run() { binding.viewPager.apply { if (currentItem + 1 Unit)?=null){ bannerAdapter.setOnClickListener(onClickListener) } /** * 设置页面切换时的监听 * @param listener Function1 */ fun setPageChangedListener(listener:(currentPage:Int)->Unit){ this.listener=listener } /** * @param data List : 更新的全量数据 * 用于更新广告数据 */ fun updateList(data: List) { bannerAdapter.updateAllList(data) // 超过一张才需要轮播 if (data.size > 1) { // 默认展示第一张图 binding.viewPager.setCurrentItem(1, false) // 初始化下方圆点指示器 binding.indicatorLayout.removeAllTabs() for (i in 0 until data.size) { val tab = binding.indicatorLayout.newTab() tab.view.isEnabled = false binding.indicatorLayout.addTab(tab) } } else { binding.indicatorLayout.removeAllTabs() } } /** * 设置视图是否自动滚动轮播 * @param autoScroll Boolean 是否自动滚动 */ fun autoScroll(autoScroll:Boolean){ viewPagerHandler.removeCallbacks(viewPagerRunnable) if(autoScroll) { viewPagerHandler.postDelayed(viewPagerRunnable, DELAY_TIME_MS) } }}对于使用者只需要
这样我们就优雅的实现了一个无限轮播图组件