前言
闲来无事,来学习下自定义View的一些知识,发现了不少的坑,在此做下笔记给大家分享下
自定义View的分类
自定义View的实现方法分挺多种的,这个简单做一下分类
-
继承View重写onDraw方法
Android Kotlin 自定义View的一些研究(一) - 继承ViewGroup派生特殊的Layout
- 继承特定的View(比如TextView)
- 继承特定的ViewGroup
这几种自定义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>
效果图

网友评论