java绘ui_UI绘制流程

本文深入解析Android应用UI的绘制流程,从Activity的setContentView开始,逐步揭示PhoneWindow、DecorView、ViewGroup以及测量、布局和绘制的过程。详细分析了如何将布局资源转换为View并添加到屏幕窗口,以及measure、layout、draw的执行流程,帮助开发者理解Android UI渲染机制。
摘要由CSDN通过智能技术生成

Activity的setContentView

从setContentView(R.layout.activity_main);入手了解UI的绘制起始过程。

下面源码,是基于android-28下的。

1. Activity.java

getWindow() 拿到的是 Window 的唯一实现类 PhoneWindow。

public void setContentView(@LayoutRes int layoutResID) {

getWindow().setContentView(layoutResID);// ①

initWindowDecorActionBar();

}

2. PhoneWindow.java

在下面代码,②处创建 DecorView。

@Override

public void setContentView(int layoutResID) {

// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window

// decor, when theme attributes and the like are crystalized. Do not check the feature

// before this happens.

if (mContentParent == null) {

installDecor();// ②

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,

getContext());

transitionTo(newScene);

} else {

mLayoutInflater.inflate(layoutResID, mContentParent);// ⑩

}

mContentParent.requestApplyInsets();

final Callback cb = getCallback();

if (cb != null && !isDestroyed()) {

cb.onContentChanged();

}

mContentParentExplicitlySet = true;

}

在下面代码,③处创建 DecorView,④处将 DecorView 转换为 ViewGroup。

private void installDecor() {

mForceDecorInstall = false;

if (mDecor == null) {

mDecor = generateDecor(-1);// ③ 生成一个DecorView(继承的FrameLayout)

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

} else {

mDecor.setWindow(this);

}

if (mContentParent == null) {

mContentParent = generateLayout(mDecor);// ④

...

}

}

下面可以看到调用了很多 setFlags() 和 requestFeature() 这也就是为什么在 Activity 中,要将 getWindow().addFlags() 以及 getWindow().requestFeature() 放在 setContentView() 之前才会生效的原因。

protected ViewGroup generateLayout(DecorView decor) {// ⑤

// Apply data from current theme.

...

if (mIsFloating) {

setLayout(WRAP_CONTENT, WRAP_CONTENT);

setFlags(0, flagsToUpdate);

} else {

setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);

}

if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {

requestFeature(FEATURE_NO_TITLE);

} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {

// Don't allow an action bar if there is no title.

requestFeature(FEATURE_ACTION_BAR);

}

...

// Inflate the window decor.

int layoutResource;

int features = getLocalFeatures();

// System.out.println("Features: 0x" + Integer.toHexString(features));

if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {

layoutResource = R.layout.screen_swipe_dismiss;

setCloseOnSwipeEnabled(true);

} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogTitleIconsDecorLayout, res, true);

layoutResource = res.resourceId;

} else {

layoutResource = R.layout.screen_title_icons;

}

// XXX Remove this once action bar supports these features.

removeFeature(FEATURE_ACTION_BAR);

// System.out.println("Title Icons!");

} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0

&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {

// Special case for a window with only a progress bar (and title).

// XXX Need to have a no-title version of embedded windows.

layoutResource = R.layout.screen_progress;

// System.out.println("Progress!");

} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {

// Special case for a window with a custom title.

// If the window is floating, we need a dialog layout

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogCustomTitleDecorLayout, res, true);

layoutResource = res.resourceId;

} else {

layoutResource = R.layout.screen_custom_title;

}

// XXX Remove this once action bar supports these features.

removeFeature(FEATURE_ACTION_BAR);

} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

// If no other features and not embedded, only need a title.

// If the window is floating, we need a dialog layout

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogTitleDecorLayout, res, true);

layoutResource = res.resourceId;

} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {

layoutResource = a.getResourceId(

R.styleable.Window_windowActionBarFullscreenDecorLayout,

R.layout.screen_action_bar);

} else {

layoutResource = R.layout.screen_title;

}

// System.out.println("Title!");

} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {

layoutResource = R.layout.screen_simple_overlay_action_mode;

} else {

// Embedded, so no decoration is needed.

layoutResource = R.layout.screen_simple;

// System.out.println("Simple!");

}

mDecor.startChanging();

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);// ⑥

// 找到id为com.android.internal.R.id.content的固定ViewGroup

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);// ⑨

...

return contentParent;

}

上面的 layoutResource,是根据是否存在 ActionBar 之类的一些控件判断,进行赋值,使用系统默认资源文件,在 sdk/platforms/android-28/data/res 目录下。

3. DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

...

mDecorCaptionView = createDecorCaptionView(inflater);

final View root = inflater.inflate(layoutResource, null);// ⑦

if (mDecorCaptionView != null) {

if (mDecorCaptionView.getParent() == null) {

addView(mDecorCaptionView,

new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mDecorCaptionView.addView(root,

new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));

} else {

// Put it below the color views.

addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));// ⑧

}

mContentRoot = (ViewGroup) root;

...

}

在⑦处,将系统默认的布局转换为 View,在⑧处,addView() 时添加到 DecorView 中。在⑨处,找到 id 为 content 的 ViewGroup,最后执行到 PhoneWindow 代码⑩处,将Activity中设置的布局添加到 mContentParent 中,完成了这一套流程,见下面两张图。

3b5bcb4d5417

Activity加载显示基本流程

3b5bcb4d5417

Activity加载UI-类图关系和视图结构

需要注意的是在7.0以后,DecorView 不再是 PhoneWindow 的内部类,上图基于android-23源码。

我们总结下 View 是如何被添加到屏幕窗口上的。

创建顶层布局容器 DecorView

在顶层布局中加载基础布局 ViewGroup

将 ContentView 添加到基础布局中的 FrameLayout 中

measure、layout、draw 的执行流程

measure:测量,测量自己有多大,如果是ViewGroup的话会同时测量里面的子控件的大小。

layout:摆放里面的子控件 bounds (left, top, right, bottom)。

draw:绘制(直接继承了View一般都会重写onDraw)。

measure 的流程

View 绘制流程

3b5bcb4d5417

DecorView添加到窗口Window的过程

我们从 ViewRootImpl 类的 requestLayout() 方法看起( ViewRootImpl 是 PhoneWindow 和 DecorView 的桥梁),再之前调用大家可以自行去看源码。

@Override

public void requestLayout() {

if (!mHandlingLayoutInLayoutRequest) {

checkThread();// 检查是否是在UI线程中执行

mLayoutRequested = true;

scheduleTraversals();// 关键代码

}

}

scheduleTraversals 中的 mTraversalRunnable 中做了关键操作。

void scheduleTraversals() {

if (!mTraversalScheduled) {

mTraversalScheduled = true;

mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

if (!mUnbufferedInputDispatch) {

scheduleConsumeBatchedInput();

}

notifyRendererOfFramePending();

pokeDrawLockIfNeeded();

}

}

mTraversalRunnable 实现了 Runnable,执行了 doTraversal()

final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

}

在这里调用里 performTraversals()

void doTraversal() {

if (mTraversalScheduled) {

mTraversalScheduled = false;

mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {

Debug.startMethodTracing("ViewAncestor");

}

performTraversals();

if (mProfile) {

Debug.stopMethodTracing();

mProfile = false;

}

}

}

继而完成了后续的performMeasure()、performLayout()、performDraw()。

private void performTraversals() {

...

// Ask host how big it wants to be

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

...

performLayout(lp, mWidth, mHeight);

...

performDraw();

...

}

3b5bcb4d5417

performTraversals方法控制View绘制流程图

最终 performMeasure() 调用了 View 的 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);

}

}

measure() 会调用 onMeasure(),之后的流程分析可以看这里,自定义控件中,measure的流程。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

...

onMeasure(widthMeasureSpec, heightMeasureSpec);

...

}

performLayout() 和 performDraw() 的流程和 performMeasure()也是类似的。

ViewGroup 绘制流程

首先思考两个问题:如何去合理的测量一颗 View 树?如果 ViewGroup 和 View 都是直接指定的宽高,我还要测量吗?

正是因为谷歌设计的自适应尺寸机制(比如 match_parent , wrap_content),造成了宽高不确定,所以就需要进程测量 measure 过程。measure 过程会遍历整颗 View 树,然后依次测量每一个 View 的真实的尺寸。(树的遍历--先序遍历)

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

...

onMeasure(widthMeasureSpec, heightMeasureSpec);

...

}

measure 方法中调用了 onMeasure。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

一般来说,继承 ViewGroup 的子类,会重写 onMeasure() ,对子控件进行测量,可以自行参考下 FrameLayout 和 DecorView 的 onMeasure(),这个在里面,就会计算 MeasureSpec 了。

MeasureSpec:测量规格,int 值,32 位,拿前面两位当做 mode,后面 30 位当做值。

mode:有三种模式。

EXACTLY:精确的。比如给了一个确定的值 100dp;再比如果父控件是 EXACTLY 确定值,子控件 match_parent 也属于精确值。

AT_MOST:根据父容器当前的大小,结合你指定的尺寸参考值来考虑你应该是多大尺寸,需要计算。比如 wrap_content(如果父控件是 AT_MOST 或者 UNSPECIFIED 不确定,子控件 match_parent 也属于这种)。

UNSPECIFIED:未指定的意思。根据当前的情况,结合你制定的尺寸参考值来考虑,在不超过父容器给你限定的尺寸的前提下,来测量你的一个恰好的内容尺寸。用的比较少,一般见于系统使用 ScrollView,ListView(大小不确定,同时大小还是变的。会通过多次测量才能真正决定好宽高。)

对于上面的结论,我们也可以通过看 FrameLayout 的源码分析出来。

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);

}

最后总结两张表格:

3b5bcb4d5417

getChildMeasureSpec方法分析

View 的 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 决定。

3b5bcb4d5417

value:宽高的值。

从 MeasureSpec 中获取 mode 和 value:

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);

final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

将 mode 和 value 合成一个 MeasureSpec:

MeasureSpec.makeMeasureSpec(resultSize, resultMode);

FrameLayout 的 onMeasure 方法内,还会调用 child.measure()。

3b5bcb4d5417

View树的源码measure流程图

经过大量测量以后,最终确定了自己的宽高,需要调用View:setMeasuredDimension(w, h)。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {

boolean optical = isLayoutModeOptical(this);

if (optical != isLayoutModeOptical(mParent)) {

Insets insets = getOpticalInsets();

int opticalWidth = insets.left + insets.right;

int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;

measuredHeight += optical ? opticalHeight : -opticalHeight;

}

setMeasuredDimensionRaw(measuredWidth, measuredHeight);

}

最终执行 setMeasuredDimensionRaw(w, h) ,保存 measuredWidth 和 measuredHeight。

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {

mMeasuredWidth = measuredWidth;

mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

}

到这里,就可以理解,写自定义控件的时候,我们要去获得自己的宽高来进行一些计算,必须先经过 measure(),才能获得到宽高,这里说的宽高不是 getWidth(),而是 getMeasuredWidth(),getWidth() 在 layout() 之后才可以拿到值。

同样的,当我们重写 onMeasure 的时候,我们需要在里面调用 child.measure() 才能获取 child 的宽高,进而去计算我们自定义 ViewGroup 的宽高。

设计 ViewGroup 的目的是什么?

作为容器处理焦点问题。

作为容器处理事件分发问题。

控制容器添加View的流程:addView(),removeView()。

抽出了一些容器的公共的工具方法:measureChildren(),measureChild(),measureChildWithMargins() 方法。

layout 的流程

从 ViewRootImpl 的 performLayout() 看起。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,

int desiredWindowHeight) {

...省略代码...

final View host = mView;

if (host == null) {

return;

}

...省略代码...

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

...省略代码...

}

上面的 host 就是 DecorView,调用了 View 的 layout() 。

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);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);

}

}

onLayout() 是一个空实现,如果是 ViewGroup 需要重写该方法,在这里面对子控件进行摆放。

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

}

总结:

调用 layout() 确定自身的位置,也就是左、上、右、下,四个点的位置。

如果是 ViewGroup,还需要调用 onLayout() 确定子 View 的位置(View不需要) 。

draw 的流程

依然从 ViewRootImpl 的 performDraw() 看起,这里面的代码太多,就不进行粘贴了,只描述下重要方法,在 ViewRootImpl 中 从performDraw()->draw()->drawSoftware(),最终在这方法里面,调用了 View 的 draw()。

View 源码 draw() 中的注释已经说的很详细。

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)

值得注意的一点是,ViewGroup 默认是不需要执行第3步的,也就是 ViewGroup 的 onDraw 方法默认是不会调用的。这个也比较好理解 LinearLayout 和 RelativeLayout 这些容器,自身没有任何可以绘制的,只需要绘制 children 就可以了,这样就是为了提高性能和效率。

在 ViewGroup 的构造方法中可以看到,调用了 initViewGroup(),简单看下源码:

private void initViewGroup() {

// ViewGroup doesn't draw by default

if (!debugDraw()) {

setFlags(WILL_NOT_DRAW, DRAW_MASK);// 设置不绘制自身

}

...省略代码...

}

如果需要 ViewGroup 执行 onDraw() ,可以在构造方法中执行如下代码:

setWillNotDraw(false);

或者设置背景,即使是透明背景,都是可以执行 onDraw() :

android:background="@android:color/transparent"

但是这样是是不可以的:

android:background="@null"

如果是 ViewGroup ,还会执行第4步,ViewGroup 中实现了 dispatchDraw(),完成对子控件的绘制。

总结 View:

绘制背景 drawBackground(canvas)

绘制自己 onDraw(canvas)

绘制前景,滚动条等装饰 onDrawBackground(canvas)

总结 ViewGroup:

绘制背景 drawBackground(canvas)

绘制自己 onDraw(canvas)

绘制子 View dispatchDraw(canvas),ViewGroup 已经帮我们实现好了。

绘制前景,滚动条等装饰 onDrawBackground(canvas)

思考

思考:如何让一个 ScrollView 里面的 ListView 全部展开?

有一种解决办法就是继承 ListView,重写 onMeasure 方法:

public void onMeasure() {

// 第一个参数是value,第二个参数是mode,通过makeMeasureSpec工具类,合并为一个int

int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);

super.onMeasure(widthMeasureSpec, expandSpec);

}

为什么要这么做?

设置 mode 为 MeasureSpec.AT_MOST?

设置 value 为 Integer.MAX_VALUE >> 2?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值