Android View的layout_width屬性是如何解析的

東風不破發表於2017-08-22

一 引言

在開發 Android UI 介面時,一般都會在 layout 目錄下新建一個XML檔案,用於編寫佈局檔案。下面是一個簡單的佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

從上面的檔案的佈局檔案可以看到,控制元件由各個屬性組成,如id、layout_width、layout_height等,這些屬性最終都會經系統解析,從而在 View 在measure、layout 及 draw的過程中使用。

那麼問題來了

layout目錄下佈局檔案各View的屬性是如何解析的?

View的屬性很多,下面以 layout_width 屬性的解析過程做簡單的分析。

主要分兩個步驟:
1. 將佈局檔案的屬性解析到 AttributeSet 中
2. 將 AttributeSet 中 layout_width屬性解析到 LayoutParams 的 width域中。

二 layout/layout_name.xml -> AttributeSet

簡單說下AttributeSet,它的作用從名字可以看出了,其實就是屬性的集合,它包含了 View 設定的所有屬性。詳細見 官網 說明。
我們知道,在 Activity onCreate方法 中,將佈局檔案資源id作為引數傳入 setContentView(int layoutResID) 方法中,從而展示佈局。也有通過 adaptergetView() 方法中通過 LayoutInflater 將佈局resID顯式 inflate 進去。其實,setContentView最終也是通過 LayoutInflater 將佈局檔案資訊填充到對應的 View 中。原始碼如下:

原始碼路徑: frameworks/base/core/java/android/app/Activity.java

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

activity setContentView方法 通過 getWindow() 獲得 Window 例項,window的實現類是 PhoneWindow, 來看下其對 setContentView*方法的實現:

@Override 
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

從上面程式碼可知,當hasFeature(FEATURE_CONTENT_TRANSITIONS) 返回為false時,會呼叫 LayoutInflater 例項的inflate方法,其中引數 layoutResID 即是我們傳入的佈局資源id。


接下來,我們就看下 LayoutInflater 類的 inflate 方法是如何解析佈局資源的。

public View inflate(int resource, ViewGroup root) {
    return inflate(resource, root, root != null);
}

inflate 方法呼叫了同名方法,跟進:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

首先,通過 getResources 方法獲取 Resource 例項,然後將 resID 傳入該例項的 getLayout 方法,從而返回一個 XmlResourceParser 例項,它是對已經編譯過的xml檔案的封裝,接下來將該 parser 作為引數傳入到 inflate,看下 這個inflate 方法實現邏輯。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
    ...
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    ...
    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, attrs, false);
    ViewGroup.LayoutParams params = null;
    if (root != null) {
        // Create layout params that match root, if supplied
        params = root.generateLayoutParams(attrs);
        if (!attachToRoot) {
            // Set the layout params for temp if we are not
            // attaching. (If we are, we use addView, below)
            temp.setLayoutParams(params);
        }
        return result;
    }
}

這裡的 inflate 方法實現邏輯較長,我們擷取了部分程式碼。重點關注 下final AttributeSet attrs = Xml.asAttributeSet(parser);
到這裡,,AttributeSet 出現了!
Xml將已經封裝的 XmlResourceParser 例項轉化為我們想要的 AttributeSet 物件。

三 將 AttributeSet 中 layout_width屬性解析為LayoutParams 的 width

接下來父View通過 AttributeSet 解析其子View的layout_width屬性,從而將 生成該父View 的LayoutParams width的值。

root.generateLayoutParams(attrs)

其中 attrs 引數是由 XmlasAttributeSet 方法將之前已經封裝的 XmlResourceParser 例項解析而來。root是該我們先前在activity傳入的資源佈局id所對應view的父容器。由其負責解析其子view的 LayoutParams。程式碼如下:

 public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
 }

繼續跟進 LayoutParams 類的建構函式實現

public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

首先看建構函式體裡面的第一行程式碼
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
obtainStyledAttributes 方法的目的是為了獲取屬性,關於該方法有許多過載方法,如下:

  1. public final TypedArray obtainStyledAttributes(int[] attrs)

    用於從系統主題中獲取 attrs 中的屬性

  2. public final TypedArray obtainStyledAttributes(int resid, int[] attrs)

    用於從資原始檔定義的 style 中讀取屬性

  3. public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs)

    layout 設定的屬性集中獲取 attrs 中的屬性

這裡用到的是第三個方法,它有兩個引數。第一個引數為 AttributeSet 引用,它是資料來源,表示屬性從哪裡來的,第二個引數 attrs 表示需要獲取哪些屬性。第二個引數傳入的是 R.styleable.ViewGroup_Layout,它定義在
frameworks/base/core/res/res/values/attrs.xml 檔案中,如下:

<declare-styleable name="ViewGroup_Layout">
    <attr name="layout_width" format="dimension">
        <enum name="fill_parent" value="-1" />
        <enum name="match_parent" value="-1" />
        <enum name="wrap_content" value="-2" />
    </attr>
    <attr name="layout_height" format="dimension">
        <enum name="fill_parent" value="-1" />
        <enum name="match_parent" value="-1" />
        <enum name="wrap_content" value="-2" />
    </attr>
</declare-styleable>

在這裡說明下,通過在 attr 檔案中定義styleable,編譯後在 R 檔案中自動生成一個int[],陣列裡面的值就是定義在 styleable 裡面的 attr 的id。下面分析
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); 的解析過程:
首先 attrs(屬性源) 引數是對我們一開始就傳入到 Activity setContentView 方法裡面的 layout 檔案id進行解析得到的屬性集合,由 LayoutInflater 載入得到的,R.styleable.ViewGroup_Layout(要獲取的屬性) 就是一個int[],裡面包含了宣告的 attr id。我們知道了屬性源,也知道了需要獲取的屬性集合,通過 obtainStyledAttributes 方法最終得到一個 TypedArray

TypedArray是什麼鬼?

TypedArray的存在簡化了我們解析屬性的步驟。比如解析 android:text="@string/my_label,text的屬性值是引用型別。如果直接使用 AttributeSet 解析該屬性,需要兩步:1. 獲取text屬性引用值的id;2. 根據該id去獲取text對應的String值。而有了TypedArray,這些工作交給它就行了。
好了,我們繼續跟著剛才 LayoutParams 建構函式裡的實現,我們進入

protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

到這裡,layout_width 屬性值最終賦給了width。其中widthAttr 的引數值是我們剛剛傳入的R.styleable.ViewGroup_Layout_layout_width。TypedArray根據名稱索引到對應的值。

四 總結

屬性解析的過程說的有些泛,只是瞭解瞭解析的流程,有些內容還需要後面繼續深入瞭解。View屬性的解析設計到兩個類:AttributeSetTypedArray,前者將View設定的所有屬性做了彙集和封裝,後者提供瞭解析屬性值的方法。

相關文章