自定义view知识点总结
1. view的基本知识
1.1 什么是View
View是Android中所有控件的基类,无论是我们常用的TextView、Button,或者包含子控件的LinearLayout、RelativeLayout甚至是第三方引入的控件,它们的共同父类都是View。
ViewGroup是控件组,它的内部包含了许多个控件,也就是拥有一组view。在Android中ViewGroup也同样继承View,也就是说包含着一组控件的控件组也是控件的一种。
1.2 View的位置参数
View的位置根据四个属性决定:top,left,right,bottom。top对应View左上角的纵坐标,left对应横坐标,右下角同理。需要注意的是,View的坐标是相对于父容器而言的,是一种相对坐标。
由此可以得到View的宽度width = right - left,高度height = bottom - top。(注意,坐标系往上往左是负数,往下往右才是正数,所以width和height都是正数)
1.3 View的绘制流程
对于单个View而言,View的绘制流程主要是走onMeasure() -> onLayout() -> onDraw()三个方法。
- onMeasure():测量视图及其内容以确定测量的宽度和测量高度。我们在自定义View的时候,一定要重写一遍这个方法,根据具体情况决定View的宽高。
- onLayout():这是布局机制的第二阶段,为视图及其所有子视图指定大小和位置,当然这个位置是相对于父容器而言,是一种相对位置。我们只有在自定义ViewGroup时,会需要重写这个方法,自定义View不需要。
- onDraw():进行具体的绘图流程,像是TextView的写Text,Button的画出按钮,都是在这个方法进行。
onMeasure()的方法在onLayout()的第一行,所以是先测量宽高,再决定具体的大小和位置
2. 自定义view的流程
下面会通过设计一个简单的TextView来简单描述一下自定义View的流程
2.1 AttributeSet和自定义属性
Android中,我们在XML文件中定义的属性,如宽高,Margin,Padding等。都会在View的构造方法中传递给attributeSet的对象,再通过TypedArray进行读取使用其中的变量
我们自定义的属性也会传给AttributeSet,首先在res/values文件夹下,新建一个attrs.xml文件夹,添加相关的元素,这里添加文本和字体大小。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="myText" format="string"/>
<attr name="myTextSize" format="integer"/>
</declare-styleable>
</resources>
然后在View的构造方法中,通过TypedArray读取。
public class MyView extends View {
private String text; // 文本
private int textSize;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView); // 获取attribute的属性
Log.i(TAG, "MyView: " + typedArray);
text = typedArray.getString(R.styleable.MyView_myText);
textSize = typedArray.getInt(R.styleable.MyView_myTextSize, 100);
typedArray.recycle(); // TypedArray使用完后需要释放内存
init();
}
2.2 进行view具体的内容布置,onDraw()
Draw过程非常简单,就是将我们的view绘制到屏幕上,绘制内容需要笔类Paint和画布类Canvas,其中Canvas是onDraw()方法自带的参数,Paint则需要我们初始化。
public class MyView extends View {
private Rect mTextBounds;
private String text; // 文本内容
private int textSize; // 字体大小
private Paint paint; // 画笔
......省略无关代码......
private void init() {
paint = new Paint();
paint.setTextSize(textSize); // 设置字体大小
mTextBounds = new Rect();
paint.getTextBounds(text, 0, text.length(), mTextBounds);
// getTextBounds 是将TextView 的文本放入一个矩形中, 测量TextView的高度和宽度,等下Measure时会用到
}
}
初始化后直接画上去就可以了,调用Canvas.drawText方法搞定。
public class MyView extends View {
private String text; // 文本内容
private Paint paint; // 画笔
......省略无关代码......
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(text, 0, mTextBounds.height(), paint); // 把字用笔写在画布上
}
}
2.3 决定view的宽高,onMeasure()
onMeasure中的参数widthMeasureSpec和 heightMeasureSpec两个参数是父控件传递来的,实际上是将两个数specMode和specSize封装到一个int值里面,要用MeasureSpec类进行拆解,才能得到原来的数据。
widthMeasureSpec和heightMeasureSpec由于是int类型,所以是32位,其中高2位是specMode,低30位是specSize。
其中specMode这个值一共有三个模式
UNSPECIFIED:不对View大小做限制,如:ListView,ScrollView
EXACTLY:确切的大小,如:100dp或者march_parent
AT_MOST:大小不可超过某数值,如:wrap_content
一般来说,当且仅当xml文件中设置为wrap_content时,这个值才为AT_MOST,其他情况下都是EXACTLY,UNSPECIFIED不太常用。
对于我们的TextView而言,如果xml中设定了具体的宽高,那就直接采用即可;但是如果是wrap_content,那我们就需要根据文本的实际长度来决定TextView的宽高,这种时候刚刚获取的Rect变量就有用了。
具体如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.AT_MOST) { // 当且仅当view的宽高设置为wrap_content时,对应的specMode才是AT_MOST,其余情况下是EXACTLY
width = mTextBounds.width(); // AT_MOST时,需要根据实际情况,自行决定控件的大小,这里就根据矩形的长宽决定
}else if(specMode == MeasureSpec.EXACTLY){
width = specWidth;
}
specMode = MeasureSpec.getMode(heightMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.AT_MOST) {
height = mTextBounds.height();
}else if(specMode == MeasureSpec.EXACTLY){
height = specHeight;
}
setMeasuredDimension(width, height); // 决定自定义控件的大小
}
2.4 运行用例
运行之前放一下我们xml文件中的设置
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.wodongx123.customviewdemo.MyView
android:layout_width="wrap_content"
android:layout_height="100dp"
app:myText="测试测试测试"
app:myTextSize="50"
android:background="@color/colorAccent"/>
</LinearLayout>
可以看到,我们的宽度设置成wrap_content,所以和文本宽度一致,高度设置成100dp,所以高出文本的内容很多。
3. 补充内容
3.1 计算宽高和view位置时应带上padding
由于我们是自定义View,如果想要让设置的Padding生效的话,需要我们手动在代码中重新计算宽高和文本的显示位置。
这样一来,onMeasure()方法和onDraw()方法要改为这样。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
// 当且仅当view的宽高设置为wrap_content时,对应的specMode才是AT_MOST,其余情况下是EXACTLY
if (specMode == MeasureSpec.AT_MOST) {
// AT_MOST时,需要根据实际情况,自行决定控件的大小,这里就根据矩形的长宽决定
width = mTextBounds.width() + getPaddingTop() + getPaddingBottom();
}else if(specMode == MeasureSpec.EXACTLY){
width = specSize + getPaddingLeft() + getPaddingRight();
}
specMode = MeasureSpec.getMode(heightMeasureSpec);
specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.AT_MOST) {
height = mTextBounds.height() + getPaddingTop() + getPaddingBottom();
}else if(specMode == MeasureSpec.EXACTLY){
height = specSize + getPaddingTop() + getPaddingBottom();
}
setMeasuredDimension(width, height); // 决定自定义控件的大小
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(text, getPaddingTop(), mTextBounds.height() + getPaddingLeft(), paint); // 把字用笔写在画布上,算上padding
}
3.2 TypedArray使用后需要recycle
每次使用完typedArray后要调用recycle释放内存
recycle方法会将内部的一些对象指向为null
由于我们的Android界面有无数个View,所以会多次调用typedArray。
typedArray会被反复的创建和销毁,对于处理器的消耗比较大
为了防止这个问题,typedArray是使用了类似线程池一样的原理
在app生成的时候,就会创建很多typedArray放到堆区中,我们通过obtain方法来获取而不是通过new方法来创建
在用完之后,调用recycle,将typedArray释放回去,以便于另一个对象重用
同理的还有Handler中的Message(两者都是用obtain方法而不是new来创建实例)
3.3 让参数可以同时在java文件和xml文件中配置
我们平时在使用TextView的时候,不仅仅是会在xml文件中设置text,有时候也会在Java代码中调用setText这个方法。
我们的自定义View也需要能够支持同时在java文件和xml中配置的能力,根据实际的情况需求添加不同的方法,这里添加一个setText方法举例
public void setText(CharSequence c){
text = (String) c;
mTextBounds = new Rect();
paint.getTextBounds(text, 0, text.length(), mTextBounds); // 获取文本
requestLayout(); //重新调用onMeasure()和onLayout()
invalidate(); // 重新调用onDraw()
}
参考材料
Android开发艺术探索
3.1.1,3.1.2(P122-123),4.1 - 4.4(P14-217)
码牛学院 android移动互联网高级开发 课程 2020.10.15 录播
加QQ:1979846055可以获取
Android自定义控件并且使其可以在xml中自定义属性_XiaoM的专栏-CSDN博客
https://blog.csdn.net/mthhk008/article/details/30070119
MeasureSpec详解 - 简书
https://www.jianshu.com/p/cecd0de7ec27
getTextBounds 方法作用_liweicai137的博客-CSDN博客
https://blog.csdn.net/liweicai137/article/details/51327096
自定义View的重新绘制和更新_qq_39700454的博客-CSDN博客
https://blog.csdn.net/qq_39700454/article/details/79272136