其实关于自定义View,网上实在是太多太多了,但是呢,还是最终决定开一个专栏来讲述自定义View。这个专栏的目的有两个:
1. 我自己对于自定义View的总结
2. 讲述我自己在学习自定义View过程中走过的弯路,旨在希望各位同学少走弯路。
3. 工作四年,从0回归学习自定义View
好了,其实自定义View知识点真的是很多,很复杂,很难理解,因此,本专栏会通过很多篇博客循序渐进的学习自定义View的相关内容。这个专栏的大纲如下:
1. 自定义View基础概念
2. View树的绘制流程
3. 自定义ViewGroup
4. 自定义ViewGroup项目中的常见问题(以ScrollView为例)
5. 自定义View
以上就是自定义View专栏的主要内容,PS:自定义ViewGroup也可以称为:自定义View。通过该专栏的学习,各位同学可掌握如下技能:
1. 能解决布局嵌套不显示问题
2. 能写出各式各样的自定义ViewGroup以及View
3. 能够明白问题的原理,以及源码的原理,轻松应对实际项目和面试中的相应问题
这个专栏我打算从基础,一层一层深入源码,并且,根据源码反推我们项目中遇到的实际问题的解决办法,这样的话,就达到了知其然而知其所以然。
闲话少叙,开始今天的表演。。。
本篇博客是第一篇博客,学习一下自定义View的基础知识,其实关于基础知识,可能可能很多人不太在意,但是通过四年的工作磨炼,我发现其实万般框架,万般花里胡哨的东西,都是有基础知识构成的,所有本篇博客我们回归基础,重温学习一下。
自定义View的分类
关于自定义View我们分为两类
- 自定义ViewGroup: 一般只需要重写onMeasure方法和onLayout方法
- 自定义View:一般只需要重写onMeasure和onDraw方法。
我们知道一般无论是自定义View还是自定义ViewGroup都无非三个方法:onMeasure(), onLayout() 和 onDraw()方法。
其实在面试中我们经常被问到:了解自定义View吗?
面试者:了解,自定义View需要重写:onMeasure,onLayout, onDraw三个方法。
没然后了,其实这样说不错,但是呢,如果你是工作3-5年的开发者,很显然,这个回答是不合格的,因为你需要回答出这三个方法是怎么调用的?内部的绘制流程是什么? 其中必然会牵扯出:ViewRootImpl,WMS,DecorView等等,这个问题,专栏之后博客会讲到。
现在我们需要明白的是三个方法:onMeasure, onLayout,onDraw。
View类简介
我们先看下View的源码:
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
我们在XML中调用的是第二个构造函数,将XML解析成了Java代码。
PS:这里面会延伸一个问题:XML是怎么转化成Java代码的呢? 这个问题之后专门博客讲述,感兴趣的小伙伴,可以看下:LayoutInflater这个类。
View的视图结构
其实关于View视图的结构,了解Activity启动模式的应该是清楚的,或者了解Dialog的也是清楚的,应为它们都是创建了PhoneWindow,在Activity创建的时候,创建了一个PhoneWindow并且创建了相应的DecorView,
其实这里的PhoneWindow是虚拟的概念,它并不是一个真正的View,而DecorView才是真正的View,而DecorView才是真正的View,而DecorView内部是一个线性布局(不同的API不同的布局)。不懂DecorView构造的看下:Android进阶3:Activity源码分析(3) —— setContentView分析(8.0)。
其实上面一坨东西,就是想说我们自己的布局就添加在DecorView中,DecorView就是顶级的View。PhoneWindow并不是一个真正的View,是个虚拟概念。记住这些,就## 标题完了。
- PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
- DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
- ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等
Android中的坐标系
其实Android中的坐标系,和我们正常中数学的坐标是不一样的:
屏幕的左上角是屏幕的中心,X轴从左到右,是逐渐增大的,Y轴从上到下是逐渐增大的。角度顺时针是逐渐增大的。
View位置(坐标)描述
View的位置是有四个顶点决定的,而都是相对于父布局的。
- Top:子View上边界到父view上边界的距离
- Left:子View左边界到父view左边界的距离
- Bottom:子View下边距到父View上边界的距离
- Right:子View右边界到父view左边界的距离
MotionEvent中,getX和getRawX的区别
getX是相对于自己的,getRawX是相对于的父布局。如下图:
MeasureSpec
其实这东西,网上很多博客,关于概念我就说一下:是一个32位的int值,高两位代表测量模式,低30位代表测量大小。
MeasureSpec有三种测量模式:
- UNSPECIFIED
父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) - EXACTLY
父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。 - AT_MOST
你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
我重点说一下它的用处,在项目中,我们可能会发现一个问题:有时候我们设置View为Match_parent,但是不起作用。 我想这个问题肯定是都遇见过的吧,今天我们掰扯一下这个东西。
既然不起作用,那么肯定是还有一个因素影响了测量的大小。 没错,就是MeasureSpec这东西,先说结果吧,一个View的最终大小是有:父布局的MeasureSpec和自己的LayoutParam决定的。
那么是怎么影响的呢? 看下ViewGroup的getChildMeasureSpec方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父布局的测量模式以及大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//这里的计算父布局的Padding以及自己的Margin
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 当父布局给的测量模式是:精确模式时,
case MeasureSpec.EXACTLY:
//这时候根据子View在XML设置的大小而确定最终的大小
if (childDimension >= 0) { // XML中设置的是大于0 的值:比如:100dp
//此时子View大小就是:100dp
resultSize = childDimension;
//测量模式时精准模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
//此时是父布局有多大,子布局你可以这么大,但是不能超过父布局
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
//父布局的测量模式是:AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
//此时父布局都不知道自己有多大,所以给了子View的测量模式也是AT_MOST,但是子view再大也不能大过父布局,
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述代码的各种情况,希望各位仔细阅读,至于为什么分析这个方法,等说到自定义ViewGroup布局的时候会说到,这里先提一下。 通过上述代码我们可以得到如下图:
上图的纵列是子View在XML中定义的大小,横列是父布局的测量模式。 因此我们的在XML中定义的大小,并不是最终的大小,而是自己想要的大小,真正的大小,还得依赖父布局的测量模式。通过测量之后,才能知道自己的确切大小,而测量的标准就是:上图。
以上就是我觉得自定义View需要掌握的很重要的基本知识点,下篇博客分享:View树的测量绘制