Q1:什么是View?
View是屏幕上的一块矩形区域。
Q2:Google为什么设计View?
设计View的目的是为了解决用户和应用之间交互。
Q3:怎么样交互?
绘制自己与事件处理两种方式与用户交互。
Q4:矩形区域在屏幕的位置和大小由谁来决定?
一个界面有很多View或者ViewGroup,google用window先去加载一个超级复合View,用它来包含住所有的其他View,这个超级复合View就叫做DecorView。界面中被包裹的View将能直接或间接被DecorView中的FrameLayout去分配位置和大小(大小的控制还和view自身有关)
Q5:DccorView中的FrameLayout怎么分配位置?
总结:普通View只能被安排,被嵌套的ViewGroup不仅要被安排,而且还要安排它的子View位置。子View的位置信息由开发者告诉父View直接控制,不需要子View的参与。
为了证明上总结我们画一个圆:
class MyView : View{
val paint: Paint//画笔
//构造方法
constructor(context: Context?) : this(context, null)
//构造方法
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
//构造方法
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
//抗锯齿
paint = Paint(Paint.ANTI_ALIAS_FLAG)
//设置画笔颜色
paint.color = Color.BLACK
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(100F,100F,100F,paint)
}
}然后布局使用layout开头的属性(除了宽度和高度的设置,后边讲解)照样管用,由此更加验证了上边总结。
Q6:怎么确定View的大小?
开发者向ViewGroup表达我这个子View需要的大小,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(生成这两个对象的方法在ViewGroup的getChildMeasureSpec方法中),通过子View的onMeasure方法传递给子类,这两个对象是ViewGroup向子View提出的要求,就相当于告诉子View:“我已经与你的使用者(开发者)商量过了,现在把我们商量确定的结果告诉你,你的宽度不能违反width MeasureSpec对象的要求,你的高度不能违反height MeasureSpec对象的要求,现在,你赶紧根据这个要求确定下自己要多大空间,只许少,不许多哦。然后告诉我”
分析getChildMeasureSpec源码
/**
* @param spec 父布局给的spec
* @param padding 父布局的边距
* @param childDimension 开发者在布局文件中要求的大小layout_width 和layout_height的值
* @return返回父布局和开发者商量过后的布局大小
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父View的测量模式和大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//计算出我能给子View的最大值
int size = Math.max(0, specSize - padding);
//初始化子View的大小和模式
int resultSize = 0;
int resultMode = 0;
//根据自身模式去给子view的大小和模式赋值
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0,即有确切的值,把这个确切的值给子view,
// 同时模式是:EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//如果是撑满,就把父View能给他的最大值给子View,
// 同时模式是:EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
//如果是包裹内容,也把父View能给他的最大值给它,
//很明显这里是包裹内容,但是确给了最大值,记住这里是个错误值,
// 它不是不想给,而是不确定也给不了,需要你自己测量后给我,然后我layout的时候才能修改成正确值
// 也很好的解释了为什么MATCH_PARENT和WRAP_CONTENT默认是撑满父布局
// 注意这里模式是不要超过这个值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 道理同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//注意这里模式改变了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
//一般是滑动父布局
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 道理同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,可以滚动的,所以这里这个限制值没有意义6.0之前返回是0,6.0之后返回是开发者要求的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,可以滚动的,所以这里这个限制值没有意义6.0之前返回是0,6.0之后返回是开发者要求的大小
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}总结
只要给一个确定大小的值,全部都是恰好模式。值也是开发者给的值。其余只要遇到包裹内容或者包裹内容模式,子类全部是包裹内容。只要遇到不确定模式,全部是不确定模式。
Q7:MeasureSpec对象中的要求怎么获取以及含义是什么?
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSize = MeasureSpec.getSize(heightMeasureSpec)假设size是100dp,Mode的取值有三种,它们代表了ViewGroup的总体态度:
- EXACTLY 标记,ViewGroup对View说,你只能用100dp,原因是多样的,可能是你的开发者说要你完全占据我的空间,而我只有100dp。也可能这是你的开发者的要求,他需要你占这么大的空间。
- AT_MOST标记,你最多只能用100dp。这是因为你的开发者说让你占据wrap_content的大小,让我跟你商量,我又不知道你到底要占多大区域,但是我告诉你,我只有100dp,你最多也只能用这么多。
- UNSPECIFIED标记,把你最理想的大小告诉我,我可以滚动,你随便来
这里就说明了必须要自己测量大小然后给父View否则wrap_content不管用。
Q8:子View怎么测量自己需要的大小?
不同的View有不同的态度,但是有几点基本的规矩是要遵守的:不要违反ViewGroup的规定,google给我们提供方便的方法来遵照这一固定resolveSize(真实需要的宽高(简单的数学算法), widthMeasureSpec(父类的要求)),返回值是优化后遵照这一规定的宽高。查看源码发现调用resolveSizeAndState(size, measureSpec, 0)方法。这里多了一个标记,目的是告诉父布局,我是委屈的宽高。
/**
*
* @param size 需要的真实大小
* @param measureSpec 父布局的规定
* @param childMeasuredState 不知道是什么
* @return 返回一个优化后的宽高
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//获取父布局的模式
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {//如果测量的比能给的还要大,就给它我能提供的最大限度
result = specSize | MEASURED_STATE_TOO_SMALL;//标记我想要的更大
} else {
result = size;//如果给的比要求的小,没有问题,直接赋值
}
break;
case MeasureSpec.EXACTLY:
result = specSize;//直接给我的最大限度,或者开发者要求的值
break;
case MeasureSpec.UNSPECIFIED://给你需要的真实值,因为我可以滚动
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}Q9:子View按照要求测量自己的大小后怎么告诉父View?
根据上边google提供的方法,就可以计算优化出一个满足父布局要求的自己的尺寸,这个时候就需要告诉父布局我需要的真实尺寸,进而去修改上边在包裹内容时候的错误数据,保证包裹内容的数据是正确的,在使用layout的时候就会用矫正过的正确值去渲染控件大小。
google给我们提供了方法setMeasuredDimension(计算优化后的真实宽,计算优化有的真实高);
到此当渲染的时候每个view都在自身储存了自己的位置和大小信息,关于View的绘制,就是一个方法onDraw,保证在当时测量的大小范围内去绘制就行。后边讲解绘制的细节。
总结:一般onMeasure方法中的代码都是告诉父布局,我真实的大小:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(resolveSize(真实需要大小, widthMeasureSpec), resolveSize(真实需要大小, heightMeasureSpec))
}Q10:View中都有哪些方法?

方法基本执行顺序:https://blog.csdn.net/anhenzhufeng/article/details/72886181
小结:
- View是一个矩形区域,设计它的目的是为了和用户交互,交互的方式有自我绘制和事件处理
- View的位置由开发者告诉父布局,父布局直接决定位置
- View的大小有开发者和父布局和自身大小共同商量决定
- 自定义View中常重写的方法,以及这些方法会改变什么
认识了什么是View,知道大小和位置是由哪些代码决定的,接下来就是绘制和处理事件,接下来几篇文章准备写一些常用的绘制方法和技巧。