让人头疼的Android-View-的工作流程和原理,一篇文章解决!

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
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;
}

现在理解起来是不是很简单呢,如果 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,这也是系统默认的行为。之后会在 onMeasure 方法中调用 setMeasuredDimension 方法来设定测量出的大小,这样 View 的 measure 过程就结束了,接下来看 ViewGroup 的 measure 过程。

ViewGroup 的测量过程

ViewGroup中定义了一个 measureChildren 方法来去测量子视图的大小,如下所示

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

从上述代码来看,除了完成自己的 measure 过程以外,还会遍历去所有在页面显示的子元素,
然后逐个调用 measureChild 方法来测量相应子视图的大小

measureChild 的实现如下

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

那么 ViewGroup 是如何创建来创建子元素的 MeasureSpec 呢,我们继续看 getChildMeasureSpec 方法源码:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
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
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.
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);
}

上面的代码理解起来很简单,为了更清晰地理解 getChildMeasureSpec 的逻辑,这里提供一个表,表中对 getChildMeasureSpec 的工作原理进行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 获取的值,从 measureChild 方法中可看出

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

  • widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
  • heightUsed, lp.height);

表如下:

通过上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

至此,View 和 ViewGroup 的测量过程就告一段落了。来个小结。

MeasureSpec 的模式和生成规则
MeasureSpec 中 specMode 有三种模式:

  1. UNSPECIFIED
    不限制:父容器不对 View 有任何限制,要多大给多大,这种情况比较少见,一般不会用到。

  2. EXACTLY
    限制固定值:父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。

  3. AT_MOST
    限制上限:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

生成规则:

  1. 对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。
  2. 对于不同 ViewGroup 中的不同 View 生成规则参照上表。

MeasureSpec 测量过程:
measure 过程主要就是从顶层父 View 向子 View 递归调用 view.measure 方法,measure 中调 onMeasure 方法的过程。

说人话呢就是,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

那么测量过后,怎么获取 View 的测量结果呢
一般情况下 View 测量大小和最终大小是一样的,我们可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法来获取视图测量出的宽高,但是必须在 setMeasuredDimension 之后调用,否则调用这两个方法得到的值都会是0。为什么要说是一般情况下是一样的呢,在下文介绍 Layout 中会具体介绍。

Layout 布局过程

测量结束后,视图的大小就已经测量好了,接下来就是 Layout 布局的过程。上文说过 ViewRoot 的 performTraversals 方法会在 measure 结束后,执行 performLayout 方法,performLayout 方法则会调用 layout 方法开始布局,代码如下

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;

final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, “Laying out " + host + " to (” +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + “)”);
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//…省略代码
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;

View 类中 layout 方法实现如下:

public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}

layout 方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的,然后会调用 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似

onLayout 源码如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

纳尼,怎么是个空方法,没错,就是一个空方法,因为 onLayout 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置,我们继续看 ViewGroup 中的 onLayout 方法

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

可以看到,ViewGroup 中的 onLayout 方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。所以呢我们如果要自定义 ViewGroup 那么就要重写 onLayout 方法。
[图片上传失败…(image-400093-1597220837770)]

public class TestViewGroup extends ViewGroup {

public TestViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}

xml 中使用

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”

<com.will.testdemo.customview.TestViewGroup
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:background=“@android:color/holo_blue_bright”

</com.will.testdemo.customview.TestViewGroup>

显示效果如下:

[图片上传失败…(image-f240eb-1597220837770)]

不知道童鞋们发现了没,我给自定义的 ViewGroup 设置了背景色,看效果貌似占满全屏了,可是我在 xml 中设置的 wrap_content 啊,这是什么情况,我们回头看看 ViewGroup 中 View 的 MeasureSpec 的创建规则

[图片上传失败…(image-88e78-1597220837770)]

从表中可看出因为 ViewGroup 的父布局设置的 match_parent 也就是限制固定值模式,而 ViewGroup 设置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么如果我们给 ViewGroup 设置固定值就会使用 我们设置的值,来改下代码。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”

<com.will.testdemo.customview.TestViewGroup
android:layout_width=“200dp”
android:layout_height=“100dp”
android:background=“@android:color/holo_blue_bright”

</com.will.testdemo.customview.TestViewGroup>

效果如下:

[图片上传失败…(image-c8aaf3-1597220837770)]

表中的其他情况,建议童鞋们自己写下代码,会理解的更好。

之前说过,一般情况下 View 测量大小和最终大小是一样的,为什么呢,因为最终大小在 onLayout 中确定,我们来改下代码:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200);
}
}

没错,onLayout 就是这么任性,所以要获取 View 的真实大小最好在 onLayout 之后获取。那么如何来获取 view 的真实大小呢,可以通过下面的代码来获取

tv_hello.post(Runnable {
log(" getMeasuredWidth() = ${tv_hello.measuredWidth}“)
log(” getWidth() = ${tv_hello.width}")
}

打印如下:

01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getMeasuredWidth() = 239
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getWidth() = 339

可以看到实际高度和测试的高度是不一样的,因为我们在 onLayout 中做了修改。

因为 View 的绘制过程和 Activity 的生命周期是不同步的,所以我们可能在 onCreate 中获取不到值。这里提供几种方法来获取

1.Activity 的 onWindowFocusChanged 方法

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus){
//获取 view 的大小
}
}

2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 这里童鞋们搜索下就可以找到使用方法,篇幅较长就不举例子了

Draw 绘制过程

确定了 View 的大小和位置后,那就要开始绘制了,Draw 过程就比较简单,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

  1. 绘制背景 background.draw (canvas)
  2. 绘制自己(onDraw)
  3. 绘制 children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*

  • Draw traversal performs several drawing steps which must be executed
  • in the appropriate order:
  •  1\. Draw the background
    
  •  2\. If necessary, save the canvas' layers to prepare for fading
    
  •  3\. Draw view's content
    
  •  4\. Draw children
    
  •  5\. If necessary, draw the fading edges and restore layers
    
  •  6\. Draw decorations (scrollbars for instance)
    

*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);

if (debugDraw()) {
debugDrawFocus(canvas);
}

// we’re done…
return;
}

//省略代码…
}

View 的绘制过程的传递是通过 dispatchDraw 实现的,dispatchdraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层一层的传递下去。和 Layout 一样 View 是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制,重写 onDraw 方法。具体可参考 TextView 或者 ImageView 的源码。

最后

View 的工作流程和原理到这就分析完了,难点主要是 MeasureSpec 测量过程,需要童鞋们认真揣摩。

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

下面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题全套解析,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。

image

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

image

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
iew 的工作流程和原理到这就分析完了,难点主要是 MeasureSpec 测量过程,需要童鞋们认真揣摩。

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

下面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题全套解析,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。

[外链图片转存中…(img-wppyTAwC-1715392844541)]

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-vJEPFEtj-1715392844541)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值