尽管Android SDK为开发者提供了各种各样的小部件来提供小型且可重用的交互元素,但开发者可能仍然需要重新使用特殊布局的较大组件。这就是我们所谓的布局复用。要有效地重新使用完整的布局,可以使用和标签在当前布局中嵌入另一个布局。
重复使用布局非常有用,因为它允许开发者创建可重用的复杂布局。例如,是/否按钮面板,或带有说明文字的自定义进度栏。这也意味着您的应用程序的任何元素都可以在多个布局中提取,分别管理,然后包含在每个布局中。因此,尽管可以通过编写自定义来创建各个UI组件,但View通过重新使用布局文件,可以更轻松地完成此任务。
include标签
include标签适用于当某个布局文件加载的视图需要在不同的页面重复使用时,例如,项目开发过程中最常见的每个页面标题栏的封装
lbjfan_title.xml
<?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="48dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_gravity="center_vertical"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
</LinearLayout>
使用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">
<include layout="@layout/lbjfan_title" />
</LinearLayout>
注意:如果include标签设置id,会覆盖掉include加载的布局中根节点设置的id,使用findViewByid时要注意空指针异常,更具体一点如果我们给lbjfan_title.xml中的根节点LinearLayout设置了id,同时给引用该布局的include设置了id,则LinearLayout的id会被覆盖掉,此时如果使用findViewById就会出现空指针异常。
View的解析最终都是通过LayoutInflater类中的rInflate方法解析的,该方法源码:
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
译文:递归解析XML文件并实例化View和子View,最后调用onFinishInflate方法完成加载
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
/.......略........../
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
//解析include标签
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
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);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
/.........略........../
}
parseInclude方法关键源码
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
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!");
}
final String childName = childParser.getName();
//include肯定不是merge标签
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
//获取include标签的id
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
ViewGroup.LayoutParams params = null;
try {
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
view.setId(id);
}
//将View添加到include的parent
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
}
Merge标签
官方文档中这样描述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.
译文:将标签包含在另一个布局中时,该标签有助于消除视图层次结构中的冗余视图组。例如,如果主布局是可以在多个布局中重用的垂直布局,则放置可重用布局需要具有其自己的根视图。但是,使用另一个LinearLayout作为可重用布局的根目录将导致垂直LinearLayout内部嵌套垂直LinearLayout。嵌套垂直的LinearLayout除了减慢你的UI性能,没有任何益处。
继续来看看inflate方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
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();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//如果是merge标签,调用rinflate方法
if (TAG_MERGE.equals(name)) {
//使用merger标签时常出现的异常
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//此时解析时传入的root已经不是merge,而是merge对应的根标签
rInflate(parser, root, inflaterContext, attrs, false);
} else {
}
}
}
return result;
}
}
rInflate方法:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} 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");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//由于root不是merge,将会执行下面代码
final View view = createViewFromTag(parent, name, context, attrs);
//获取merger对应的ViewGroup
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//解析子View
rInflateChildren(parser, view, attrs, true);
//将子View添加到ViewGroup中
viewGroup.addView(view, params);
}
}
}
可以看到,merge标签解析时是将merge标签中的元素直接添加到对应的ViewGroup中,因此使用merge标签将极大减少ViewGroup的嵌套。
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属性来设置。
使用:
1.直接在布局文件中设置layout属性,然后在代码中通过inflate方法来显示
2.在布局文件中先通过setLayoutResource方法来加载布局,然后通过inflate显示
关键源码分析:
构造方法:
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
//获取layout属性加载的布局Id
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
//获取id,默认值NO_ID
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
//设置不可见
setVisibility(GONE);
//设置不绘制,因此ViewStub可以理解为占位符
setWillNotDraw(true);
onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//宽高均为0
setMeasuredDimension(0, 0);
}
setLayoutResource方法
public void setLayoutResource(@LayoutRes int layoutResource) {
//等价于布局文件中设置的layout属性
mLayoutResource = layoutResource;
}
inflate方法
public View inflate() {
//获取ViewStub的parent(ViewGroup)
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view =
//调用inflateViewNoAdd方法通过mLayoutResource获取到显示的View
inflateViewNoAdd(parent);
replaceSelfWithView(view, parent);
//使用弱引用存储ViewStub需要加载显示的View,setVisibility时会用到
mInflatedViewRef = new WeakReference<>(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");
}
}
inflateViewNoAdd方法
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
final View view = factory.inflate(mLayoutResource, parent, false);
//将ViewStub的id设置给需要加载的View
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
replaceSelfWithView方法
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
//移除自己
parent.removeViewInLayout(this);
获取ViewStub对应的Param
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
//将需要显示的View添加到ViewStub在ViewGroup中对应的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
setVisibility方法
public void setVisibility(int visibility) {
//mInflatedViewRef在inflate方法中创建,说明已经调用过
if (mInflatedViewRef != null) {
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) {
//没有调用过直接调用inlate方法
inflate();
}
}
}
可以看见ViewStub加载布局时都是通过ViewStub方法,然后使用弱引用换存,需要设置可见性时,如果弱引用中存在加载的View则直接设置,否咋通过inflate方法显示。除此之外,使用ViewStub标签时,需要注意一下几点:
1.对ViewStub的inflate操作只能进行一次,因为inflate的时候是将其指向的布局文件解析inflate并替换掉当前ViewStub本身(由此体现出了ViewStub“占位符”性质),一旦替换后,此时原来的布局文件中就没有ViewStub控件了,因此,如果多次对ViewStub进行infalte,getParent()方法获得的Parent就为空,然后出现错误信息:ViewStub must have a non-null ViewGroup viewParent。
2.ViewStub中是否设置了inflatedId,如果设置了则需要通过inflatedId来查找目标布局的根元素。
2.ViewStub占位符:当一个布局加载一次时可以使用,一开始不显示,宽高都是0,通过inflate和setVisibility可以让其显示,只能inflate一次,原因是inflate时会解析自己,使用Layout的元素代替自己,同时也需注意id设置,会覆盖根节点的id,必须有一个ViewGroup否则会抛出异常,同时还需注意加载Layout时,使用的LayoutParam是ViewStub的LayoutParam,源码都可见。