欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2025年07月04日 08:53:46   作者:沒有了遇見  
項(xiàng)目需要一個(gè)自動(dòng)且循環(huán)播放的輪播圖,忽然想起來原先都是搞個(gè)三方庫直接展示了,沒靜下心來搞過這個(gè)需求.趁此機(jī)會(huì),梳理實(shí)現(xiàn)了一下自動(dòng)且循環(huán)播放的輪播圖,需要的朋友可以參考下

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自定義View驗(yàn)證碼輸入框

    Android自定義View驗(yàn)證碼輸入框

    這篇文章主要為大家詳細(xì)介紹了自定義View驗(yàn)證碼輸入框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-04-04
  • 理解Android中的自定義屬性

    理解Android中的自定義屬性

    這篇文章主要介紹了理解Android中的自定義屬性,在android相關(guān)應(yīng)用開發(fā)過程中,固定的一些屬性可能滿足不了開發(fā)的需求,所以需要自定義控件與屬性,本文將以此問題進(jìn)行詳細(xì)介紹,需要的朋友可以參考下
    2016-01-01
  • Android震動(dòng)與提示音實(shí)現(xiàn)代碼

    Android震動(dòng)與提示音實(shí)現(xiàn)代碼

    這篇文章主要為大家詳細(xì)介紹了Android震動(dòng)與提示音實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • Android調(diào)用堆棧跟蹤實(shí)例分析

    Android調(diào)用堆棧跟蹤實(shí)例分析

    這篇文章主要介紹了Android調(diào)用堆棧跟蹤的方法,以實(shí)例形式較為詳細(xì)的分析了Android錯(cuò)誤信息分析的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-10-10
  • android利用消息機(jī)制獲取網(wǎng)絡(luò)圖片

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

    這篇文章主要介紹了Android實(shí)現(xiàn)面包屑功能的代碼(支持Fragment聯(lián)動(dòng)),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05
  • Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載

    Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載

    這篇文章主要為大家詳細(xì)介紹了Android控件PullRefreshViewGroup實(shí)現(xiàn)下拉刷新和上拉加載效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • Android 列表選擇框 Spinner詳解及實(shí)例

    Android 列表選擇框 Spinner詳解及實(shí)例

    這篇文章主要介紹了Android 列表選擇框 Spinner詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下
    2017-06-06
  • Android ListView分頁功能實(shí)現(xiàn)方法

    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)

    這篇文章主要介紹了Android studio配置國內(nèi)鏡像源的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11

最新評(píng)論