您的位置:首页 > 编程语言

四、界面编程(二) 深入解析View的绘制流程

2017-11-03 16:17 267 查看
从Activity到关联的Window
1 PhoneWindow介绍

2 setContentView介绍

3 LayoutInflaterinflate 详解

View的绘制简介

View的绘制过程
1 准备知识
11 ViewGroupLayoutParams

12 MeasureSpec简介

13 MeasureSpec类的使用

2 measure过程
21 单个View的meaure过程

22 ViewGroup的measure过程

3 layout过程

4 draw过程

本文将详细介绍Android中View的绘制流程,侧重点在于对整体流程的分析,特定细节可以再查看相应的源码去学习理解。

1.从Activity到关联的Window

1.1 PhoneWindow介绍

  PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。

1.2 setContentView()介绍

  setContentView这个方法实际上是完成了Activity的ContentView的创建,并没有执行View的绘制流程。当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
. . .
}


  getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:

@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
// mContentParent不为null,则移除decorView的所有子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 开启了Transition,做相应的处理,我们不讨论这种情况
// 感兴趣的同学可以参考源码
. . .
} else {
// 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
// 填充布局也就是把我们设置的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
. . .
// cb即为该Window所关联的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
cb.onContentChanged();
}

. . .
}


1.3 LayoutInflater.inflate( )详解

  在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
. . .
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}


  在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
. . .
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;

View result = root;

try {
// Look for the root node.
int type;
// 一直读取xml文件,直到遇到开始标记
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 最先遇到的不是开始标记,报错
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

final String name = parser.getName();
. . .
// 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
if (TAG_MERGE.equals(name)) {
// 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

// 递归地填充布局

4000
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp为xml布局文件的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
. . .
// 获取父容器的布局参数(LayoutParams)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
temp.setLayoutParams(params);
}

}

// 递归加载根View的所有子View
rInflateChildren(parser, temp, attrs, true);
. . .

if (root != null && attachToRoot) {
// 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
root.addView(temp, params);
}

// 若root为空或是attachToRoot为false,则以根View作为返回值
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {
. . .
} catch (Exception e) {
. . .
} finally {

. . .
}
return result;
}
}


在上面的源码中,首先对于布局文件中的标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:

void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 获取当前标记的深度,根标记的深度为0
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 不是开始标记则继续下一次迭代
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 对一些特殊标记做单独处理
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
// 对<include>做处理
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 对一般标记的处理
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
// 递归地加载子View
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}

if (finishInflate) {
parent.onFinishInflate();
}
}


我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}


  从源码中我们可以知道,rInflateChildren()方法实际上调用了rInflate()方法。

  到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来,我们开始进入正题,分析View的绘制流程。

2. View的绘制简介

  View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。

  当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查发起布局请求的线程是否为主线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}


  上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

  View的绘制过程只要是指measure、layout、draw这三大流程,也就是测量、布局和绘制,他们各自的实现的功能如下:

measure: 确定View的测量宽/高;
layout: 确定View的最终宽/高和四个顶点的位置;
draw: 绘制View到屏幕上。


3. View的绘制过程

3.1 准备知识

在了解measure 过程前,我们需要先了解measure过程中传递尺寸(宽 / 高测量值)的两个类:

ViewGroup.LayoutParams (View 自身的布局参数)
MeasureSpecs 类(父视图对子视图的测量要求)


3.1.1 ViewGroup.LayoutParams

这个类我们很常见,用来指定视图的高度(height)和宽度(width)等布局参数,可以通过以下参数进行指定:

android:layout_weight="wrap_content" //自适应大小
android:layout_weight="match_parent" //与父视图等高
android:layout_weight="fill_parent" //与父视图等高
android:layout_weight="100dip" //精确设置高度值为 100dip


  ViewGroup的子类有其对应的ViewGroup.LayoutParams子类;ViewGroup的子类包括RelativeLayout、LinearLayout等;例如RelativeLayout的ViewGroup.LayoutParams的子类就是RelativeLayoutParams。

3.1.2 MeasureSpec简介

  MeasureSpec类,主要作用是通过宽测量值(widthMeasureSpec)和高测量值(heightMeasureSpec)来决定View的大小。MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小,构成如下图:



  其中,Mode模式共分为三类:EXACTLY(即精确模式),AT_MOST(即最大值模式),UNSPECIFIED(不指定其大小测量模式),具体说明如下图:



3.1.3 MeasureSpec类的使用

  MeasureSpec、Mode和Size都封装在View类中的一个内部类里——MeasureSpec类。

  MeasureSpec类通过二进制,将mode和size打包成一个int值来减少对象内存分配,用一个变量携带两个数据(size,mode),并提供了打包和解包的方法。具体源码如下:

public class MeasureSpec {
//进位大小为2的30次方
//int的大小为32位,所以进位30位就是要使用int的32和31位做标志位)
private static final int MODE_SHIFT = 30;

// 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)
// 遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

// 0向左进位30 = 00后跟30个0,即00 00000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

// 1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;

// 2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;

/* 根据提供的size和mode得到一个详细的测量结果 */
public static int makeMeasureSpec(int size, int mode) {
// measureSpec = size + mode
//注:二进制的加法,不是十进制的加法!
return size + mode;
//设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
}

/* 通过详细测量结果获得mode */
public static int getMode(int measureSpec) {
// mode = measureSpec & MODE_MASK;
// MODE_MASK = 11 00000000000(11后跟30个0)
//原理:用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
return (measureSpec & MODE_MASK
12e05
);
}

/* 通过详细测量结果获得size */
public static int getSize(int measureSpec) {
// size = measureSpec & ~MODE_MASK;
// 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
return (measureSpec & ~MODE_MASK);
}

}

// 可以通过下面方式获取specMode和SpecSize
//获取specModeint
specMode = MeasureSpec.getMode(measureSpec)

//获取SpecSizeint
specSize = MeasureSpec.getSize(measureSpec)

//也可以通过这两个值生成新的SpecModeint
measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
}


  子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。逻辑如下图:



接下来我们看下getChildMeasureSpec( )的源码分析:

//作用:
// 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
//即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性

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

/**参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
*/

//父view的测量模式
int specMode = MeasureSpec.getMode(spec);

//父view的大小
int specSize = MeasureSpec.getSize(spec);

//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);

//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;

//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小

// 当父view的模式为EXACITY时,父view强加给子view确切的值
//一般是父view设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子view的LayoutParams>0,即有确切的值
if (childDimension >= 0) {
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;

// 当子view的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;

// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}


  getChildMeasureSpec中对于子View测量模式和大小判断的逻辑较复杂,可以参考下图:



规律总结:(以子View为标准,横向观察)

(1) 当子View采用具体数值(dp / px)时

   无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;

(2) 当子View采用match_parent时

   子View的测量模式与父容器的测量模式一致

   若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间

(3) 当子View采用wrap_parent时

  无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。

(4) UNSPECIFIED模式:由于适用于系统内部多次measure情况,很少用到,故此处不讨论

3.2 measure过程

3.2.1 单个View的meaure过程

  在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。 这一过程如下图:



下面将对每个核心方法进行分析

measure()介绍

  这个方法的主要作用是测量逻辑的基本判断,然后调用onMeasure方法;属于final类型,所以子类不能重写此方法

源码如下:

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

//参数说明:View的宽 / 高测量规格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 计算视图大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...

}


onMeasure()介绍

这个方法用于调用getDefaultSize来定义View尺寸的测量逻辑和调用setMeasureDimension存储测量后的View的宽/高

源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//参数说明:View的宽 / 高测量规格

//setMeasuredDimension() 用于获得View宽/高的测量值
//这两个参数是通过getDefaultSize()获得的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}


setMeasuredDimension()介绍

这个方法用于存储测量后的View的宽和高

源码如下:

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

//参数说明:测量后子View的宽 / 高值

//将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}


getDefaultSize()介绍

这个方法会根据View宽和高的测量规格来计算View的宽高

源码如下:

public static int getDefaultSize(int size, int measureSpec) {

//参数说明:// 第一个参数size:提供的默认大小// 第二个参数:宽/高的测量规格(含模式 & 测量大小)

//设置默认大小
int result = size;

//获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小
// 即第一个参数:size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值
// 即measureSpec中的specSize
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}

//返回View的宽/高值
return result;
}


  当模式是UNSPECIFIED时,使用的是提供的默认大小(即第一个参数size);默认大小是在onMeasure方法中,getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)中传入的默认大小是getSuggestedMinimumWidth()。

getSuggestedMinimumWidth方法的源码如下:

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}

//getSuggestedMinimumHeight()同理


  从源码中可以看到,如果View没有设置背景,则View的宽度为mMinWidth,如果设置了背景,View的宽度为mMinWidth和mBackground.getMinimunWidth()中的最大值。

接下来看getMinimumWidth的源码:

public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}


  由源码可知,mBackground.getMinimumWidth()的大小具体是指背景图Drawable的原始宽度,如果没有原始宽度,则默认为0。

getDefaultSize计算View宽和高的逻辑如下图:



单个View的measure过程中对于每个方法的总结如下图:



3.2.2 ViewGroup的measure过程

  自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout(含有子View)。原理就是通过遍历所有的子View进行子View的测量,然后将所有的子View的尺寸进行合并,最终得到ViewGroup父视图的测量值。ViewGroup的measure过程如下图:



下面对每个方法进行分析:

measureChildren()方法

  和单一View的measure过程是从measure()开始不同,ViewGroup的measure过程是从measureChildren()开始的。ViewGroup是一个抽象类,自身并没有重写View的onMeasure方法。这个方法的主要作用就是遍历子View并调用measureChild进行下一步测量。

源码分析如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
//参数说明:父视图的测量规格(MeasureSpec)
final int size = mChildrenCount;
final View[] children = mChildren;

//遍历所有的子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
//如果View的状态不是GONE就调用measureChild()去进行下一步的测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}


measureChild()方法

  这个方法的主要作用是用于计算单个子View的MeasureSpec,调用子View的measure方法进行每个子View最后的宽高测量

源码分析如下:

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {

// 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();

// 调用getChildMeasureSpec(),根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
// getChildMeasureSpec()请回看上面的解析
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);

// 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}


measure()方法

这个方法和单个View的measure()方法是一致的,不再详细分析。

onMeasure()方法

  ViewGroup是一个抽象类,自身并没有重写View的onMeasure()方法。不同的ViewGroup子类(如LinearLayout,RelativeLayout等)具备不同的布局特性,这导致他们子View的测量方法各有不同,而onMeasure方法的作用在于测量View的宽高值,因此ViewGroup无法对onMeasure方法做统一实现。

  在定义ViewGroup中,关键在于根据自定义的View去复写onMeasure()从而实现你的子View测量逻辑。重写onMeasure的模板如下:

//根据自身的测量逻辑复写onMeasure()

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//定义存放测量后的View宽/高的变量
int widthMeasure ;
int heightMeasure ;

//定义测量方法
void measureCarson{
//定义测量的具体逻辑
}

//记得!最后使用setMeasuredDimension() 存储测量后View宽/高的值
setMeasuredDimension(widthMeasure, heightMeasure);
}

//最终setMeasuredDimension()会像上面单一View的measure过程中提到的,存储好测量后View宽/高的值并进行传递。


  单一View的measure过程与ViewGroup过程最大的不同:单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。当然,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需要进行更精细的测量。所以有时候也需要重写onMeasure()。

对每个方法的总结如下:



3.3 layout过程

layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。

单个View的layout过程如下图:



ViewGroup的layout过程是先调用layout方法计算自身的位置,然后再遍历子View并调用子View layout()来确定自身子View的位置,过程如下:



我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:

public void layout(int l, int t, int r, int b) {
// l为本View左边缘与父View左边缘的距离
// t为本View上边缘与父View上边缘的距离
// r为本View右边缘与父View左边缘的距离
// b为本View下边缘与父View上边缘的距离
. . .
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);
. . .

}
. . .
}


  这个方法会调用setFrame()方法来设置View的mLeft、mTop、mRight和mBottom四个参数,这四个参数描述了View相对其父View的位置(分别赋值为l, t, r, b),在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View进行重新布局,对子View的局部是通过onLayout()方法实现了。由于普通View( 非ViewGroup)不含子View,所以View类的onLayout()方法为空。因此接下来,我们看看ViewGroup类的onLayout()方法的实现。

  实际上ViewGroup类的onLayout()方法是abstract,这是因为不同的布局管理器有着不同的布局方式。

  这里我们以decorView,也就是FrameLayout的onLayout()方法为例,分析ViewGroup的布局过程:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;

if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}

final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}

case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;

}

switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;

case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;

case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;

default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}


  在上面的方法中,parentLeft表示当前View为其子View显示区域指定的一个左边界,也就是子View显示区域的左边缘到父View的左边缘的距离,parentRight、parentTop、parentBottom的含义同理。确定了子View的显示区域后,接下来,用一个for循环来完成子View的布局。

  在确保子View的可见性不为GONE的情况下才会对其进行布局。首先会获取子View的LayoutParams、layoutDirection等一系列参数。上面代码中的childLeft代表了最终子View的左边缘距父View左边缘的距离,childTop代表了子View的上边缘距父View的上边缘的距离。会根据子View的layout_gravity的取值对childLeft和childTop做出不同的调整。最后会调用child.layout()方法对子View的位置参数进行设置,这时便转到了View.layout()方法的调用,若子View是容器View,则会递归地对其子View进行布局。

  到这里,layout阶段的大致流程我们就分析完了,这个阶段主要就是根据上一阶段得到的View的测量宽高来确定View的最终显示位置。显然,经过了measure阶段和layout阶段,我们已经确定好了View的大小和位置,那么接下来就可以开始绘制View了。

3.4 draw过程

绘制过程如下:

1. 绘制view背景

2. 绘制view内容

3. 绘制子View

4. 绘制装饰(渐变框,滑动条等等)

  对于本阶段的分析,我们以decorView.draw()作为分析的起点,也就是View.draw()方法,它的源码如下:

public void draw(Canvas canvas) {
. . .
// 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}

. . .

// 绘制自身内容
if (!dirtyOpaque) onDraw(canvas);

// 绘制子View
dispatchDraw(canvas);

. . .
// 绘制滚动条等
onDrawForeground(canvas);

}


  简单起见,在上面的代码中我们省略了实现滑动时渐变边框效果相关的逻辑。实际上,View类的onDraw()方法为空,因为每个View绘制自身的方式都不尽相同,对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的。dispatchDraw()方法用于绘制子View,显然普通View(非ViewGroup)并不能包含子View,所以View类中这个方法的实现为空。

  ViewGroup类的dispatchDraw()方法中会依次调用drawChild()方法来绘制子View,drawChild()方法的源码如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}


  这个方法调用了View.draw(Canvas, ViewGroup,long)方法来对子View进行绘制。在draw(Canvas, ViewGroup, long)方法中,首先对canvas进行了一系列变换,以变换到将要被绘制的View的坐标系下。完成对canvas的变换后,便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。进入View.draw(Canvas)方法后,会继续重复执行绘制过程的步骤。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐