原文 GcsSloop大神的自定义View系列
在这个基础上简单整理学习。
上一篇对View的坐标位置等一些基础概念进行了介绍,这篇开始对自定义View的流程进行分析,后面再通过一个简单的实战来巩固。
首先看这么一个图,基本概述了整个自定view的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKPhXzf3-1585814037311)(https://evanzch.oss-cn-beijing.aliyuncs.com/img/20200318193000.png)]
视图View主要分为两类
类别 | 解释 | 特点 |
---|---|---|
单个View | 即一个View,如TextView | 不包含子View |
多个View | 即多个View组成的ViewGroup,如LinearLayout | 包含子View |
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。
例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。
在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。
例如:制作一个支持自动加载网络图片的ImageView,制作图表等。
上面给了总的自定义View流程图,下面会对每个流程进行介绍,部分重要的流程也会在后面的文章中重点介绍。
在讲构造函数之前,我们先看View类
View类简介
我们在自定义View或者自定义ViewGroup的时候,需要继承View
或者ViewGroup
,一般要重写四个构造方法,比如我这里定义了MyViewGroup
继承至ViewGroup
,重写的构造方法如下:
MyViewGroup myViewGroup = new MyViewGroup(this);
<com.evan.androidnote.ui.MyViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent" />
在第二个构造函数里面我们看到了AttributeSet
这个参数,这里简单对这个参数介绍一下,后面会有文章具体讲到。
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="EvanZch"
android:textColor="@color/colorPrimary"
android:textSize="20sp" />
我们定义一个最简单的TextView
,我们可以看到,里面有设置了各种配置属性,比如文本内容设置使用android:text
,字体大小设置使用android:textSize
这个就是AttributeSet
那我们怎么自定义属性呢?一般要遵循下面四个步骤:
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyViewGroupStyle);
int color = array.getColor(R.styleable.MyViewGroupStyle_color, Color.RED);
String text = array.getString(R.styleable.MyViewGroupStyle_text);
LogUtil.d(TAG + "--MyViewGroup color=" + color + ",text=" + text);
}
测量View大小
onMeasure()
方法用来测量View的大小,我们为什么还要测量View的大小呢?
因为View的大小不仅由自身确定,也会受到父控件的影响,为了我们自定义的View能更好的展示,我们一般需要自己来进行测量,根据测量结果合理的定义View大小。
测量View的大小需要重写onMeasure()
方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽度的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 获取宽度具体数值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高度的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 获取高度的具体数值
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}
可以看到在onMeasure()
方法中有widthMeasureSpec
和heightMeasureSpec
两个参数,看名字肯定跟宽高有关系,但是他们并不是表示宽高具体的值,要获取到具体的值需要使用MeasureSpec
这个类来获取,我们看到对应宽高,MeasureSpec
分别通过getMode
和getSize
两个方法来获取具体的数值,getSize
可以知道是获取具体的值,那getMode
又是干啥的?
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
通过传入的widthMeasureSpec
和heightMeasureSpec
获取对应的模式,那模式又是啥?我们看到返回值有UNSPECIFIED
、AT_MOST
、EXACTLY
三种,它们大致区别如下,可以先大概了解,后面后面也会具体介绍。
模式 | 描述 |
---|---|
UNSPECIFIED | 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。 |
EXACTLY | 表示父控件已经确切的指定了子View的大小。 |
AT_MOST | 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。 |
注意
如果在onMeasure()
方法中我们对View的宽高进行了修改,就不需要在调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)
这个方法,直接通过setMeasuredDimension(widthSize, heightSize)
来设置。
确定View大小
这个方法只有在视图发生改变的时候才会回调,通过方法名字我们也能看出来。
我们在前面已经通过onMeasure()
方法测量了View的大小,然后通过setMeasuredDimension(widthSize, heightSize)
来进行设置, 为什么还要再次确认View的大小?
原因是View的大小一是要由自身控制,而且受父控件的影响,所以,我们最好在onSizeChanged()
方法中确定View的具体大小
onSizeChanged
/**
* w : View的宽
* h : View的高
* oldw : View上一次的宽
* oldh : View上一次的高
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LogUtil.d(TAG + "--onSizeChanged w=" + w + ",h=" + h);
}
我们直接通过方法中的w和h就可以拿到对应的宽高。
确定子View的布局使用onLayout()
方法,用来确定子View的View,通常在自定义ViewGroup中会用上,调用的也是各个子View的layout
方法
在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。
view.layout(l, t, r, b);
四个参数分别为:
名称 | 说明 | 对应的函数 |
---|---|---|
l | View左侧距父View左侧的距离 | getLeft(); |
t | View顶部距父View顶部的距离 | getTop(); |
r | View右侧距父View左侧的距离 | getRight(); |
b | View底部距父View顶部的距离 | getBottom(); |
我们自定义View真正绘制的地方,所有的绘制都在这个方法下进行,后面会重点介绍。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
通过调用各种方法,能实现在画布上画出各种想要的View。
源码地址,注释基本都写了。
本篇对自定义View的整体流程大概走了一遍,对几个重要的方法简单进行了分析,这些方法在后面的自定义View的都会用上,到时候会再详细的介绍。