自定义View基础
view树绘制起点
android中大多数情况下我们的界面都是使用view进行显示,(目前我知道的除过使用surface进行显示的视图,其余的都是使用View进行显示,如果有其他方式大家可以一起讨论~)
首先介绍两个类的主要作用:
- ViewRootImpl:
视图层次结构的顶层,实现了WindowManager和View之间的协议,包括View的绘制过程,包括measure、layout、draw过程;event事件分发,如按键/触屏等事件。- DecorView:
是界面中最顶层的View
android中界面使用view进行显示的时候界面可以看作是一颗树,父节点会包含对应的孩子节点,而这棵树的根节点为DecorView,界面上的视图都在这个父节点中显示。而Activity只负责生命周期和事件处理,不进行ui和视图控制相关的处理,视图的绘制和控制是由ViewRoot完成的。
当activity创建完毕后,会将ViewRoot和DecorView进行关联,
root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView);
源码见WindowManagerGlobal.java,Activity创建的时候如何调用到这里可以参见博客 链接
ViewRoot和DecorView进行关联的时候调用了ViewRoot的setVIew方法,(ViewRoot实现对应于ViewRootImpl类,具体源码可以在该类中进行查看),setView方法中调用了requestLayout()方法,
而requestLayout会调用scheduleTraversal,在scheduleTraversal方法中会发送mTraversalRunnable事件,
mTraversalRunnable中会执行doTraversal方法,该方法中会调用performTraversal方法,从该方法开始,view树开始调用performMeasure(),performLayout(),performDraw()进行绘制。
performTraversal绘制流程
performTraversal()方法调用后会首先调用父容器的measure,layout,draw方法,然后递归调用到子View的measure,layout,draw方法。
MeasureSpec介绍
查看MeasureSpec的注解:
MeasureSpec携带了父view对子view宽高的要求,而该要求是通过view自身的layoutParams和父容器施加的转换规则转换得到的。
MeasureSpec实现时为了减少创建对象的开销,使用了数字进行实现。使用高两位表示SpecMode,使用低30位表示SpecSize,MeasureSpec在实现的时候也提供了相应的方法进行Mode和Size的提取,以及Mode和Size的组合。
该方法进行mode和size的组合
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
下面的两个方法则分别获得相应的size和mode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec在实现的时候规定了三种mode,如下:
UNSPECIFIED :这种情况下父容器不对view的大小做任何限制。
EXACTLY :父容器已经给出view的大小,这个时候SpecSize大小就是View最后的大小。这种情况对应于layoutparam为match_parent和具体数值的这两种情况,给出view一个明确的大小。
AT_MOST :父容器指定了一个SpecSize,View的大小不能大于这个值,具体是什么值需要看不用view的具体实现。这种情况对应于layoutparams为wrap_content,限定了view的范围,但是没有明确要求view的大小。
View在进行绘制的时候是以自身的MeasureSpec为标准的,而View 的MeasureSpec的是根据父容器的MeasureSpec和View自身的Layoutparams得到的,具体的方法参见getChildMeasureSpec()方法,将该方法中的规则总结出来表格如下:
而对于顶级View(DecorView),其MeasureSpec由自身的LayoutParams决定,具体可以参见方法ViewRootImpl中的getRootMeasureSpec()方法。
LayoutParames | SpecMode |
---|---|
MATCH_PARENT | EXACTLY,大小就是窗口的大小 |
WRAP_CONTENT | AT_MOST,大小不能超过屏幕大小 |
固定大小如(100px) | 精确模式,大小为指定的大小 |
将顶层view的测量可以总结成上表 |
View绘制流程介绍
measure
View的测量过程由measure方法开始,该方法为final方法,无法被继承,measure的过程主要由onMeasure方法完成,子类在使用的时候重写该方法即可,通过该方法我们可以使用viewgroup中传递过来的measureSpec来确定view测量出的大小:
android.view.View#onMeasure
查看上述代码,onMeasure()方法中会调用setMeasuredDimension方法保存view的宽和高,view的宽和高通过getDefaultSize和getSuggestedMinmumWidth方法获取。
ViewGroup的measure过程:
相比于view的measure过程,ViewGroup在measure的过程会调用子view的measure方法,进行子View的大小的测量,然后进行自身大小的测量
但是ViewGroup自身是一个抽象类,没有定义具体的测量过程,即没有实现onMeasure方法,因为不同布局的测量方式不同,比如说RelativeLayout和LinearLayout的测量方式必然不同,所以onMeasure的具体过程需要子类去自己实现。以LinearLayout为例
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure方法会区分当前配置,进行垂直方向或者竖直方向的测量
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
...
// 循环遍历所有child,调用View#measure进行测量
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
...
} else {
...
// 测量child的宽高
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// child测量完成后,获取测量后的高度
final int childHeight = child.getMeasuredHeight();
...
// 将child的高度累加到mTotalLength中
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
...
}
...
// 调用View#setMeasuredDimension方法设置LinearLayout的的测量宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
...
}
上面的测量流程中
- 先for循环遍历子View,测量子View的大小
- 子View测量完毕然后进行自身大小的测量。
其它博客:measure流程介绍:
https://blog.csdn.net/a553181867/article/details/51494058
layout
Layout方法用来确定view自身的位置,而onLayout方法用来确认所有的子元素的位置,在layout方法中会调用setFrame方法首先确定自身的位置,
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
然后调用onLayout方法布局孩子的位置,
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
而onLayout方法可以看到view自身并没有实现,因为每一个布局自己对自己的孩子的布局方式不同,具体的实现需要相应的子类去重写。
例如在LinearLayout的onLayout中,会根据当前布局样式显示对应的布局:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
layoutVertical 和 layoutHorizontal方法中会调用子View的Layout方法进行布局。
void layoutVertical(int left, int top, int right, int bottom) {
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
...
// 这里的setChildFrame方法中调用的就是child.layout方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
...
}
}
}
setChildFrame方法中调用的就是child.layout方法
其他博客:layout详细过程:
https://blog.csdn.net/a553181867/article/details/51524527
https://blog.csdn.net/qq_26287435/article/details/94402563
draw
View在绘制的时候分为如下几步:
...
drawBackground(canvas); // 1.绘制背景
...
if (!dirtyOpaque) onDraw(canvas);// 2.绘制自身内容
dispatchDraw(canvas);//3.绘制孩子(该方法需要子类重写,从而根据子类自身的规则去绘制相应的view)
drawAutofilledHighlight(canvas);// 4.绘制相应的装饰
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas); // 4.绘制相应的装饰
}
onDrawForeground(canvas);// 4.绘制相应的装饰
drawDefaultFocusHighlight(canvas);// 4.绘制相应的装饰
ViewGroup在绘制的时候setWillNotDraw()会在中设置为true,用来优化绘制,如果我们需要在ViewGroup中绘制内容时,需setWillNotDraw()为false来关闭优化。
View的绘制流程可以概括为如下:
- 先进行自身当前View的绘制调用draw方法,onDraw中对应实现
- 然后进行子View的绘制,对于View的话dispatchDraw方法为空实现,ViewGroup会实现该方法,dispatchDraw中调用子类的绘制。
自定义View
实现效果: 支持padding,支持自定义属性实现颜色的自定义,同时支持wrap_content。
通过重写onDraw()我们去实现一个圆形,代码实现比较简单,但是我们需要注意
1:对直接继承自View和ViewGroup的控件,padding属性是默认无法生效的,需要自己处理。
2:继承View和ViewGroup的控件,自己需要处理wrap_content属性
处理padding属性无效的问题:在我们确定宽和高的大小进行绘制的时候我们需要将padding计算在内
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
处理wrap_content无效的问题:
通过上表:我们可以看到当设置为wrap_content的时候specSize为parentSize,查看getDefaultSize的源码,mode为AT_MOST的时候view的大小就是specSize,所以设置wrap_content等价于match_parent:
因此当我们自定义view使用wrap_content属性的时候,我们需要自己处理,处理的代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 下面的过程处理view的书信给wrap_content
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
上面的代码中,当我们判断view的specMode为AT_MOST的时候,我们设置大小为200(这个值可以自己根据自己的需要设置),通过上面的处理方式我们解决wrap_content不生效的问题。
看到这里自己脑子里面突然有个疑问
红色的圈出来的那一部分也是会让view的specMode为AT_MOST,那我们重写了view对AT_MOST的处理,当我们设置view为match_parent的时候那岂不match_parent没办法生效了?
自己想了想,什么时候父容器的specMode为AT_MOST呢,查看上表《DecorView的测量规则》,只有当我们设置为wrap_content属性的时候才是AT_MOST,那这就意味大小只要满足孩子的大小即可,即我们自己实现的wrap_content。
增加自定义属性
首先我们在values目录 下面新建一个自定义属性文件,文件名字可以随意起,在这里我新建一个attrs.xml文件,文件中我们自定义自己需要的属性,这里定义了color属性:
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
上面的format中我们还可以填写如下值,
format | 含义 |
---|---|
reference | 代表资源id |
dimension | 尺寸 |
基本数据类型(string/integer/boolean) | 需使用者自己定义 |
代码中我们通过下面的方式使用 | |
在xml跟布局上面增加一个app命名空间,然后我们在该命名空间下使用我们自定义的属性 |
<LinearLayout ...
xmlns:app="http://schemas.android.com/apk/res-auto"
...>
<com.example.customizeview.CircleView
...
app:circle_color="@color/colorAccent" />
</LinearLayout>
最后我们需要在代码中获取我们自定义的属性
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.i(TAG, "CircleView: enter constructor with attrs");
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
typedArray.recycle();
init();
}
mColor就是我们自己定义的颜色。
完整代码见 CircleView