建立自己的王国:Android 自定义封装View(2)

指示器(IndicatorView)
场景:Fragment的指示器,虽然有很多IndicatorView可以使用,但是自给自足还是好的。
国际惯例:效果


1.png

思路:
View里通过canvas.drawCircle(x坐标,y坐标,半径,Paint)来达到画圆的目的。
需要解决的问题是按照不同的数量,要计算出圆的具体坐标。
探索:

  1. 利用canvas的宽度等分,高度在中间。
    一只圆的时候:


    circle1.png

    圆刚好在中间。
    结论: x=View的宽度/2,y=View的高度/2

两只圆的时候:


circle2.png

可以得出:x1=View的宽度/3, y1=View的高度/2
x2=2*View的宽度/3,y2=View的高度/2

那么总体的坐标公式为:
canvas?.drawCircle(i * mWidth / (count + 1).toFloat(), mHeight / 2F, radius, mPaint)

实现:

  1. 定义需要的自定义Styleable属性:
  <!--    IndicatorView-->
      <!--    IndicatorView-->
    <declare-styleable name="IndicatorView">
<!--        总数-->
        <attr name="indicatorCount" format="integer" />
<!--        指示器位置-->
        <attr name="indicatePosition" format="integer" />
<!--        指示器半径-->
        <attr name="indicatorRadius" format="float" />
<!--        指示器主颜色-->
        <attr name="indicateMainColor" format="color" />
<!--        指示器副颜色-->
        <attr name="indicateSecondColor" format="color" />
<!--        view背景-->
        <attr name="android:background" format="color" />
    </declare-styleable>

2.自定义View主结构:
必须重写的方法类

class IndicatorView : View {
   //实例化时
   constructor(mContext: Context) : super(mContext) {
        initView(mContext, null)
    }

//sdk 24以上时调用
    constructor(mContext: Context, attributes: AttributeSet) : super(mContext, attributes) {
        initView(mContext, attributes)
    }
//如果有主题时
    constructor(mContext: Context, attributes: AttributeSet, theme: Int) : super(
        mContext,
        attributes,
        theme
    ) {
        initView(mContext, attributes)
    }
  1. 在InitView方法里解析sylable:
    /**
       *   mainColor 主颜色
       */
    private var mainColor by Delegates.notNull<Int>()
    /**
     * Second color 副颜色
     */
    private var secondColor by Delegates.notNull<Int>()
    /**
     * Count 总数
     */
    var count by Delegates.notNull<Int>()
     /**
      *position 当前位置
      */
    var position: Int = 0

    private lateinit var mPaint: Paint

    /**
     * Bg color 画布背景颜色
     */
    private var bgColor by Delegates.notNull<Int>()

    /**
     * Radius 需要的圆的背景。
     */
    private var radius by Delegates.notNull<Float>()

    /**
     * M width 所要的宽度
     */
    private var mWidth by Delegates.notNull<Int>()

    /**
     * M height 所需高度
     */
    private var mHeight by Delegates.notNull<Int>()

 private fun initView(mContext: Context, attributes: AttributeSet?) {
       初始化
        mPaint = Paint()
        val ta = mContext.obtainStyledAttributes(attributes, R.styleable.IndicatorView)
        mainColor = ta.getColor(R.styleable.IndicatorView_indicateMainColor, Color.RED)
        secondColor = ta.getColor(R.styleable.IndicatorView_indicateSecondColor, Color.GRAY)
        position = ta.getInt(R.styleable.IndicatorView_indicatePosition, 1)
        bgColor = ta.getColor(R.styleable.IndicatorView_android_background, Color.WHITE)
        radius = ta.getFloat(R.styleable.IndicatorView_indicatorRadius, 10F)
        count = ta.getInt(R.styleable.IndicatorView_indicatorCount, 3)
        /*    //判断总数不要0
            if (count==0){
                throw  IllegalArgumentException("Can not set 0 to the Total")
            }
            //判断position不能大于总数
            if (count < position){
                throw IllegalArgumentException("Position must be greater than Count")
            }*/
        //回收
        ta.recycle()
    }

注意:初始化的时候必须走查逻辑,保证不能出现总数为0,或者当前位置超过总数的情况,也可以Throw Exception来抛出异常。

重写OnDraw方法,按照总数,指示器位置等信息画圆。

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        mPaint.isAntiAlias = true
        //画背景先
        canvas?.drawColor(bgColor)
        //遍历时画图
        //保证不能0
        if (count != 0) {
            for (i: Int in 1..count) {
                //如果是当前位置,颜色要MainColor。
                if (position == i) {
                    mPaint.color = mainColor
                } else {
                    //如果普通位置,用副颜色。
                    mPaint.color = secondColor
                }
                //画圆,参数中高度要刚好在View中间。
                canvas?.drawCircle(i * mWidth / (count + 1).toFloat(), mHeight / 2F, radius, mPaint)
            }
        }
        canvas?.save()
    }

需要计算mWidth,mHeight的值,在onMeasure的时候,重写。

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
      //宽度的设定为:全部圆的直径+圆之间的空间距离。可以灵活设定。
        mWidth = getMySize(30 * (count + 2), widthMeasureSpec)
       //高度就20dp(圆的直径),
        mHeight = getMySize(20, heightMeasureSpec)
      //使用上面的值作为view的宽高。
        setMeasuredDimension(mWidth, mHeight)
    }

分别对应View的三个模式(UNSPASSIFIED, EXACTLY, ATMOST)进行不同的设计。
我的是:

    private fun getMySize(size: Int, measureSpec: Int): Int {
        var result = size
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        when (specMode) {
                //未指定或 wrap_content的时候,使用计算出来的值。
            MeasureSpec.UNSPECIFIED,
            MeasureSpec.AT_MOST -> {
                result = size
                return result
            }
                //Match_parent的时候.
            MeasureSpec.EXACTLY -> {
                result = specSize
                return result
            }
        }
        return result
    }

最后写一个public方法,让外部更新View的当前指示器位置(状态)

  /**
     * Update position
     *
     * @param mPosition 需要更新的位置
     */
    fun updatePosition(mPosition: Int) {
      //判断一下,免得超过了总数.
        position = if (mPosition <= count) {
            mPosition
        } else {
            1
        }
        //重绘
        this.postInvalidate()
    }

Github直通车 https://github.com/Neo-Turak/learning

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容