【Android自定义view】指示器IndicatorView,可搭配ViewPager、轮播图使用

80 阅读4分钟

前言

在模仿荣耀手机上的天气App时,天气界面底部的指示器小圆点用原生TabLayout控件实现较为复杂,于是自己用自定义view的方式简单实现了一个。


一、实现方案

1. 原理

确定ViewPager2包含fragment的总数量和fragment当前的位置,切换页面时重新绘制指示器样式。

2. 直接继承 view的方式,复杂度不高,只需要重写onMeasure方法和onDraw方法。

onMeasure方法中实现了根据控件宽高和指示器个数动态计算圆点的直径

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mIndicatorWidth = MeasureSpec.getSize(widthMeasureSpec)
        mIndicatorHeight = MeasureSpec.getSize(heightMeasureSpec)
        // item宽度 = 指示器宽度 / (item个数 + 间隔个数)
        mItemWidth = mIndicatorWidth.div(mIndicatorItemCount + mIndicatorItemCount - 1)
        // item高度 = item宽度和指示器高度中的小值,避免绘制不全
        mItemHeight = mItemWidth.coerceAtMost(mIndicatorHeight)
        // 绘制item的起始位置 = 指示器宽度/2 - 绘制区域/2,保持绘制区域居中显示
        mStartPos =
            mIndicatorWidth.div(2f) - ((mIndicatorItemCount + mIndicatorItemCount - 1) * mItemHeight).div(
                2f
            )
        // 不需要改变原控件大小,此处不需要重绘
//        setMeasuredDimension(mIndicatorWidth, mIndicatorHeight)
    }

这里绘制的是圆点指示器,其余形状的大家有兴趣可以自己实现。

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val dy = mIndicatorHeight.div(2f)
        // 圆半径
        val cr = mItemHeight.div(2f)
        for (i in 0 until mIndicatorItemCount) {
            // 指示器为圆形
            mIndicatorItemDistance = mItemHeight
            // 动态计算每个item的起始绘制位置
            val dx = mStartPos + i * mItemHeight + i * mIndicatorItemDistance + cr
            // item选中态在大小和颜色上有所不同
            canvas.drawCircle(
                dx,
                dy,
                if (i == mCurrentSelectedPosition) cr else cr.div(1.5f),
                if (i == mCurrentSelectedPosition) mSelectedPaint else mUnSelectedPaint
            )
        }
    }

3. 添加自定义属性attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IndicatorView">
        <!--指示器选中颜色-->
        <attr name="colorSelected" format="color|reference"/>
        <!--指示器未选中颜色-->
        <attr name="colorUnSelected" format="color|reference"/>
    </declare-styleable>
</resources>

二、使用步骤

1.在xml定义控件

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/weather_vp2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/vp2_indicator"
        app:layout_constraintTop_toBottomOf="@id/main_toolbar"
        android:layout_marginBottom="12dp" />

    <com.kkw.smallweather.view.IndicatorView
        android:id="@+id/vp2_indicator"
        android:layout_width="match_parent"
        android:layout_height="12dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:colorUnSelected="#60efefef"
        android:layout_marginBottom="12dp" />

2.在代码中调用

        // 设置指示器个数
        mBinding.vp2Indicator.setIndicatorItemCount(vp2Adapter.itemCount)
        // 监听vp2界面变化
        mBinding.weatherVp2.registerOnPageChangeCallback(object :
            ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                // 显示在哪个页面就重绘对应的指示器
                mBinding.vp2Indicator.setCurrentSelectedPosition(mBinding.weatherVp2.currentItem)
                mBinding.vp2Indicator.postInvalidate()
            }
        })

三、附上完整代码

/**
 * 自定义指示器圆点样式
 * @author kkw
 */
class IndicatorView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 指示器容器 宽/高
    private var mIndicatorWidth = 0
    private var mIndicatorHeight = 0

    // 指示器item 宽/高
    private var mItemWidth = 0
    private var mItemHeight = 0

    // 指示器item的间隔
    private var mIndicatorItemDistance = 0

    // 指示器item的个数
    private var mIndicatorItemCount = 0

    // 首个item的起点
    private var mStartPos = 0f

    // item画笔 选中态/未选中态
    private val mSelectedPaint: Paint = Paint()
    private val mUnSelectedPaint: Paint = Paint()

    // item画笔颜色 选中态/未选中态
    private var mColorSelected = Color.WHITE
    private var mColorUnSelected = Color.GRAY

    // 当前选中的位置
    private var mCurrentSelectedPosition = 0

    // item是否为圆点
    private var isCircle = true

    init {
        // 自定义属性
        val a = context.obtainStyledAttributes(attrs, R.styleable.IndicatorView)
        mColorSelected = a.getColor(R.styleable.IndicatorView_colorSelected, Color.WHITE)
        mColorUnSelected = a.getColor(R.styleable.IndicatorView_colorUnSelected, Color.GRAY)
        a.recycle()

        // 配置paint画笔
        mSelectedPaint.apply {
            style = Paint.Style.FILL
            isAntiAlias = true
            color = mColorSelected
        }

        mUnSelectedPaint.apply {
            style = Paint.Style.FILL
            isAntiAlias = true
            color = mColorUnSelected
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mIndicatorWidth = MeasureSpec.getSize(widthMeasureSpec)
        mIndicatorHeight = MeasureSpec.getSize(heightMeasureSpec)
        // item宽度 = 指示器宽度 / (item个数 + 间隔个数)
        mItemWidth = mIndicatorWidth.div(mIndicatorItemCount + mIndicatorItemCount - 1)
        // item高度 = item宽度和指示器高度中的小值,避免绘制不全
        mItemHeight = mItemWidth.coerceAtMost(mIndicatorHeight)
        // 绘制item的起始位置 = 指示器宽度/2 - 绘制区域/2,保持绘制区域居中显示
        mStartPos =
            mIndicatorWidth.div(2f) - ((mIndicatorItemCount + mIndicatorItemCount - 1) * mItemHeight).div(
                2f
            )
        // 不需要改变原控件大小,此处不需要重绘
//        setMeasuredDimension(mIndicatorWidth, mIndicatorHeight)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val dy = mIndicatorHeight.div(2f)
        // 圆半径
        val cr = mItemHeight.div(2f)
        for (i in 0 until mIndicatorItemCount) {
            // 指示器为圆形
            mIndicatorItemDistance = mItemHeight
            // 动态计算每个item的起始绘制位置
            val dx = mStartPos + i * mItemHeight + i * mIndicatorItemDistance + cr
            // item选中态在大小和颜色上有所不同
            canvas.drawCircle(
                dx,
                dy,
                if (i == mCurrentSelectedPosition) cr else cr.div(1.5f),
                if (i == mCurrentSelectedPosition) mSelectedPaint else mUnSelectedPaint
            )
        }
    }

    /**
     * 控制指示器显示隐藏
     */
    private fun indicatorVisibility() {
        if (mCurrentSelectedPosition >= mIndicatorItemCount) {
            mCurrentSelectedPosition = mIndicatorItemCount - 1
        }
        // 小于1个不显示
        visibility = if (mIndicatorItemCount <= 1) GONE else VISIBLE
    }

    /**
     * 设置指示器item个数
     */
    fun setIndicatorItemCount(count: Int) {
        mIndicatorItemCount = count
        indicatorVisibility()
    }

    /**
     * 设置当前位置
     */
    fun setCurrentSelectedPosition(pos: Int) {
        this.mCurrentSelectedPosition = pos
    }
}

四、实现效果

上面是荣耀天气,下面是实现效果 ![华为荣耀天气](img-blog.csdnimg.cn/c2496ed9b8c… =100x) ![测试天气](img-blog.csdnimg.cn/34b43460380… =100x)


总结

本文只是根据需求简单实现了一个圆形的方案,大家可自行扩展。 IndicatorView不止可搭配ViewPager2使用,水平切换的场景下都可以。

版权声明:本文为CSDN博主「kinsrain」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/weixin_4224…