View的绘制流程及源码分析

这篇文章主要讲解的是View的绘制流程以及源码分析,讲解这些的主要目的是为了能够在理解View的工作原理上更好的自定义View。

首先讲解一下布局文件是如何展现到屏幕上的。

一、布局文件是如何呈现在屏幕上的

我们从Activity的setContentView(R.layout.activity_main)入手了解UI绘制的起始过程。点进源码,我们会看到

Activity.java

public void setContentView(@LayoutRes int layoutResID) {

//调用的是PhoneWindow的setContentView

getWindow().setContentView(layoutResID);

initWindowDecorActionBar();

}

private Window mWindow;

public Window getWindow() {

return mWindow;

}

从源码我们可以看到,会调用getWindow,而返回的是一个Window的mWindow,我们再搜索源码,发现mWindow其实是一个PhoneWindow,那么,可以看出,activity的setContentView实际上最终调用的是PhoneWindow的setContentView,我们继续看深入,

PhoneWindow.java(这个文件位于系统源码中,不是在sdk源码中)

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

//初始化DecorView和mContentParent

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;

}

我们可以可以看到如果mContentParent == null,则installDecor();我们首次加载肯定是为null的,那么我们继续点开查看:

private void installDecor() {

mForceDecorInstall = false;

if (mDecor == null) {

//实例化DecorView

mDecor = generateDecor(-1);

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

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

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

} else {

mDecor.setWindow(this);

}

if (mContentParent == null) {

//初始化mContentParent

mContentParent = generateLayout(mDecor);

//省略部分代码

}

}

在此函数中,通过generateDecor实例化了DecorView,继续往下看:

protected DecorView generateDecor(int featureId) {

// System process doesn't have application context and in that case we need to directly use

// the context we have. Otherwise we want the application context, so we don't cling to the

// activity.

Context context;

if (mUseDecorContext) {

Context applicationContext = getContext().getApplicationContext();

if (applicationContext == null) {

context = getContext();

} else {

context = new DecorContext(applicationContext, getContext().getResources());

if (mTheme != -1) {

context.setTheme(mTheme);

}

}

} else {

context = getContext();

}

return new DecorView(context, featureId, this, getAttributes());

}

直接new了一个DecorView,那么DecorView是何物?通过继续点开源码其实DecorView就是一个FramenLayout,我们继续看installDecor(),如果mContentParent为空,则generateLayout(mDecor);

protected ViewGroup generateLayout(DecorView 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;

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

//将布局文件id传递给DecorView

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

//省略部分代码



return contentParent;

}

主要逻辑是将布局文件的id传递给DecorView的onResourcesLoaded,在onresourcesLoaded中:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

mStackId = getStackId();



if (mBackdropFrameRenderer != null) {

loadBackgroundDrawablesIfNeeded();

mBackdropFrameRenderer.onResourcesLoaded(

this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,

mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),

getCurrentColor(mNavigationColorViewState));

}



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;

initializeElevation();

}

可以看出,是将这个View add进了DecorView,这个布局文件到底是什么,我们选择一个来看,

R.layout.screen_simple:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:fitsSystemWindows="true"

android:orientation="vertical">

<ViewStub android:id="@+id/action_mode_bar_stub"

android:inflatedId="@+id/action_mode_bar"

android:layout="@layout/action_mode_bar"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:theme="?attr/actionBarTheme" />

<FrameLayout

android:id="@android:id/content"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:foregroundInsidePadding="false"

android:foregroundGravity="fill_horizontal|top"

android:foreground="?android:attr/windowContentOverlay" />

</LinearLayout>

实际上就是一个LineaLayout,里面有个content,而且ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);,int ID_ANDROID_CONTENT = com.android.internal.R.id.content;,我们就可以知道这个mContentParent就是这个id为content的FrameLayout,我们再看PhoneWindow的setContentView,其中有 mLayoutInflater.inflate(layoutResID, mContentParent);,而且这个其实就是将我们的自己写的布局,放在content的里面

到此,我们可以就可以总结为下图:

调用过程:Activity setContentView->PhoneWindow setContentView->installDecor->generateLayout,层层深入,至此我们已经知道了一个activity的页面的布局结构是什么样子了,下面来看View的绘制流程

二、View的绘制流程

View的绘制入口是从ViewRootImpl的requestLayout方法开始的,经过measure,layout,draw最终将一个VIew绘制出来。measure用来View的测量,layout用来View的摆放,draw用来View的绘制,可以总结为下图

源码:

ViewRootImpl:

@Override

public void requestLayout() {

if (!mHandlingLayoutInLayoutRequest) {

checkThread();

mLayoutRequested = true;

scheduleTraversals();

}

}

void scheduleTraversals() {

//省略部分代码

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

//省略部分代码

}

}

还可以看到mTraversalRunnable其实是个Runnable子类

final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

}

又异步执行了doTraversal()方法:

void doTraversal() {

//省略部分代码

performTraversals();

//省略部分代码

}

}

最终调用了performTraversals()方法,而在performTraversals()中,其实主要就干了三件事:

performTranversal(){

//省略部分代码

// Ask host how big it wants to be

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

//省略部分代码

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

//省略部分代码

performDraw();

//省略部分代码

}

执行了三个方法,我们着重分析一下performMeasure,这个是View的绘制中相对比较难理解的地方,我们来看perfromMeasure的源码:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

try {

//调用了View的measure方法

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

}

View.java:

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

//省略部分代码

//最终调用了onMeasure

onMeasure(widthMeasureSpec, heightMeasureSpec);

//省略部分代码

}

其实最终调用了我们熟悉的onMeasure方法,我们还可以知道View的子类分为ViewGroup(比如FrameLayout)和单纯的View(TextView),那么onMeasure有什么不同的,我们先看看FrameLayout的源码:

FrameLayout.java:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int count = getChildCount();



final boolean measureMatchParentChildren =

MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||

MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

mMatchParentChildren.clear();



int maxHeight = 0;

int maxWidth = 0;

int childState = 0;



for (int i = 0; i < count; i++) {

final View child = getChildAt(i);

if (mMeasureAllChildren || child.getVisibility() != GONE) {

//测量子View

measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

//省略部分代码

}

}

//测量自己

// Account for padding too

maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();

maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();



// Check against our minimum height and width

maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());

maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());



// Check against our foreground's minimum height and width

final Drawable drawable = getForeground();

if (drawable != null) {

maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());

maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());

}

//设置当前FrameLayout自身宽高

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),

resolveSizeAndState(maxHeight, heightMeasureSpec,

childState << MEASURED_HEIGHT_STATE_SHIFT));

//省略部分代码

}

我们只看关键部分,在FrameLayout的onMeasure中,通过for循环遍历每个子View,并调用measureChildWithMargins,我们从名称就可以猜出来,其实是测量子View的意思,测量完子View,再测量自己,最后通过setMeasuredDimension设置自己的最终尺寸,我们可以总结为下图:

我们再来深究测量子View的方法:measureChildWithMargins,

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

//获取子View的LayoutParams

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

//生成子View 宽的MeasureSpec

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

//进行子View的测量

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

代码很简单,具体的解释也在注释里面说了,这里面提到一个MeasureSpec的概念,这个需要详细说一下

关于MeasureSpec

MeasureSpec你可以理解为一种“测量规格”,在测量过程中,系统会将View的LayoutParams以及父容器的测量规格转换成此View的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高,这个宽高是测量宽高,并不一定是最终的宽高

MeasureSpec是一个int值,32位,前两位是SpecMode,后面的30位是SpecSize:

public static int makeMeasureSpec(int size, int mode) {

if (sUseBrokenMakeMeasureSpec) {

return size + mode;

} else {

return (size & ~MODE_MASK) | (mode & MODE_MASK);

}

}

public static int getMode(int measureSpec) {

return (measureSpec & MODE_MASK);

}

public static int getSize(int measureSpec) {

return (measureSpec & ~MODE_MASK);

}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值避免过多的创建对象,节省内存。

SpecMode有三种:

EXECTLY:父容器已经测量出这个View的精确值,此时这个View的值就是SpecSize所指定的值,它对应于具体的数值以及LayoutParams中的match_parent

AT_MOST:父容器指定了一个可以使用的大小,也就是SpecSize,View的大小不能大于这个值,具体多大,就得看具体的实现,对应于LayoutParams中的wrap_parent

UNSPECIFIED:父容器不对Viwe有任何显示,要多大有多大,一般用于系统内部,或者ScrollView等

我们再来分析一下FrameLayout中的getChildMeasureSpec,看看系统是如何通过父View的MeasureSpec以及子View的LayoutParams来生成子View的MeasureSpec的:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

//获取父View的specMode和specSize

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

//父View是EXACTLY的specMode

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {//如果子View设置的是具体的数值,不是match_parent或者wrap_content

//子View的尺寸就是设置的值

resultSize = childDimension;

//子View的SpecMode就是MeasureSpec.EXACTLY

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View设置的是match_parent

// Child wants to be our size. So be it.

//子View的尺寸就是size

resultSize = size;

//specMode就是EXACTLY

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

//子View设置的是wrap_content,则自View的可用大小是父View中的size,模式是AT_MOST

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;

}

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

以上代码看着挺多,实则不难,实际上就是根据父View的MeasureSpec再结合子View设置的尺寸值,来决定子View的MeasureSpec,最终,子View再根据这个MeasureSpec来进行测量

到这里基本上就把View的测量介绍完了,还剩下onLayout和onDraw,这两个在View的绘制流程中比较简单,就不再赘余,最后放上简单的自动换行自定义ViewGroup:

public class AutoWrapLayout extends ViewGroup {

private int horizontalSpace = 20;

private int verticalSpace = 20;



public AutoWrapLayout(Context context) {

super(context);

}



public AutoWrapLayout(Context context, AttributeSet attrs) {

super(context, attrs);

}



public AutoWrapLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

}



@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);

final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);



int resultWidth = 0;

int resultHeight = 0;

//measure children

int childCount = getChildCount();

for (int i = 0; i < childCount; i++) {

View child = getChildAt(i);

LayoutParams lp = child.getLayoutParams();

int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);

int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height);

child.measure(childWidthSpec, childHeightSpec);

}

//measure self

switch (widthMode) {

case MeasureSpec.EXACTLY:

resultWidth = widthSize;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.UNSPECIFIED:

resultWidth = getPaddingLeft() + getPaddingRight();

break;

}

switch (heightMode) {

case MeasureSpec.EXACTLY:

resultHeight = heightSize;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.UNSPECIFIED:

resultHeight = getPaddingTop() + getPaddingBottom();

break;

}

setMeasuredDimension(resultWidth, resultHeight);



}



@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int left = getPaddingLeft();

int top = getPaddingTop();

int right = 0;

int bottom = 0;

int maxHeightPerRow = 0;

int childCount = getChildCount();

for (int i = 0; i < childCount; i++) {

View child = getChildAt(i);

if (child.getMeasuredWidth() > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {

throw new IllegalArgumentException("child view is larger than parent view");

}

right = left + child.getMeasuredWidth();

if (right > getMeasuredWidth() - getPaddingRight()) {

left = getPaddingLeft();

top += maxHeightPerRow + verticalSpace;

maxHeightPerRow = 0;

right = left + child.getMeasuredWidth();

}

bottom = top + child.getMeasuredHeight();

maxHeightPerRow = Math.max(maxHeightPerRow, child.getMeasuredHeight());

child.layout(left, top, right, bottom);

left = right + horizontalSpace;

}

}

}

发布了29 篇原创文章 · 获赞 24 · 访问量 4万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览