Android 自定义属性,自定义控件、自定义View以及View的常见Error

Android 自定义属性,自定义控件、自定义View以及View的常见Error


自定义属性

1 要使用 系统的属性
可以使用所继承的控件的属性 如继承的TextView 或者 View 则相关的属性都可以不用声明直接使用

2 否则 要styleable中定义

 <attr name="android:text" />

但是不用指定 format

3 在attrs.xml 中 编写 styleable和item等标签元素

在xml布局文件中使用该属性

在 java类中 通过TypedArray获取

attributeSet 可以获取 属性的名称和值
但是 若是属性值为 引用(如 @string @dimen)则得到的值为 值的id而非值
这样就需要根据ID再获取值

而通过TypedArray则简化了该步骤
直接用

TypedArray typedArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.xxx);

这里的XXX 用的是 styleable的name + _ + 属性名
如:CircleProgressBar_hint


自定义控件

流程:
1 styleable 定义 xml中自定义组件使用的一些属性 如 颜色 文字等
2 自定义xml文件 为自定义组件的样式
3 自定义java文件 设置组件的 文字 颜色 等
4 组件名称即为包名+java类名

其实3 4 就可以实现控件了
只是要在 xml中就初始化设置值 就得再styleable中定义

实例见:Android自定义TopBar


自定义View


xml中直接用包名+类名使用
而java 直接创建对象使用
那些属性则 用 style.xml设置,stylable的名称建议与java的类名一致


注意:自定义View的构造方法不能少了 attributeSet: AttributeSet 这个参数!!!!!!!!!!!!


避免 LayoutInflater.from(context).inflate(R.layout.template01, this) 传null
这样会导致没有LayoutParams,最终viewgroup会给你生成一个默认的设置
替代View 为 RelativeLayout 用this
或者 自己制定一个root


View是可以有一个onDraw方法 提供了 画布的 可以在画布上绘制自己想要的内容
尽量别用onDraw方法去 绘制View 除非是自定义的 比较复杂的 drawBitmap drawText直接放在组件上就好


当不需要java类去初始化 操作一些东西 只有xml界面 就可以 用< include> 标签否则就自定义View用 <包名.类名>


问题1:oncreate中获取view.getMeasuredHeight 会为0
解决:手动调用测量方法


问题2:java.lang.ClassCastException: android.widget.RelativeLayout cannot be cast to cn.xxx.xxxxxx.View.Template01
Template01也是 继承 RelativeLayout的 为啥cast不了?
解决: 在 xml的RelativeLayout中 加context tools:context=”cn.sihao.mainfunctionmodule.View.Template01”
即可


问题3:java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child’s parent first.
原因:因为 这个view已经有了 父亲 得把他解除绑定才能 让他去加入新的布局


自定义View经验:

1 initAttrs(attrs)
先定好属性,长宽,颜色,半径,各种值等等 哪些是要xml设置的 哪些是根据 sizeChanged设置的
哪些是xml可以设置 可以不设置的(有默认值的)

2 initPaint()
定好形状 定好画笔 画圆内 还是 圆弧 尾部是否圆角 画笔颜色 宽度 是否抗锯齿等等
如 对于这种:
画两次drawCircle 一个的画笔是STROKE的 一个是FILL的
而要画不是完整360度的圆弧,则用drawArc

3 initObject()
初始化对象 一般如 圆心点 Point 矩形 RectF 等等
并且该调用在 initPaint() 之前

4 onMeasure
要用setMeasuredDimension设置好尺寸

setMeasuredDimension(getMeasureSize(true, widthMeasureSpec), getMeasureSize(false, heightMeasureSpec))

一般是固定的写法 当然可以根据需要更改


/**
     * 获取View尺寸
     *
     * @param isWidth 是否是width,不是的话,是height
     */
    private fun getMeasureSize(isWidth: Boolean, measureSpec: Int): Int {

        var result = 0

        val specSize = View.MeasureSpec.getSize(measureSpec)
        val specMode = View.MeasureSpec.getMode(measureSpec)

        when (specMode) {
            View.MeasureSpec.UNSPECIFIED -> result = if (isWidth) {
                suggestedMinimumWidth
            } else {
                suggestedMinimumHeight
            }
            View.MeasureSpec.AT_MOST -> result = if (isWidth)
                Math.min(specSize, mWidth)
            else
                Math.min(specSize, mHeight)
            View.MeasureSpec.EXACTLY -> result = specSize
        }
        return result
    }

5 onSizeChanged
定实际的大小 半径 这个方法在第一次初始化时会调用一次 并且在 View的大小更改了也会再调用
尽量通过 控件之间的固定比例来设置大小 而不是 让用户去设置太多的大小(除非需要)
这里设置的大小是会随View大小变化而变化的 在xml中设置的值这是固定的

6 onDraw 进行View的绘制
canvas.drawArc .drawCircle .drawLine 等等

7 加载定时器 或者属性动画 调用 invalidate或者postInvalidate 进行onDraw方法的调用 进行View的更新
如:mScheduledCalTime.scheduleAtFixedRate

8 onDetachedFromWindow (重要) 一定别漏了
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
removeCallbacks(mScheduledCalTimeRunnable)
}
进行 定时器或者属性动画的关闭


取图片的色值
用android-studio 导入图片 左上角有个取色器的 可以获取到 #ARGB的值

只刷新指定的部分
invalidate或者postInvalidate中设置参数可以设置一个矩形区域指定要刷新的地方
不写参数则默认刷新整个自定义View 当然 若View没有显示或者没有改变 也是不会刷新的


实践:

贝塞尔曲线

// 画线
canvas.drawLine(起点x,起点y,终点x,终点y)

// 将画笔移至某点 
mpath.moveTo(x,y)   
// 结合 moveTo 画出一条直线 若无moveTo则默认从 0,0点开始
mPath.lineTo(x,y) 

// x1,y1 为控制点,x2,y2为结束数据点    
// 结合 moveTo设置数据点起点 用于绘制二阶贝塞尔曲线 
mpath.quadTo(x1,y1,x2,y2)   

//x1,y1 , x2,y2 为控制点 x3,y3 为数据点结束点
//也是结合 moveTo设置数据点起点 ,用于绘制 三阶贝塞尔曲线
mPath.cubicTo(x1,y1,x2,y2,x3,y3) 

//x1,y1起点坐标 宽度 高度  画矩形
mRecF = new RectF(x1,y1,width,height)

//ovalRectF 为圆或者椭圆的矩形 startAngle 开始角度 sweepAngle 为结束角度
//手机坐标 起点是 左上角  x轴为横向 指向右侧 y轴为纵向 指向下侧
//起点 是从 x轴正方向开始   然后绕着 顺时针方向  即如果 是0,90 则所绘制的圆弧为右下角
mPath.arcTo(ovalRectF,startAngle,sweepAngle)

// 最后 绘制出路径
canvas.drawPath(mpath,mPaint);

实例之 自定义时钟:

主要就是利用三角函数 进行 时分秒针的位置定位

遇到的问题:
1 圆四个顶点有缺角:
因为 半径是不包括圆弧宽度的,因此 若圆弧宽度比较大,则会显示不全,因此在计算半径时要减去圆弧宽度*2

<declare-styleable name="MyCustomClockView">
        <attr name="outerMostArcColor" />
        <attr name="innerMostArcColor" />

        <attr name="outerMostArcWidth" format="dimension" />

        <attr name="innerMostCircleColor" />

        <attr name="hourPointerWidth" format="dimension" />
        <attr name="minutePointerWidth" format="dimension" />
        <attr name="secondPointerWidth" format="dimension" />

        <attr name="hourPointerColor" />
        <attr name="minutePointerColor" />
        <attr name="secondPointerColor" />

    </declare-styleable>

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import com.orhanobut.logger.Logger
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit

/**
 * 自定义时钟View
 */
class MyCustomClockView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private var mContext: Context? = null

    /** 控件宽高 默认120dp */
    private var mWidth: Int = dp2Px(120)
    private var mHeight: Int = dp2Px(120)

    /** 圆心位置 */
    private var mCenterPoint: Point? = null
    /** 外圆的内接矩形 */
    private var mOutermostRectF: RectF? = null
    /** 内圆的内接矩形 */
    private var mInnermostRectF: RectF? = null

    /** 最外圈的圆的半径 */
    private var mOutermostCircleRadius: Float = 0f
    /** 最内圈的圆的半径 */
    private var mInnermostCircleRadius: Float = 0f

    /** 最外圈的圆弧宽度 */
    private var mOutermostArcWidth: Float? = null
    /** 最内圈的圆弧宽度 */
    private var mInnermostArcWidth: Float? = null

    /** 最外圈的圆弧的颜色 */
    private var mOutermostArcColor: Int? = null
    /** 最内圈的圆弧的颜色 */
    private var mInnermostArcColor: Int? = null

    /** 最内圈的圆内的填充颜色 */
    private var mInnermostCircleColor: Int? = null


    /** 时针,分针,秒针的长度 */
    private var mHourPointerLength: Float = 0f
    private var mMinutePointerLength: Float = 0f
    private var mSecondPointerLength: Float = 0f
    /** 时针,分针,秒针的宽度 */
    private var mHourPointerWidth: Float = 0f
    private var mMinutePointerWidth: Float = 0f
    private var mSecondPointerWidth: Float = 0f
    /** 时针,分针,秒针的颜色 */
    private var mHourPointerColor: Int? = null
    private var mMinutePointerColor: Int? = null
    private var mSecondPointerColor: Int? = null

    /** 内圆的画笔 */
    private var mInnermostCirclePaint: Paint? = null
    /** 内圆弧的画笔 */
    private var mInnermostArcPaint: Paint? = null
    /** 外圆弧的画笔 */
    private var mOutermostArcPaint: Paint? = null
    /** 时针,分针,秒针的画笔 */
    private var mHourPointerPaint: Paint? = null
    private var mMinutePointerPaint: Paint? = null
    private var mSecondPointerPaint: Paint? = null


    /** 当前系统的时分秒时刻 */
    private var mCurrentHour: Int = 0
    private var mCurrentMinute: Int = 0
    private var mCurrentSecond: Int = 0

    private val mScheduledCalTime: ScheduledExecutorService = Executors.newScheduledThreadPool(1)

    private val mScheduledCalTimeRunnable: Runnable = Runnable { getCurrentTime() }

    companion object {
        private const val TAG = "MyCustomClockView"
    }

    init {
        mContext = context
        initAttrs(attrs)
        initObject()
        initPaint()
        mScheduledCalTime.scheduleAtFixedRate(mScheduledCalTimeRunnable, 0, 1, TimeUnit.SECONDS)
    }

    /**
     * 加载属性
     */
    private fun initAttrs(attributeSet: AttributeSet?) {
        try {
            if (attributeSet != null) {
                val typedArray = mContext?.obtainStyledAttributes(attributeSet, R.styleable.MyCustomClockView)
                val defColor = Color.BLACK
                mOutermostArcColor = typedArray?.getColor(R.styleable.MyCustomClockView_outerMostArcColor, defColor)
                mInnermostArcColor = typedArray?.getColor(R.styleable.MyCustomClockView_innerMostArcColor, defColor)
                mInnermostCircleColor = typedArray?.getColor(R.styleable.MyCustomClockView_innerMostCircleColor, defColor)

                mOutermostArcWidth = typedArray?.getDimension(R.styleable.MyCustomClockView_outerMostArcWidth, dp2Px(2).toFloat())

                mHourPointerWidth = typedArray?.getDimension(R.styleable.MyCustomClockView_hourPointerWidth, dp2Px(6).toFloat())!!
                mMinutePointerWidth = typedArray.getDimension(R.styleable.MyCustomClockView_minutePointerWidth, dp2Px(4).toFloat())
                mSecondPointerWidth = typedArray.getDimension(R.styleable.MyCustomClockView_secondPointerWidth, dp2Px(2).toFloat())

                mHourPointerColor = typedArray.getColor(R.styleable.MyCustomClockView_hourPointerColor, defColor)
                mMinutePointerColor = typedArray.getColor(R.styleable.MyCustomClockView_minutePointerColor, defColor)
                mSecondPointerColor = typedArray.getColor(R.styleable.MyCustomClockView_secondPointerColor, defColor)

                Logger.t(TAG).d("外圆弧宽度为: $mOutermostArcWidth 时针宽度为: $mHourPointerWidth " +
                        "分针宽度为: $mMinutePointerWidth 秒针宽度为: $mSecondPointerWidth")

                typedArray.recycle()
            } else {
                Logger.t(TAG).d("attributeSet为空")
            }
        } catch (npe: NullPointerException) {
            Logger.t(TAG).e("typedArray为空: $npe")
        } catch (e: Exception) {
            Logger.t(TAG).e("exception: $e")
        }
    }

    /** 初始化对象 */
    private fun initObject() {
        mCenterPoint = Point()
        mOutermostRectF = RectF()
        mInnermostRectF = RectF()
    }

    /** 加载画笔 */
    private fun initPaint() {
        try {
            mInnermostCirclePaint = Paint()
            mInnermostCirclePaint?.isAntiAlias = true
            mInnermostCirclePaint?.style = Paint.Style.FILL
            mInnermostCirclePaint?.color = mInnermostCircleColor!!

            mInnermostArcPaint = Paint()
            mInnermostArcPaint?.isAntiAlias = true
            mInnermostArcPaint?.style = Paint.Style.STROKE
            mInnermostArcPaint?.color = mInnermostArcColor!!

            mOutermostArcPaint = Paint()
            mOutermostArcPaint?.isAntiAlias = true
            mOutermostArcPaint?.style = Paint.Style.STROKE
            mOutermostArcPaint?.color = mOutermostArcColor!!
            mOutermostArcPaint?.strokeWidth = mOutermostArcWidth!!

            mHourPointerPaint = Paint()
            mHourPointerPaint?.isAntiAlias = true
            mHourPointerPaint?.style = Paint.Style.FILL_AND_STROKE
            mHourPointerPaint?.strokeCap = Paint.Cap.ROUND
            mHourPointerPaint?.color = mHourPointerColor!!
            mHourPointerPaint?.strokeWidth = mHourPointerWidth

            mMinutePointerPaint = Paint()
            mMinutePointerPaint?.isAntiAlias = true
            mMinutePointerPaint?.style = Paint.Style.FILL_AND_STROKE
            mMinutePointerPaint?.strokeCap = Paint.Cap.ROUND
            mMinutePointerPaint?.color = mMinutePointerColor!!
            mMinutePointerPaint?.strokeWidth = mMinutePointerWidth

            mSecondPointerPaint = Paint()
            mSecondPointerPaint?.isAntiAlias = true
            mSecondPointerPaint?.style = Paint.Style.FILL_AND_STROKE
            mSecondPointerPaint?.strokeCap = Paint.Cap.ROUND
            mSecondPointerPaint?.color = mSecondPointerColor!!
            mSecondPointerPaint?.strokeWidth = mSecondPointerWidth

        } catch (npe: NullPointerException) {
            Logger.t(TAG).e("画笔颜色或宽度为空: $npe")
        } catch (e: Exception) {
            Logger.t(TAG).e("exception: $e")
        }
    }

    /**
     * View绘制中的测量方法
     * @param widthMeasureSpec   宽度规格
     * @param heightMeasureSpec  高度规格
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(getMeasureSize(true, widthMeasureSpec), getMeasureSize(false, heightMeasureSpec))
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        try {
            Logger.t(TAG).d("onSizeChanged: w = $w; h = $h; oldw = $oldw; oldh = $oldh")
            mWidth = w
            mHeight = h
            // 圆心坐标
            mCenterPoint?.x = w / 2
            mCenterPoint?.y = h / 2
            //求控件宽高的最小值作为外圆半径 并且减去圆弧的宽度(x2),否则会造成部分圆弧绘制在外围
            val widgetMinSize = Math.min(w - 2 *mOutermostArcWidth!!, h - 2 *mOutermostArcWidth!!)
            // 用最小值作为外圆的半径
            mOutermostCircleRadius = (widgetMinSize / 2)

            // 内圆半径为外圆的 1/24
            mInnermostCircleRadius = (mOutermostCircleRadius / 24)
            // 内圆弧宽度为内圆半径的 0.6
            mInnermostArcWidth = mInnermostCircleRadius * 0.6.toFloat()
            // 时针长度为半径的0.68
            mHourPointerLength = mOutermostCircleRadius * 0.68.toFloat()
            // 分针长度为半径的0.80
            mMinutePointerLength = mOutermostCircleRadius * 0.80.toFloat()
            // 秒针长度为半径的0.90
            mSecondPointerLength = mOutermostCircleRadius * 0.90.toFloat()


            //绘制外圆弧的矩形边界   即 圆弧边宽的中心
            mOutermostRectF?.left = mCenterPoint!!.x.toFloat() - mOutermostCircleRadius - mOutermostArcWidth!!.div(2)
            mOutermostRectF?.top = mCenterPoint!!.y.toFloat() - mOutermostCircleRadius - mOutermostArcWidth!!.div(2)
            mOutermostRectF?.right = mCenterPoint!!.x.toFloat() + mOutermostCircleRadius + mOutermostArcWidth!!.div(2)
            mOutermostRectF?.bottom = mCenterPoint!!.y.toFloat() + mOutermostCircleRadius + mOutermostArcWidth!!.div(2)

            //绘制内圆弧的矩形边界
            mInnermostRectF?.left = mCenterPoint!!.x.toFloat() - mInnermostCircleRadius - mInnermostArcWidth!!.div(2)
            mInnermostRectF?.top = mCenterPoint!!.y.toFloat() - mInnermostCircleRadius - mInnermostArcWidth!!.div(2)
            mInnermostRectF?.right = mCenterPoint!!.x.toFloat() + mInnermostCircleRadius + mInnermostArcWidth!!.div(2)
            mInnermostRectF?.bottom = mCenterPoint!!.y.toFloat() + mInnermostCircleRadius + mInnermostArcWidth!!.div(2)

            Logger.t(TAG).d("控件宽度(mWidth)为: $mWidth" + " 控件高度(mHeight)为: $mHeight" + " 圆心x坐标为: ${mCenterPoint?.x}" + "\n" +
                    "圆心y坐标为: ${mCenterPoint?.y}" + " 外圆半径为: $mOutermostCircleRadius" + " 内圆半径为: $mInnermostCircleRadius" + "\n" +
                    "内圆弧宽度为: $mInnermostArcWidth" + " 时针长度为: $mHourPointerLength" + " 分针长度为: $mMinutePointerLength" + "\n" +
                    "秒针长度为: $mSecondPointerLength")
        } catch (npe: NullPointerException) {
            Logger.t(TAG).e("存在空值: $npe")
        } catch (e: Exception) {
            Logger.t(TAG).e("exception: $e")
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawOutermostArc(canvas)
        drawHourPointer(canvas)
        drawMinutePointer(canvas)
        drawSecondPointer(canvas)
        drawInnermostArc(canvas)
        drawInnermostCircle(canvas)
    }

    /**
     * 绘制外圆弧
     */
    private fun drawOutermostArc(canvas: Canvas) {
        canvas.drawArc(mOutermostRectF, 0f, 360f, false, mOutermostArcPaint)
    }

    /**
     * 绘制内圆弧
     */
    private fun drawInnermostArc(canvas: Canvas) {
        mInnermostArcPaint?.strokeWidth = mInnermostArcWidth!!
        canvas.drawArc(mInnermostRectF, 0f, 360f, false, mInnermostArcPaint)
    }

    /**
     * 绘制内圆
     */
    private fun drawInnermostCircle(canvas: Canvas) {
        canvas.drawCircle(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), mInnermostCircleRadius, mInnermostCirclePaint)
    }

    /**
     * 绘制时针
     */
    private fun drawHourPointer(canvas: Canvas) {
    // 以秒来算 因为不是说到了一小时才更新一次时针 而是一秒更新一次
        val curHourPointerAngle = (mCurrentHour * 60 * 60 + mCurrentMinute * 60 + mCurrentSecond).toFloat().div(12 * 60 * 60) * 360 + 270
        val curHourPointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curHourPointerAngle) * mHourPointerLength
        val curHourPointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curHourPointerAngle) * mHourPointerLength

        canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curHourPointerX.toFloat(), curHourPointerY.toFloat(), mHourPointerPaint)
    }

    /**
     * 绘制分针
     */
    private fun drawMinutePointer(canvas: Canvas) {
        val curMinutePointerAngle = (mCurrentMinute * 60 + mCurrentSecond).toFloat().div(60 * 60) * 360 + 270
        val curMinutePointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curMinutePointerAngle) * mMinutePointerLength
        val curMinutePointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curMinutePointerAngle) * mMinutePointerLength

        canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curMinutePointerX.toFloat(), curMinutePointerY.toFloat(), mMinutePointerPaint)
    }

    /**
     * 绘制秒针
     */
    private fun drawSecondPointer(canvas: Canvas) {
    // 这里 +270度  因为 android绘制是以三点钟方向为起点  而这里是以 十二点钟方向为起点
        val curSecondPointerAngle = mCurrentSecond.toFloat().div(60f).times(360) + 270
        Logger.t(TAG).e(curSecondPointerAngle.toString())
        // 这里+就好 因为三角函数是会有正负的 不用根据情况进行 + - 操作 并且这里的x用cos y用sin 以数学的坐标系的角度来看
        val curSecondPointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curSecondPointerAngle) * mSecondPointerLength
        val curSecondPointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curSecondPointerAngle) * mSecondPointerLength

        canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curSecondPointerX.toFloat(), curSecondPointerY.toFloat(), mSecondPointerPaint)
    }


    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        removeCallbacks(mScheduledCalTimeRunnable)
    }


    /**
     * 获取当前系统时间
     */
    private fun getCurrentTime() {
        val calendar = Calendar.getInstance()
        mCurrentHour = calendar.get(Calendar.HOUR).rem(12)
        mCurrentMinute = calendar.get(Calendar.MINUTE)
        mCurrentSecond = calendar.get(Calendar.SECOND)

        Logger.t(TAG).d("当前的小时(12小时制)为:$mCurrentHour 当前的分钟为:$mCurrentMinute 当前的秒为:$mCurrentSecond")
        postInvalidate()
    }

    /**
     * 获取View尺寸
     *
     * @param isWidth 是否是width,不是的话,是height
     */
    private fun getMeasureSize(isWidth: Boolean, measureSpec: Int): Int {

        var result = 0

        val specSize = View.MeasureSpec.getSize(measureSpec)
        val specMode = View.MeasureSpec.getMode(measureSpec)

        when (specMode) {
            View.MeasureSpec.UNSPECIFIED -> result = if (isWidth) {
                suggestedMinimumWidth
            } else {
                suggestedMinimumHeight
            }
            View.MeasureSpec.AT_MOST -> result = if (isWidth)
                Math.min(specSize, mWidth)
            else
                Math.min(specSize, mHeight)
            View.MeasureSpec.EXACTLY -> result = specSize
        }
        return result
    }

    private fun dp2Px(dp: Int): Int {
        val scale = context.resources.displayMetrics.density
        return (dp * scale + 0.5f).toInt()
    }


}


<com.xxx.xxx.xxx.MyCustomClockView
    android:layout_width="480dp"
    android:layout_height="480dp"
    android:layout_marginLeft="40dp"
    android:layout_marginTop="15dp"
    custom:outerMostArcColor="@color/outerMostArcColor"
    custom:innerMostArcColor="@color/innerMostArcColor"

    custom:innerMostCircleColor="@color/innerMostCircleColor"

    custom:hourPointerColor="@color/hourPointerColor"
    custom:minutePointerColor="@color/minutePointerColor"
    custom:secondPointerColor="@color/secondPointerColor"

    custom:outerMostArcWidth="@dimen/outer_most_arc_width"


    android:layout_marginStart="40dp" />

实例之 自定义圆环进度条:

效果:

一、在Sytleable中 设置 进度条的属性名 定义 xml中自定义组件使用的一些属性

 <!-- 圆形进度条 -->
    <declare-styleable name="CircleProgressBar">
        <attr name="antiAlias" />
        <attr name="startAngle" />
        <attr name="sweepAngle" />
        <attr name="animTime" />
        <attr name="maxValue" />
        <attr name="value" />
        <attr name="precision" />
        <attr name="valueSize" />
        <attr name="valueColor" />
        <attr name="textOffsetPercentInRadius" />
        <!-- 绘制内容相应的提示语 -->
        <attr name="hint" />
        <attr name="hintSize" />
        <attr name="hintColor" />
        <!-- 绘制内容的单位 -->
        <attr name="unit" />
        <attr name="unitSize" />
        <attr name="unitColor" />
        <!-- 圆弧宽度 -->
        <attr name="arcWidth" />
        <attr name="arcColors" />
        <!-- 背景圆弧颜色 -->
        <attr name="bgArcColor" />
        <!-- 背景圆弧宽度 -->
        <attr name="bgArcWidth" format="dimension" />
    </declare-styleable>

    <!-- colors.xml -->
    <color name="green">#00FF00</color>
    <color name="blue">#EE9A00</color>
    <color name="red">#EE0000</color>
    <!-- 渐变颜色数组 -->
    <integer-array name="gradient_arc_color">
        <item>@color/green</item>
        <item>@color/blue</item>
        <item>@color/red</item>
    </integer-array>

二、不用创建XML文件了 因为是自定义View 自己绘制的

三、建立一个继承 View的子类 命名为 CircleBar
主要设置
1 属性,画笔,测量 layout的onSizeChanged,draw方法
2 init方法 初始化 各种 对象 如 属性动画 如 圆的外接矩形 圆心坐标 画笔加载 属性加载
3 重写onMeasure方法 记得要调用setMeasuredDimension,来存储这个View经过测量得到的measured width and height
4 重写 onSizeChanged 方法 // 在控件大小发生改变时调用
5 调用onDraw()方法 再调用 DrawArc绘制圆弧和DrawText()方法绘制文字
并且这里 调用 了 canvas.save(); 从进度圆弧结束的地方开始重新绘制,既是优化性能又美观
onDraw方法会在收到WM_PAINT时调用
7 如界面初始化 调用 showWindow 或者 updateWindow时 又比如 自己调用invalidate方法

8 用 SweepGradient 去 设置渐变
9 并且 给 进度设置属性动画 调用 invalidate()方法 若视图大小无变化则不用执行layout 只重绘更改的部分 也是可以性能优化
10 记得释放资源 onDetachedFromWindow

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import cn.sihao.customercirclebar.Util.MeasureUtil;

public class CircleBar extends View {

    private static final int mDefaultSize = 150;    // 这里默认设置150 而那个demo是要将他转为px 并且 数据写在Constant类中 作为 static final
    private static final boolean ANTI_ALIAS = true;
    private static final int DEFAULT_HINT_SIZE = 15;
    private static final int DEFAULT_UNIT_SIZE = 30;
    private static final int DEFAULT_MAX_VALUE = 100;
    private static final int DEFAULT_VALUE = 50;
    public static final int DEFAULT_START_ANGLE = 270;
    public static final int DEFAULT_SWEEP_ANGLE = 360;
    public static final int DEFAULT_ANIM_TIME = 1000;
    public static final int DEFAULT_ARC_WIDTH = 15;
    public static final int DEFAULT_VALUE_SIZE = 15;
    private final String TAG = CircleBar.class.getSimpleName();

    //绘制提示
    private TextPaint mHintPaint;
    private CharSequence mHint;
    private int mHintColor;
    private float mHintSize;
    private float mHintOffset;

    //绘制单位
    private TextPaint mUnitPaint;
    private CharSequence mUnit;
    private int mUnitColor;
    private float mUnitSize;
    private float mUnitOffset;

    //绘制背景圆弧
    private Paint mBgArcPaint;
    private int mBgArcColor;
    private float mBgArcWidth;

    //绘制圆弧
    private Paint mArcPaint;
    private float mArcWidth;
    private float mStartAngle, mSweepAngle;
    private RectF mRectF;
    private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
    //圆心坐标
    private Point mCenterPoint;
    private float mTextOffsetPercentInRadius;

    //当前进度,[0.0f,1.0f]
    private float mPercent;
    //动画时间
    private long mAnimTime;
    //属性动画
    private ValueAnimator mAnimator;

    //绘制数值
    private TextPaint mValuePaint;
    private float mValue;
    private float mMaxValue;
    private float mValueOffset;
    private int mPrecision;
    private String mPrecisionFormat;
    private int mValueColor;
    private float mValueSize;

    //是否开启抗锯齿
    private boolean antiAlias;

    private Context mContext;
    public CircleBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
    }

    // 初始化对象
    private void init(Context context,AttributeSet attributeSet){
        mContext = context;
        // 初始化属性动画
        mAnimator = new ValueAnimator();
        // 初始化圆的外接矩形
        mRectF = new RectF();
        // 初始化圆心坐标
        mCenterPoint = new Point();
        // 加载属性 如抗锯齿 默认字体的样式和值 进度条的值等
        initAttrs(attributeSet);
        // 加载画笔
        initPaint();
        setValue(mValue);
    }

    /**
     *  连接 styleable中的属性
     * @param attributeSet   属性
     */
    private void initAttrs(AttributeSet attributeSet){

        TypedArray typedArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.CircleProgressBar);
        // 设置是否开启抗锯齿
        antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, ANTI_ALIAS);
        // 无数据时的默认字体样式与内容
        mHint = typedArray.getString(R.styleable.CircleProgressBar_hint);
        mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK);
        mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize,DEFAULT_HINT_SIZE);
        // 进度条当前值 与 最大值
        mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, DEFAULT_VALUE);
        mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue,DEFAULT_MAX_VALUE);
        //内容数值精度,格式,颜色和大小
        mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0);
        mPrecisionFormat = MeasureUtil.getPrecisionFormat(mPrecision);
        mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK);
        mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, DEFAULT_VALUE_SIZE);
        // 单位 值 颜色 与大小
        mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit);
        mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK);
        mUnitSize = typedArray.getDimension(R.styleable.CircleProgressBar_unitSize, DEFAULT_UNIT_SIZE);
        // 圆弧的宽度 角度 和 渐变色的角度 后面的是默认值
        mArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, DEFAULT_ARC_WIDTH);
        mStartAngle = typedArray.getFloat(R.styleable.CircleProgressBar_startAngle, DEFAULT_START_ANGLE);
        mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, DEFAULT_SWEEP_ANGLE);
        // 背景颜色 背景宽度
        mBgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.WHITE);
        mBgArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_bgArcWidth, DEFAULT_ARC_WIDTH);
        mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.CircleProgressBar_textOffsetPercentInRadius, 0.33f);

        //mPercent = typedArray.getFloat(R.styleable.CircleProgressBar_percent, 0);
        mAnimTime = typedArray.getInt(R.styleable.CircleProgressBar_animTime, DEFAULT_ANIM_TIME);
        // 渐变色
        int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);
        // 设置渐变色
        if (gradientArcColors != 0) {
            try {
                int[] gradientColors = getResources().getIntArray(gradientArcColors);
                if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值
                    int color = getResources().getColor(gradientArcColors);
                    mGradientColors = new int[2];
                    mGradientColors[0] = color;
                    mGradientColors[1] = color;
                } else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色
                    mGradientColors = new int[2];
                    mGradientColors[0] = gradientColors[0];
                    mGradientColors[1] = gradientColors[0];
                } else {                                // 否则 颜色数组 就是 渐变数组
                    mGradientColors = gradientColors;
                }
            } catch (Resources.NotFoundException e) {
                throw new Resources.NotFoundException("the give resource not found.");
            }
        }
        // 记得写这个方法
        typedArray.recycle();
    }

    /**
     * 加载画笔
     */
    private void initPaint() {
        // 创建默认文字画笔
        mHintPaint = new TextPaint();
        // 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢 但是边界更圆润
        mHintPaint.setAntiAlias(antiAlias);
        // 设置绘制文字大小
        mHintPaint.setTextSize(mHintSize);
        // 设置画笔颜色
        mHintPaint.setColor(mHintColor);
        // 从中间向两边绘制,不需要再次计算文字   注意这里是相对于原点的绘制方向 若是Left则 是从左往右绘制 在原点右侧
        mHintPaint.setTextAlign(Paint.Align.CENTER);
        // 创建值的文字画笔
        mValuePaint = new TextPaint();
        // 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢 但是边界更圆润
        mValuePaint.setAntiAlias(antiAlias);
        mValuePaint.setTextSize(mValueSize);
        mValuePaint.setColor(mValueColor);
        // 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
        mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);
        mValuePaint.setTextAlign(Paint.Align.CENTER);
        // 创建 单位 (即“步”) 的画笔
        mUnitPaint = new TextPaint();
        mUnitPaint.setAntiAlias(antiAlias);
        mUnitPaint.setTextSize(mUnitSize);
        mUnitPaint.setColor(mUnitColor);
        mUnitPaint.setTextAlign(Paint.Align.CENTER);
        // 创建 角度绘制画笔
        mArcPaint = new Paint();
        mArcPaint.setAntiAlias(antiAlias);
        // 设置画笔的样式,为FILL 填充内部 ,FILL_AND_STROKE,或STROKE 描边
        mArcPaint.setStyle(Paint.Style.STROKE);
        // 设置画笔粗细
        mArcPaint.setStrokeWidth(mArcWidth);
        // 当画笔样式为STROKE或FILL_AND_STROKE时,设置油漆的填充样式,如圆形样式Cap.ROUND,方形样式Cap.SQUARE
        // 也可以理解为 结尾处的样式 圆角ROUND 方形SQUARE 或者 无样式BUTT
        mArcPaint.setStrokeCap(Paint.Cap.ROUND);
        // 背景画笔 即当没有值时候的默认圆弧背景颜色
        mBgArcPaint = new Paint();
        mBgArcPaint.setAntiAlias(antiAlias);
        mBgArcPaint.setColor(mBgArcColor);
        mBgArcPaint.setStyle(Paint.Style.STROKE);
        mBgArcPaint.setStrokeWidth(mBgArcWidth);
        mBgArcPaint.setStrokeCap(Paint.Cap.ROUND);
    }

    /**
     * View绘制中的测量方法
     * @param widthMeasureSpec   宽度规格
     * @param heightMeasureSpec  高度规格
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 记得要调用setMeasuredDimension,来存储这个View经过测量得到的measured width and height
        // 这里将 根据测量规格设置宽度/高度 的方法 封装了起来
        setMeasuredDimension(MeasureUtil.measure(widthMeasureSpec, mDefaultSize),
                MeasureUtil.measure(heightMeasureSpec, mDefaultSize));
    }

    /**
     * 在控件大小发生改变时调用  初始化时会调用一次    该方法在layout中的setFrame中
     * @param w 新宽
     * @param h 新高
     * @param oldw 改变之前的宽
     * @param oldh 改变之前的高
     *             这里用于 设置圆的半径和 文字的baseLine
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
        //求圆弧和背景圆弧的最大宽度
        float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
        //求控件宽高的最小值作为实际值 并且减去圆弧的宽度,否则会造成部分圆弧绘制在外围
        int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
                h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
        // 用最小值作为圆的半径
        float mRadius = minSize / 2;
        //获取圆的横纵坐标
        mCenterPoint.x = w / 2;
        mCenterPoint.y = h / 2;
        //绘制圆弧的矩形边界   即 圆弧边宽的中心
        mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;
        mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;
        mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;
        mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;
        //计算文字绘制时的 baseline
        //由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算
        //若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算
        mValueOffset = mCenterPoint.y - (mValuePaint.descent() + mValuePaint.ascent()) / 2;   // ascent 从baseline线到最高的字母顶点到距离,负值
        mHintOffset = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2; // descent 从baseline线到字母最低点到距离
        mUnitOffset = mCenterPoint.y * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2;

        //由于设置渐变需要每次都创建一个新的 SweepGradient 对象,所以最好不要放到 onDraw 方法中去更新,最好在初始化的时候就设置好,避免频繁创建导致内存抖动
        updateArcPaint();

        Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"
                + "圆心坐标 = " + mCenterPoint.toString()
                + ";圆半径 = " + mRadius
                + ";圆的外接矩形 = " + mRectF.toString());
    }

    private float getBaselineOffsetFromY(Paint paint) {
        return MeasureUtil.measureTextHeight(paint) / 2;
    }

    /**
     * 绘制文字 和 圆角
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawTheText(canvas);
        drawTheArc(canvas);
    }

    private void drawTheArc(Canvas canvas) {
        // 绘制背景圆弧
        // 从进度圆弧结束的地方开始重新绘制,优化性能
        canvas.save();
        float currentAngle = mSweepAngle * mPercent;
        //为了方便计算,绘制圆弧的时候使用了 Canvas 的 rotate() 方法,对坐标系进行了旋转
        canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
        // +2 是因为绘制的时候出现了圆弧起点有尾巴的问题  drawArc的布尔值为是否填充 一般画圆弧都是false不填充
        canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint);
        canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
        canvas.restore();
    }

    private void drawTheText(Canvas canvas) {
        canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);

        if (mHint != null) {
            canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);
        }

        if (mUnit != null) {
            canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);
        }
    }

    /**
     * 更新圆弧画笔 设置渐变
     */
    private void updateArcPaint() {
        // 设置渐变
        /*
         * @param cx 渲染中心点x坐标
         * @param cy 渲染中心点y坐标
         * @param colors 围绕中心渲染的颜色数组,至少要有两种颜色值
         * @param positions 相对位置的颜色数组,可为null,  若为null,可为null,颜色沿渐变线均匀分布。一般不需要设置该参数
         */
        //渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色
        SweepGradient mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
        mArcPaint.setShader(mSweepGradient);
    }

    /**
     * 设置属性动画 对进度属性设置动画
     * @param start   开始百分比   而每次结束了 又把此时的百分比设置为 开始百分比 (从这里开始再变化)
     * @param end     结束百分比
     * @param animTime 动画的时间
     */
    private void startAnimator(float start, float end, long animTime) {
        mAnimator = ValueAnimator.ofFloat(start, end);
        mAnimator.setDuration(animTime);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPercent = (float) animation.getAnimatedValue();
                mValue = mPercent * mMaxValue;
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
                            + ";currentAngle = " + (mSweepAngle * mPercent)
                            + ";value = " + mValue);
                }
                invalidate();   // 去调用onDraw 并且当视图大小未变化不会执行layout
            }
        });
        mAnimator.start();
    }

    public float getValue() {
        return mValue;
    }


    public float getMaxValue() {
        return mMaxValue;
    }

    /**
     * 暴露方法给外部设置最大值
     *
     * @param maxValue 最大值
     */
    public void setMaxValue(float maxValue) {
        mMaxValue = maxValue;
    }


    public int getPrecision() {
        return mPrecision;
    }

    /**
     * 暴露方法给外部设置精度
     *
     * @return 精度
     */
    public void setPrecision(int precision) {
        mPrecision = precision;
        mPrecisionFormat = MeasureUtil.getPrecisionFormat(precision);
    }

    /**
     * 设置当前值
     *
     * @param value 当前值
     */
    public void setValue(float value) {
        if (value > mMaxValue) {
            value = mMaxValue;
        }
        float start = mPercent;
        float end = value / mMaxValue;
        startAnimator(start, end, mAnimTime);
    }

    public int[] getGradientColors() {
        return mGradientColors;
    }

    /**
     * 暴露方法给外部设置渐变
     *
     * @param gradientColors 渐变颜色数组
     */
    public void setGradientColors(int[] gradientColors) {
        mGradientColors = gradientColors;
        updateArcPaint();
    }

    public long getAnimTime() {
        return mAnimTime;
    }

    /**
     * 暴露方法给外部设置动画时间
     * @param animTime 动画时间
     */
    public void setAnimTime(long animTime) {
        mAnimTime = animTime;
    }

    /**
     * 暴露方法给外部 让百分比重置
     */
    public void reset() {
        startAnimator(mPercent, 0.0f, 1000L);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //释放资源
    }

}

四、xml 使用 自定义的 View
名称就是 包名+java的类名 然后设置在Styleable中定义的各种参数
自定义的View通过暴露方法 reset 和 setValue 让 主界面去 设置或者重置 进度条的值

 <cn.sihao.customercirclebar.CircleBar
        android:id="@+id/circle_progress_bar1"
        android:layout_width="268dp"
        android:layout_height="268dp"
        android:layout_gravity="center_horizontal"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="5dp"
        android:layout_below="@+id/btn_reset_all"
        app:antiAlias="true"
        app:arcWidth="@dimen/small"
        app:bgArcColor="@color/colorAccent"
        app:bgArcWidth="@dimen/small"
        app:hint="今天运动步数:"
        app:hintSize="15sp"
        app:maxValue="10000"
        app:startAngle="135"
        app:sweepAngle="270"
        app:unit="步"
        app:unitSize="15sp"
        app:value="10000"
        app:valueSize="25sp" />

注意:
1 调用 onMeasure 必须要调用setMeasuredDimension
2 那个圆的问题 是边宽的中间开始作外接矩形的
3 坐标是从左上到右下的
4 文字绘制 baseLine 相当于是文字的x轴
上为负 下为正
5 drawArc的 startAngle 是3点钟方向为起点
6 一定要记得 最后要 DetachedFromWindow 取消视图的订阅
7 CircleProgressBar_antiAlias 即Sytleable名为 CircleProgressBar 的 attr 名为 antiAlias
8 要及时的取消draw 避免内存泄漏


参考:https://github.com/MyLifeMyTravel/CircleProgress/blob/master/circleprogress/src/main/java/com/littlejie/circleprogress/CircleProgress.java


猜你喜欢

转载自blog.csdn.net/weixin_37577039/article/details/79939371