Android Layout Resource分析

作者: 小芸论 | 来源:发表于2017-04-04 18:13 被阅读230次

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

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

    相关文章

      网友评论

        本文标题:Android Layout Resource分析

        本文链接:https://www.haomeiwen.com/subject/icaaottx.html