Android Layout Resource分析

1. 概述

layout资源文件定义了Activity或者某个组件的用户界面结构。

layout资源文件位置
res/layout/filename.xml
filename 将会被用做资源ID.

被编译的layout资源类型
layout资源文件的节点是View类型或者View子类。

layout资源的引用
In Java: R.layout.filename
In XML: @[package:]layout/filename

语法:
<?xml version="1.0" encoding="utf-8"?>
<ViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@[+][package:]id/resource_name"
    android:layout_height=["dimension" | "match_parent" | "wrap_content"]
    android:layout_width=["dimension" | "match_parent" | "wrap_content"]
    [ViewGroup-specific attributes] >
    <View
        android:id="@[+][package:]id/resource_name"
        android:layout_height=["dimension" | "match_parent" | "wrap_content"]
        android:layout_width=["dimension" | "match_parent" | "wrap_content"]
        [View-specific attributes] >
        <requestFocus/>
    </View>
    <ViewGroup >
        <View />
    </ViewGroup>
    <include layout="@layout/layout_resource"/>
</ViewGroup>
注:根元素可以是ViewGroup的某些子类、View或者<merge>元素,但每个layout文件必须只有一个根元素,
并且根元素必须包含用来描述Android命名空间的属性xmlns:Android。

2. layout资源文件中的元素类型

可以作为根元素的元素类型有很多(ViewGroup的某些子类、View或者<merge>元素)。
只可以作为根元素的资源类型是<merge>元素。
不可以作为根元素的资源类型是<include>元素、<requestFocus>元素或者<ViewStub>元素。

2.1 ViewGroup

作为其他View元素的容器。ViewGroup有很多子类,例如LinearLayout、RelativeLayout 和 FrameLayout,每一个子类都可以指定其子元素以特定方式排列。并不是所有的ViewGroup派生的子类都可以嵌套视图,一些ViewGroup实现了AdapterView类,从而决定它的子元素只能来自于Adapter。

2.2 View

一个单独的用户界面组件,通常被称为“widget”。不同的View对象包括TextView,、Button 和 CheckBox.

2.3 <requestFocus>元素

任何代表View对象的元素都可以包含<requestFocus>元素(该元素是没有任何属性,就是空元素),它可以使它的父元素得到焦点,每一个layout文件只可以包含一个<requestFocus>元素。

2.4 <include>元素

用来在某个layout文件中包含另一个layout文件,从而达到layout代码的重用和模块化。

属性:
layout
    Layout resource. Required. Reference to a layout resource.

android:id
    Resource ID. 覆盖掉被包含layout资源文件根元素的ID。 

android:layout_height
    Dimension or keyword. 只有include元素的android:layout_width属性也被声明时,
该属性才会有效果(即覆盖掉被包含layout资源文件根元素的android: layout_height属性的值),否者该属性
没有效果。

android:layout_width
    Dimension or keyword. 只有include元素的android:layout_height属性也被声明时,
该属性才会有效果(即覆盖掉被包含layout资源文件根元素的android: layout_width属性的值),否者该属性
没有效果。

只要被包含layout资源文件根元素支持某个layout属性,你就可以在<include>元素中添加该layout属性,
被添加到<include>元素中的这些属性会覆盖掉被包含layout资源文件根元素对应的属性。

下面是我总结的属性覆盖规则(A代表include中的属性,B代表被包含layout资源文件根元素的属性)
id属性覆盖规则
    A中有B中有则覆盖,A中有B中无则添加,A中无而B中有则B中生效。
layout属性(以layout_为前缀的属性)的覆盖规则:
    必须先同时在A中声明android:layout_height和android:layout_width属性,这样A中才会生效
(即A中有B中有则覆盖,A中有B中无则添加),否者B中所有layout属性不会被A中覆盖(即B中生效)。

举例如下:
activity_include.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="match_parent"
    android:orientation="vertical"
    android:paddingTop="20dp">

    <include
        android:id="@+id/include_import_layout"
        android:layout_width="160dp"
        android:layout_height="80dp"
        layout="@layout/import_layout_include" />

</LinearLayout>

import_layout_include.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/import_layout"
    android:layout_width="match_parent"
    android:layout_marginLeft="20dp"
    android:orientation="vertical">

    <Button
        android:id="@+id/test_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/include_button" />

</LinearLayout>

IncludeActivity.java文件:
public class IncludeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_include);
        View view = findViewById(R.id.include_import_layout);
        view.setBackgroundColor(Color.GREEN);
    }
}

注意:include元素中的layout属性不要写成android:layout="@layout/import_layout_include",
而在ViewStub标签中却要写成android:layout="@layout/import_layout_merge"。

运行结果如下:


由运行结果可以证明:
1> id属性覆盖规则中的 B中有则覆盖
2>layout属性(以layout_为前缀的属性)的覆盖规则中的 B中有则覆盖,B中无则添加

其他的规则我就不一一验证了,大家有兴趣的可以自己研究。

2.5 <ViewStub>元素

除了上面一种包含layout资源文件方式,<ViewStub>元素是另外一种方式,
Google对ViewStub给的说明:
A ViewStub is an invisible, zero-sized View that can be used to lazily inflate layout resources at runtime. When a ViewStub is made visible, or when inflate() is invoked, the layout resource is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views. Therefore, the ViewStub exists in the view hierarchy until setVisibility(int) or inflate() is invoked. The inflated View is added to the ViewStub's parent with the ViewStub's layout parameters. Similarly, you can define/override the inflate View's id by using the ViewStub's inflatedId property.

与<include>元素的异同:
属性覆盖规则与<include>元素相同,唯一不同的地方是<ViewStub>元素有懒加载的特点(其实ViewStub就是一个宽高都为0的一个View,它默认是不可见的,只有通过调用setVisibility函数或者Inflate函数才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果)。

举例如下:
activity_view_stub.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="match_parent"
    android:orientation="vertical">

    <ViewStub
        android:id="@+id/stub_import_layout"
        android:layout_width="160dp"
        android:layout_height="80dp"
        android:inflatedId="@+id/stub_import_layout_root"
        android:layout="@layout/import_layout_stub" />

</LinearLayout>

import_layout_stub.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="20dp"
    android:orientation="vertical">

    <Button
        android:id="@+id/test_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/include_button" />

</LinearLayout>


ViewStubActivity.java 文件
public class ViewStubActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_view_stub);

        ViewStub view = (ViewStub) findViewById(R.id.stub_import_layout);
        view.setVisibility(View.VISIBLE);

        LinearLayout linearLayout = (LinearLayout) findViewById(R.id.stub_import_layout_root);
        linearLayout.setBackgroundColor(Color.GREEN);
    }
}

运行结果与上图一样

要想获取被包含布局的根元素,使用的是ViewStub元素中的id获取(ViewStub元素中的id没有被设置时,就可以用包含布局的根元素的id获取)。
这是为什么呢 ?
看一下ViewStub源代码,就一目了然了:

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

可以看到setVisibility方法最后会通过inflate()函数加载目标布局,在该函数中将加载目标布局,获取到根元素后,如果mInflatedId不为NO_ID则把mInflatedId设置为根元素的id,这也是为什么我们在获取LinearLayout时会使用findViewById(R.id.stub_import_layout_root)来获取,其中的stub_import_layout_root就是ViewStub的inflatedId, 当然如果你没有设置inflatedId的话还是可以通过的LinearLayout的id来获取的,例如findViewById(R.id. layout_root);然后就是ViewStub从parent中移除、把目标布局的根元素添加到parent中;最后会把目标布局的根元素返回。因此我们可以直接调用 inflate()函数从而直接获得根元素,省掉了findViewById的过程。

下面就是通过直接调用ViewStub的inflate()方法加载目标布局的示例代码 :


public class ViewStub2Activity extends Activity {

    LinearLayout linearLayout = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_view_stub);

        ViewStub view = (ViewStub) findViewById(R.id.stub_import_layout);
        if (null == linearLayout) {
            linearLayout = (LinearLayout) view.inflate();
        }
        linearLayout.setBackgroundColor(Color.GREEN);
    }
}

运行结果与上图一样

2.6 <merge>元素

官方给的说明如下:
The <merge /> 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.
我的理解:
在A布局中通过<include>元素将以<merge>元素为根元素的B布局包含进来,<merge>元素会会被系统忽略,然后用<merge>元素的子元素替换掉<include>元素。

注意:ViewStub目前有个缺陷就是还不支持 <merge /> 标签。

关于merge标签如何使用,可以看一下下面的例子:
activity_merge.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <include
        android:id="@+id/merge_import_layout"
        layout="@layout/import_layout_merge" />

</FrameLayout>

import_layout_merge.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
    <Button android:id="@+id/test_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/include_button"/>

</merge>

MergeAcyivity.java文件:
public class MergeAcyivity extends Activity {

    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_merge);

        Button testButton = (Button) findViewById(R.id.test_button);
        testButton.setText("chenyang");
    }
}

<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();
               
               // 如果是merge标签,那么调用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)) {
               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);
               // 获取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();
   }

上面的注释已经很清晰了,这里就多做解释了,从而证明了我的理解是正确的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容