春天里的故事 發表於 2025-7-4 08:53:46

Android实现自动循环播放轮播图(Banner)功能

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>1.需求梳理</li><li>2.实现路径</li><ul class="second_class_ul"><li>2.1 自动播放实现</li><li>2.2 循环播放</li><li>2.3 Vp2切换动画速度以及插值器处理</li><li>2.4 处理滑动时暂停自动切换的逻辑</li><li>2.5 添加指针</li></ul><li>3.核心代码</li><ul class="second_class_ul"><li>3.1 自定义属性</li><li>3.2 自定义BannerView</li><li>3.3 指针View</li><li>3.4 xml adapter</li></ul><li>4.总结</li><ul class="second_class_ul"></ul></ul></div><p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202507/20257484602178.gif" /></p>
<p class="maodian"></p><h2>1.需求梳理</h2>
<p>下面是要实现的需求</p>
<ul><li><code>自动播放</code></li><li><code>循环播放</code></li><li>触摸暂停自动播放</li><li>优化自动播放的时候页面切换的<code>速度</code>和插值器(未自定义属性)</li><li>圆角/指针/矩形和圆形</li><li><code>指针</code>间距/指针<code>位置</code></li></ul>
<p>即是要实现一个能<code>自动</code>,<code>循环</code>,且配置了圆形和矩形指针的控件</p>
<p class="maodian"></p><h2>2.实现路径</h2>
<p>整理下要实现的需求,<code>自动</code>,<code>循环</code>,<code>触摸暂停</code>,<code>切换速度</code>,<code>指针样式</code>,这些功能一步步分解实现.然后再结合成控件.</p>
<p><code>实现组成:</code></p>
<ul><li>ViewPager2(展示内容)</li><li>自定义指针(指针)</li></ul>
<p class="maodian"></p><h3>2.1 自动播放实现</h3>
<p>因为 用的是ViewPager2实现的此需求 所以自动播放的实现 定时调用切换Vp2 就可以了</p>
<p>定时器实现多种多样可自己选择实现:</p>
<ul><li>Handler</li><li>Timer</li><li>协程+死循环</li></ul>
<div class="jb51code"><pre class="brush:java;">
// 协程作用域,使用 Main 调度器
private val viewJob = SupervisorJob()
private val coroutineScope = CoroutineScope(viewJob + Dispatchers.Main)
// 轮播任务
private var bannerJob: Job? = null


/**
* 开始自动轮播
*/
fun startAutoScroll() {
    // 如果已经有轮播任务或者数据不足,则不启动
    if (bannerJob != null || (mAdapter?.itemCount ?: 0) &lt;= 1) return
    bannerJob = coroutineScope.launch {
      while (isActive) {
            delay(delayMillis.toLong())
            binding.viewPager.post {
                val currentItem: Int = binding.viewPager.getCurrentItem()
                MyPagerHelper.setCurrentItem(binding.viewPager, currentItem + 1, 800)
            }
      }
    }
}

/**
* 停止自动轮播
*/
fun stopAutoScroll() {

    bannerJob?.cancel()
    bannerJob = null
}
</pre></div>
<p class="maodian"></p><h3>2.2 循环播放</h3>
<p>循环播放是通过将条目数无限大 然后再根据具体的条目数算出来展示那条数据实现的</p>
<div class="jb51code"><pre class="brush:java;">
/**
* 开始自动轮播
*/
fun startAutoScroll() {
    // 如果已经有轮播任务或者数据不足,则不启动
    if (bannerJob != null || (mAdapter?.itemCount ?: 0) &lt;= 1) return
    bannerJob = coroutineScope.launch {
      while (isActive) {
            delay(delayMillis.toLong())
            binding.viewPager.post {
                val currentItem: Int = binding.viewPager.getCurrentItem()
                //切换到指定的条目binding.viewPager.setCurrentItem(currentItem + 1, true)
                // 处理条目切换 动画
                MyPagerHelper.setCurrentItem(binding.viewPager, currentItem + 1, 800)
            }
      }
    }
}



class BannerAdapter(var mContext: Context,var radius:Int): BaseRvAdapter&lt;BannerItem, ItemBannerBinding&gt;() {
    override fun onCreateViewHolder(
      parent: ViewGroup, viewType: Int
    ): BaseRvViewHolder&lt;ItemBannerBinding&gt; {
      return BaseRvViewHolder(ItemBannerBinding.inflate(LayoutInflater.from(mContext),parent,false))
    }

    override fun onBindViewHolder(
      holder: BaseRvViewHolder&lt;ItemBannerBinding&gt;,
      position: Int
    ) {
      val realPosition: Int = position % getData().size
      val bean: BannerItem? = getItem(realPosition)
      holder.binding.imageView.shapeAppearanceModel = holder.binding.imageView.shapeAppearanceModel
            .toBuilder()
            .setAllCorners(CornerFamily.ROUNDED, radius.toFloat())
            .build()
      GlideUtil.getInstance().loadImage(mContext,bean?.imageUrl?:"",holder.binding.imageView)
    }

   override fun getItemCount(): Int {
      // 返回极大值,实现无限循环效果
      return if (getData().size &gt; 1) Int.Companion.MAX_VALUE else getData().size
    }


}
</pre></div>
<p class="maodian"></p><h3>2.3 Vp2切换动画速度以及插值器处理</h3>
<div class="jb51code"><pre class="brush:java;">/**
* 设置当前Item 切换时长
* @param pager    viewpager2
* @param item   下一个跳转的item
* @param duration scroll时长
*/
fun setCurrentItem(pager: ViewPager2, item: Int, duration: Long) {
    val currentItem = pager.currentItem
    // 1. 目标页面与当前页面相同时,直接返回,避免无效动画
    if (item == currentItem) {
      return
    }

    // 2. 处理 ViewPager2 未测量的情况(宽度为 0 时,等待布局完成后再执行)
    val pagePxWidth = pager.width
    if (pagePxWidth &lt;= 0) {
      pager.post { setCurrentItem(pager, item, duration) }
      return
    }

    // 3. 计算需要拖拽的总像素(支持正向/反向滑动)
    val pxToDrag = pagePxWidth * (item - currentItem)

    // 4. 使用局部变量保存 previousValue,避免多实例共享冲突(核心优化)
    var previousValue = 0

    val animator = ValueAnimator.ofInt(0, pxToDrag)
    animator.addUpdateListener { animation -&gt;
      val currentValue = animation.animatedValue as Int
      val currentPxToDrag = (currentValue - previousValue).toFloat()
      // 调用 fakeDragBy 实现滑动(注意负号:模拟用户拖拽方向)
      pager.fakeDragBy(-currentPxToDrag)
      previousValue = currentValue
    }

    animator.addListener(object : Animator.AnimatorListener {
      private var isFakeDragStarted = false

      override fun onAnimationStart(animation: Animator) {
            // 开始假拖拽,标记状态
            pager.beginFakeDrag()
            isFakeDragStarted = true
      }

      override fun onAnimationEnd(animation: Animator) {
            if (isFakeDragStarted) {
                pager.endFakeDrag() // 结束假拖拽
                isFakeDragStarted = false
            }
      }

      override fun onAnimationCancel(animation: Animator) {
            // 2. 动画取消时必须结束假拖拽,避免状态残留
            if (isFakeDragStarted) {
                pager.endFakeDrag()
                isFakeDragStarted = false
            }
      }

      override fun onAnimationRepeat(animation: Animator) {}
    })

    animator.interpolator = AccelerateDecelerateInterpolator()
    animator.duration = duration
    animator.start()
}
</pre></div>
<p class="maodian"></p><h3>2.4 处理滑动时暂停自动切换的逻辑</h3>
<p>Vp2 拦截onTouch事件 所以处理触摸滑动 无法直接实现 需要在父布局做拦截分发实现或者直接监听滑动状态 取消自动播放 这里选择后者</p>
<div class="jb51code"><pre class="brush:java;">   binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
      override fun onPageScrollStateChanged(state: Int) {
            super.onPageScrollStateChanged(state)
            if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
                // 用户开始拖拽,暂停自动播放
                stopAutoScroll()
            } else if (state == ViewPager2.SCROLL_STATE_IDLE) {
                // 滑动结束,恢复自动播放
                startAutoScroll()
            }
      }
      // 处理Vp2切换的时候指针切换 onPageSelect 方法比较慢 在这里处理
      override fun onPageScrolled(
            position: Int, positionOffset: Float, positionOffsetPixels: Int
      ) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels)

            val indicatorCount = binding.indicatorContainer.childCount
            if (indicatorCount == 0) return

            // 计算当前滑动的两个页面对应的指示器
            val currentPos = position % indicatorCount
            val nextPos = (position + 1) % indicatorCount
            if (indicatorType!=2){
                // 当滑动超过一半时,提前更新指示器状态
                if (positionOffset &gt; 0.5f) {
                  updateIndicatorStatus(nextPos)
                } else {
                  updateIndicatorStatus(currentPos)
                }
            }

      }
    })


</pre></div>
<p class="maodian"></p><h3>2.5 添加指针</h3>
<p>设置数据的时候添加指针</p>
<div class="jb51code"><pre class="brush:java;">/**
* 设置 Banner 数据
* @param data Banner 数据列表
*/
fun setBannerData(data: List&lt;BannerItem&gt;) {
    if (data.isEmpty()) return
    mAdapter?.setNewData(data.toMutableList())
    // 计算初始位置,确保可以双向滚动
    val initialPosition = Int.MAX_VALUE / 2 - (Int.MAX_VALUE / 2 % data.size)
    binding.viewPager.setCurrentItem(initialPosition, false)
    if (indicatorType!=2){
      for (i in 0 until data.size) {
            if (i == initialPosition % data.size) {
                curPosition = i
            }
            val indicator = RoundedRectangleIndicatorView(context).apply {
                setDefaultBackgroundColor(indicatorDefaultColor)
                setSelectedBackgroundColor(indicatorSelectedColor)
                setIndicatorWidth(indicatorCustomWidth.toFloat())
                setIndicatorHeight(indicatorCustomHeight.toFloat())
                setCornerRadius(indicatorCornerRadius.toFloat())
                setIndicatorSpacing(indicatorSpacing.toFloat())
                if (indicatorType == 1) {
                  setIndicatorShape(RoundedRectangleIndicatorView.Shape.CIRCLE)
                } elseif (indicatorType == 0){
                  setIndicatorShape(RoundedRectangleIndicatorView.Shape.RECTANGLE)
                }

                // 初始状态:第一个指示器选中
                setSelectedStatus(i == initialPosition % data.size)
            }
            // 设置指示器间距(通过布局参数)
            val lp = FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
            )
            if (i &gt; 0) lp.leftMargin = indicatorSpacing // 从第二个开始添加左间距
            binding.indicatorContainer.addView(indicator, lp)

      }
    }


    // 如果启用自动轮播且数据数量大于1,则开始轮播
    if (isAutoPlay &amp;&amp; data.size &gt; 1) {
      startAutoScroll()
    }
}
</pre></div>
<p class="maodian"></p><h2>3.核心代码</h2>
<p class="maodian"></p><h3>3.1 自定义属性</h3>
<div class="jb51code"><pre class="brush:xml;">&lt;declare-styleable name="AutoBannerViewStyle"&gt;
    &lt;!-- 轮播相关 --&gt;
    &lt;attr name="delayTime" format="integer" /&gt; &lt;!-- 轮播间隔(毫秒) --&gt;
    &lt;attr name="bannerCornerSize" format="dimension" /&gt; &lt;!-- 轮播图圆角大小 --&gt;
    &lt;attr name="isAutoPlay" format="boolean" /&gt; &lt;!-- 是否自动轮播 --&gt;
    &lt;!-- 指示器位置:在ViewPager下方(默认)/与ViewPager底部对齐 --&gt;
    &lt;attr name="indicatorPosition" format="enum"&gt;
      &lt;enum name="belowViewPager" value="0" /&gt; &lt;!-- 在ViewPager下方 --&gt;
      &lt;enum name="alignViewPagerBottom" value="1" /&gt; &lt;!-- 与ViewPager底部对齐 --&gt;
    &lt;/attr&gt;

    &lt;attr name="indicatorGravity" format="enum"&gt;
      &lt;enum name="left" value="0x03" /&gt;   &lt;!-- Gravity.LEFT --&gt;
      &lt;enum name="center" value="0x01" /&gt;   &lt;!-- Gravity.CENTER_HORIZONTAL --&gt;
      &lt;enum name="right" value="0x05" /&gt;    &lt;!-- Gravity.RIGHT --&gt;
      &lt;enum name="start" value="0x800003" /&gt; &lt;!-- Gravity.START --&gt;
      &lt;enum name="end" value="0x800005" /&gt;   &lt;!-- Gravity.END --&gt;
    &lt;/attr&gt;
    &lt;!-- 指示器相关 --&gt;
    &lt;attr name="indicatorMargin" format="dimension" /&gt; &lt;!-- 指示器顶部边距(距离轮播图底部) --&gt;
    &lt;attr name="indicatorMarginSpacing" format="dimension" /&gt; &lt;!-- 指示器之间的间距 --&gt;
    &lt;attr name="indicatorStartSpacing" format="dimension" /&gt; &lt;!-- 指示器距离两边距离 --&gt;

    &lt;attr name="indicatorDefaultColor" format="color" /&gt; &lt;!-- 指示器默认颜色 --&gt;
    &lt;attr name="indicatorSelectedColor" format="color" /&gt; &lt;!-- 指示器选中颜色 --&gt;
    &lt;attr name="indicatorCustomWidth" format="dimension" /&gt; &lt;!-- 指示器宽度 --&gt;
    &lt;attr name="indicatorCustomHeight" format="dimension" /&gt; &lt;!-- 补充:指示器高度(可选) --&gt;
    &lt;attr name="indicatorCornerRadius" format="dimension" /&gt; &lt;!-- 补充:指示器圆角(可选) --&gt;
    &lt;attr name="indicatorType" format="enum"&gt;
      &lt;enum name="rectangle" value="0" /&gt;
      &lt;enum name="circle" value="1" /&gt;
      &lt;enum name="none" value="2" /&gt;
    &lt;/attr&gt;
&lt;/declare-styleable&gt;
&lt;!-- 指针自定义属性 --&gt;
&lt;declare-styleable name="RoundedRectangleControl"&gt;
    &lt;attr name="defaultColor" format="color" /&gt;
    &lt;attr name="selectedColor" format="color" /&gt;
    &lt;attr name="cornerIndicatorRadius" format="dimension" /&gt;
    &lt;attr name="isSelected" format="boolean" /&gt;
    &lt;attr name="indicatorPadding" format="dimension" /&gt;
    &lt;attr name="indicatorSpacing" format="dimension" /&gt;
    &lt;attr name="indicatorWidth" format="dimension" /&gt;&lt;!-- 指示器宽度 --&gt;
    &lt;attr name="indicatorHeight" format="dimension" /&gt; &lt;!-- 指示器高度 --&gt;
    &lt;attr name="indicatorShape" format="enum"&gt;
      &lt;enum name="rectangle" value="0" /&gt;
      &lt;enum name="circle" value="1" /&gt;
    &lt;/attr&gt;
&lt;/declare-styleable&gt;
</pre></div>
<p class="maodian"></p><h3>3.2 自定义BannerView</h3>
<div class="jb51code"><pre class="brush:java;">package com.qianrun.voice.common.view.banner

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.viewpager2.widget.ViewPager2
import com.blankj.utilcode.util.SizeUtils
import com.qianrun.voice.common.R
import com.qianrun.voice.common.databinding.LayoutAutoBannerBinding
import com.qianrun.voice.common.view.adapter.BannerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch


/**
* 自动轮播 Banner 组件
* 支持自定义轮播间隔、圆角大小、指示器样式等属性
*/
class AutoBannerView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    // 使用 ViewBinding 绑定布局
    private val binding: LayoutAutoBannerBinding = LayoutAutoBannerBinding.inflate(LayoutInflater.from(context), this, true)

    // 协程作用域,使用 Main 调度器
    private val viewJob = SupervisorJob()
    private val coroutineScope = CoroutineScope(viewJob + Dispatchers.Main)
    // 轮播任务
    private var bannerJob: Job? = null

    // Banner 适配器
    private var mAdapter: BannerAdapter? = null

    // 轮播配置参数
    private var delayMillis = 3000          // 轮播间隔时间(毫秒)
    private var cornerSize = 20             // 圆角大小(dp)

    private var isAutoPlay = true         // 是否自动轮播

    // 指示器配置参数(从自定义属性获取)
    private var indicatorMarginTop = SizeUtils.dp2px(10f) // 指示器距离轮播图底部的距离(px)
    private var indicatorStartSpacing = SizeUtils.dp2px(5f) // 指示器距离轮播图底部的距离(px)
    private var indicatorSpacing = SizeUtils.dp2px(10f)   // 指示器之间的间距(px)
    private var indicatorDefaultColor = 0xFFE0F2FE.toInt() // 指示器默认颜色
    private var indicatorSelectedColor = 0xFF3B82F6.toInt() // 指示器选中颜色
    private var indicatorCustomWidth = SizeUtils.dp2px(9f)// 指示器宽度(px)
    private var indicatorCustomHeight = SizeUtils.dp2px(3f) // 指示器高度(px)
    private var indicatorCornerRadius = SizeUtils.dp2px(2f) // 指示器圆角(px)
    private var isAlignViewPagerBottom = false // 是否与ViewPager底部对齐(默认false:在下方)
    private var indicatorGravity = 2 // 指针内容位置
    private var indicatorType = 2 // 指针样式 0 时矩形 1 是圆形 2无指针

    init {
      initAttrs(attrs)
      initView()
    }

    /**
   * 初始化自定义属性
   */
    @SuppressLint("CustomViewStyleable")
    private fun initAttrs(attrs: AttributeSet?) {
      attrs?.let {
            context.obtainStyledAttributes(it, R.styleable.AutoBannerViewStyle).apply {
                // 指针位置
                isAlignViewPagerBottom = getInt(R.styleable.AutoBannerViewStyle_indicatorPosition, 0) == 1
                //指针内容位置
                indicatorGravity = getInt(R.styleable.AutoBannerViewStyle_indicatorGravity, Gravity.CENTER)
                // 指针类型
                indicatorType = getInt(R.styleable.AutoBannerViewStyle_indicatorType, 2)
                // 切换是时间
                delayMillis = getInteger(R.styleable.AutoBannerViewStyle_delayTime, 3000)
                //轮播图圆角
                cornerSize = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_bannerCornerSize, SizeUtils.dp2px(10f))
                //指针轮播图山下距离
                indicatorMarginTop = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_indicatorMargin, SizeUtils.dp2px(10f))
                //距离两边距离
                indicatorStartSpacing = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_indicatorStartSpacing, SizeUtils.dp2px(10f))
                //间距
                indicatorSpacing = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_indicatorMarginSpacing, SizeUtils.dp2px(10f))
                //是否自动播放
                isAutoPlay = getBoolean(R.styleable.AutoBannerViewStyle_isAutoPlay, true)


                // 指示器样式相关
                indicatorDefaultColor = getColor(R.styleable.AutoBannerViewStyle_indicatorDefaultColor, 0xFFE0F2FE.toInt())
                indicatorSelectedColor = getColor(R.styleable.AutoBannerViewStyle_indicatorSelectedColor, 0xFF3B82F6.toInt())
                //指针宽度
                indicatorCustomWidth = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_indicatorCustomWidth, SizeUtils.dp2px(9f))
                // 高度
                indicatorCustomHeight = getDimensionPixelSize(R.styleable.AutoBannerViewStyle_indicatorCustomHeight, SizeUtils.dp2px(3f))

                recycle()
            }
      }
    }

    /**
   * 核心:修改约束实现位置切换
   */
    private fun updateIndicatorPosition(alignBottom: Boolean) {
      // 获取两者的布局参数(约束布局参数)
      val viewPagerLp = binding.viewPager.layoutParams as ConstraintLayout.LayoutParams
      val indicatorLp = binding.indicatorContainer.layoutParams as ConstraintLayout.LayoutParams
      if (alignBottom) {
            // 场景2:与ViewPager底部对齐(在ViewPager内部底部)
            // 1. ViewPager的底部约束到父容器(充满高度)
            viewPagerLp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.bottomMargin = 0

            // 2. 指示器容器的底部也约束到父容器(与ViewPager底部齐平)
            if (indicatorType!=2){
                indicatorLp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                indicatorLp.bottomMargin = indicatorMarginTop // 可根据需求添加与父容器底部的间距
                }
      } else {
            // 场景1:在ViewPager下方(有间距)
            // 1. ViewPager的底部约束到指示器容器的顶部(ViewPager高度不包含指示器)
            viewPagerLp.bottomToTop = binding.indicatorContainer.id
            viewPagerLp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
            viewPagerLp.bottomMargin = indicatorMarginTop
            viewPagerLp.height = 0
            if (indicatorType!=2){
                // 2. 指示器容器的顶部约束到ViewPager的底部,并添加间距
                indicatorLp.topMargin = indicatorMarginTop // 间距
                indicatorLp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID // 指示器底部贴父容器
                indicatorLp.bottomMargin = 0
            }


      }

      if (indicatorType!=2){
            if (indicatorGravity == Gravity.START || indicatorGravity == Gravity.LEFT) {
                indicatorLp.marginStart = indicatorStartSpacing
                indicatorLp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
                indicatorLp.endToEnd = ConstraintLayout.LayoutParams.UNSET
            } else if (indicatorGravity == Gravity.END || indicatorGravity == Gravity.RIGHT) {
                indicatorLp.marginEnd = indicatorStartSpacing
                indicatorLp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
                indicatorLp.startToStart = ConstraintLayout.LayoutParams.UNSET
            } else {
                indicatorLp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
                indicatorLp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
            }
            binding.indicatorContainer.layoutParams = indicatorLp
      }


      // 应用修改后的约束
      binding.viewPager.layoutParams = viewPagerLp

    }


    /**
   * 初始化视图
   */
    private fun initView() {
      updateIndicatorPosition(isAlignViewPagerBottom)
      mAdapter = BannerAdapter(context, cornerSize)
      binding.viewPager.offscreenPageLimit = 3
      binding.viewPager.adapter = mAdapter
      // 设置初始位置,实现无限轮播效果
      binding.viewPager.setCurrentItem(Int.MAX_VALUE / 2, false)
      binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                super.onPageScrollStateChanged(state)
                if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
                  // 用户开始拖拽,暂停自动播放
                  stopAutoScroll()
                } else if (state == ViewPager2.SCROLL_STATE_IDLE) {
                  // 滑动结束,恢复自动播放
                  startAutoScroll()
                }
            }

            override fun onPageScrolled(
                position: Int, positionOffset: Float, positionOffsetPixels: Int
            ) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels)

                val indicatorCount = binding.indicatorContainer.childCount
                if (indicatorCount == 0) return

                // 计算当前滑动的两个页面对应的指示器
                val currentPos = position % indicatorCount
                val nextPos = (position + 1) % indicatorCount
                if (indicatorType!=2){
                  // 当滑动超过一半时,提前更新指示器状态
                  if (positionOffset &gt; 0.5f) {
                        updateIndicatorStatus(nextPos)
                  } else {
                        updateIndicatorStatus(currentPos)
                  }
                }

            }
      })

    }

    var curPosition = 0

    // 抽取通用的更新方法
    private fun updateIndicatorStatus(selectPosition: Int) {
      if (selectPosition == curPosition) return // 避免重复更新
      binding.indicatorContainer.post {
            (binding.indicatorContainer.getChildAt(
                curPosition
            ) as? RoundedRectangleIndicatorView)?.setSelectedStatus(false)
            (binding.indicatorContainer.getChildAt(
                selectPosition
            ) as? RoundedRectangleIndicatorView)?.setSelectedStatus(true)
            curPosition = selectPosition
      }
    }


    /**
   * 设置 Banner 数据
   * @param data Banner 数据列表
   */
    fun setBannerData(data: List&lt;BannerItem&gt;) {
      if (data.isEmpty()) return
      mAdapter?.setNewData(data.toMutableList())
      // 计算初始位置,确保可以双向滚动
      val initialPosition = Int.MAX_VALUE / 2 - (Int.MAX_VALUE / 2 % data.size)
      binding.viewPager.setCurrentItem(initialPosition, false)
      if (indicatorType!=2){
            for (i in 0 until data.size) {
                if (i == initialPosition % data.size) {
                  curPosition = i
                }
                val indicator = RoundedRectangleIndicatorView(context).apply {
                  setDefaultBackgroundColor(indicatorDefaultColor)
                  setSelectedBackgroundColor(indicatorSelectedColor)
                  setIndicatorWidth(indicatorCustomWidth.toFloat())
                  setIndicatorHeight(indicatorCustomHeight.toFloat())
                  setCornerRadius(indicatorCornerRadius.toFloat())
                  setIndicatorSpacing(indicatorSpacing.toFloat())
                  if (indicatorType == 1) {
                        setIndicatorShape(RoundedRectangleIndicatorView.Shape.CIRCLE)
                  } elseif (indicatorType == 0){
                        setIndicatorShape(RoundedRectangleIndicatorView.Shape.RECTANGLE)
                  }

                  // 初始状态:第一个指示器选中
                  setSelectedStatus(i == initialPosition % data.size)
                }
                // 设置指示器间距(通过布局参数)
                val lp = FrameLayout.LayoutParams(
                  FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
                )
                if (i &gt; 0) lp.leftMargin = indicatorSpacing // 从第二个开始添加左间距
                binding.indicatorContainer.addView(indicator, lp)

            }
      }


      // 如果启用自动轮播且数据数量大于1,则开始轮播
      if (isAutoPlay &amp;&amp; data.size &gt; 1) {
            startAutoScroll()
      }
    }

    /**
   * 开始自动轮播
   */
    fun startAutoScroll() {
      // 如果已经有轮播任务或者数据不足,则不启动
      if (bannerJob != null || (mAdapter?.itemCount ?: 0) &lt;= 1) return
      bannerJob = coroutineScope.launch {
            while (isActive) {
                delay(delayMillis.toLong())
                binding.viewPager.post {
                  val currentItem: Int = binding.viewPager.getCurrentItem()

                  MyPagerHelper.setCurrentItem(binding.viewPager, currentItem + 1, 800)
                }
            }
      }
    }

    /**
   * 停止自动轮播
   */
    fun stopAutoScroll() {
   
      bannerJob?.cancel()
      bannerJob = null
    }

    /**
   * 释放资源
   */
    fun release() {
      stopAutoScroll()
      coroutineScope.cancel()
    }

    override fun onAttachedToWindow() {
      super.onAttachedToWindow()
      // 视图附加到窗口时,如果启用了自动轮播,则启动
      if (isAutoPlay &amp;&amp; (mAdapter?.itemCount ?: 0) &gt; 1) {
            startAutoScroll()
      }
    }

    override fun onDetachedFromWindow() {
      super.onDetachedFromWindow()
      // 视图从窗口分离时停止轮播
      stopAutoScroll()
    }

    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
      super.onWindowFocusChanged(hasWindowFocus)
      // 窗口获得/失去焦点时控制轮播
      if (hasWindowFocus &amp;&amp; isAutoPlay &amp;&amp; (mAdapter?.itemCount ?: 0) &gt; 1) {
            startAutoScroll()
      } else {
            stopAutoScroll()
      }
    }
}
</pre></div>
<p class="maodian"></p><h3>3.3 指针View</h3>
<div class="jb51code"><pre class="brush:java;">package com.qianrun.voice.common.view.banner

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.content.withStyledAttributes
import com.fasterxml.jackson.annotation.JsonFormat.Shape
import com.qianrun.voice.common.R

class RoundedRectangleIndicatorView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 默认属性值
    private var defaultBackgroundColor = Color.parseColor("#E0F2FE")
    private var selectedBackgroundColor = Color.parseColor("#3B82F6")
    private var cornerRadius = 8f
    private var isSelectedState = false
    private var indicatorPadding = 0f
    private var indicatorSpacing = 8f

    // 新增:宽高相关属性
    private var indicatorWidth = 24f// 指示器默认宽度
    private var indicatorHeight = 8f// 指示器默认高度

    // 画笔
    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
      style = Paint.Style.FILL
    }

    // 绘制区域
    private val rect = RectF()

    // 点击监听器
    private var onStateChangeListener: ((Boolean) -&gt; Unit)? = null
    private var indicatorShape = Shape.RECTANGLE // 默认矩形
    // 新增:形状枚举
    enum class Shape {
      RECTANGLE, CIRCLE
    }
    init {
      // 从XML属性中获取配置(包括宽高)
      context.withStyledAttributes(attrs, R.styleable.RoundedRectangleControl) {
            // 原有属性...
            defaultBackgroundColor = getColor(
                R.styleable.RoundedRectangleControl_defaultColor,
                defaultBackgroundColor
            )
            selectedBackgroundColor = getColor(
                R.styleable.RoundedRectangleControl_selectedColor,
                selectedBackgroundColor
            )
            cornerRadius = getDimension(
                R.styleable.RoundedRectangleControl_cornerIndicatorRadius,
                cornerRadius
            )
            isSelectedState = getBoolean(
                R.styleable.RoundedRectangleControl_isSelected,
                isSelectedState
            )
            indicatorPadding = getDimension(
                R.styleable.RoundedRectangleControl_indicatorPadding,
                indicatorPadding
            )
            indicatorSpacing = getDimension(
                R.styleable.RoundedRectangleControl_indicatorSpacing,
                indicatorSpacing
            )

            // 新增:从XML获取宽高属性
            indicatorWidth = getDimension(
                R.styleable.RoundedRectangleControl_indicatorWidth,
                indicatorWidth
            )
            indicatorHeight = getDimension(
                R.styleable.RoundedRectangleControl_indicatorHeight,
                indicatorHeight
            )
            // 新增:获取形状属性
            indicatorShape = when (getInt(R.styleable.RoundedRectangleControl_indicatorShape, 0)) {
                1 -&gt; Shape.CIRCLE
                else -&gt; Shape.RECTANGLE}
      }

      isClickable = true
    }

    /**
   * 测量控件尺寸
   * 优先使用XML中设置的尺寸,若无则使用默认宽高
   */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      // 计算测量后的宽高(考虑父容器限制)
      val measuredWidth = measureDimension(indicatorWidth.toInt(), widthMeasureSpec)
      val measuredHeight = measureDimension(indicatorHeight.toInt(), heightMeasureSpec)

      // 如果是圆形,确保宽高相等(取较大值)
      if (indicatorShape == Shape.CIRCLE) {
            val size = maxOf(measuredWidth, measuredHeight)
            setMeasuredDimension(size, size)
      } else {
            setMeasuredDimension(measuredWidth, measuredHeight)
      }
    }

    /**
   * 辅助计算测量尺寸
   * @param defaultSize 控件默认尺寸
   * @param measureSpec 父容器传来的尺寸限制
   */
    private fun measureDimension(defaultSize: Int, measureSpec: Int): Int {
      var result = defaultSize
      val specMode = MeasureSpec.getMode(measureSpec)
      val specSize = MeasureSpec.getSize(measureSpec)

      when (specMode) {
            // 父容器未限制尺寸,使用默认值
            MeasureSpec.UNSPECIFIED -&gt; result = defaultSize
            // 父容器强制限制尺寸,使用限制值
            MeasureSpec.EXACTLY -&gt; result = specSize
            // 父容器建议尺寸,取默认值与建议值中的较小者
            MeasureSpec.AT_MOST -&gt; result = minOf(defaultSize, specSize)
      }
      return result
    }

    override fun onDraw(canvas: Canvas) {
      super.onDraw(canvas)
      // 绘制区域(考虑内边距)
      // 根据形状选择绘制方式
      when (indicatorShape) {
            Shape.RECTANGLE -&gt; drawRectangle(canvas)
            Shape.CIRCLE -&gt; drawCircle(canvas)
      }
    }

    /**
   * 绘制圆角矩形
   */
    private fun drawRectangle(canvas: Canvas) {
      // 绘制区域(考虑内边距)
      rect.set(
            indicatorPadding,
            indicatorPadding,
            width.toFloat() - indicatorPadding,
            height.toFloat() - indicatorPadding
      )

      // 根据选中状态设置背景色
      backgroundPaint.color = if (isSelectedState) selectedBackgroundColor else defaultBackgroundColor
      // 绘制圆角矩形
      canvas.drawRoundRect(rect, cornerRadius, cornerRadius, backgroundPaint)
    }

    // 新增:设置形状
    fun setIndicatorShape(shape: Shape) {
      if (indicatorShape != shape) {
            indicatorShape = shape
            requestLayout()// 可能需要重新调整尺寸
            invalidate()   // 重新绘制
      }
    }

    /**
   * 绘制圆形
   */
    private fun drawCircle(canvas: Canvas) {
      // 计算圆心和半径(考虑内边距)
      val centerX = width / 2f
      val centerY = height / 2f
      val radius = minOf(width, height) / 2f - indicatorPadding

      // 根据选中状态设置背景色
      backgroundPaint.color = if (isSelectedState) selectedBackgroundColor else defaultBackgroundColor
      // 绘制圆形
      canvas.drawCircle(centerX, centerY, radius, backgroundPaint)
    }

    // 触摸事件处理(保持不变)
    override fun onTouchEvent(event: MotionEvent): Boolean {
      when (event.action) {
            MotionEvent.ACTION_UP -&gt; {
                toggleState()
                performClick()
                return true
            }
      }
      return super.onTouchEvent(event)
    }

    override fun performClick(): Boolean {
      super.performClick()
      return true
    }

    // 新增:动态设置指示器宽度
    fun setIndicatorWidth(width: Float) {
      if (indicatorWidth != width) {
            indicatorWidth = width
            // 触发重新测量和绘制
            requestLayout()// 重新计算尺寸
            invalidate()   // 重新绘制
      }
    }

    // 新增:动态设置指示器高度
    fun setIndicatorHeight(height: Float) {
      if (indicatorHeight != height) {
            indicatorHeight = height
            requestLayout()
            invalidate()
      }
    }

    // 原有方法(保持不变)
    fun toggleState() {
      isSelectedState = !isSelectedState
      invalidate()
      onStateChangeListener?.invoke(isSelectedState)
    }

    fun setSelectedStatus(selected: Boolean) {
      if (isSelectedState != selected) {
            isSelectedState = selected
            invalidate()
            onStateChangeListener?.invoke(isSelectedState)
      }
    }

    fun isSelectedStatus(): Boolean = isSelectedState

    fun setOnStateChangeListener(listener: (Boolean) -&gt; Unit) {
      onStateChangeListener = listener
    }

    fun setDefaultBackgroundColor(color: Int) {
      defaultBackgroundColor = color
      if (!isSelectedState) invalidate()
    }

    fun setSelectedBackgroundColor(color: Int) {
      selectedBackgroundColor = color
      if (isSelectedState) invalidate()
    }

    fun setCornerRadius(radius: Float) {
      cornerRadius = radius
      invalidate()
    }

    fun setIndicatorPadding(padding: Float) {
      indicatorPadding = padding
      invalidate()
    }

    fun setIndicatorSpacing(spacing: Float) {
      indicatorSpacing = spacing
      parent?.requestLayout()
    }

    fun getIndicatorSpacing(): Float = indicatorSpacing
}
</pre></div>
<p class="maodian"></p><h3>3.4 xml adapter</h3>
<p>layout_auto_banner.xml</p>
<div class="jb51code"><pre class="brush:xml;">&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"&gt;

    &lt;androidx.viewpager2.widget.ViewPager2
      android:id="@+id/viewPager"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_constraintBottom_toTopOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" /&gt;

    &lt;LinearLayout
      android:id="@+id/indicatorContainer"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent" /&gt;
&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
</pre></div>
<p>item_banner.xml</p>
<div class="jb51code"><pre class="brush:xml;">&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"

    android:layout_height="match_parent"&gt;

    &lt;com.google.android.material.imageview.ShapeableImageView
      android:id="@+id/imageView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:scaleType="centerCrop" /&gt;
&lt;/FrameLayout&gt;
</pre></div>
<p>BannerAdapter</p>
<div class="jb51code"><pre class="brush:java;">package com.qianrun.voice.common.view.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.google.android.material.shape.CornerFamily
import com.qianrun.voice.basic.adapter.BaseRvAdapter
import com.qianrun.voice.basic.adapter.holder.BaseRvViewHolder
import com.qianrun.voice.common.databinding.ItemBannerBinding
import com.qianrun.voice.common.glide.GlideUtil
import com.qianrun.voice.common.view.banner.BannerItem


/**
*
*@Author: wkq
*
*@Time: 2025/7/2 10:45
*
*@Desc:
*/
class BannerAdapter(var mContext: Context,var radius:Int): BaseRvAdapter&lt;BannerItem, ItemBannerBinding&gt;() {
    override fun onCreateViewHolder(
      parent: ViewGroup, viewType: Int
    ): BaseRvViewHolder&lt;ItemBannerBinding&gt; {
      return BaseRvViewHolder(ItemBannerBinding.inflate(LayoutInflater.from(mContext),parent,false))
    }

    override fun onBindViewHolder(
      holder: BaseRvViewHolder&lt;ItemBannerBinding&gt;,
      position: Int
    ) {
      val realPosition: Int = position % getData().size
      val bean: BannerItem? = getItem(realPosition)
      holder.binding.imageView.shapeAppearanceModel = holder.binding.imageView.shapeAppearanceModel
            .toBuilder()
            .setAllCorners(CornerFamily.ROUNDED, radius.toFloat())
            .build()
      GlideUtil.getInstance().loadImage(mContext,bean?.imageUrl?:"",holder.binding.imageView)
    }

   override fun getItemCount(): Int {
      // 返回极大值,实现无限循环效果
      return if (getData().size &gt; 1) Int.Companion.MAX_VALUE else getData().size
    }


}
</pre></div>
<p class="maodian"></p><h2>4.总结</h2>
<p>简单的实现了自动,循环播放的Banner,未处理定制Banner图片展示样式的处理.有需要,Banner样式以及指针样式可以自己定制修改 在添加指针和数据的地方传入特定的View 就可以了.有什么好的思路欢迎一起沟通进步,就这样,结束.</p>
<p>以上就是Android实现自动循环播放轮播图(Banner)功能的详细内容,更多关于Android自动循环播放轮播图的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>Android实现基于ViewPager的无限循环自动播放带指示器的轮播图CarouselFigureView控件</li><li>android实现轮播图引导页</li><li>Android自定义轮播图效果</li><li>android ViewPager实现一个无限轮播图</li><li>Android Banner本地和网络轮播图使用介绍</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Android实现自动循环播放轮播图(Banner)功能