【Android】ViewPager2+TabLayout实现无限轮播图

本文介绍了如何在Android应用中使用ViewPager2实现无限循环轮播,包括设置Adapter、ViewHolder、处理自动滑动和TabLayout指示器,以及将这些功能封装成可复用的组件。
摘要由CSDN通过智能技术生成

需求分析

在这里插入图片描述

  • 需要无限循环向右轮播,间隔为3S
  • 支持手动向左或向右滑动
  • 支持点击跳转目标页面
  • 支持关闭

ViewPager2

ViewPager2是ViewPager的升级版本并且在功能和性能上有所提升,ViewPager2主要用在APP中进行页面切换。

ViewPager2本身支持左右滑动,且兼容嵌套滑动容器,这两点为我们的轮播图减少了不少工作量。

如何使用

ViewPager2内部实现依靠的是RecyclerView,所以它的使用方法和RecyclerView是一模一样的。都需要定义Adapter、ViewHolder。

<!--layout_banner.xml-->

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/dp_14"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        />
    
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginBottom="@dimen/dp_8"
        >

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/indicatorLayout"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_8"
            android:layout_gravity="center"
            app:tabBackground="@drawable/circle_red_white_selector"
            app:tabGravity="center"
            app:tabPadding="0dp"
            app:tabMinWidth="0dp"
            app:tabMaxWidth="@dimen/dp_8"
            app:tabIndicatorHeight="0dp"
            />
    </FrameLayout>
    
</merge>

上面的xml代码,定义了我们轮播图的骨架,由于关闭按钮比较简单,这里省略了。

<!--layout_banner_item.xml-->
<?xml version="1.0" encoding="utf-8"?>
<ImageView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/imageView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minHeight="@dimen/dp_64"
    android:scaleType="fitXY"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

上面的代码定义了每一个轮播图的Item布局,在我们这个需求中只需要展示图片即可。

// PagerViewHolder.kt
class 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)
            }
        }
    }

以上是ViewHolder的定义

// BannerAdapter.kt
class BannerAdapter(val list: MutableList<BannerData>) :
    RecyclerView.Adapter<PagerViewHolder>() {

    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])
    }
}
    
    

以上是Adapter的定义,到这里ViewPager已经可以正常显示了,这些流程也是和RecyclerView的使用一模一样了。

如何无限循环

到这里ViewPager虽然可以正常显示了,但是在使用的时候我们会发现,虽然可以左右滑,但是滑到最后一张的时候,不能继续左滑,或者在第一张的时候,不可以右滑。

解决这个问题,需要在数据源的首位插入2张图片,详细见下图:

在这里插入图片描述

当我们滑动到最右边的3时,也就是理论上的最后一张,此时继续左滑就可以滑动到最右边的1,营造一种进入循环的假象。此时需要调用viewPager的setCurrentItem手动把当前的Item移动到左边的1,如下图所示:

在这里插入图片描述

同样的道理,如果当前处于最左边的1时,此时向右滑,会滑动到最左边的3,这时候需要再次调用setCurrentItem移动到最右边的3

在这里插入图片描述

对上面的理论进行代码实践:

//BannerAdapter.kt
class BannerAdapter {
    // ...
    
    /**
     * @param data List<AdBean> : 更新的全量数据
     * 图片大于一张时,需要在首尾插入图片达到无限轮播的目的
     */
    fun updateAllList(data: List<BannerData>) {
        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)
}

以上代码手动添加了正确数量的Tab,除此之外,还需要考虑当ViewPager滑动时TabLayout需要跟随切换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 <= total - 2) {
            position - 1
        } else {
            null
        }
        if(curPosition != null && lastPage!=curPosition) {
            listener?.invoke(curPosition)
            binding.indicatorLayout.getTabAt(curPosition)?.select()
            lastPage=curPosition
        }

    }

    override fun onPageScrollStateChanged(state: Int) {
        super.onPageScrollStateChanged(state)
        // ...同上
    }
}

如下图所示:我们可以知道tab的坐标和viewPager的坐标关系是y = position - 1的关系,再过滤掉边界条件就可以了。

在这里插入图片描述

从上面我们可以看到还记录了lastPage这个属性,这是为什么呢?

这是因为我们实现无限轮播用的“障眼法”每次到边界时,都会回调两次相同的positon,这样会导致我们统计上有偏差,所以需要做一个过滤。

封装成组件

以上已经完全实现了所有需求,最后我们把以上部分都封装到一个View中,达到复用的目的

// Banner.kt
class Banner @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
    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 < (adapter?.itemCount ?: 0)) {
                        setCurrentItem(currentItem + 1, true)
                    }
                }
                viewPagerHandler.postDelayed(this, DELAY_TIME_MS)
            }
        }
    }

    /**
     * 设置item点击处理
     * @param onClickListener Function1<[@kotlin.ParameterName] BannerData, Unit>?
     */
    fun setOnClickListener(onClickListener:((bannerData: BannerData)->Unit)?=null){
        bannerAdapter.setOnClickListener(onClickListener)
    }

    /**
     * 设置页面切换时的监听
     * @param listener Function1<[@kotlin.ParameterName] Int, Unit>
     */
    fun setPageChangedListener(listener:(currentPage:Int)->Unit){
        this.listener=listener
    }


    /**
     * @param data List<BannerData> : 更新的全量数据
     * 用于更新广告数据
     */
    fun updateList(data: List<BannerData>) {
        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)
        }
    }

}

对于使用者只需要

<Banner
    android:id="@+id/banner"
    android:visibility="gone"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
/>

这样我们就优雅的实现了一个无限轮播图组件

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现轮播图的方式有很多种,其中比较常见的一种是使用ViewPager和Fragment来实现。下面是一个简单的实现步骤: 1. 添加ViewPager和指示器控件 在布局文件中添加ViewPager和指示器控件,例如: ```xml <androidx.viewpager.widget.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="200dp"/> <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabIndicatorColor="@color/colorAccent" app:tabSelectedTextColor="@color/colorAccent" app:tabTextColor="@color/colorPrimary"/> ``` 2. 创建Fragment 创建一个Fragment,用于显示轮播图中的每一张图片。例如: ```java public class BannerFragment extends Fragment { private static final String ARG_IMAGE_URL = "image_url"; private String mImageUrl; public static BannerFragment newInstance(String imageUrl) { BannerFragment fragment = new BannerFragment(); Bundle args = new Bundle(); args.putString(ARG_IMAGE_URL, imageUrl); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mImageUrl = getArguments().getString(ARG_IMAGE_URL); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ImageView imageView = new ImageView(getContext()); Picasso.get().load(mImageUrl).into(imageView); return imageView; } } ``` 这里使用了Picasso库来加载图片,你也可以使用其他库或者自己写代码来实现图片的加载和显示。 3. 创建PagerAdapter 创建一个PagerAdapter,用于管理Fragment。例如: ```java public class BannerPagerAdapter extends FragmentPagerAdapter { private List<String> mImageUrls; public BannerPagerAdapter(FragmentManager fm, List<String> imageUrls) { super(fm); mImageUrls = imageUrls; } @Override public Fragment getItem(int position) { return BannerFragment.newInstance(mImageUrls.get(position)); } @Override public int getCount() { return mImageUrls.size(); } } ``` 4. 设置ViewPager和指示器 在Activity或者Fragment中,将PagerAdapter设置给ViewPager,将ViewPager设置给TabLayout即可。例如: ```java ViewPager viewPager = findViewById(R.id.view_pager); TabLayout tabLayout = findViewById(R.id.tab_layout); List<String> imageUrls = new ArrayList<>(); // 添加图片链接到imageUrls列表中 BannerPagerAdapter pagerAdapter = new BannerPagerAdapter(getSupportFragmentManager(), imageUrls); viewPager.setAdapter(pagerAdapter); tabLayout.setupWithViewPager(viewPager); ``` 以上就是一个简单的实现轮播图的例子。当然,实现轮播图的方式还有很多种,可以根据自己的需求和喜好选择合适的方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值