Android實(shí)現(xiàn)自動(dòng)循環(huán)播放輪播圖(Banner)功能

1.需求梳理
下面是要實(shí)現(xiàn)的需求
自動(dòng)播放循環(huán)播放- 觸摸暫停自動(dòng)播放
- 優(yōu)化自動(dòng)播放的時(shí)候頁面切換的
速度和插值器(未自定義屬性) - 圓角/指針/矩形和圓形
指針間距/指針位置
即是要實(shí)現(xiàn)一個(gè)能自動(dòng),循環(huán),且配置了圓形和矩形指針的控件
2.實(shí)現(xiàn)路徑
整理下要實(shí)現(xiàn)的需求,自動(dòng),循環(huán),觸摸暫停,切換速度,指針樣式,這些功能一步步分解實(shí)現(xiàn).然后再結(jié)合成控件.
實(shí)現(xiàn)組成:
- ViewPager2(展示內(nèi)容)
- 自定義指針(指針)
2.1 自動(dòng)播放實(shí)現(xiàn)
因?yàn)?用的是ViewPager2實(shí)現(xiàn)的此需求 所以自動(dòng)播放的實(shí)現(xiàn) 定時(shí)調(diào)用切換Vp2 就可以了
定時(shí)器實(shí)現(xiàn)多種多樣可自己選擇實(shí)現(xiàn):
- Handler
- Timer
- 協(xié)程+死循環(huán)
// 協(xié)程作用域,使用 Main 調(diào)度器
private val viewJob = SupervisorJob()
private val coroutineScope = CoroutineScope(viewJob + Dispatchers.Main)
// 輪播任務(wù)
private var bannerJob: Job? = null
/**
* 開始自動(dòng)輪播
*/
fun startAutoScroll() {
// 如果已經(jīng)有輪播任務(wù)或者數(shù)據(jù)不足,則不啟動(dòng)
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)
}
}
}
}
/**
* 停止自動(dòng)輪播
*/
fun stopAutoScroll() {
bannerJob?.cancel()
bannerJob = null
}
2.2 循環(huán)播放
循環(huán)播放是通過將條目數(shù)無限大 然后再根據(jù)具體的條目數(shù)算出來展示那條數(shù)據(jù)實(shí)現(xiàn)的
/**
* 開始自動(dòng)輪播
*/
fun startAutoScroll() {
// 如果已經(jīng)有輪播任務(wù)或者數(shù)據(jù)不足,則不啟動(dòng)
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)
// 處理?xiàng)l目切換 動(dòng)畫
MyPagerHelper.setCurrentItem(binding.viewPager, currentItem + 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 {
// 返回極大值,實(shí)現(xiàn)無限循環(huán)效果
return if (getData().size > 1) Int.Companion.MAX_VALUE else getData().size
}
}
2.3 Vp2切換動(dòng)畫速度以及插值器處理
/**
* 設(shè)置當(dāng)前Item 切換時(shí)長
* @param pager viewpager2
* @param item 下一個(gè)跳轉(zhuǎn)的item
* @param duration scroll時(shí)長
*/
fun setCurrentItem(pager: ViewPager2, item: Int, duration: Long) {
val currentItem = pager.currentItem
// 1. 目標(biāo)頁面與當(dāng)前頁面相同時(shí),直接返回,避免無效動(dòng)畫
if (item == currentItem) {
return
}
// 2. 處理 ViewPager2 未測量的情況(寬度為 0 時(shí),等待布局完成后再執(zhí)行)
val pagePxWidth = pager.width
if (pagePxWidth <= 0) {
pager.post { setCurrentItem(pager, item, duration) }
return
}
// 3. 計(jì)算需要拖拽的總像素(支持正向/反向滑動(dòng))
val pxToDrag = pagePxWidth * (item - currentItem)
// 4. 使用局部變量保存 previousValue,避免多實(shí)例共享沖突(核心優(yōu)化)
var previousValue = 0
val animator = ValueAnimator.ofInt(0, pxToDrag)
animator.addUpdateListener { animation ->
val currentValue = animation.animatedValue as Int
val currentPxToDrag = (currentValue - previousValue).toFloat()
// 調(diào)用 fakeDragBy 實(shí)現(xiàn)滑動(dòng)(注意負(fù)號(hào):模擬用戶拖拽方向)
pager.fakeDragBy(-currentPxToDrag)
previousValue = currentValue
}
animator.addListener(object : Animator.AnimatorListener {
private var isFakeDragStarted = false
override fun onAnimationStart(animation: Animator) {
// 開始假拖拽,標(biāo)記狀態(tài)
pager.beginFakeDrag()
isFakeDragStarted = true
}
override fun onAnimationEnd(animation: Animator) {
if (isFakeDragStarted) {
pager.endFakeDrag() // 結(jié)束假拖拽
isFakeDragStarted = false
}
}
override fun onAnimationCancel(animation: Animator) {
// 2. 動(dòng)畫取消時(shí)必須結(jié)束假拖拽,避免狀態(tài)殘留
if (isFakeDragStarted) {
pager.endFakeDrag()
isFakeDragStarted = false
}
}
override fun onAnimationRepeat(animation: Animator) {}
})
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = duration
animator.start()
}
2.4 處理滑動(dòng)時(shí)暫停自動(dòng)切換的邏輯
Vp2 攔截onTouch事件 所以處理觸摸滑動(dòng) 無法直接實(shí)現(xiàn) 需要在父布局做攔截分發(fā)實(shí)現(xiàn)或者直接監(jiān)聽滑動(dòng)狀態(tài) 取消自動(dòng)播放 這里選擇后者
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
// 用戶開始拖拽,暫停自動(dòng)播放
stopAutoScroll()
} else if (state == ViewPager2.SCROLL_STATE_IDLE) {
// 滑動(dòng)結(jié)束,恢復(fù)自動(dòng)播放
startAutoScroll()
}
}
// 處理Vp2切換的時(shí)候指針切換 onPageSelect 方法比較慢 在這里處理
override fun onPageScrolled(
position: Int, positionOffset: Float, positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
val indicatorCount = binding.indicatorContainer.childCount
if (indicatorCount == 0) return
// 計(jì)算當(dāng)前滑動(dòng)的兩個(gè)頁面對(duì)應(yīng)的指示器
val currentPos = position % indicatorCount
val nextPos = (position + 1) % indicatorCount
if (indicatorType!=2){
// 當(dāng)滑動(dòng)超過一半時(shí),提前更新指示器狀態(tài)
if (positionOffset > 0.5f) {
updateIndicatorStatus(nextPos)
} else {
updateIndicatorStatus(currentPos)
}
}
}
})
2.5 添加指針
設(shè)置數(shù)據(jù)的時(shí)候添加指針
/**
* 設(shè)置 Banner 數(shù)據(jù)
* @param data Banner 數(shù)據(jù)列表
*/
fun setBannerData(data: List<BannerItem>) {
if (data.isEmpty()) return
mAdapter?.setNewData(data.toMutableList())
// 計(jì)算初始位置,確??梢噪p向滾動(dòng)
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)
}
// 初始狀態(tài):第一個(gè)指示器選中
setSelectedStatus(i == initialPosition % data.size)
}
// 設(shè)置指示器間距(通過布局參數(shù))
val lp = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
)
if (i > 0) lp.leftMargin = indicatorSpacing // 從第二個(gè)開始添加左間距
binding.indicatorContainer.addView(indicator, lp)
}
}
// 如果啟用自動(dòng)輪播且數(shù)據(jù)數(shù)量大于1,則開始輪播
if (isAutoPlay && data.size > 1) {
startAutoScroll()
}
}
3.核心代碼
3.1 自定義屬性
<declare-styleable name="AutoBannerViewStyle">
<!-- 輪播相關(guān) -->
<attr name="delayTime" format="integer" /> <!-- 輪播間隔(毫秒) -->
<attr name="bannerCornerSize" format="dimension" /> <!-- 輪播圖圓角大小 -->
<attr name="isAutoPlay" format="boolean" /> <!-- 是否自動(dòng)輪播 -->
<!-- 指示器位置:在ViewPager下方(默認(rèn))/與ViewPager底部對(duì)齊 -->
<attr name="indicatorPosition" format="enum">
<enum name="belowViewPager" value="0" /> <!-- 在ViewPager下方 -->
<enum name="alignViewPagerBottom" value="1" /> <!-- 與ViewPager底部對(duì)齊 -->
</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>
<!-- 指示器相關(guān) -->
<attr name="indicatorMargin" format="dimension" /> <!-- 指示器頂部邊距(距離輪播圖底部) -->
<attr name="indicatorMarginSpacing" format="dimension" /> <!-- 指示器之間的間距 -->
<attr name="indicatorStartSpacing" format="dimension" /> <!-- 指示器距離兩邊距離 -->
<attr name="indicatorDefaultColor" format="color" /> <!-- 指示器默認(rèn)顏色 -->
<attr name="indicatorSelectedColor" format="color" /> <!-- 指示器選中顏色 -->
<attr name="indicatorCustomWidth" format="dimension" /> <!-- 指示器寬度 -->
<attr name="indicatorCustomHeight" format="dimension" /> <!-- 補(bǔ)充:指示器高度(可選) -->
<attr name="indicatorCornerRadius" format="dimension" /> <!-- 補(bǔ)充:指示器圓角(可選) -->
<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
/**
* 自動(dòng)輪播 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)
// 協(xié)程作用域,使用 Main 調(diào)度器
private val viewJob = SupervisorJob()
private val coroutineScope = CoroutineScope(viewJob + Dispatchers.Main)
// 輪播任務(wù)
private var bannerJob: Job? = null
// Banner 適配器
private var mAdapter: BannerAdapter? = null
// 輪播配置參數(shù)
private var delayMillis = 3000 // 輪播間隔時(shí)間(毫秒)
private var cornerSize = 20 // 圓角大?。╠p)
private var isAutoPlay = true // 是否自動(dòng)輪播
// 指示器配置參數(shù)(從自定義屬性獲?。?
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() // 指示器默認(rèn)顏色
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底部對(duì)齊(默認(rèn)false:在下方)
private var indicatorGravity = 2 // 指針內(nèi)容位置
private var indicatorType = 2 // 指針樣式 0 時(shí)矩形 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
//指針內(nèi)容位置
indicatorGravity = getInt(R.styleable.AutoBannerViewStyle_indicatorGravity, Gravity.CENTER)
// 指針類型
indicatorType = getInt(R.styleable.AutoBannerViewStyle_indicatorType, 2)
// 切換是時(shí)間
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))
//是否自動(dòng)播放
isAutoPlay = getBoolean(R.styleable.AutoBannerViewStyle_isAutoPlay, true)
// 指示器樣式相關(guān)
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()
}
}
}
/**
* 核心:修改約束實(shí)現(xiàn)位置切換
*/
private fun updateIndicatorPosition(alignBottom: Boolean) {
// 獲取兩者的布局參數(shù)(約束布局參數(shù))
val viewPagerLp = binding.viewPager.layoutParams as ConstraintLayout.LayoutParams
val indicatorLp = binding.indicatorContainer.layoutParams as ConstraintLayout.LayoutParams
if (alignBottom) {
// 場景2:與ViewPager底部對(duì)齊(在ViewPager內(nèi)部底部)
// 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 // 可根據(jù)需求添加與父容器底部的間距
}
} 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
}
// 應(yīng)用修改后的約束
binding.viewPager.layoutParams = viewPagerLp
}
/**
* 初始化視圖
*/
private fun initView() {
updateIndicatorPosition(isAlignViewPagerBottom)
mAdapter = BannerAdapter(context, cornerSize)
binding.viewPager.offscreenPageLimit = 3
binding.viewPager.adapter = mAdapter
// 設(shè)置初始位置,實(shí)現(xiàn)無限輪播效果
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) {
// 用戶開始拖拽,暫停自動(dòng)播放
stopAutoScroll()
} else if (state == ViewPager2.SCROLL_STATE_IDLE) {
// 滑動(dòng)結(jié)束,恢復(fù)自動(dòng)播放
startAutoScroll()
}
}
override fun onPageScrolled(
position: Int, positionOffset: Float, positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
val indicatorCount = binding.indicatorContainer.childCount
if (indicatorCount == 0) return
// 計(jì)算當(dāng)前滑動(dòng)的兩個(gè)頁面對(duì)應(yīng)的指示器
val currentPos = position % indicatorCount
val nextPos = (position + 1) % indicatorCount
if (indicatorType!=2){
// 當(dāng)滑動(dòng)超過一半時(shí),提前更新指示器狀態(tài)
if (positionOffset > 0.5f) {
updateIndicatorStatus(nextPos)
} else {
updateIndicatorStatus(currentPos)
}
}
}
})
}
var curPosition = 0
// 抽取通用的更新方法
private fun updateIndicatorStatus(selectPosition: Int) {
if (selectPosition == curPosition) return // 避免重復(fù)更新
binding.indicatorContainer.post {
(binding.indicatorContainer.getChildAt(
curPosition
) as? RoundedRectangleIndicatorView)?.setSelectedStatus(false)
(binding.indicatorContainer.getChildAt(
selectPosition
) as? RoundedRectangleIndicatorView)?.setSelectedStatus(true)
curPosition = selectPosition
}
}
/**
* 設(shè)置 Banner 數(shù)據(jù)
* @param data Banner 數(shù)據(jù)列表
*/
fun setBannerData(data: List<BannerItem>) {
if (data.isEmpty()) return
mAdapter?.setNewData(data.toMutableList())
// 計(jì)算初始位置,確??梢噪p向滾動(dòng)
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)
}
// 初始狀態(tài):第一個(gè)指示器選中
setSelectedStatus(i == initialPosition % data.size)
}
// 設(shè)置指示器間距(通過布局參數(shù))
val lp = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
)
if (i > 0) lp.leftMargin = indicatorSpacing // 從第二個(gè)開始添加左間距
binding.indicatorContainer.addView(indicator, lp)
}
}
// 如果啟用自動(dòng)輪播且數(shù)據(jù)數(shù)量大于1,則開始輪播
if (isAutoPlay && data.size > 1) {
startAutoScroll()
}
}
/**
* 開始自動(dòng)輪播
*/
fun startAutoScroll() {
// 如果已經(jīng)有輪播任務(wù)或者數(shù)據(jù)不足,則不啟動(dòng)
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)
}
}
}
}
/**
* 停止自動(dòng)輪播
*/
fun stopAutoScroll() {
bannerJob?.cancel()
bannerJob = null
}
/**
* 釋放資源
*/
fun release() {
stopAutoScroll()
coroutineScope.cancel()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// 視圖附加到窗口時(shí),如果啟用了自動(dòng)輪播,則啟動(dòng)
if (isAutoPlay && (mAdapter?.itemCount ?: 0) > 1) {
startAutoScroll()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 視圖從窗口分離時(shí)停止輪播
stopAutoScroll()
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
// 窗口獲得/失去焦點(diǎn)時(shí)控制輪播
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) {
// 默認(rèn)屬性值
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
// 新增:寬高相關(guān)屬性
private var indicatorWidth = 24f // 指示器默認(rèn)寬度
private var indicatorHeight = 8f // 指示器默認(rèn)高度
// 畫筆
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
// 繪制區(qū)域
private val rect = RectF()
// 點(diǎn)擊監(jiān)聽器
private var onStateChangeListener: ((Boolean) -> Unit)? = null
private var indicatorShape = Shape.RECTANGLE // 默認(rèn)矩形
// 新增:形狀枚舉
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
}
/**
* 測量控件尺寸
* 優(yōu)先使用XML中設(shè)置的尺寸,若無則使用默認(rèn)寬高
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 計(jì)算測量后的寬高(考慮父容器限制)
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)
}
}
/**
* 輔助計(jì)算測量尺寸
* @param defaultSize 控件默認(rèn)尺寸
* @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) {
// 父容器未限制尺寸,使用默認(rèn)值
MeasureSpec.UNSPECIFIED -> result = defaultSize
// 父容器強(qiáng)制限制尺寸,使用限制值
MeasureSpec.EXACTLY -> result = specSize
// 父容器建議尺寸,取默認(rèn)值與建議值中的較小者
MeasureSpec.AT_MOST -> result = minOf(defaultSize, specSize)
}
return result
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 繪制區(qū)域(考慮內(nèi)邊距)
// 根據(jù)形狀選擇繪制方式
when (indicatorShape) {
Shape.RECTANGLE -> drawRectangle(canvas)
Shape.CIRCLE -> drawCircle(canvas)
}
}
/**
* 繪制圓角矩形
*/
private fun drawRectangle(canvas: Canvas) {
// 繪制區(qū)域(考慮內(nèi)邊距)
rect.set(
indicatorPadding,
indicatorPadding,
width.toFloat() - indicatorPadding,
height.toFloat() - indicatorPadding
)
// 根據(jù)選中狀態(tài)設(shè)置背景色
backgroundPaint.color = if (isSelectedState) selectedBackgroundColor else defaultBackgroundColor
// 繪制圓角矩形
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, backgroundPaint)
}
// 新增:設(shè)置形狀
fun setIndicatorShape(shape: Shape) {
if (indicatorShape != shape) {
indicatorShape = shape
requestLayout() // 可能需要重新調(diào)整尺寸
invalidate() // 重新繪制
}
}
/**
* 繪制圓形
*/
private fun drawCircle(canvas: Canvas) {
// 計(jì)算圓心和半徑(考慮內(nèi)邊距)
val centerX = width / 2f
val centerY = height / 2f
val radius = minOf(width, height) / 2f - indicatorPadding
// 根據(jù)選中狀態(tài)設(shè)置背景色
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
}
// 新增:動(dòng)態(tài)設(shè)置指示器寬度
fun setIndicatorWidth(width: Float) {
if (indicatorWidth != width) {
indicatorWidth = width
// 觸發(fā)重新測量和繪制
requestLayout() // 重新計(jì)算尺寸
invalidate() // 重新繪制
}
}
// 新增:動(dòng)態(tài)設(shè)置指示器高度
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 {
// 返回極大值,實(shí)現(xiàn)無限循環(huán)效果
return if (getData().size > 1) Int.Companion.MAX_VALUE else getData().size
}
}
4.總結(jié)
簡單的實(shí)現(xiàn)了自動(dòng),循環(huán)播放的Banner,未處理定制Banner圖片展示樣式的處理.有需要,Banner樣式以及指針樣式可以自己定制修改 在添加指針和數(shù)據(jù)的地方傳入特定的View 就可以了.有什么好的思路歡迎一起溝通進(jìn)步,就這樣,結(jié)束.
以上就是Android實(shí)現(xiàn)自動(dòng)循環(huán)播放輪播圖(Banner)功能的詳細(xì)內(nèi)容,更多關(guān)于Android自動(dòng)循環(huán)播放輪播圖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android震動(dòng)與提示音實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android震動(dòng)與提示音實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
android利用消息機(jī)制獲取網(wǎng)絡(luò)圖片
這篇文章主要為大家詳細(xì)介紹了android利用消息機(jī)制獲取網(wǎng)絡(luò)圖片的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android實(shí)現(xiàn)面包屑功能的代碼(支持Fragment聯(lián)動(dòng))
這篇文章主要介紹了Android實(shí)現(xiàn)面包屑功能的代碼(支持Fragment聯(lián)動(dòng)),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05
Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載
這篇文章主要為大家詳細(xì)介紹了Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android 列表選擇框 Spinner詳解及實(shí)例
這篇文章主要介紹了Android 列表選擇框 Spinner詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android ListView分頁功能實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android ListView分頁功能的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05
Android studio配置國內(nèi)鏡像源的實(shí)現(xiàn)
這篇文章主要介紹了Android studio配置國內(nèi)鏡像源的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11

