android继承图,Android图形系统(三)-View绘制流程

接上篇 绘制优化-原理篇2-DecorView布局加载流程 讲到的ViewRootImpl,在ViewRootImpl的setView()方法里主要做两件事:

1.执行requestLayout()方法完成view的绘制流程

2.通过WindowSession将View和InputChannel添加到WmS中,从而将View添加到Window上并且接收触摸事件。

2的部分 window加载视图已经介绍了,那么今天就来讲讲1的部分:执行requestLayout()方法完成view的绘制流程

//ViewRootImpl

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

//在 Window add之前调用,确保 UI 布局绘制完成 --> measure , layout , draw

requestLayout();//View的绘制流程

...

//通过WindowSession进行IPC调用,将View添加到Window上

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

getHostVisibility(), mDisplay.getDisplayId(),

mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,

mAttachInfo.mOutsets, mInputChannel);

}

...

}

一、从requestLayout开始

从requestLayout代码一层层往下追(具体源码不贴了,非常简单),最终确认view的绘制流程是从performTraversals开始。顺一下整个流程:

58d22426e79e

1.1 performTraversals

private void performTraversals() {

//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高

WindowManager.LayoutParams lp = mWindowAttributes;

...

//顶层视图DecorView所需要窗口的宽度和高度

int desiredWindowWidth;

int desiredWindowHeight;

...

//在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView

if (mFirst) {

mFullRedrawNeeded = true;

mLayoutRequested = true;

//如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏

if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL

|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {

// NOTE -- system code, won't try to do compat mode.

Point size = new Point();

mDisplay.getRealSize(size);

desiredWindowWidth = size.x;

desiredWindowHeight = size.y;

} else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高

DisplayMetrics packageMetrics =

mView.getContext().getResources().getDisplayMetrics();

desiredWindowWidth = packageMetrics.widthPixels;

desiredWindowHeight = packageMetrics.heightPixels;

}

}

...

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

...

// Ask host how big it wants to be

//执行测量操作

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

...

//执行布局操作

performLayout(lp, mWidth, mHeight);

...

//执行绘制操作

performDraw();

}

performTraversals()中做了非常多的处理,代码接近800行,这里我们重点关注绘制相关流程。

1.2 MeasureSpec

在分析绘制过程之前,我们需要先了解MeasureSpec,它是干什么的呢?简而言之,MeasureSpec 是View的尺寸一种封装手段。

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SepcSize. 这样的打包方式好处是避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法:

public static class MeasureSpec {

private static final int MODE_SHIFT = 30;

private static final int MODE_MASK = 0x3 << MODE_SHIFT;

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

public static final int EXACTLY = 1 << MODE_SHIFT;

public static final int AT_MOST = 2 << MODE_SHIFT;

...

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,

@MeasureSpecMode int mode) {

if (sUseBrokenMakeMeasureSpec) {

return size + mode;

} else {

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

}

}

...

public static int getMode(int measureSpec) {

//noinspection ResourceType

return (measureSpec & MODE_MASK); //高2位运算

}

public static int getSize(int measureSpec) {

return (measureSpec & ~MODE_MASK);//低30位运算

}

}

getMode方法中ModeMask 为 0x3 << 30 转换成二进制为 0011 << 30 ,也就是向左移动30位 则 ModeMask高两位为1,低三十位为0,整形measureSpec 为32位, measureSpec & mode_mask 就是高2位的运算,getSize方法中 ~Mode_MASK 则是除了高两位为0 外剩下的低30位均为 1,那么和measure 进行 & 运算就是在求得低30为中存储的值。

SpecMode:测量模式

模式

描述

UNSPECIFIED

父容器不作限制,一般用于系统内部

EXACTLY

精确模式,大小为SpecSize,对应LayoutParams中的match_parent或者具体数值

AT_MOST

最大模式,大小不能大于SpecSize,对应于LayoutParams中的warp_content

SpecSize:对应某种测量模式下的尺寸大小

下面针对DecorView和普通View分别来看看其MeasureSpec的组成:

//ViewRootImpl

private static int getRootMeasureSpec(int windowSize, int rootDimension) {

DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定,

}

//ViewGroup

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

普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

}

对普通View的MeasureSpec的创建规则进行总结:

58d22426e79e

图片摘自Android开发艺术探索

这个表怎么用呢?举个例子:

如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,这种模式下,它的宽高为specSize, 而查表可得View的specSize是parentSize,而parentSize是当前父容器剩余空间大小,这种效果和在布局中使用match_parent完全一致,所以如果是对尺寸有具体要求的自定义控件需要指定specSize大小。

注:

LayoutParams类是用于子视图向父视图传达自己尺寸意愿的一个参数包,包含了Layout的高、宽信息。LayoutParams在LayoutInflater.inflater过程中与View一起被解析成对象,保存在WindowManagerGlobal集合中。

二、View绘制流程

performTraversals里面执行了三个方法,分别是performMeasure()、performLayout()、performDraw()这三个方法,这三个方法分别完成DecorView的measure、layout、和draw这三大流程,其中performMeasure()中会调用measure()方法,在measure()方法中又会调用onMeasure()方法,在onMeasure()方法中会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就实现了从DecorView开始对整个View树的遍历测量,measure过程就这样完成了。同理,performLayout()和performDraw()也是类似的传递流程。针对performTraveals()的大致流程,可以用以下流程图来表示:

58d22426e79e

from:https://www.jianshu.com/p/4a68f9dc8f7c

以上的流程图只是一个为了便于理解而简化版的流程,真正的流程应该分为以下五个工作阶段:

预测量阶段:这是进入performTraversals()方法后的第一个阶段,它会对View树进行第一次测量。在此阶段中将会计算出View树为显示其内容所需的尺寸,即期望的窗口尺寸。(调用measureHierarchy())

窗口布局阶段:根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。(调用relayoutWindow())

测量阶段:预测量的结果是View树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多,WMS不一定会将窗口准确地布局为View树所要求的尺寸,而迫于WMS作为系统服务的强势地位,View树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对View树进行最终测量。(调用performMeasure())

布局阶段:完成最终测量之后便可以对View树进行布局了。(调用performLayout())

绘制阶段:这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对View树进行绘制了。(调用performDraw())

下面分别来阐述:

2.1 预测量阶段(performTraversals())

这个阶段在performTraversals中最先发生,对View树进行第一次测量,会判断当前期望窗口尺寸是否能满足布局要求。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,

final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {

int childWidthMeasureSpec;

int childHeightMeasureSpec;

// 表示测量结果是否可能导致窗口的尺寸发生变化

boolean windowSizeMayChange = false;

//goodMeasure表示了测量是否能满足View树充分显示内容的要求

boolean goodMeasure = false;

//测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下

if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

//第一次协商。measureHierarchy()使用它最期望的宽度限制进行测量。

//这一宽度限制定义为一个系统资源。

//可以在frameworks/base/core/res/res/values/config.xml找到它的定义

final DisplayMetrics packageMetrics = res.getDisplayMetrics();

res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);

// 宽度限制被存放在baseSize中

int baseSize = 0;

if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {

baseSize = (int)mTmpValue.getDimension(packageMetrics);

}

if (baseSize != 0 && desiredWindowWidth > baseSize) {

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

//第一次测量。调用performMeasure()进行测量

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

//View树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。

//View树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位

if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {

goodMeasure = true; // 控件树对测量结果满意,测量完成

} else {

//第二次协商。上次的测量结果表明View树认为measureHierarchy()给予的宽度太小,在此

//在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制

baseSize = (baseSize+desiredWindowWidth)/2;

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);

//第二次测量

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

// 再次检查控件树是否满足此次测量

if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {

// 控件树对测量结果满意,测量完成

goodMeasure = true;

}

}

}

}

if (!goodMeasure) {

//最终测量。当View树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制

//做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没

//有更多的空间供其使用了

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);

childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

//如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口尺寸的调整

if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {

windowSizeMayChange = true;

}

}

// 返回窗口尺寸是否可能需要发生变化

return windowSizeMayChange;

}

measureHierarchy()方法最终也是调用了performMeasure()方法对View树进行测量,只是多了协商测量的过程。

2.2 窗口布局阶段(relayoutWindow())

调用relayoutWindow()来请求WindowManagerService服务计算Activity窗口的大小以及过扫描区域边衬大小和可见区域边衬大小。计算完毕之后,Activity窗口的大小就会保存在成员变量mWinFrame中,而Activity窗口的内容区域边衬大小和可见区域边衬大小分别保存在ViewRoot类的成员变量mPendingOverscanInsets和mPendingVisibleInsets中。

2.3 测量过程(performMeasure())

WMS的布局结果已经确定了,不管是否满意都得开始终极布局过程了,下面介绍下measure:

measure是对View进程测量,确定各View的尺寸的过程,这个过程分View和ViewGroup两种情况来看,对于View,通过measure完成自身的测量就行了,而ViewGroup除了完成自身的测量外,还需要遍历去调用所有子view的measure方法,各个子view递归去执行这个过程。

那么先从performMeasure开始:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

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

try {

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

}

这里的mView是 ViewRootImpl setView传进来的rootView.

接下来我们看下View的measure()方法

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

...

//根据widthMeasureSpec和heightMeasureSpec计算key值,在下面用key值作为键,缓存我们测量得到的结果

long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;

if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

//forceLayout 是通过上次的mPrivateFlags标记位来判断这次是否需要触发重绘

//View中有个forceLayout()方法可以设置mPrivateFlags.

// needsLayout 简单看就是spec发生了某些规则约束下的变化

if (forceLayout || needsLayout) {

// first clears the measured dimension flag

mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

//在View真正进行测量之前,View还想进一步确认能不能从已有的缓存mMeasureCache中读取缓存过的测量结果 //如果是强制layout导致的测量,那么将cacheIndex设置为-1,即不从缓存中读取测量结果 //如果不是强制layout导致的测量,那么我们就用上面根据measureSpec计算出来的key值作为缓存索引cacheIndex,这时候有可能找到相应的值,找到就返回对应索引;也可能找不到,找不到就返回-1

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);

if (cacheIndex < 0 || sIgnoreMeasureCache) {

//在缓存中找不到相应的值或者需要忽略缓存结果的时候,重新测量一次 //此处调用onMeasure方法,并把尺寸限 制条件widthMeasureSpec和heightMeasureSpec传入进去 //onMeasure方法中将会进行实际的测量工作,并把测量的结果保存到成员变量中

onMeasure(widthMeasureSpec, heightMeasureSpec);

mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

} else {

//如果运行到此处,那么表示当前的条件允许View从缓存成员变量mMeasureCache中读取测量过的结果

//用上面得到的cacheIndex从缓存mMeasureCache中取出值,不必在调用onMeasure方法进行测量了

long value = mMeasureCache.valueAt(cacheIndex);

//一旦我们从缓存中读到值,我们就可以调用setMeasuredDimensionRaw方法将当前测量的结果保存到成员变量中

setMeasuredDimensionRaw((int) (value >> 32), (int) value);

mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

}

...

}

...

}

...

}

先判断一下是否有必要进行测量操作,如果有,先看是否能在缓存mMeasureCache中找到上次的测量结果,如果找到了那直接从缓存中获取就可以了,如果找不到,那么乖乖地调用onMeasure()方法去完成实际的测量工作,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec传递给onMeasure()方法。

另外,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架. 主要看onMeasure()方法,这里才是真正去测量并设置View大小的地方。

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

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;

}

很显然看出:

AT_MOST 和 EXACTLY两种情况返回的就是specSize。

UNSPECIFIED返回的是size, 即getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值。

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

mMinWidth 对应于android:minWidth属性指定的值。总结:如果View没有设置背景,那么返回mMinWidth的值,否则返回mMinWidth和背景最小宽度的最大值。

2.3.2 ViewgGroup的measure过程:

ViewGroup 继承自 View,我们知道View的 measure是 final方法,那这个方法是肯定会走的,但是具体实现是在onMeasure中,ViewGroup提供了几个方法来帮助ViewGroup的子类来实现onMeasure逻辑,包括:

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

}

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

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

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

仔细看其实最终还是让child去执行自己对于的measure,只是getChildMeasureSpec有差别,这里加上了margin 和 padding.

具体onMeasure的实现可以参考LinearLayout、FrameLayout、RelativeLayout等。

另外需要关注的是ViewGroup 的 getChildMeasureSpec方法,我们从上面代码中很明显看出,传入的Spec是父容器的measureSpec

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

}

很明显看出来,对于普通View来说,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec 。

measure总结:

MeasureSpec 由specMode和specSize组成:

DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定。

普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,以及getChildMeasureSpec方法 ,供具体实现ViewGroup的子类重写onMeasure的时候方便使用。

只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

比较常用的方式:

view.post(runnable)

view.measure(0,0)之后 get

measure整体执行流程:

58d22426e79e

form: 工匠若水

2.4 布局过程 (performLayout())

Layout的作用是ViewGroup用来确定子view的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子view并调用其layout方法,在layout方法中onLayout又被调用。

先从performLayout看起:

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

int desiredWindowHeight) {

...

//标记当前开始布局

mInLayout = true;

//mView就是DecorView

final View host = mView;

...

//DecorView请求布局 layout参数分别是 左 上 右 下

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

//标记布局结束

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;

}

//保存上一次View的四个位置

int oldL = mLeft;

int oldT = mTop;

int oldB = mBottom;

int oldR = mRight;

//设置当前视图View的左,顶,右,底的位置,并且判断布局是否有改变

boolean changed = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//如果布局有改变,条件成立,则视图View重新布局

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

//调用onLayout,将具体布局逻辑留给子类实现

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

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;

}

首先关注下需要重新layout的条件:

boolean changed = isLayoutModeOptical(mParent) ?

setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

其中setOpticalFrame内部也会调用setFrame,所以就看下setFrame好了:

protected boolean setFrame(int left, int top, int right, int bottom) {

boolean changed = false;

...

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

changed = true;

// Remember our drawn bit

int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;

int oldHeight = mBottom - mTop;

int newWidth = right - left;

int newHeight = bottom - top;

boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position

invalidate(sizeChanged);

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

...

}

return changed;

}

通过setFrame方法设定View的四个顶点的位置,并更新本地值,同时判断顶点位置较之前是否有变化,并return是否有变化的boolean值,如果有变化还会执行invalidate(sizeChanged)。

然后,咱们再看看onLayout方法:

View中的onLayout是个空方法,可实现可不实现

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

}

ViewGroup中是个抽象方法,子类必须实现

@Override

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

下面以RelativeLayout为例,对onLayout具体实现做简单的分析:

@Override

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

// The layout has actually already been performed and the positions

// cached. Apply the cached values to the children.

final int count = getChildCount();

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

View child = getChildAt(i);

if (child.getVisibility() != GONE) {//只有不为GONE的才会执行布局

RelativeLayout.LayoutParams st =

(RelativeLayout.LayoutParams) child.getLayoutParams();

child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);

}

}

}

遍历所有子view,并通过其LayoutParams 获取四个方向的位置值,将位置信息传入子view的layout方法进行布局。

layout总结:

Layout的作用是ViewGroup用来确定子view的位置, 是ViewGroup需要干的活,View不需要,所以View中是空方法,而ViewGroup中是抽象方法,但是View你也可以重写,大多数是利用这个生命周期阶段加写逻辑操作。

当我们的视图View在布局中使用 android:visibility=”gone” 属性时,是不占据屏幕空间的,因为在布局时ViewGroup会遍历每个子视图View,判断当前子视图View是否设置了 Visibility==GONE,如果设置了,当前子视图View就不会添加到父容器上,因此也就不占据屏幕空间。具体可以参考RelativeLayout的onLayout.

必须在View布局完之后调用getHeight( )和getWidth( )方法获取到的View的宽高才大于0.

layout的整体执行流程:

58d22426e79e

from:工匠若水

2.5 绘制过程 (performDraw())

Draw作用是将View绘制到屏幕上.过程相对比较简单。

draw是从performDraw开始

//ViewRootImpl

private void performDraw() {

...

draw(fullRedrawNeeded);

...

}

然后看ViewRootImpl的draw方法:

//ViewRootImpl

private void draw(boolean fullRedrawNeeded) {

Surface surface = mSurface;

...

if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {

return;

}

...

}

再看ViewRootImpl的drawSoftware方法:

//ViewRootImpl

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,

boolean scalingRequired, Rect dirty) {

...

mView.draw(canvas);

...

}

最终在drawSoftware方法中,会走到View的draw并传入了canvas画布。这部分先不细说,之后的Surface部分会分析。

那么接着往下的话就是真正View绘制的部分了:

//View

public void draw(Canvas canvas) {

......

/*

* 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

......

if (!dirtyOpaque) {

drawBackground(canvas);

}

// skip step 2 & 5 if possible (common case)

......

// Step 2, save the canvas' layers

......

if (drawTop) {

canvas.saveLayer(left, top, right, top + length, null, flags);

}

......

// Step 3, draw the content

if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers

......

if (drawTop) {

matrix.setScale(1, fadeHeight * topFadeStrength);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

p.setShader(fade);

canvas.drawRect(left, top, right, top + length, p);

}

......

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

......

}

从摘要可以看出,绘制过程分如下几步:

绘制背景 background.draw(canvas)

private void drawBackground(Canvas canvas) {

//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable

final Drawable background = mBackground;

......

//根据layout过程确定的View位置来设置背景的绘制区域

if (mBackgroundSizeChanged) {

background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);

mBackgroundSizeChanged = false;

rebuildOutline();

}

......

//调用Drawable的draw()方法来完成背景的绘制工作

background.draw(canvas);

......

}

绘制自己(onDraw)

View中onDraw是一个空方法,ViewGroup也没有重新实现。

protected void onDraw(Canvas canvas) {

}

绘制children(dispatchDraw)

View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他。所以ViewGroup肯定重写了,来看看:

@Override

protected void dispatchDraw(Canvas canvas) {

......

final int childrenCount = mChildrenCount;

final View[] children = mChildren;

......

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

......

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {

more |= drawChild(canvas, child, drawingTime);

}

}

......

// Draw any disappearing views that have animations

if (mDisappearingChildren != null) {

......

for (int i = disappearingCount; i >= 0; i--) {

......

more |= drawChild(canvas, child, drawingTime);

}

}

......

}

可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

return child.draw(canvas, this, drawingTime);

}

绘制装饰(onDrawScrollBars)

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。这部分不做详细分析了。

draw拓展点:

1)View的setWillNotDraw方法:

如果一个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化,如果需要通过onDraw绘制内容,则需要设置为false。(默认View是false ViewGroup是true)这是个优化手段。

2)区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

3)默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

draw整体执行流程:

58d22426e79e

from:工匠若水

三、forceLayout 、invalidate 、requestLayout简述

在之前分析的绘制流程中,我们或多或少都见过这三个方法,他们到底是干什么的,下面做下简单说明:

View#forceLayout( )

public void forceLayout() {

if (mMeasureCache != null) mMeasureCache.clear();

mPrivateFlags |= PFLAG_FORCE_LAYOUT;

mPrivateFlags |= PFLAG_INVALIDATED;

}

官方描述:强制此视图在下一次布局传递期间进行布局。此方法不调用父类的requestLayout()或forceLayout()。

每个View都有个成员变量:mPrivateFlags,在不同的绘制执行路径会对它赋值。在measure方法内部forceLayout用来判断是否执行onMeasure:

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

View#invalidate( ) 和 View#postInvalidate( )

invalidate和postInvalidate:都是用来重绘View,区别就是invalidate只能在主线程中调用,postInvalidate可以在子线程中调用.

View#requestLayout

requestLayout: 当前view及其以上的viewGroup部分都重新走ViewRootImpl 重新绘制 ,分别重新onMeasure onLayout onDraw ,其中onDraw比较特殊,有内容变化才会触发。

最后一张图总结下invalidate/postInvalidate 和 requestLayout

58d22426e79e

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 中,可以使用 SurfaceView 进行图形绘制,SurfaceView 继承自 View 类,但是与普通的 View 不同,它可以在新的线程中进行绘制操作,因此可以避免在主线程中进行图形绘制导致的卡顿现象。 下面是基本的 SurfaceView 实现流程: 1. 创建一个 SurfaceView 对象,并将其添加到布局中。 2. 实现 SurfaceHolder.Callback 接口,该接口包括三个方法:surfaceCreated、surfaceDestroyed、surfaceChanged。 3. 在 surfaceCreated 回调方法中获取 SurfaceHolder 对象,并通过该对象获取 Canvas 对象进行图形绘制。 4. 在 surfaceChanged 回调方法中实现屏幕旋转、大小变化等操作。 5. 在 surfaceDestroyed 回调方法中释放资源,停止绘制线程等操作。 下面是一个简单的 SurfaceView 实现示例: ```java public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder mHolder; private Paint mPaint; private Thread mThread; private boolean mRunning; public MySurfaceView(Context context) { super(context); init(); } public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mHolder = getHolder(); mHolder.addCallback(this); mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); } @Override public void surfaceCreated(SurfaceHolder holder) { mRunning = true; mThread = new Thread(new Runnable() { @Override public void run() { while (mRunning) { draw(); } } }); mThread.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mRunning = false; try { mThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } private void draw() { Canvas canvas = mHolder.lockCanvas(); if (canvas != null) { canvas.drawColor(Color.WHITE); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); mHolder.unlockCanvasAndPost(canvas); } } } ``` 在该示例中,我们在 surfaceCreated 回调方法中启动了一个新的线程进行图形绘制,每次循环都会调用 draw 方法,该方法中获取 Canvas 对象进行绘制,最后通过 unlockCanvasAndPost 方法提交绘制结果。在 surfaceDestroyed 回调方法中停止绘制线程,释放资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值