kotlin-自定View

作为小白的我,一直都觉得自定义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里面直接使用了啦!!!好了,到此结束,反正你们都不点赞,我给自己看就足够了!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342