kotlin-自定View

2020-11-09  本文已影响0人  jeffrey12138

作为小白的我,一直都觉得自定义View是个很难的课题,现在也是这么觉得,虽然使用起来还是挺方便,不过也是需要加入自己的想象力,无奈我想象力不太丰富,好了开始说下这个应该如何使用的吧
最近也开始使用Xmind去做一个思维导图,我觉得这样也有利于去理解,所以我也附上了这个图
顺便说,如果想更深一层去了解自定View的话,看下这位大佬的文章,分析的很透彻
https://juejin.im/post/6844903607855218702#heading-16

image.png
按照这个思维导图的顺序,开始撸代码:
1 自定义属性:
 <declare-styleable name="TestView">
        <attr name="test_boolean" format="boolean"></attr>
        <attr name="test_string" format="string"></attr>
        <attr name="test_integer" format="integer"></attr>
        <attr name="test_enum" format="enum">
            <enum name="top" value="1"></enum>
            <enum name="bottom" value="2"></enum>
        </attr>
        <attr name="test_dimension" format="dimension"></attr>
    </declare-styleable>

 <declare-styleable name="RoundCircleProgressBar">
        <attr name="color" format="color"/>
        <attr name="line_width" format="dimension"/>
        <attr name="radius" format="dimension"/>
        <attr name="android:progress"/>
        <attr name="android:textSize"/>
    </declare-styleable>

然后是控件的编写,里面写得都比较详细了啦,请慢慢看,我把两个案例都放进去吧
1 TextView

class TextVIew(context: Context, attrs: AttributeSet?) :
    View(context, attrs) {
    val typedArray=context.obtainStyledAttributes(attrs,R.styleable.TextVIew)
    private var mTextColor=0
    private var mTextSize=0
    private var mTextContent=""
    private lateinit var mPaint: Paint
    init {
        //通过for循环确保用户未输入属性值的时候,能使用默认属性值
        for (i in 0..typedArray.indexCount)
        {
            when(typedArray.getIndex(i)){
                R.styleable.TextVIew_text_color->mTextColor=typedArray.getColor(R.styleable.TextVIew_text_color,0xFFFF00)
                R.styleable.TextVIew_text_size->mTextSize=typedArray.getInt(R.styleable.TextVIew_text_size,dpTosp(30).toInt())
                R.styleable.TextVIew_text_content->mTextContent=
                    typedArray.getString(R.styleable.TextVIew_text_content).toString()
            }
        }
        typedArray.recycle()
        initPaint()
    }

    private fun initPaint() {
        mPaint= Paint()
        //STROKE为空心,FILL为实心
        mPaint.style=Paint.Style.STROKE
        mPaint.color=-0x10000
        mPaint.strokeWidth=6f
        mPaint.isAntiAlias=true
    }

    private fun dpTosp(dpValue:Int): Float {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,dpValue.toFloat(),resources.displayMetrics)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
        //MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec = mode + size。
        val widthMode=MeasureSpec.getMode(widthMeasureSpec)
        val widthSize=MeasureSpec.getSize(widthMeasureSpec)
        var width=0
        //EXACTLY:精准模式。父容器已经解决了子元素所需要的精准大小,这时候子 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值
        //AT_MOST:最大模式。父最大模式容器指定了一个可用大小即 SpecSize,子元素最大不可以超过指定的这个值。它对应于LayoutParams 中的 wrap_content
        //UNSPECIFIED:父容器不对子元素作任何约束,子 View 想要多大就给多大。这种情况一般用在系统内部,表示一种测量的状态用于类型可滑动的布局中,例如ListView之类的
        width = if (widthMode==MeasureSpec.EXACTLY){
            widthSize
        }else {
            val needWidth = measuredWidth() + paddingLeft + paddingRight
            if (widthMode == MeasureSpec.AT_MOST) {
                needWidth.coerceAtMost(widthSize)
            } else {
                needWidth
            }
        }
        val heightMode=MeasureSpec.getMode(heightMeasureSpec)
        val heightSize=MeasureSpec.getSize(heightMeasureSpec)
        var height=0
        height = if (heightMode==MeasureSpec.EXACTLY){
            heightSize
        }else{
            val needHeight=measuredHeight()+paddingBottom+paddingTop
            if (heightMode==MeasureSpec.AT_MOST){
                needHeight.coerceAtMost(width)
            }else{
                needHeight
            }
        }
        //为view传入绘制后的宽高,这个很重要哦,要不然前面的工作就白做了
        setMeasuredDimension(width,height)
    }
    private fun measuredWidth():Int{
        return 0
    }
    private fun measuredHeight():Int{
        return 0
    }

    override fun onDraw(canvas: Canvas?) {

//        canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mPaint.getStrokeWidth() / 2, mPaint);
//        mPaint.setStrokeWidth(1);
//        canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
//        canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
        mPaint.textSize = 72f
        mPaint.style = Paint.Style.FILL
        mPaint.strokeWidth = 0f
        canvas?.drawText(mTextContent, 0, mTextContent.length, 0f, height.toFloat(), mPaint)
    }
    //使用状态的存储和复位之后,记得在xml中对应的控件需要添加id,否则控件无法复位
    override fun onSaveInstanceState(): Parcelable? {
        val bundle=Bundle()
        //这一步是为了把当前控件的状态呢进行存储,因为在继承别的View的时候,可能他们自身已经有一套存储的方法,
        // 所以为了避免使自身的存储失效,所以要在这里进行状态的存储
        bundle.putParcelable(INSTANCE,super.onSaveInstanceState())
        bundle.putString(KEY_TEXT,mTextContent)
        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        if (state is Bundle){
            val bundle= state as Bundle
            val parcelable=bundle.getParcelable<Parcelable>(INSTANCE)
            super.onRestoreInstanceState(parcelable)
            mTextContent = bundle.getString(KEY_TEXT).toString()
            return
        }
        super.onRestoreInstanceState(state)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        mTextContent="哦豁"
        //刷新控件
        invalidate()
        return true
    }

    companion object{
        val INSTANCE="instance"
        val KEY_TEXT="key_text"
    }
}

2 圆形进度条

class CircleProgress(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private val mRadius: Int
    private val mColor: Int
    private val mLineWidth: Int
    private val mTextSize: Int
    private var mProgress: Int
    private var mPaint: Paint? = null
    private fun dp2px(dpVal: Int): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dpVal.toFloat(), resources.displayMetrics
        )
    }

    private fun initPaint() {
        mPaint = Paint()
        mPaint!!.isAntiAlias = true
        mPaint!!.color = mColor
    }

    var progress: Int
        get() = mProgress
        set(progress) {
            mProgress = progress
            invalidate()
        }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        var width = 0
        width = if (widthMode == MeasureSpec.EXACTLY) {
            widthSize
        } else {
            val needWidth = measureWidth() + paddingLeft + paddingRight
            if (widthMode == MeasureSpec.AT_MOST) {
                needWidth.coerceAtMost(widthSize)
            } else {
                needWidth
            }
        }
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        var height = 0
        height = if (heightMode == MeasureSpec.EXACTLY) {
            heightSize
        } else {
            val needHeight = measureHeight() + paddingTop + paddingBottom
            if (heightMode == MeasureSpec.AT_MOST) {
                needHeight.coerceAtMost(heightSize)
            } else  //MeasureSpec.UNSPECIFIED
            {
                needHeight
            }
        }
        setMeasuredDimension(width, height)
    }

    private fun measureHeight(): Int {
        return mRadius * 2
    }

    private fun measureWidth(): Int {
        return mRadius * 2
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        mPaint!!.style = Paint.Style.STROKE
        mPaint!!.strokeWidth = mLineWidth * 1.0f / 4
        val width = width
        val height = height
        canvas.drawCircle(
            width / 2.toFloat(), height / 2.toFloat(),
            width / 2 - paddingLeft - mPaint!!.strokeWidth / 2, mPaint!!
        )

        mPaint!!.strokeWidth = mLineWidth.toFloat()
        canvas.save()
        canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat())
        val angle = mProgress * 1.0f / 100 * 360
        canvas.drawArc(
            RectF(
                0f,
                0f,
                (width - paddingLeft * 2).toFloat(),
                (height - paddingLeft * 2).toFloat()
            ),
            0f,
            angle,
            false,
            mPaint!!
        )
        canvas.restore()
        val text = "$mProgress%"
        //        text = "张鸿洋";
        mPaint!!.strokeWidth = 0f
        mPaint!!.textAlign = Paint.Align.CENTER
        mPaint!!.textSize = mTextSize.toFloat()
        val y = getHeight() / 2
        val bound = Rect()
        mPaint!!.getTextBounds(text, 0, text.length, bound)
        val textHeight = bound.height()
        canvas.drawText(
            text,
            0,
            text.length,
            getWidth() / 2.toFloat(),
            y + textHeight / 2.toFloat(),
            mPaint!!
        )
        mPaint!!.strokeWidth = 0f
        //        canvas.drawLine(0, height / 2, width, height / 2, mPaint);
    }

    override fun onSaveInstanceState(): Parcelable? {
        val bundle = Bundle()
        bundle.putInt(KEY_PROGRESS, mProgress)
        bundle.putParcelable(INSTANCE, super.onSaveInstanceState())
        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable) {
        if (state is Bundle) {
            val parcelable =
                state.getParcelable<Parcelable>(INSTANCE)
            super.onRestoreInstanceState(parcelable)
            mProgress = state.getInt(KEY_PROGRESS)
            return
        }
        super.onRestoreInstanceState(state)
    }

    companion object {
        private const val INSTANCE = "instance"
        private const val KEY_PROGRESS = "key_progress"
    }

    init {
        val ta = context.obtainStyledAttributes(attrs, R.styleable.RoundCircleProgressBar)
        mRadius = ta.getDimension(R.styleable.RoundCircleProgressBar_radius, dp2px(30)).toInt()
        mColor = ta.getColor(R.styleable.RoundCircleProgressBar_color, -0x10000)
        mLineWidth =
            ta.getDimension(R.styleable.RoundCircleProgressBar_line_width, dp2px(3)).toInt()
        mTextSize =
            ta.getDimension(R.styleable.RoundCircleProgressBar_android_textSize, dp2px(36)).toInt()
        mProgress = ta.getInt(R.styleable.RoundCircleProgressBar_android_progress, 30)
        ta.recycle()
        initPaint()
    }
}

emmm剩下就是在xml里面直接使用了啦!!!好了,到此结束,反正你们都不点赞,我给自己看就足够了!!

上一篇下一篇

猜你喜欢

热点阅读