概述
android开发中,系统控件有时候不适用于 具体的业务场景,此时就需要我们创造出自己的自定义View。 自定义View的方式大体上分为两种:
-
- 继承系统成熟控件(LinearLayout,RelativeLayout,ImageView等),然后进行
魔改
- 继承系统成熟控件(LinearLayout,RelativeLayout,ImageView等),然后进行
-
- 继承View/ViewGroup 并自己绘制内容
方式1-半自定义(魔改)
如下图:继承RelativeLayout,然后在构造函数中动态添加一些view
以下是我们需要的效果:
自定义属性
比如上面的例子,我们需要对 左边的图,右边的图,以及中间的文案进行静态设置(布局xml中设定)。
必须先编写属性: 在 res/values/attr.xml中,声明如下内容
<declare-stylable name="CustomToolBar">
然后在 java部分的源代码中,获取自定义属性的值:
方式2:完全自定义
这种方式下,我们需要考虑几个问题:
- 如何将UI元素绘制到界面上
- 如何控制控件宽高
- 如果是自定义ViewGroup,如何确定子view的摆放
这3个问题分别对应View/ViewGroup的以下几个方法:
- onDraw
- onMeasure
- onLayout
onDraw 绘制
onDraw方法会接收一个 Canvas类型的参数,而Canvas中,存在以下这些基础的绘制函数 在安卓底层,绘制引擎其实是和Flutter一样的 skia,我们在java代码中看到的 canvas对象,其实是c++层 canvas对象的代理。当我们操作java层Canvas的时候,其实也就是在间接操作 c++层的canvas画布。
上图中可以看到在draw的时候有一个Painter(画笔)类型的入参,它决定了画笔的属性:
举个draw的例子: 当我们要画出一个圆形进度条,首先在外围画出一个圆,然后在内层画出一个弧形。
布局中的使用很简单:
同时可以在java代码中动态设定 进度值:
显示效果如下:
onMeasure 测量
上面的例子中,我们在使用 自定义组件时,是直接设定了 宽高为300dp,但是很多时候,我们不知道具体的宽高值,而是要根据父容器的宽高来综合决定。
Android中提供了 wrap_content(自适应大小) 和 match_parent(填满父容器) 两种属性来规范控件的范围,这两种情况其实是没有指定具体数值的。所以,我们在自定义View的时候,需要考虑这两种情况下,控件的宽高应该如何表现。
以下是view的测量方法: 这里可以看到 两个入参都是int类型的,可能以为这两个入参是 宽高的具体数值,其实并不是。 每一个int类型,为4个字节,每个字节为8位,一共32位,高2位,为测量模式 ,低30位为 具体数值(最大值为2^30-1)。
测量模式
一共有3种:
- EXACT 具体数值 也就是我们在xml中直接指定了300dp这种情况 以及 XML中指定了 match_parent
- AT_MOST 对应了 XML中设定的 WRAP_CONTENT
- UNSPECIFIED 未指定模式,也就是说不加以限制,你甚至可以超出父容器大小
在int中获取测量模式和具体数值的方法如下: MeasureSpec.getMode()
决定宽高的方法
下图是最终决定控件宽高的方法:setMeasuredDimension()
也就是说,setMeasuredDimension(200,300) 之后,也就意味着,宽高被定死为 宽200像素,高300像素。
上图中为View源码中的默认大小,默认为 父视图的可用空间。
正确的onMeasure写法
上面的圆形进度条控件中,如果我们在xml中不去设定它的具体宽高300dp,而是指定为 wrap_content,那么它的测量模式就是 AT_MOST。那么它就默认使用了父容器的剩余可用空间,于是 就显示为:
绘制的内容超出了边界,这不是我们想要的结果。 回到它源代码中,重写它的onMeasure方法如下:
代码的含义为:如果测量模式为 AT_MOST(对应XML中的wrap_content),我们手动指定宽高中的较小值为自定义View的绘制边界。
ViewGroup的onMeasure
如果我们自定义的是一个ViewGroup容器,那么onMeasure方法会更复杂。因为 作为一个容器,很多情况下必须先测量出内部子view的大小,然后决定自身大小。
比如如下代码:一个LinearLayout中放了3个TextView,而且长短不一。
显示结果如下:
LinearLayout的宽度,由子View的最大宽度决定,高度由所有子View的总宽度决定。
所以,当我们自定义一个ViewGroup的时候,要综合考虑子View的宽高情况。 比如做一个流式布局的自定义ViewGroup容器。 以下是伪代码:
java
@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.getMode(heightMeasureSpec);
int childCount = getChildCount();
int totalHeight = 0;// 记录累计的高度
int totalWidth = 0; // 自身的最大宽度
for(int i=0;i<= childCount-1;i++){
View child = getChildAt(i);
measureChild(child,widthMeasureSpec,heightMeasureSpec); // 测量子view
// 测量完毕之后,子view就有了自己的宽高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
// ... 接下开就计算出每个子View的高度
totalWidth = Math.max(childWidth,totalWidth);
totalHeight+=childHeight;
}
setMeasuredDimension(totalWidth,totalHeight);
}
上面的代码的目的为:
- 调用 measureChild方法递归测量子view
- 通过叠加每一行的高度,决定最后ViewGrop的高度
- 通过每一次的max计算,得出ViewGroup的宽度
onLayout 放置
上面的onDraw和onMeasure决定了绘制的内容以及 控件的宽高,而还剩下最后一个问题,控件放在哪个坐标上。 下图为onLayout的源码:
我们的自定义ViewGroup需要重写onLayout方法来决定每一个子view的排布。
伪代码如下:
java
@override
protected void onLayout(boolean changed,int l,int t,int l,int b){
int childCount = getChildCount();
for(int i = 0 ;i <= childCount -1;i++){
View childView = getChildAt(i);
// 布局逻辑,计算外间距等。。。
childView.layout(,,,); // 指定具体的放置坐标
}
}
上面的代码中,遍历了每一个子view,并且进行布局逻辑的计算,然后分别指定每一个子view的排布坐标。
总结
自定义View离不开3个方法。
- onDraw:负责绘制UI元素
- onMeasure:负责测量控件宽高
- onLayout:在自定义ViewGroup时用于决定子view的坐标。