开发者

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

目录
  • 1.需求梳理
  • 2.实现路径
    • 2.1 自动播放实现
    • 2.2 循环播放
    • 2.3 Vp2切换动画速度以及插值器处理
    • 2.4 处理滑动时暂停自动切换的逻辑
    • 2.5 添加指针
  • 3.核心代码
    • 3.1 自定义属性
    • 3.2 自定义BannerView
    • 3.3 指针View
    • 3.4 XML adapter
  • 4.总结

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

    1.需求梳理

    下面是要实现的需求

    • 自动播放
    • 循环播放
    • 触摸暂停自动播放
    • 优化自动播放的时候页面切换的速度和插值器(未自定义属性)
    • 圆角/指针/矩形和圆形
    • 指针间距/指针位置

    即是要实现一个能自动,循环,且配置了圆形和矩形指针的控件

    2.实现路径

    整理下要实现的需求,自动,循环,触摸暂停,切换速度,指针样式,这些功能一步步分解实现.然后再结合成控件.

    实现组成:

    • Viewpager2(展示内容)
    • 自定义指针(指针)

    2.1 自动播放实现

    因为 用的是ViewPager2实现的此需求 所以自动播放的实现 定时调用切换Vp2 就可以了

    定时器实现多种多样可自己选择实现:

    • Handler
    • Timer
    • 协程+死循环
    // 协程作用域,使用 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) <= 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
    }
    

    2.2 循环播放

    循环播放是通过将条目数无限大 然后再根据具体的条目数算出来展示那条数据实现的

    /**
     * 开始自动轮播
     */
    fun startAutoScroll() {
        // 如果已经有轮播任务或者数据不足,则不启动
        if (bannerJob != null || (mAdapter?.itemCount ?: 0) <= 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, currentIte编程客栈m + 1, 800)
                }
            }
        }
    }
    
    
    
    class BannerAdapter(var mContext: Context,var radius:Int): BaseRvAdapter<BannerItem, ItemBannerBinding>() {
        override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int
        ): BaseRvViewHolder<ItemBannerBinding> {
            return BaseRvViewHolder(ItemBannerBinding.inflate(LayoutInflater.from(mContext),parent,false))
        }
    
        override fun onBindViewHolder(
            holder: BaseRvViewHolder<ItemBannerBinding>,
            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 > 1) Int.Companion.MAX_VALUE else getData().size
        }
    
    
    }
    

    2.3 Vp2切换动画速度以及插值器处理

    /**
     * 设置当前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 <= 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 ->
            val currentValue = animation.animatedValue as www.devze.comInt
            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()
    }
    

    2.4 处理滑动时暂停自动切换的逻辑

    Vp2 拦截onTouch事件 所以处理触摸滑动 无法直接实现 需要在父布局做拦截分发实现或者直接监听滑动状态 取消自动播放 这里选择后者

       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 > 0.5f) {
                        updateIndicatorStatus(nextPos)
                    } else {
                        updateIndicatorStatus(currentPos)
                    }
                }
    
            }
        })
    
    
    

    2.5 添加指针

    设置数据的时候添加指针

    /**
     * 设置 Banner 数据
     * @param data Banner 数据列表
     */
    fun setBannerData(data: List<BannerItem>) {
        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)
                    } else  if (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 > 0) lp.leftMargin = indicatorSpacing // 从第二个开始添加左间距
                binding.indicatorContainer.addView(indicator, lp)
    
            }
        }
    
    
        // 如果启用自动轮播且数据数量大于1,则开始轮播
        if (isAutoPlay && data.size > 1) {
            startAutoScroll()
        }
    }
    

    3.核心代码

    3.1 自定义属性

    <declare-styleable name="AutoBannerViewStyle">
        <!-- 轮播相关 -->
        <attr name="delayTime" format="integer" /> <!-- 轮播间隔(毫秒) -->
        <attr name="bannerCornerSize" format="dimension" /> <!-- 轮播图圆角大小 -->
        <attr name="isAutoPlay" format="bowww.devze.comolean" /> <!-- 是否自动轮播 -->
        <!-- 指示器位置:在ViewPager下方(默认)/与ViewPager底部对齐 -->
        <attr name="indicatorPosition" format="enum">
            <enum name="belowViewPager" value="0" /> <!-- 在ViewPager下方 -->
            <enum name="alignViewPagerBottom" value="1" /> <!-- 与ViewPager底部对齐 -->
        </attr>
    
        <attr name="indicatorGravity" format="enum">
            <enum name="left" value="0x03" />     <!-- Gravity.LEFT -->
            <enum name="center" value="0x01" />   <!-- Gravity.CENTER_HORIZONTAL -->
            <enum name="right" value="0x05" />    <!-- Gravity.RIGHT -->
            <enum name="start" value="0x800003" /> <!-- Gravity.START -->
            <enum name="end" value="0x800005" />   <!-- Gravity.END -->
        </attr>
        <!-- 指示器相关 -->
        <attr name="indicatorMargin" format="dimension" /> <!-- 指示器顶部边距(距离轮播图底部) -->
        <attr name="indicatorMarginSpacing" format="dimension" /> <!-- 指示器之间的间距 -->
        <attr name="indicatorStartSpacing" format="dimension" /> <!-- 指示器距离两边距离 -->
    
        <attr name="indicatorDefaultColor" format="color" /> <!-- 指示器默认颜色 -->
        <attr name="indicatorSelectedColor" format="color" /> <!-- 指示器选中颜色 -->
        <attr name="indicatorCustomWidth" format="dimension" /> <!-- 指示器宽度 -->
        <attr name="indicatorCustomHeight" format="dimension" /> <!-- 补充:指示器高度(可选) -->
        <attr name="indicatorCornerRadius" format="dimension" /> <!-- 补充:指示器圆角(可选) -->
        <attr name="indicatorType" format="enum">
            <enum name="rectangle" value="0" />
            <enum name="circle" value="1" />
            <enum name="none" value="2" />
        </attr>
    </declare-styleable>
    <!-- 指针自定义属性 -->
    <declare-styleable name="RoundedRectangleControl">
        <attr name="defaultColor" format="color" />
        <attr name="selectedColor" format="color" />
        <attr name="cornerIndicatorRadius" format="dimension" />
        <attr name="isSelected" format="boolean" />
        <attr name="indicatorPadding" format="dimension" />
        <attr name="indicatorSpacing" format="dimension" />
        <attr name="indicatorWidth" format="dimension" />  <!-- 指示器宽度 -->
        <attr name="indicatorHeight" format="dimension" /> <!-- 指示器高度 -->
        <attr name="indicatorShape" format="enum">
            <enum name="rectangle" value="0" />
            <enum name="circle" value="1" />
        </attr>
    </declare-styleable>
    

    3.2 自定义BannerView

    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_indicatorMpythonargin, 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 > 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<BannerItem>) {
            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)
                        } else  if (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 > 0) lp.leftMargin = indicatorSpacing // 从第二个开始添加左间距
                    binding.indicatorContainer.addView(indicator, lp)
    
                }
            }
    
    
            // 如果启用自动轮播且数据数量大于1,则开始轮播
            if (isAutoPlay && data.size > 1) {
                startAutoScroll()
            }
        }
    
        /**
         * 开始自动轮播
         */
        fun startAutoScroll() {
            // 如果已经有轮播任务或者数据不足,则不启动
            if (bannerJob != null || (mAdapter?.itemCount ?: 0) <= 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 && (mAdapter?.itemCount ?: 0) > 1) {
                startAutoScroll()
            }
        }
    
        override fun onDetachedFromWindow() {
            super.onDetachedFromWindow()
            // 视图从窗口分离时停止轮播
            stopAutoScroll()
        }
    
        override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
            super.onWindowFocusChanged(hasWindowFocus)
            // 窗口获得/失去焦点时控制轮播
            if (hasWindowFocus && isAutoPlay && (mAdapter?.itemCount ?: 0) > 1) {
                startAutoScroll()
            } else {
                stopAutoScroll()
            }
        }
    }
    

    3.3 指针View

    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) -> 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 -> Shape.CIRCLE
                    else -> 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 -> result = defaultSize
                // 父容器强制限制尺寸,使用限制值
                MeasureSpec.EXACTLY -> result = specSize
                // 父容器建议尺寸,取默认值与建议值中的较小者
                MeasureSpec.AT_MOST -> result = minOf(defaultSize, specSize)
            }
            return result
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            // 绘制区域(考虑内边距)
            // 根据形状选择绘制方式
            when (indicatorShape) {
                Shape.RECTANGLE -> drawRectangle(canvas)
                Shape.CIRCLE -> 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 -> {
                    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) -> 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
    }
    

    3.4 xml adapter

    layout_auto_banner.xml

    <?xml version="1.0" encoding="utf-8"?>
    <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">
    
        <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" />
    
        <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" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    item_banner.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
    
        android:layout_height="match_parent">
    
        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop" />
    </FrameLayout>
    

    BannerAdapter

    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<BannerItem, ItemBannerBinding>() {
        override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int
        ): BaseRvViewHolder<ItemBannerBinding> {
            return BaseRvViewHolder(ItemBannerBinding.inflate(LayoutInflater.from(mContext),parent,false))
        }
    
        override fun onBindViewHolder(
            holder: BaseRvViewHolder<ItemBannerBinding>,
            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 > 1) Int.Companion.MAX_VALUE else getData().size
        }
    
    
    }
    

    4.总结

    简单的实现了自动,循环播放的Banner,未处理定制Banner图片展示样式的处理.有需要,Banner样式以及指针样式可以自己定制修改 在添加指针和数据的地方传入特定的View 就可以了.有什么好的思路欢迎一起沟通进步,就这样,结束.

    以上就是Android实现自动循环播放轮播图(Banner)功能的详细内容,更多关于Android自动循环播放轮播图的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜