美文网首页Android自定义View
Android Kotlin 自定义View的一些研究(一)

Android Kotlin 自定义View的一些研究(一)

作者: 水天滑稽天照八野滑稽石 | 来源:发表于2019-06-06 11:08 被阅读79次

前言

闲来无事,来学习下自定义View的一些知识,发现了不少的坑,在此做下笔记给大家分享下

自定义View的分类

自定义View的实现方法分挺多种的,这个简单做一下分类

这几种自定义View的实现方式有所不同,自然实现的效果也不一样,下面我们一一研究并踩踩里面的坑呗,因为篇幅较长,所以会分几篇来讲
那我们开始吧

继承View重写onDraw方法

这种方法只要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,玩玩需要静态或者动态的显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。

这里我们来以简单绘制的一个圆作为demo来研究吧

class CircleView @JvmOverloads constructor(context: Context,
                                           attrs: AttributeSet? = null,
                                           defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){

    private var color: Int = Color.RED
    private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.color = color
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var radius = Math.min(width,height)/2f //width和height是getWidth()和getHeight()
        canvas?.drawCircle(width/2f,height/2f,radius,paint)
    }
}

上面代码实现了一个具有圆形效果的自定义View,它会在自己的中心以宽/高的最小值为直径绘制一个红心的实体圆

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context=".MainActivity">

        <com.example.diyview.CircleView
            android:id="@+id/circle_view"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#000000"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

在布局里面使用并运行一下看效果


符合我们的预期效果,一个背景颜色为黑色的红色实体圆View
然后我们更改一下布局参数,为其设置20dp的margin
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context=".MainActivity">

        <com.example.diyview.CircleView
            android:id="@+id/circle_view"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#000000"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_margin="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

符合预期,那么我们再来设置下20dp的padding
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context=".MainActivity">

        <com.example.diyview.CircleView
            android:id="@+id/circle_view"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#000000"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_margin="20dp"
            android:padding="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

emmmmm,没有任何变化,padding属性失效
看来继承View来实现自定义view,padding是默认不生效的,需要我们手动处理一下
既然不生效,那么我们可以在绘制的时候考虑一下padding即可,所以对onDraw()方法稍作修改就行了

class CircleView @JvmOverloads constructor(context: Context,
                                           attrs: AttributeSet? = null,
                                           defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){

    private var color: Int = Color.RED
    private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.color = color
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var viewWidth = width - paddingLeft - paddingRight
        var viewHeight = height - paddingTop - paddingBottom
        var radius = Math.min(viewHeight,viewWidth)/2f
        canvas?.drawCircle(paddingLeft+viewWidth/2f,paddingTop+viewHeight/2f,radius,paint)
    }
}

然后继续修改布局的属性,我们将match_parent改成wrap_content

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context=".MainActivity">

        <com.example.diyview.CircleView
            android:id="@+id/circle_view"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:background="#000000"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_margin="20dp"
            android:padding="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

emmmmm....
发现坑了,warp_content不生效,这其实是没有设置warp_content的默认宽高导致的,这个先给出解决方案,至少为什么这样稍后再说(简短答案,系统的控件如TextView里的warp_content生效其实是设置了默认值)
重写onMeasure()方法

package com.example.diyview

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View

class CircleView @JvmOverloads constructor(context: Context,
                                           attrs: AttributeSet? = null,
                                           defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr){

    private var color: Int = Color.RED
    private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.color = color
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var viewWidth = width - paddingLeft - paddingRight
        var viewHeight = height - paddingTop - paddingBottom
        var radius = Math.min(viewHeight,viewWidth)/2f
        canvas?.drawCircle(paddingLeft+viewWidth/2f,paddingTop+viewHeight/2f,radius,paint)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //设置warp_content默认宽高为200dp
        val mWidth = 200
        val mHeight = 200
        var widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        var widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        var heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        var heightSpceSize = MeasureSpec.getSize(heightMeasureSpec)
        MeasureSpec.AT_MOST.let {//Kotlin写法,MeasureSpec.AT_MOST用it来表示
          when(true) {
                widthSpecMode == it && heightSpecMode == it -> setMeasuredDimension(mWidth, mHeight)
                widthSpecMode == it                         -> setMeasuredDimension(mWidth, heightSpceSize)
                heightSpceSize == it                        -> setMeasuredDimension(widthSpecSize, mHeight)
            }
        }
    }
}
解释为什么warp_content失效,可以跳过不看

可以看到我们的解决方案是重写了onMeasure(),所以是什么原因导致的我们要求了解一下onMeasure()

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

onMeasure()的代码非常的简洁,但简洁不意味的简单,其中setMeasuredDimension()是用来设置View的宽/高测量值的,因此我们需要看的是getDefaultSize()这个方法:

   public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

先解释下MeasureSpec的三个枚举值吧

  • UNSPECIFIFD 父容器不对view有任何限制,要多大给多大,这种情况一般用于系统内部,表示一直测量状态
  • EXACTLY 父容器已经检测出View说需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的match_parent和具体的数组这两种模式。
  • AT_MOST 父容器指定了一个可用大小及SpecSize,View的大小不能大于这个值,具体是什么得看不同View的实现。它对应LayoutParams中的warp_content

所以我们只需要看AT_MOST和EXACTLY这两种情况就可以了。
可以很容易看出,getDefaultSize()这个方法,他返回的大小就是measureSpec中的specSize ,但是AT_MOST,也就是warp_content的话,是什么都不返回的,所以我们继承View的话需要手动处理warp_content,即给一个默认值

自定义属性

很多情况下,自定义View仅靠系统提供的属性是不够用的,所以我们需要添加自定义属性
1. 在Values目录下创建自定义属性的XML,如attr.xml(名字随便取),并创建如下文本内容

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
        <!--用法:<CircleView app:circle_color = "#00FF00" />-->
    </declare-styleable>
</resources>

在上面的XML里面,声明了一个自定义属性集合“CircleView”,在这个集合里面可以有许多自定义属性,但这个只定义了一个格式(format)为“color”的属性“circle_color”,这里的格式为“color”指的是颜色,除此之外,还有其他很多格式:

  • reference 参考某一资源ID
    xml自定义属性声明方法同color,我就不重复了
<CircleView app:cirlce_background = "@drawable/图片ID"/>
  • boolean 布尔值
<CircleView app:cirlce_focusable =  "true"/>
  • dimension 尺寸值
<CircleView app:cirlce_layout_width =  "421dp"/>
  • float 浮点值
<CircleView app:cirlce_fromAlpha =  "1.0"/>
  • integer 整型值
<CircleView app:cirlce_framesCount =  "12"/>
  • string 字符串
<CircleView app:cirlce_text =  "我是文本"/>
  • enum 枚举值
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circleview_orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
    </declare-styleable>
</resources>
<CircleView app:circleview_orientation =  "vertical"/>
  • 混合类型 属性定义时可以指定多种类型值
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
    <attr name = "circleview_background" format = "reference|color" />
    </declare-styleable>
</resources>
<CircleView 
app:circleview_background = "@drawable/图片ID" />
或者:
<CircleView 
app:circleview_background = "#00FF00" />
  • flag 位或运算
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
    <attr name="circleview_gravity">
            <flag name="top" value="0x30" />
            <flag name="bottom" value="0x50" />
            <flag name="left" value="0x03" />
            <flag name="right" value="0x05" />
            <flag name="center_vertical" value="0x10" />
    </attr>
    </declare-styleable>
</resources>
<CircleView app:circleview_gravity="bottom|left"/>/>

2. 在View的构造方法里面解析自定义属性的值并做处理
在Kotlin里面则是在init{}代码块做处理(相当于在构造方面里面,因为init{}就是在构造时调用的)

  init {
        var attrs = context.obtainStyledAttributes(attrs,R.styleable.CircleView)
        var mColor = attrs.getColor(R.styleable.CircleView_circle_color,Color.RED)
        paint.color = mColor
        attrs .recycle()
    }

首先是获取自定义属性集合CircleView,然后解析CircleView属性集合中的circle_color属性,它的id为R.styleable.CircleView_circle_color。在这一步骤中,如果使用时没有指定circle_color这个属性,那么就会选择红色作为默认的颜色值,解析完自定义属性之后,通过recycle()方法来释放资源。
3. 在布局文件使用自定义属性

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context=".MainActivity">

        <com.example.diyview.CircleView
            android:id="@+id/circle_view"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:background="#000000"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:circle_color="@color/colorPrimary"
            android:layout_margin="20dp"
            android:padding="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

效果图


相关文章

网友评论

    本文标题:Android Kotlin 自定义View的一些研究(一)

    本文链接:https://www.haomeiwen.com/subject/vuebzqtx.html