一、include
首先用得最多的应该是 include,按照官方的意思,
include 就是为了解决重复
定义相同布局的问题。例如你有五个界面,这五个界面的顶部都有布局一模一样
的一个返回按钮和一个文本控件,在不使用 include 的情况下你在每个界面都需
要重新在 xml 里面写同样的返回按钮和文本控件的顶部栏,这样的重复工作会
相当的恶心。使用 include 标签,我们只需要把这个会被多次使用的顶部栏独立
成一个 xml 文件,然后在需要使用的地方通过 include 标签引入即可。其实就
相当于 C 语言、C++中的 include 头文件一样,我们把一些常用的、底层的 API
封装起来,然后复用,需要的时候引入它即可,而不必每次都自己写一遍。示例
如下 :
my_title_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/my_title_parent_id"
android:layout_height="wrap_content" >
<ImageButton
android:id="@+id/back_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/back_btn"
android:gravity="center"
android:text="我的 title"
android:textSize="18sp" />
</RelativeLayout>
include 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<include
android:id="@+id/my_title_ly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/my_title_layout" />
<!-- 代码省略 -->
</LinearLayout>
这样我们就可以使用 my_title_layout 了。
注意事项使用 include 最常见的问题就是 findViewById 查找不到目标控件,这个
问题出现的前提是在 include 时设置了 id,而在 findViewById 时却用了被
include 进来的布局的根元素 id。例如上述例子中,
include 时设置了该布
局 的 id 为 my_title_ly , 而 my_title_layout.xml 中 的 根 视 图 的 id 为
my_title_parent_id。此时如果通过 findViewById 来找 my_title_parent_id
这个控件,然后再查找 my_title_parent_id 下的子控件则会抛出空指针。
代码如下 :
View titleView = findViewById(R.id.my_title_parent_id) ;
// 此时 titleView 为空,找不到。此时空指针
TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;
titleTextView.setText("new Title");
其正确的使用形式应该如下:
// 使用 include 时设置的 id,即 R.id.my_title_ly
View titleView = findViewById(R.id.my_title_ly) ;
// 通过 titleView 找子控件
TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;
titleTextView.setText("new Title");
或者更简单的直接查找它的子控件:
TextView titleTextView = (TextView)findViewById(R.id.title_tv) ;
titleTextView.setText("new Title");那么使用 findViewById(R.id.my_title_parent_id)为什么会报空指针呢? 我们
来分析它的源码看看吧。对于布局文件的解析,最终都会调用到 LayoutInflater
的 inflate 方法,该方法最终又会调用 rInflate 方法,我们看看这个方法。
/**
* Recursive method used to descend down the xml hierarchy and
instantiate
* views, instantiate their children, and then call onFinishInflate().
*/
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
// 迭代 xml 中的所有元素,挨个解析
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_INCLUDE.equals(name)) {
// 如果 xml 中的节点是include 节点,则调用 parseInclude 方法
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be
the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the
root element");
} else if (TAG_1995.equals(name)) {
final View view = new BlinkLayout(mContext, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);viewGroup.addView(view, params);
} else {
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}
这个方法其实就是遍历 xml 中的所有元素,然后挨个进行解析。例如解析
到一个标签,那么就根据用户设置的一些 layout_width、layout_height、id 等
属性来构造一个 TextView 对象,然后添加到父控件(ViewGroup 类型)中。标签
也是一样的,我们看到遇到 include 标签时,会调用 parseInclude 函数,这就
是对标签的解析,我们看看吧。
private void parseInclude(XmlPullParser parser, View parent, AttributeSet
attrs)
throws XmlPullParserException, IOException {int type;
if (parent instanceof ViewGroup) {
final int layout = attrs.getAttributeResourceValue(null,
"layout", 0);
if (layout == 0) {// include 标签中没有设置 layout 属性,会抛出
异常
final String value = attrs.getAttributeValue(null, "layout");
if (value == null) {
throw new InflateException("You must specifiy a layout in the" + " include tag: <include layout=\"@layout/layoutID\" />");
} else {
throw new InflateException("You must specifiy a valid layout " + "reference. The layout ID " + value + " is not valid.");
}
} else {
final XmlResourceParser childParser =getContext().getResources().getLayout(layout);
try {// 获取属性集,即在 include 标签中设置的属性
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while((type=childParser.next())!=XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(childParser.getPositionDescription() + ": No start tag found!");
}
// 1、解析 include 中的第一个元素
final String childName = childParser.getName();
// 如果第一个元素是 merge 标签,那么调用 rInflate 函
数解析
if (TAG_MERGE.equals(childName)) {// Inflate all children.
rInflate(childParser, parent, childAttrs, false);
} else {// 2、我们例子中的情况会走到这一步,首先根据
include 的属性集创建被 include 进来的 xml 布局的根 view
// 这里的根 view 对应为 my_title_layout.xml 中的
RelativeLayout
final View view = createViewFromTag(parent,
childName, childAttrs);
final ViewGroup group = (ViewGroup) parent;//
include 标签的 parent view
ViewGroup.LayoutParams params = null;
try {// 获 3、取布局属性
params= group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
params = group.generateLayoutParams(childAttrs);
} finally {
if (params != null) {// 被 inlcude 进 来 的 根
view 设置布局参数
view.setLayoutParams(params);}
}
// 4、Inflate all children. 解析所有子控件
rInflate(childParser, view, childAttrs, true);
// Attempt to override the included layout's
android:id with the
// one set on the <include /> tag itself.
TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, 0, 0);
int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
// While we're at it, let's try to override
android:visibility.int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
a.recycle();
// 5、将 include 中设置的 id 设置给根 view,因此实
际上 my_title_layout.xml 中的 RelativeLayout 的 id 会变成 include 标签中的 id,
include 不设置 id,那么也可以通过 relative 的找到.
if (id != View.NO_ID) {
view.setId(id);
}
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
// 6、将根 view 添加到父控件中
group.addView(view);
}
} finally {
childParser.close();
}
}} else {
throw new InflateException("<include /> can only be used
inside of a ViewGroup");
}
final int currentDepth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth()
> currentDepth)
&& type
!=
XmlPullParser.END_DOCUMENT) {
// Empty
}
}
整个过程就是根据不同的标签解析不同的元素,首先会解析 include 元素,然后
再解析被 include 进来的布局的 root view 元素。在我们的例子中对应的 root
view 就是 id 为 my_title_parent_id 的 RelativeLayout,然后再解析 root view
下面的所有元素,这个过程是从上面注释的 2~4 的过程,然后是设置布局参数。
我们注意看注释 5 处,这里就解释了为什么 include 标签和被引入的布局的根元
素都设置了 id 的情况下,通过被引入的根元素的 id 来查找子控件会找不到的情
况。我们看到,注释 5 处的会判断 include 标签的 id 如果不是 View.NO_ID 的
话会把该 id 设置给被引入的布局根元素的 id,即此时在我们的例子中被引入的
id 为 my_title_parent_id 的根元素 RelativeLayout 的 id 被设置成了 include
标签中的 id,即 RelativeLayout 的 id 被动态修改成了”my_title_ly”。因此此时我们再通过“my_title_parent_id”这个 id 来查找根元素就会找不到了!
所以结论就是: 如果 include 中设置了 id,那么就通过 include 的 id 来查找被
include 布局根元素的 View;如果 include 中没有设置 Id, 而被 include 的布
局的根元素设置了 id,那么通过该根元素的 id 来查找该 view 即可。拿到根元
素后查找其子控件都是一样的。
二、ViewStub
我们先看看官方的说明:
ViewStub is a lightweight view with no dimension and doesn’t draw
anything or participate in the layout. As such, it’s cheap to inflate and
cheap to leave in a view hierarchy. Each ViewStub simply needs to
include the android:layout attribute to specify the layout to inflate.
其实 ViewStub 就是一个宽高都为 0 的一个 View,它默认是不可见的,只有通
过调用 setVisibility 函数或者 Inflate 函数才会将其要装载的目标布局给加载出
来,从而达到延迟加载的效果,这个要被加载的布局通过 android:layout 属性
来设置。例如我们通过一个 ViewStub 来惰性加载一个消息流的评论列表,因为
一个帖子可能并没有评论,此时我可以不加载这个评论的 ListView,只有当有
评论时我才把它加载出来,这样就去除了加载 ListView 带来的资源消耗以及延
时,示例如下 :
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/stub_comm_lv"android:layout="@layout/my_comment_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" /
my_comment_layout.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/my_comm_lv"
android:layout_height="match_parent" >
</ListView>
在运行时,我们只需要控制 id 为 stub_import 的 ViewStub 的可见性或者
调用 inflate()函数来控制是否加载这个评论列表即可。示例如下 :
public class MainActivity extends Activity {
public void onCreate(Bundle b){
// main.xml 中包含上面的 ViewStub
setContentView(R.layout.main);// 方式 1,获取 ViewStub,
ViewStub
listStub
=
(ViewStub)
findViewById(R.id.stub_import);
// 加载评论列表布局
listStub.setVisibility(View.VISIBLE);
// 获取到评论 ListView,注意这里是通过 ViewStub 的 inflatedId
来获取
ListView commLv = findViewById(R.id.stub_comm_lv);
if ( listStub.getVisibility() == View.VISIBLE ) {
// 已经加载, 否则还没有加载
}
}
}
通过 setVisibility(View.VISIBILITY)来加载评论列表,此时你要获取到评论
ListView 对象的话,则需要通过 findViewById 来查找,而这个 id 并不是就是
ViewStub 的 id。
这是为什么呢 ?
我们先看 ViewStub 的部分代码吧:
@SuppressWarnings({"UnusedDeclaration"})
public ViewStub(Context context, AttributeSet attrs, int defStyle){
TypedArray
a
=
context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.ViewStub,
defStyle, 0);
// 获取 inflatedId 属性
mInflatedId
=
a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource
=
a.getResourceId(R.styleable.ViewStub_layout, 0);
a.recycle();
a
=
context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.View, defStyle, 0);
mID = a.getResourceId(R.styleable.View_id, NO_ID);
a.recycle();
initialize(context);
}
private void initialize(Context context) {
mContext = context;setVisibility(GONE);// 设置不可教案
setWillNotDraw(true);// 设置不绘制
}
@Override
protected
void
onMeasure(int
widthMeasureSpec,
int
heightMeasureSpec) {
setMeasuredDimension(0, 0);// 宽高都为 0
}
@Override
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {// 如果已经加载过则只设置
Visibility 属性
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called
on un-referenced view");
}} else {// 如果未加载,这加载目标布局
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();// 调用 inflate 来加载目标布局
}
}
}
/**
* Inflates the
layout
resource identified
by
{@link
#getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated
layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup)
{if (mLayoutResource != 0) {
final
ViewGroup
parent
=
(ViewGroup)
viewParent;// 获取 ViewStub 的 parent view,也是目标布局根元素的 parent
view
final
LayoutInflater
factory
=
LayoutInflater.from(mContext);
final View view = factory.inflate(mLayoutResource,
parent,
false);// 1、加载目标布局
// 2 、 如 果 ViewStub 的 inflatedId 不 是 NO_ID 则 把
inflatedId 设置为目标布局根元素的 id,即评论 ListView 的 id
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);// 3、将 ViewStub
自身从 parent 中移除
final ViewGroup.LayoutParams layoutParams =
getLayoutParams();
if (layoutParams != null) {parent.addView(view, index, layoutParams);//
4、将目标布局的根元素添加到 parent 中,有参数
} else {
parent.addView(view, index);// 4、将目标布局的
根元素添加到 parent 中
}
mInflatedViewRef
=
new
WeakReference<View>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
} else {
throw new IllegalArgumentException("ViewStub
must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a
non-null ViewGroup viewParent");}
}
可以看到,其实最终加载目标布局的还是 inflate()函数,在该函数中将加载
目标布局,获取到根元素后,如果 mInflatedId 不为 NO_ID 则把 mInflatedId
设置为根元素的 id,这也是为什么我们在获取评论 ListView 时会使用
findViewById(R.id.stub_comm_lv) 来 获 取 , 其 中 的 stub_comm_lv 就 是
ViewStub 的 inflatedId。当然如果你没有设置 inflatedId 的话还是可以通过评
论列表的 id 来获取的,例如 findViewById(R.id.my_comm_lv)。然后就是
ViewStub 从 parent 中移除、把目标布局的根元素添加到 parent 中。最后会把
目标布局的根元素返回,因此我们在调用 inflate()函数时可以直接获得根元素,
省掉了 findViewById 的过程。
还有一种方式加载目标布局的就是直接调用 ViewStub 的 inflate()方法,示例如
下 :
public class MainActivity extends Activity {
// 把 commLv2 设置为类的成员变量
ListView commLv2 = null;
//
public void onCreate(Bundle b){
// main.xml 中包含上面的 ViewStub
setContentView(R.layout.main);// 方式二
ViewStub
listStub2
=
(ViewStub)
findViewById(R.id.stub_import) ;
// 成员变量 commLv2 为空则代表未加载
if ( commLv2 == null ) {
// 加载评论列表布局, 并且获取评论 ListView,inflate 函数直接返回
ListView 对象
commLv2 = (ListView)listStub2.inflate();
} else {
// ViewStub 已经加载
}
}
}
注意事项
1. 判断是否已经加载过, 如果通过 setVisibility 来加载,那么通过判断可
见性即可;如果通过 inflate()来加载是不可以通过判断可见性来处理的,
而需要使用方式 2 来进行判断。
2. findViewById 的问题,注意 ViewStub 中是否设置了 inflatedId,如果
设置了则需要通过 inflatedId 来查找目标布局的根元素。
三、Merge首先我们看官方的说明:
The tag helps eliminate redundant view groups in your view hierarchy
when including one layout within another. For example, if your main
layout is a vertical LinearLayout in which two consecutive views can be
re-used in multiple layouts, then the re-usable layout in which you place
the two views requires its own root view. However, using another
LinearLayout as the root for the re-usable layout would result in a vertical
LinearLayout inside a vertical LinearLayout. The nested LinearLayout
serves no real purpose other than to slow down your UI performance.
其实就是减少在 include 布局文件时的层级。标签是这几个标签中最让我费解
的,大家可能想不到,标签竟然会是一个 Activity,里面有一个 LinearLayout
对象。
/**
* Exercise <merge /> tag in XML files.
*/
public class Merge extends Activity {
private LinearLayout mLayout;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);mLayout = new LinearLayout(this);
mLayout.setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(this).inflate(R.layout.merge_tag, mLayout);
setContentView(mLayout);
}
public ViewGroup getLayout() {
return mLayout;
}
}
使用 merge 来组织子元素可以减少布局的层级。例如我们在复用一个含有多个
子控件的布局时,肯定需要一个 ViewGroup 来管理,例如这样 :
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"android:scaleType="center"
android:src="@drawable/golden_gate" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"
android:padding="12dip"
android:background="#AA000000"
android:textColor="#ffffffff"
android:text="Golden Gate" />
</FrameLayout>
使用 merge 标签就会消除上图中蓝色的 FrameLayout 层级。示例如下 :
<merge
xmlns:android="http://schemas.android.com/apk/res/android"><ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="center"
android:src="@drawable/golden_gate" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"
android:padding="12dip"
android:background="#AA000000"
android:textColor="#ffffffff"
android:text="Golden Gate" />
</merge>
那么它是如何实现的呢,我们还是看源码吧。相关的源码也是在 LayoutInflater的 inflate()函数中。
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;
View result = root;
try {
// Look for the root node.
int type;
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();
// m 如果是 erge 标签,那么调用 rInflate 进行解析
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be
used only with a valid "
+
"ViewGroup
root
and
attachToRoot=true");
}
// 解析 merge 标签
rInflate(parser, root, attrs, false);
} else {
// 代码省略
}
} catch (XmlPullParserException e) {
// 代码省略
}return result;
}
}
void rInflate(XmlPullParser parser, View parent, final AttributeSet
attrs,
boolean
finishInflate)
throws
XmlPullParserException,
IOException {
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_INCLUDE.equals(name)) {
// 代码省略
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root
element");
} else if (TAG_1995.equals(name)) {
final View view = new BlinkLayout(mContext, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final
ViewGroup.LayoutParams
params
=
viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
} else { // 我们的例子会进入这里
final View view = createViewFromTag(parent, name,
attrs);
// 获取 merge 标签的 parent
final ViewGroup viewGroup = (ViewGroup) parent;
// 获取布局参数final
ViewGroup.LayoutParams
params
=
viewGroup.generateLayoutParams(attrs);
// 递归解析每个子元素
rInflate(parser, view, attrs, true);
// 将子元素直接添加到 merge 标签的 parent view 中
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}
其实就是如果是 merge 标签,那么直接将其中的子元素添加到 merge 标签
parent 中,这样就保证了不会引入额外的层级。