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閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容