详论View体系
Activity加载布局过程
加载布局是如何进一步显示的,书籍里大多贴以源码,文字说明却是云里雾里,这里就不对源码进行解读,贴出我做的图片,供大家在宏观局面了解一二。
纠错:下图黄色背景Activity.setCintentView应该为Activity.setContentView(..)
,并且它为入口。
一个Activity包含一个Window对象(由PhoneWindow实现),PhoneWindow将DecorView作为整个应用的根View,DecorView又见布局划分为TextView做标题和给开发者嵌入自己的布局。
View的事件分发机制
手指触摸屏幕,产生的事件就会被封装成“MotionEvent
”,系统会将这个传递给View层级,这个传递过程就是事件分发。先提前了解些方法:
方法 | 说明 |
---|---|
dispatchTouchEvent(MotionEvent) | 用来进行事件的分发 |
onInterceptTouchEvent(MotionEvent) | 用来进行事件的拦截,在dispatchTouchEvent调用,注意View没有该方法 |
onTouchEvent(MotionEvent) | 处理点击事件,在dispatchTouchEvent进行调用 |
接下来我们看其分发机制:
总结一下,当ViewGroup要拦截事件时,后续的事件都要交给它处理,而不用调用onInterceptTouchEvent方法了。故:onInterceptTouchEvent不是每次事件都会调用的。
onInterceptTouchEvent
onInterceptTouchEvent的代码非常简单:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
默认不进行拦截,如果要让ViewGroup拦截事件,应该在自定义的ViewGroup中重写这个方法。
dispatchTouchEvent
dispatchTouchEvent非常多,这里只写出核心代码,并做解释:
// 第一部分:
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--)
{
[code...]
// 第二部分:
if (
!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)
) {
[code...]
continue;
}
[code...]
// 第三部分:
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
{ [code...] }
}
第一部分,首先(倒序遍历,即从最上层的子View开始内层遍历)遍历ViweGroup的子元素,判断是否能接收到点击事件,若能,交给子元素处理。
第二部分,判断触摸点的位置是否在子View的范围内,或是否在播放动画。若都不符合,则执行continue语句,表示这个子View不符合条件,开始遍历下个子View。
第三部分,先看dispatchTransformedTouchEvent:
1. 如果有子控件,则调用其dispatchTouchEvent;若没有,则调用super.dispatchTouchEvent
。ViewGroup继承自View,所以调用的也就是view的dispatchTouchEvent
:
2. view的dispatchTouchEvent
:若OnTouchListener
不为null且onTouch返回ntrue,则表示事件被消费,不再执行onTouchEvent方法。否则,会执行onTouchEvent。(OnTouchhListener
的onTouch方法优先级高于onTouchEvent
方法)
3. view的onTouchEvent
方法:只要View的CLICKABLE
和LONG_CLICKABLE
(View代表可被点击和可被长按点击)有一个为true,即可消耗此事件。
CLICKABLE
和LONG_CLICKABLE
可以通过setClickable
和setLongClickable
方法来设置,也可以通过View的setOnClickListener
和setOnLongClickListener
来设置。
4. view的`onTouchEvent`方法接着在**ACTION_UP**事件中调用`performClick`方法。 5. `performClick`方法:如果View设置了OnClickListener,那么就会执行OnClickListener的onClick方法。 # View的事件传递规则 这三个重要关系,可以如下代码表示:
fun dispatchTouchEvent(ev : MotionEvent) : Boolean
{
return if(onInterceptTouchEvent(ev)) {
onTouchEvent(ev)
} else {
child_view.dispatchTouchEvent(ev)
}
}
具体流程图如下:
点击事件由上自下传递给子控件,子控件自下而上消耗此事件。事件由下传递上返回规则是:若onTOuchEvent返回true,不继续向上传递;为false则不处理继续向上传。
View工作流程
View工作流程指的是measure
、layout
、draw
。其中,measure用于测量view宽、高。layout确定view位置,draw则用来绘制view。
当DecorView被加载到Window中时,通过Activity的创建过程,当调用Activity的startActivity方法时,调用流程如下:
我们细看ViewRootImpl的方法performTraversals()
(android 29):
private void performTraversals()
{
// TODO: In the CL "ViewRootImpl: Fix issue with early draw report in
// seamless rotation". We moved processing of RELAYOUT_RES_BLAST_SYNC
// earlier in the function, potentially triggering a call to
// reportNextDraw(). That same CL changed this and the next reference
// to wasReportNextDraw, such that this logic would remain undisturbed
// (it continues to operate as if the code was never moved). This was
// done to achieve a more hermetic fix for S, but it's entirely
// possible that checking the most recent value is actually more
// correct here.
if (!mStopped || wasReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || dispatchApplyInsets ||
updatedConfiguration)
{
// 第一部分
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
// 第二部分
if (didLayout) {
performLayout(lp, mWidth, mHeight);
[code..]
}
// 第三部分
if (!cancelDraw) {
performDraw();
}
}
我们只看第一部分的childWidthMeasureSpec
和childHeightMeasureSpec
,这样来引出MeasureSpec。
MeasureSpec的理解
MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View宽高信息。他的作用是:在Measure过程中,系统将View的LayoutParams根据父容器所施加的规则转换成对应MeasureSpec,然后在onMeasure()方法根据这个MeasureSpec确定View的宽高。
先看measure的常量代码:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
}
MeasureSpec 代表了32位的int值,最高2位代表specMode
,剩余低30位代表了specSize
。
specMode
: 测量模式,有三种:
specMode | 说明 |
---|---|
UNSPECIFIED | 未指定模式,View大小不被父容器限制,想多大就多大,一般用于系统内部测量 |
EXACTLY | 精确模式, 对应match_parent 属性和具体数值,父容器测量出view所需要大小,即specSize 值 |
AT_MOST | 最大模式,对应warp_parent 属性,子view由父布局指定大小,并且不能超过这个值 |
对于每一个view 都持有一个 保存了该view的尺寸规格的MeasureSpec。在View测量流程中,通过makeMeasureSpec
来保存宽高信息。通过getMode
或getSize
得到模式和宽高。
MeasureSpec受自身LayoutParams和父容器的MeasureSpec共同影响。
作为顶层view DecorView
,并没有父容器,是由自身LayoutParams和窗口尺寸决定的。
我们再来看ViewRootImpl的performTraversals
方法:
// 第一部分
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
getRootMeasureSpec
会根据自身layoutParams来得到不同MeasureSpec。这样就能理解传入的参数是什么了。
performMeasure的代码很简单,就是调用子控件的measure:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
View 和 ViewGroup 的 measure流程
measure用来测量View的宽和高。它的流程分为View流程和ViewGroup流程;ViewGroup除了完成自己的测量之外,还要遍历调用子元素的measure方法。
View 的 measure流程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
setMeasuredDimension
方法是
用来设置view宽高的。而getDefaultSize
方法如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
// 测量模式
int specMode = MeasureSpec.getMode(measureSpec);
// view大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
&emsp在AT_MOST
、EXACTLY
模式下,都返回specSize这个值,也就是这两种模式都取决于specSize,换而言之,对于一个直接继承View的自定义View来说,warp_parent
和match_parent
属性效果都是一样的。因此,自定义view要加以区分两种模式,需要重写onMeasure方法,对warp_parent
进行处理。
UNSPECIFIED
模式下,返回的是getSuggestedMinimumWidth()
/ getSuggestedMinimumHeight()
,我们取其中一个分析:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
大意是:设置没有背景,则取值为mMinWidth
,这个字段可以由Android:minWidth
或View.setMinimunWidth(int)
设置值;不指定默认为0。
若有设置背景,则取值 max{ mMinWidth, mBackground.getMinimumWidth() },
mBackground.getMinimumWidth() 指的是 Drawable
的方法,得到Drawable固有宽度,若宽小于0,返回0。
总结getSuggestedMinimumWidth(): 若view没有设置背景,返回mMinWidth,否则返回 mMinWidth
和Drawable最小宽度
的最大那一个。
ViewGroup 的 measure流程
不仅测量自身,还遍历调用子元素measure方法。ViewGroup 没有onMeasure
方法,只定义了:measureChildren
,在之中调用measureChild(..)
,如下所示:
getChildMeasureSpec中,有一点值得注意:若父容器MeasureSpec属性为AT_MOST
,子元素的LayoutParams
属性为Wrap_Content
,那么,子元素MeasureSpec属性也为AT_MOST,他的specSize的值为父容器的specSize - padding
,也就是说这和子元素设置LayoutParams属性为match_parent效果是一样的。
这个问题的解决思路是,在LayoutParams属性为WRAP_CONTENT
指定默认宽高。
ViewGroup为了派生类有不同布局需要,没有提供onMeasure方法,我们这里以LinearLayout的measure流程为例,看看它的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
以measureVertical为例来分析:
measureVertical方法定义了mTotalLength
来存储LinearLayout
在垂直方向高度,然后遍历子元素,根据子元素的MeasureSpec
模式分别计算每一个子元素的高度。如果是WRAP_CONTENT
,则将每一个子元素的高度和margin
垂直高度、padding
的值相加并给予mTotalLength
,如果是具体的值,就加之。
view的layout流程
layout方法是用来确定元素的位置。ViewGroup中的,确定子元素位置;View中的,确定自身位置。我们依然以图为例。
View的draw流程
官方注释对此清楚的说明每一步做法:
- 如需要则绘制背景
- 保存当前canvas层
- 绘制view内容
- 绘制子view
- 如需要则绘制view边缘阴影
- 绘制装饰(滚动条之类)
- 绘制默认焦点突出(高版本)
第二步和第五步这里跳过,不做讲解。
1. 绘制背景
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
// Attempt to use a display list if requested.
【code】
final int scrollX = mScrollX;
final int scrollY = mScrollY;
// 第一部分
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
第一部分考虑了偏移参数,如果偏移值不为0,会在偏移后的canvas绘制背景。
3. 绘制view内容
oonDraw是个空方法,需要自己实现。
4. 绘制子view
调用dispatchDraw
方法,这依旧是空方法。但viewGroup重写了这个方法,可见是对子view进行遍历,调用他们的drawChild方法,然后drawChild调用子view的draw方法,
在view的draw方法里,步骤1-7都有调用,并且,优先加载缓存显示。
6. 绘制装饰
绘制形如foreground, scrollbars装饰,并在视图内容上显示。
7. 绘制焦点
原代码是:
/**
* Draw the default focus highlight onto the canvas if there is one and this view is focused.
* @param canvas the canvas where we're drawing the highlight.
*/
private void drawDefaultFocusHighlight(Canvas canvas) {
if (mDefaultFocusHighlight != null && isFocused()) {
if (mDefaultFocusHighlightSizeChanged) {
mDefaultFocusHighlightSizeChanged = false;
final int l = mScrollX;
final int r = l + mRight - mLeft;
final int t = mScrollY;
final int b = t + mBottom - mTop;
mDefaultFocusHighlight.setBounds(l, t, r, b);
}
mDefaultFocusHighlight.draw(canvas);
}
}
小结
这样,我们就系统梳理了一遍view体系,至于具体实现,可以对症下药,再检索有关问题不迟。