三个内容:
measure过程
WRAP_CONTENT、MATCH_PARENT/FILL_PARENT属性的原理说明
xml布局文件解析成View树的流程分析。
希望对大家能有帮助。- - 分析版本基于Android 2.3 。
1、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT
初入Android殿堂的同学们,对这三个属性一定又爱又恨。爱的是使用起来挺爽地—照葫芦画瓢即可,恨的
却是时常混淆这几个属性地意义,需要三思而后行。在带着大家重温下这几个属性的用法吧(希望我没有啰嗦)。
这三个属性都用来适应视图的水平或垂直大小,一个以视图的内容或尺寸为基础的布局比精确地指定视图范围
更加方便。
① fill_parent
设置一个视图的布局为fill_parent将强制性地使视图扩展至父元素大小。
② match_parent
Android 中match_parent和fill_parent意思一样,但match_parent更贴切,于是从2.2开始两个词都可以
用,但2.3版本后建议使用match_parent。
③ wrap_content
自适应大小,强制性地使视图扩展以便显示其全部内容。以TextView和ImageView控件为例,设置为
wrap_content将完整显示其内部的文本和图像。布局元素将根据内容更改大小。
可不要重复造轮子,以上摘自«Android fill_parent、wrap_content和match_parent的区别»。
当然,我们可以设置View的确切宽高,而不是由以上属性指定。
01.android:layout_weight=“wrap_content” //自适应大小
02.android:layout_weight=“match_parent” //与父视图等高
03.android:layout_weight=“fill_parent” //与父视图等高
04.android:layout_weight=“100dip” //精确设置高度值为 100dip
接下来,我们需要转换下视角,看看ViewGroup.LayoutParams类及其派生类。
2、ViewGroup.LayoutParams类及其派生类
2.1、 ViewGroup.LayoutParams类说明
Android API中如下介绍:
LayoutParams are used by views to tell their parents how they want to be laid out.
意思大概是说: View通过LayoutParams类告诉其父视图它想要地大小(即,长度和宽度)。
因此,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,View类依赖于ViewGroup.LayoutParams。
路径:frameworks\base\core\java\android\view\View.java
01.public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
路径位于:frameworks\base\core\java\android\view\ViewGroup.java
01.public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public static class LayoutParams {
/**
* Special value for the height or width requested by a View.
* FILL_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. This value is deprecated
* starting in API Level 8 and replaced by {@link #MATCH_PARENT}.
*/
@Deprecated
public static final int FILL_PARENT = -1; // 注意值为-1,Android2.2版本不建议使用
/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1; // 注意值为-1
/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2; // 注意值为-2
/**
* Information about how wide the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT ,
* in API Level 8) or WRAP_CONTENT. or an exact size.
*/
public int width; //该View的宽度,可以为WRAP_CONTENT/MATCH_PARENT 或者一个具体值
/**
* Information about how tall the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT ,
* in API Level 8) or WRAP_CONTENT. or an exact size.
*/
public int height; //该View的高度,可以为WRAP_CONTENT/MATCH_PARENT 或者一个具体值
/**
* Used to animate layouts.
*/
public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
/**
* Creates a new set of layout parameters. The values are extracted from
* the supplied attributes set and context. The XML attributes mapped
* to this set of layout parameters are:、
*/
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
/**
* Creates a new set of layout parameters with the specified width
* and height.
*/
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
/**
* Copy constructor. Clones the width and height values of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
/**
* Used internally by MarginLayoutParams.
* @hide
*/
LayoutParams() {
}
/**
* Extracts the layout parameters from the supplied attributes.
*
* @param a the style attributes to extract the parameters from
* @param widthAttr the identifier of the width attribute
* @param heightAttr the identifier of the height attribute
*/
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
87.} 设置为负值的目的是为了区别View的具体值(an exact size) 总是大于0的。
ViewGroup子类可以实现自定义LayoutParams,自定义LayoutParams提供了更好地扩展性,例如LinearLayout
就有LinearLayout. LayoutParams自定义类(见下文)。整个LayoutParams类家族还是挺复杂的。
ViewGroup.LayoutParams及其常用派生类的类图(部分类图)如下:
该类图是在太庞大了,大家有兴趣的去看看Android API吧。
前面我们说过,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,下面我们的疑问是Android框架
中时如何为View设置其LayoutParams属性的。
有两种方法会设置View的LayoutParams属性:
1、 直接添加子View时,常见于如下几种方法:ViewGroup.java
01.//Adds a child view.
02.void addView(View child, int index)
03.//Adds a child view with this ViewGroup's default layout parameters
04.//and the specified width and height.
05.void addView(View child, int width, int height)
06.//Adds a child view with the specified layout parameters.
07.void addView(View child, ViewGroup.LayoutParams params)
三个重载方法的区别只是添加View时构造LayoutParams对象的方式不同而已,稍后我们探寻一下它们的源码。
2、 通过xml布局文件指定某个View的属性为:android:layout_heigth=””以及android:layout_weight=”” 时。
总的来说,这两种方式都会设定View的LayoutParams属性值—-指定的或者Default值。
方式1流程分析:
直接添加子View时,比较容易理解,我们先来看看这种方式设置LayoutParams的过程:
路径:\frameworks\base\core\java\android\view\ViewGroup.java
01.public abstract class ViewGroup extends View implements ViewParent, ViewManager {
* Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.
*
* @param child the child view to add
*
* @see #generateDefaultLayoutParams()
*/
addView(child, -1);
* Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.
*
* @param child the child view to add
* @param index the position at which to add the child
*
* @see #generateDefaultLayoutParams()
*/
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams(); //返回默认地LayoutParams类,作为该View的属性值
if (params == null) {//如果不能获取到LayoutParams对象,则抛出异常。
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
* Adds a child view with this ViewGroup's default layout parameters and the
* specified width and height.
*
* @param child the child view to add
*/
//返回默认地LayoutParams类,作为该View的属性值
final LayoutParams params = generateDefaultLayoutParams();
params.width = width; //重新设置width值
params.height = height; //重新设置height值
addView(child, -1, params); //这儿,我们有指定width、height的大小了。
* Adds a child view with the specified layout parameters.
*
* @param child the child view to add
* @param params the layout parameters to set on the child
*/
addView(child, -1, params);
* Adds a child view with the specified layout parameters.
*
* @param child the child view to add
* @param index the position at which to add the child
* @param params the layout parameters to set on the child
*/
...
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate();
addViewInner(child, index, params, false);
* Returns a set of default layout parameters. These parameters are requested
* when the View passed to {@link #addView(View)} has no layout parameters
* already set. If null is returned, an exception is thrown from addView.
*
* @return a set of default layout parameters or null
*/
//width 为 WRAP_CONTENT大小 , height 为WRAP_CONTENT
//ViewGroup的子类可以重写该方法,达到其特定要求。稍后会以LinearLayout类为例说明。
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
boolean preventRequestLayout) {
if (!checkLayoutParams(params)) { //params对象是否为null
params = generateLayoutParams(params); //如果params对象是为null,重新构造个LayoutParams对象
}
//preventRequestLayout值为false
if (preventRequestLayout) {
child.mLayoutParams = params; //为View的mLayoutParams属性赋值
} else {
child.setLayoutParams(params);//为View的mLayoutParams属性赋值,但会调用requestLayout()请求重新布局
}
//if else 语句会设置View为mLayoutParams属性赋值
...
LinearLayout重写函数地实现为:
01.public class LinearLayout extends ViewGroup {
return new LinearLayout.LayoutParams(getContext(), attrs);
//该LinearLayout是水平方向还是垂直方向
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
return new LayoutParams(p);
* Per-child layout information associated with ViewLinearLayout.
*
* @attr ref android.R.styleable#LinearLayout_Layout_layout_weight
* @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity
*/ //自定义的LayoutParams类
/**
* Indicates how much of the extra space in the LinearLayout will be
* allocated to the view associated with these LayoutParams. Specify
* 0 if the view should not be stretched. Otherwise the extra pixels
* will be pro-rated among all views whose weight is greater than 0.
*/
@ViewDebug.ExportedProperty(category = "layout")
public float weight; // 见于属性,android:layout_weight="" ;
/**
* Gravity for the view associated with these LayoutParams.
*
* @see android.view.Gravity
*/
public int gravity = -1; // 见于属性, android:layout_gravity="" ;
/**
* {@inheritDoc}
*/
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
a.recycle();
}
/**
* {@inheritDoc}
*/
public LayoutParams(int width, int height) {
super(width, height);
weight = 0;
}
/**
* Creates a new set of layout parameters with the specified width, height
* and weight.
*
* @param width the width, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param height the height, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param weight the weight
*/
public LayoutParams(int width, int height, float weight) {
super(width, height);
this.weight = weight;
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
例如,LinearLayout进行measure过程,使用了LinearLayout.LayoutParam对象,有如下代码:
01.public class LinearLayout extends ViewGroup {
//判断是垂直方向还是水平方向,这儿我们假设是VERTICAL垂直方向,
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
/**
* Measures the children when the orientation of this LinearLayout is set
* to {@link #VERTICAL}.
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec Vertical space requirements as imposed by the parent.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onMeasure(int, int)
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
...
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i); //获得索引处为i的子VIew
...
//注意,我们将类型为 ViewGroup.LayoutParams的实例对象强制转换为了LinearLayout.LayoutParams,
//即父对象转换为了子对象,能这样做的原因就是LinearLayout的所有子View的LayoutParams类型都为
//LinearLayout.LayoutParams
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
...
}
PS : Android 2.3源码Launcher2中也实现了自定义的LayoutParams类,在IDLE界面的每个View至少包含如下
信息:所在X方向的单元格索引和高度、所在Y方向的单元格索引和高度等。
路径: packages\apps\Launcher2\src\com\android\launcher2\CellLayout.java
01.public class CellLayout extends ViewGroup {
/**
* Horizontal location of the item in the grid.
*/
public int cellX; //X方向的单元格索引
/**
* Vertical location of the item in the grid.
*/
public int cellY; //Y方向的单元格索引
/**
* Number of cells spanned horizontally by the item.
*/
public int cellHSpan; //水平方向所占高度
/**
* Number of cells spanned vertically by the item.
*/
public int cellVSpan; //垂直方向所占高度
...
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
cellHSpan = 1; //默认为高度 1
cellVSpan = 1;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source); //默认为高度 1
cellHSpan = 1;
cellVSpan = 1;
}
public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
this.cellX = cellX;
this.cellY = cellY;
this.cellHSpan = cellHSpan;
this.cellVSpan = cellVSpan;
}
...
}
方法2流程分析:
使用属性android:layout_heigth=””以及android:layout_weight=”” 时,为某个View设置LayoutParams值。
其实这种赋值方法其实也如同前面那种,只不过它需要一个前期孵化过程—需要利用XML解析将布局文件解析成一个完整的View树,可别小看它了,所有Xxx.xml的布局文件都需要解析成一个完整的View树。下面,我们就来仔细走这个过程,重点关注如下两个方面
①、xml布局是如何解析成View树的 ;
②、android:layout_heigth=””和android:layout_weight=””的解析。
PS: 一直以来,我都想当然android:layout_heigth以及android:layout_weight这两个属性的解析过程是在View.java内部完成的,但当我真正去找寻时,却一直没有在View.java类或者ViewGroup.java类找到。直到一位网友的一次提问,才发现它们的藏身之地。
3、布局文件解析流程分析
解析布局文件时,使用的类为LayoutInflater。 关于该类的使用请参考如下博客:
>
主要有如下API方法:
public View inflate (XmlPullParser parser, ViewGroup root, boolean attachToRoot)
public View inflate (int resource, ViewGroup root)
public View inflate (int resource, ViewGroup root, boolean attachToRoot)
这三个类主要迷惑之处在于地三个参数attachToRoot,即是否将该View树添加到root中去。具体可看这篇博客:
«关于inflate的第3个参数»
当然还有LayoutInflater的inflate()的其他重载方法,大家可以自行了解下。
我利用下面的例子给大家走走这个流程 :
01.public class MainActivity extends Activity {
super.onCreate(savedInstanceState);
//1、该方法最终也会调用到 LayoutInflater的inflate()方法中去解析。
setContentView(R.layout.main);
//2、使用常见的API方法去解析xml布局文件,
LayoutInflater layoutInflater = (LayoutInflater)getSystemService();
View root = layoutInflater.inflate(R.layout.main, null);
路径:\frameworks\base\core\java\android\app\ContextImpl.java
01./**
return WindowManagerImpl.getDefault();
synchronized (mSync) {
LayoutInflater inflater = mLayoutInflater;
//是否已经赋值,如果是,直接返回引用
if (inflater != null) {
return inflater;
}
//返回一个LayoutInflater对象,getOuterContext()指的是我们的Activity、Service或者Application引用
mLayoutInflater = inflater = PolicyManager.makeNewLayoutInflater(getOuterContext());
return inflater;
}
return getActivityManager();
路径:frameworks\base\core\java\com\android\internal\policy\PolicyManager.java
01.public final class PolicyManager {
// Pull in the actual implementation of the policy at run-time
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
}
...
return sPolicy.makeNewLayoutInflater(context); //继续去实现类中去查找
01.//Simple implementation of the policy interface that spawns the right
02.//set of objects
03.public class Policy implements IPolicy{
//实际上返回的是PhoneLayoutInflater类。
return new PhoneLayoutInflater(context);
* Instead of instantiating directly, you should retrieve an instance
* through {@link Context#getSystemService}
*
* @param context The Context in which in which to find resources and other
* application-specific things.
*
* @see Context#getSystemService
*/
super(context);
LayoutInflater中完成地。
Step 2、调用inflate()方法去解析布局文件。
01.public abstract class LayoutInflater {
//继续看下个函数,注意root为null
return inflate(resource, root, root != null);
//获取一个XmlResourceParser来解析XML文件---布局文件。
//XmlResourceParser类以及xml是如何解析的,大家自己有兴趣找找。
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
* Close this interface to the resource. Calls on the interface are no
* longer value after this call.
*/
Step 3 、真正地开始解析工作 。
01.public abstract class LayoutInflater {
…
/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
*/
//我们传递过来的参数如下: root 为null , attachToRoot为false 。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext; //该mConstructorArgs属性最后会作为参数传递给View的构造函数
View result = root; //根View
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
...
final String name = parser.getName(); //节点名,即API中的控件或者自定义View完整限定名。
if (TAG_MERGE.equals(name)) { // 处理标签
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//将标签的View树添加至root中,该函数稍后讲到。
rInflate(parser, root, attrs);
} else {
// Temp is the root view that was found in the xml
//创建该xml布局文件所对应的根View。
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
//根据AttributeSet属性获得一个LayoutParams实例,记住调用者为root。
params = root.generateLayoutParams(attrs);
if (!attachToRoot) { //重新设置temp的LayoutParams
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp
//添加所有其子节点,即添加所有字View
rInflate(parser, temp, attrs);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
}
...
return result;
}
}
/*
* default visibility so the BridgeInflater can override it.
*/
View createViewFromTag(String name, AttributeSet attrs) {
//节点是否为View,如果是将其重新赋值,形如
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
View view = (mFactory == null) ? null : mFactory.onCreateView(name,
mContext, attrs); //没有设置工厂方法
if (view == null) {
//通过这个判断是Android API的View,还是自定义View
if (-1 == name.indexOf('.')) {
view = onCreateView(name, attrs); //创建Android API的View实例
} else {
view = createView(name, null, attrs);//创建一个自定义View实例
}
}
return view;
}
...
}
//获得具体视图的实例对象
public final View createView(String name, String prefix, AttributeSet attrs) {
Constructor constructor = sConstructorMap.get(name);
Class clazz = null;
//以下功能主要是获取如下三个类对象:
//1、类加载器 ClassLoader
//2、Class对象
//3、类的构造方法句柄 Constructor
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name);
...
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
...
}
}
//传递参数获得该View实例对象
Object[] args = mConstructorArgs;
args[1] = attrs;
return (View) constructor.newInstance(args);
}
...
}
122.}
这段代码的作用是获取xml布局文件的root View,做了如下两件事情
1、获取xml布局的View实例,通过createViewFromTag()方法获取,该方法会判断节点名是API 控件
还是自定义控件,继而调用合适的方法去实例化View。
2、判断root以及attachToRoot参数,重新设置root View值以及temp变量的LayoutParams值。
如果仔细看着段代码,不知大家心里有没有疑惑:当root为null时,我们的temp变量的LayoutParams值是为
null的,即它不会被赋值?有个View的LayoutParams值为空,那么,在系统中不会报异常吗?见下面部分
代码:
01.//我们传递过来的参数如下: root 为null , attachToRoot为false 。
02.public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
...
try {
...
if (TAG_MERGE.equals(name)) { // 处理标签
...
} else {
// Temp is the root view that was found in the xml
//创建该xml布局文件所对应的根View。
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
//注意!!! root为null时,temp变量的LayoutParams属性不会被赋值的。
if (root != null) {
// Create layout params that match root, if supplied
//根据AttributeSet属性获得一个LayoutParams实例,记住调用者为root。
params = root.generateLayoutParams(attrs);
if (!attachToRoot) { //重新设置temp的LayoutParams
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
...
}
}
...
接下来,我们关注另外一个函数,rInflate(),该方法会递归调用每个View下的子节点,以当前View作为根View形成一个View树。
01./**
throws XmlPullParserException, IOException {
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_INCLUDE.equals(name)) { //处理标签
if (parser.getDepth() == 0) {
throw new InflateException(" cannot be the root element");
}
parseInclude(parser, parent, attrs);//解析节点
} else if (TAG_MERGE.equals(name)) { //处理标签
throw new InflateException(" must be the root element");
} else {
//根据节点名构建一个View实例对象
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
//调用generateLayoutParams()方法返回一个LayoutParams实例对象,
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs); //继续递归调用
viewGroup.addView(view, params); //OK,将该View以特定LayoutParams值添加至父View中
}
实例,然后在addView()方法中为其赋值。参见如下代码:ViewGroup.java
01.public abstract class ViewGroup extends View implements ViewParent, ViewManager {
…
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams {
... //会调用这个构造函数
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
21.}
好吧 ~~ 我们还是探寻根底,去TypeArray类的getLayoutDimension()看看。
路径:/frameworks/base/core/java/android/content/res/TypedArray.java
01.public class TypedArray {
* Special version of {@link #getDimensionPixelSize} for retrieving
* {@link android.view.ViewGroup}'s layout_width and layout_height
* attributes. This is only here for performance reasons; applications
* should use {@link #getDimensionPixelSize}.
*
* @param index Index of the attribute to retrieve.
* @param name Textual name of attribute for error reporting.
*
* @return Attribute dimension value multiplied by the appropriate
* metric and truncated to integer pixels.
*/
index *= AssetManager.STYLE_NUM_ENTRIES;
final int[] data = mData;
//获得属性对应的标识符 , Identifies,目前还没有仔细研究相关类。
final int type = data[index+AssetManager.STYLE_TYPE];
if (type >= TypedValue.TYPE_FIRST_INT
&& type <= TypedValue.TYPE_LAST_INT) {
return data[index+AssetManager.STYLE_DATA];
} else if (type == TypedValue.TYPE_DIMENSION) { //类型为dimension类型
return TypedValue.complexToDimensionPixelSize(
data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
}
//没有提供layout_weight和layout_height会来到此处 ,这儿会报异常!
//因此布局文件中的View包括自定义View必须加上属性layout_weight和layout_height。
throw new RuntimeException(getPositionDescription()
+ ": You must supply a " + name + " attribute.");
Step 3 主要做了如下事情:
首先,获得了了布局文件地root View,即布局文件中最顶层的View。
其次,通过递归调用,我们形成了整个View树以及设置了每个View的LayoutParams对象。
总结:通过对布局文件的解析流程的学习,也就是转换为View树的过程,我们明白了解析过程的个中奥妙,以及
设置ViewLayoutParams对象的过程。但是,我们这儿只是简单的浮光掠影,更深层次的内容希望大家能深入学习。
本来是准备接下去往下写的,但无奈贴出来的代码太多,文章有点长而且自己也有点凌乱了,因此决定做两篇博客发表吧。下篇内容包括如下方面:
MeasureSpec类说明 ;
measure过程中如何正确设置每个View的长宽 ;
UI框架正确设置顶层View的LayoutParams对象,对Activity而言,顶层View则是DecorView,其他的皆是普通View了。
上篇文章中,我们了解了View树的转换过程以及如何设置View的LayoutParams的。本文继续沿着既定轨迹继续未完成的job。
主要知识点如下:
MeasureSpc类说明
measure过程详解(揭秘其细节);
root View被添加至窗口时,UI框架是如何设置其LayoutParams值得。
在讲解measure过程前,我们非常有必要理解MeasureSpc类的使用,否则理解起来也只能算是囫囵吞枣。
1、MeasureSpc类说明
1.1 SDK 说明如下
A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.
即:
MeasureSpc类封装了父View传递给子View的布局(layout)要求。每个MeasureSpc实例代表宽度或者高度(只能是其一)要求。 它有三种模式:
①、UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
②、EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
③、AT_MOST(至多),子元素至多达到指定大小的值。
常用的三个函数:
static int getMode(int measureSpec) : 根据提供的测量值(格式)提取模式(上述三个模式之一)
static int getSize(int measureSpec) : 根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
static int makeMeasureSpec(int size,int mode) : 根据提供的大小值和模式创建一个测量值(格式)
1.2 MeasureSpc类源码分析 其为View.java类的内部类,路径:\frameworks\base\core\java\android\view\View.java
01.public class View implements … {
...
public static class MeasureSpec {
private static final int MODE_SHIFT = 30; //移位位数为30
//int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//向右移位30位,其值为00 + (30位0) , 即 0x0000(16进制表示)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//向右移位30位,其值为01 + (30位0) , 即0x1000(16进制表示)
public static final int EXACTLY = 1 << MODE_SHIFT;
//向右移位30位,其值为02 + (30位0) , 即0x2000(16进制表示)
public static final int AT_MOST = 2 << MODE_SHIFT;
//创建一个整形值,其高两位代表mode类型,其余30位代表长或宽的实际值。
可以是WRAP_CONTENT、MATCH_PARENT或具体大小exactly size
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
//获取模式 ,与运算
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取长或宽的实际值 ,与运算
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
①、右移运算,使int 类型的高两位表示模式的实际值,其余30位表示其余30位代表长或宽的实际值—-可以是
WRAP_CONTENT、MATCH_PARENT或具体大小exactly size。
②、通过掩码MODE_MASK进行与运算 “&”,取得模式(mode)以及长或宽(value)的实际值。
2、measure过程详解
2.1 measure过程深入分析
之前的一篇博文« Android中View绘制流程以及invalidate()等相关方法分析»,我们从”二B程序员”的角度简单 解了measure过程的调用过程。过了这么多,我们也该升级了,- - 。现在请开始从”普通程序员”角度去理解这个过程。我们重点查看measure过程中地相关方法。
我们说过,当UI框架开始绘制时,皆是从ViewRoot.java类开始绘制的。ViewRoot类简要说明: 任何显示在设备中的窗口,例如:Activity、Dialog等,都包含一个ViewRoot实例,该类主要用来与远端 WindowManagerService交互以及控制(开始/销毁)绘制。
Step 1、 开始UI绘制 , 具体绘制方法则是:
01.路径:\frameworks\base\core\java\android\view\ViewRoot.java
02.public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {
...
//这两个值我们在后面讨论时,在回过头来看看是怎么赋值的。现在只需要记住其值MeasureSpec.
makeMeasureSpec()构建的。 int childWidthMeasureSpec; //其值由MeasureSpec类构建 , makeMeasureSpec
int childHeightMeasureSpec;//其值由MeasureSpec类构建 , makeMeasureSpec
// Ask host how big it wants to be
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
第三部分时我们在来攻克它,现在只需要记住其值MeasureSpec.makeMeasureSpec()构建的。
Step 2 、调用measure()方法去做一些前期准备
measure()方法原型定义在View.java类中,final修饰符修饰,其不能被重载:
01.public class View implements … {
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
* @see #onMeasure(int, int)
*/
//判断是否为强制布局,即带有“FORCE_LAYOUT”标记 以及 widthMeasureSpec或heightMeasureSpec发生了改变
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
//清除MEASURED_DIMENSION_SET标记 ,该标记会在onMeasure()方法后被设置
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
// measure ourselves, this should set the measured dimension flag back
// 1、 测量该View本身的大小 ; 2 、 设置MEASURED_DIMENSION_SET标记,否则接写来会报异常。
onMeasure(widthMeasureSpec, heightMeasureSpec);
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling" + " setMeasuredDimension()");
}
mPrivateFlags |= LAYOUT_REQUIRED; //下一步是layout了,添加LAYOUT_REQUIRED标记
}
mOldWidthMeasureSpec = widthMeasureSpec; //保存值
mOldHeightMeasureSpec = heightMeasureSpec; //保存值
会在下面步骤中详解。
measure()方法显示判断是否需要重新调用设置改View大小,即调用onMeasure()方法,然后操作两个标识符:
①、重置MEASURED_DIMENSION_SET : onMeasure()方法中,需要添加该标识符,否则,会报异常;
②、添加LAYOUT_REQUIRED : 表示需要进行layout操作。
最后,保存当前的widthMeasureSpec和heightMeasureSpec值。
Step 3 、调用onMeasure()方法去真正设置View的长宽值,其默认实现为:
01./**
The requirements are encoded with
The requirements are encoded with
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据不同的mode值,取得宽和高的实际值。
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //表示该View的大小父视图未定,设置为默认值
result = size;
break;
case MeasureSpec.AT_MOST: //表示该View的大小由父视图指定了
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
int suggestedMinWidth = mMinWidth; // android:minHeight
if (mBGDrawable != null) { // 背景图片对应地Width。
final int bgMinWidth = mBGDrawable.getMinimumWidth();
if (suggestedMinWidth < bgMinWidth) {
suggestedMinWidth = bgMinWidth;
}
}
return suggestedMinWidth;
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET; //设置了MEASURED_DIMENSION_SET标记
这儿只是一般的View类型地实现方法。一般来说,父View,也就是ViewGroup类型,都需要在重写onMeasure() 方法,遍历所有子View,设置每个子View的大小。基本思想如下:遍历所有子View,设置每个子View的大小。伪代码表示为:
01.//某个ViewGroup类型的视图
02.protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
// getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
要想回答这个问题,我们看是去源代码里找找答案吧。在ViewGroup.java类中,为我们提供了三个方法,去设置每个子View的大小,基本思想也如同我们之前描述的思想:遍历所有子View,设置每个子View的大小。
主要有如下方法:
01./**
*/
07.//widthMeasureSpec 和 heightMeasureSpec 表示该父View的布局要求
08.//遍历每个子View,然后调用measureChild()方法去实现每个子View大小
09.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) { // 不处于 “GONE” 状态
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
18.}
20./**
*/
29.//测量每个子View高宽时,清楚了该View本身的边距大小,即android:padding属性 或android:paddingLeft等属性标记
30.protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams(); // LayoutParams属性
//设置子View的childWidthMeasureSpec属性,去除了该父View的边距值 mPaddingLeft + mPaddingRight
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//设置子View的childHeightMeasureSpec属性,去除了该父View的边距值 mPaddingTop + mPaddingBottom
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
41.}
measureChildren()方法:遍历所有子View,调用measureChild()方法去设置该子View的属性值。
measureChild() 方法 : 获取特定子View的widthMeasureSpec、heightMeasureSpec,调用measure()方法设置子View的实际宽高值。
getChildMeasureSpec()就是获取子View的widthMeasureSpec、heightMeasureSpec值。
01./**
//1.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST 。
}
break;
//2.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
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; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
//2.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
break;
//3.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY
}
//3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
//3.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
break;
getChildMeasureSpec()方法的主要功能如下:
根据父View的measureSpec值(widthMeasureSpec,heightMeasureSpec)值以及子View的子View内部LayoutParams属性值,共同决定子View的measureSpec值的大小。主要判断条件主要为MeasureSpec的mode类型以及LayoutParams的宽高实际值(lp.width,lp.height),见于以上所贴代码中的列表项: 1、 1.1 ; 1.2 ; 1.3 ; 2、2.1等。
例如,分析列表3:假设当父View为MeasureSpec.UNSPECIFIED类型,即未定义时,只有当子View的width或height指定时,其mode才为MeasureSpec.EXACTLY,否者该View size为 0 ,mode为MeasureSpec.UNSPECIFIED时,即处于未指定状态。
由此可以得出, 每个View大小的设定都事由其父View以及该View共同决定的。但这只是一个期望的大小,每个View在测量时最终大小的设定是由setMeasuredDimension()最终决定的。因此,最终确定一个View的“测量长宽“是由以下几个方面影响:
父View的MeasureSpec属性;
子View的LayoutParams属性 ;
setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
setMeasuredDimension()原型:
01.//设置View在measure过程中宽和高
02.protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
这张表格更能帮助我们分析View的MeasureSpec的确定条件关系。
为了帮助大家理解,下面我们分析某个窗口使用地xml布局文件,我们弄清楚该xml布局文件中每个View的
MeasureSpec值的组成。
01.
02.<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:id=“@+id/llayout”
android:orientation="vertical"
android:layout_width=“match_parent”
android:layout_height="match_parent">
<TextView android:id=“@+id/tv”
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
14.
该布局文件共有两个View: ①、id为llayout的LinearLayout布局控件 ;②、id为tv的TextView控件。
假设LinearLayout的父View对应地widthSpec和heightSpec值皆为MeasureSpec.EXACTLY类型(Activity窗口的父View为DecorView,具体原因见第三部分说明)。
对LinearLayout而言比较简单,由于 android:layout_width=“match_parent”,因此其width对应地widthSpec mode值为MeasureSpec.EXACTLY , size由父视图大小指定 ; 由于android:layout_height = “match_parent”,因此其height对应地heightSpec mode值为MeasureSpec.EXACTLY,size由父视图大小指定 ;
对TextView而言 ,其父View为LinearLayout的widthSpec和heightSpec值皆为MeasureSpec.EXACTLY类型,由于android:layout_width=“match_parent” , 因此其width对应地widthSpec mode值为MeasureSpec.EXACTLY,size由父视图大小指定 ; 由于android:layout_width=“wrap_content” , 因此其height对应地widthSpec mode值为MeasureSpec.AT_MOST,size由父视图大小指定 。
我们继续窥测下LinearLayout类是如何进行measure过程的:
measureVertical(widthMeasureSpec, heightMeasureSpec);
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
mTotalLength = 0; //该LinearLayout测量子View时的总高度。
...
final int count = getVirtualChildCount(); //子View的个数
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
...
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//满足该条件地View会在该LinearLayout有剩余高度时,才真正调用measure()
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
...
} else {
int oldHeight = Integer.MIN_VALUE;
//如果View的hight值为0,并且设置了android:layout_weight属性,重新纠正其height值为WRAP_CONTENT
if (lp.height == 0 && lp.weight > 0) {
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
//对每个子View调用measure()方法
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
//这三行代码做了如下两件事情:
//1、获得该View的measuredHeight值,每个View都会根据他们地属性正确设置值 > 0 ;
//2、更新mTotalLength值:取当前高度mTotalLength值与mTotalLength + childHeight 的最大值
// 于是对于android:layout_height="wrap_height"属性地LinearLayout控件也就知道了它的确切高度值了。
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
...
}
//后续还有很多处理,包括继续measure()某些符合条件地子View
...
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
01./**
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
1、遍历每个子View,对其调用measure()方法;
2、子View measure()完成后,需要取得该子View地宽高实际值,继而做处理(例如:LinearLayout属性为android:widht=“wrap_content"时,LinearLayout的实际width值则是每个子View的width值的累加值)。
2.2 WRAP_CONTENT、MATCH_PARENT以及measure动机揭秘子View地宽高实际值 ,即child.getMeasuredWidth()值得返回最终会是一个确定值? 难道WRAP_CONTENT(其值为-2) 、MATCH_PARENT(值为-1)或者说一个具体值(an exactly size > 0)。前面我们说过,View最终“测量”值的确定是有三个部分组成地:
①、父View的MeasureSpec属性;
②、子View的LayoutParams属性 ;
③、setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
因此,一个View必须以某种合适地方法确定它地最终大小。例如,如下自定义View:
01.//自定义View
02.public Class MyView extends View {
//针对不同地mode值,设置本View地大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
//获得父View传递给我们地测量需求
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = 0 ;
int height = 0 ;
//对UNSPECIFIED 则抛出异常
if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)
throw new RuntimeException("widthMode or heightMode cannot be UNSPECIFIED");
//精确指定
if(widthMode == MeasureSpec.EXACTLY){
width = 100 ;
}
//模糊指定
else if(widthMode == MeasureSpec.AT_MOST )
width = 50 ;
//精确指定
if(heightMode == MeasureSpec.EXACTLY){
height = 100 ;
}
//模糊指定
else if(heightMode == MeasureSpec.AT_MOST )
height = 50 ;
setMeasuredDimension(width , height) ;
}
34.} 对于TextView而言,如果它地mode不是Exactly类型 , 它会根据一些属性,例如:android:textStyle、android:textSizeandroid:typeface等去确定TextView类地需要占用地长和宽。
因此,如果你地自定义View必须手动对不同mode做出处理。否则,则是mode对你而言是无效的。
Android框架中提供地一系列View/ViewGroup都需要去进行这个measure()过程地 ,因为在layout()过程中,父View需要调用getMeasuredWidth()或getMeasuredHeight()去为每个子View设置他们地布局坐标,只有确定布局坐标后,才能真正地将该View 绘制(draw)出来,否则该View的layout大小为0,得不到期望效果。我们继续看看LinearLayout的layout布局过程:
01.public class LinearLayout extends ViewGroup {
//假定是垂直方向布局
if (mOrientation == VERTICAL) {
layoutVertical();
} else {
layoutHorizontal();
}
...
final int count = getVirtualChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) { //一般为非null
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//获得子View测量时的实际宽高值,
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
...
// 封装了child.layout()方法,见如下
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
child.layout(left, top, left + width, top + height);
在前面一篇博客«Android中滑屏初探 —- scrollTo 以及 scrollBy方法使用说明»中,我们自定义了一个 ViewGroup, 并且重写了onMeasure()和onLayout()方法去分别操作每个View。就该ViewGroup而言,我们只需要重写onLayout()操作即可,因为我们知道如何layout每个子View。如下代码所示:
01.//自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置
02.public class MultiViewGroup extends ViewGroup {
// 初始化3个 LinearLayout控件
LinearLayout oneLL = new LinearLayout(mContext);
oneLL.setBackgroundColor(Color.RED);
addView(oneLL);
...
// TODO Auto-generated method stub
Log.i(TAG, "--- start onLayout --");
int startLeft = 0; // 每个子视图的起始布局坐标
int startTop = 10; // 间距设置为10px 相当于 android:marginTop= "10px"
int childCount = getChildCount();
Log.i(TAG, "--- onLayout childCount is -->" + childCount);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(startLeft, startTop,
startLeft + MultiScreenActivity.screenWidth,
startTop + MultiScreenActivity.scrrenHeight);
startLeft = startLeft + MultiScreenActivity.screenWidth ; //校准每个子View的起始布局位置
//三个子视图的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960]
}
老子道德经有言:“道生一,一生二,二生三,三生万物。” UI绘制也就是个递归过程。理解其基本架构后,也就“掌握了一个中心点”了。在第一节中,我们没有说明开始UI绘制时 ,没有说明mView.measure()参数地由来,参数也就是我们本节需要弄懂的“道” — root View的 widthMeasureSpec和heightMeasureSpec 是如何确定的。
对于如下布局文件: main.xml
01.
02.<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
07.<TextView
任何一个View被添加至窗口时,都需要利用WindowManager类去操作。例如,如下代码:
01.//显示一个悬浮窗吧 , just so so
02.public void showView()
03.{
// 以屏幕左上角为原点,设置x、y初始值
Step 1 、获得WindowManager对象服务 ,具体实现类在ContextImpl.java内中
路径: /frameworks/base/core/java/android/app/ContextImpl.java
01.@Override
02.public Object getSystemService(String name) {
return WindowManagerImpl.getDefault();
Step 2 、 获得WindowManagerImpl的单例对象,以及部分源码分析
路径: /frameworks/base/core/java/android/view/WindowManagerImpl.java
01.public class WindowManagerImpl implements WindowManager{
return mWindowManager;
addView(view, params, false);
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
ViewRoot root;
View panelParentView = null; //该子窗口对应地父窗口View
synchronized (this) {
...//需要对传递过来地参数进行检测...
//对每个窗口皆构建一个ViewRoot对象
root = new ViewRoot(view.getContext());
root.mAddNesting = 1;
//设置root View 的LayoutParams为wparams,即WindowManager.LayoutParams类型
view.setLayoutParams(wparams);
...//对参数检测,以及拷贝原有数组...
//将窗口对应地view、root、wparams保存在属性集合中
mViews[index] = view;
mRoots[index] = root;
mParams[index] = wparams;
}
// do this last because it fires off messages to start doing things
// 调用ViewRoot对象去通知系统添加一个窗口
root.setView(view, wparams, panelParentView);
Step 3、
01.public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {
/**
* We have one child
*/
View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mWindowAttributes.copyFrom(attrs); //保存WindowManager.LayoutParams属性值
attrs = mWindowAttributes;
...
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout(); //请求UI开始绘制。
mInputChannel = new InputChannel(); //创建一个InputChannel对象,接受消息
try {
//通知WindowManagerService添加一个窗口
res = sWindowSession.add(mWindow, mWindowAttributes,
getHostVisibility(), mAttachInfo.mContentInsets,
mInputChannel);
}
...
view.assignParent(this); //将root View的父View设置为该ViewRoot对象(实现了ViewParent接口)
...
}
}
setView()方法地主要功能如下:
保存相关属性值,例如:mView、mWindowAttributes等;
调用requestLayout()方法请求UI绘制,由于ViewRoot是个Handler对象,异步请求;
通知WindowManagerService添加一个窗口;
注册一个事件监听管道,用来监听:按键(KeyEvent)和触摸(MotionEvent)事件。
我们这儿重点关注 requestLayout()方法请求UI绘制地流程。
Step 4、异步调用请求UI绘制
01./**
mTraversalScheduled = true; //防止多次调用
sendEmptyMessage(DO_TRAVERSAL); //异步请求UI绘制
case DO_TRAVERSAL:
performTraversals(); //开始UI绘制
break;
heightSpecSize值。
01.private void performTraversals() {
|| mNewSurfaceNeeded;
mWindowAttributesChanged = false;
surfaceChanged = true;
params = lp;
fullRedrawNeeded = true;
mLayoutRequested = true;
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
//第一次绘制时desiredWindowWidth,desiredWindowHeight 值大小为屏幕大小
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
...
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
...
...//获得root View的widthMeasureSpec 和 heightMeasureSpec值
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//开始measure过程
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
|| attachInfo.mRecomputeGlobalAttributes;
... //layout过程
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
...
mFullRedrawNeeded = false;
draw(fullRedrawNeeded);
...
69.}01./**
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
差不多。贴出来的代码只是简简单单列出了measure 、layout 、 draw 过程的调用点,里面有很多逻辑处理,