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.总结
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)其它相关文章!
精彩评论