源碼分析Android?rinflate的使用
這里接上一篇LayoutInflater源碼分析繼續(xù)分析。
rinflate源碼解析
這里詳細(xì)理一理rinflate方法,作用就是找到傳入的XmlPullParser當(dāng)前層級所有的view并add到parent上:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
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 {
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);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}很明顯rinflate是個遞歸方法,代碼很簡單,遞歸-判斷類型決定是否繼續(xù)遞歸-遞歸。
遞歸
我們知道,遞歸最重要的就是結(jié)束條件的選取,這里的結(jié)束條件有這么幾個:
- type != XmlPullParser.END_TAG
- parser.getDepth() > depth
- type != XmlPullParser.END_DOCUMENT
其實(shí)1和3都是常規(guī)的結(jié)束條件,最重要的是2這個條件,這個結(jié)束條件保證了當(dāng)前循環(huán)只讀取本層的view,我們結(jié)合一個例子來看一下。
下面是一個很簡單的XmlPullParser解析的例子:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_1"
android:layout_width="80dp"
android:layout_height="45dp" />
</LinearLayout>
<Button
android:id="@+id/btn_2"
android:layout_width="match_parent"
android:layout_height="60dp" />
</RelativeLayout>
解析代碼如下:
public void readMainXml() {
//1. 拿到資源文件
InputStream is = getResources().openRawResource(R.raw.activity_main);
//2. 拿到解析器對象
XmlPullParser parser = Xml.newPullParser();
final int depth = parser.getDepth();
try {
//3. 初始化xp對象
parser.setInput(is, "utf-8");
//4.開始解析
//獲取當(dāng)前節(jié)點(diǎn)的事件類型
int type = parser.getEventType();
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
switch (type) {
case XmlPullParser.START_TAG:
int attrCount = parser.getAttributeCount();
LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + " 標(biāo)簽開始");
for (int i = 0; i < attrCount; i++) {
String attrName = parser.getAttributeName(i);
String attrValue = parser.getAttributeValue(i);
//layout_width : match_parent
LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + "屬性: " + attrName + " : " + attrValue);
}
break;
case XmlPullParser.END_TAG:
LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + "標(biāo)簽結(jié)束");
break;
default:
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// D: depth:1 - RelativeLayout 標(biāo)簽開始
// D: depth:1 - RelativeLayout屬性: layout_width : match_parent
// D: depth:1 - RelativeLayout屬性: layout_height : match_parent
// D: depth:2 - LinearLayout 標(biāo)簽開始
// D: depth:2 - LinearLayout屬性: layout_width : wrap_content
// D: depth:2 - LinearLayout屬性: layout_height : wrap_content
// D: depth:3 - Button 標(biāo)簽開始
// D: depth:3 - Button屬性: id : @+id/btn_1
// D: depth:3 - Button屬性: layout_width : 80dp
// D: depth:3 - Button屬性: layout_height : 45dp
// D: depth:3 - Button標(biāo)簽結(jié)束
// D: depth:2 - LinearLayout標(biāo)簽結(jié)束
// D: depth:2 - Button 標(biāo)簽開始
// D: depth:2 - Button屬性: id : @+id/btn_2
// D: depth:2 - Button屬性: layout_width : match_parent
// D: depth:2 - Button屬性: layout_height : 60dp
// D: depth:2 - Button標(biāo)簽結(jié)束
// D: depth:1 - RelativeLayout標(biāo)簽結(jié)束
這里展示一個簡單的XmlPullParser的例子,可以看到RelativeLayout有兩個子View,分別是LinearLayout和Button2,depth都是2,結(jié)合上面的rinflate的代碼可以理解,在View的遞歸樹上,XmlPullParser的depth保證了層級,只會處理當(dāng)前層級的View。
類型判斷
方法體中做了類型的判斷,特殊判斷了幾種類型如下:
TAG_REQUEST_FOCUS
非容器控件標(biāo)簽中放標(biāo)簽,表示將當(dāng)前控件設(shè)為焦點(diǎn),可以放到標(biāo)簽里面,多個EditText的時候使用標(biāo)簽首先獲得焦點(diǎn)。
TAG_TAG
標(biāo)簽里面都可以放,類似于代碼中使用View.setTag:
private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
throws XmlPullParserException, IOException {
final Context context = view.getContext();
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
final CharSequence value = ta.getText(R.styleable.ViewTag_value);
view.setTag(key, value);
ta.recycle();
consumeChildElements(parser);
}根據(jù)id獲取value,并把id當(dāng)做key,設(shè)置parent的Tag??梢钥聪旅孢@個例子:
<EditText
android:id="@+id/et_1"
android:layout_width="match_parent"
android:layout_height="50dp">
<tag
android:id="@+id/tag1"
android:value="tag_value" />
</EditText>可以使用findViewById(R.id.et_1).getTag(R.id.tag1),得到tag_value值,注意不可以使用getTag(),有參數(shù)無參數(shù)獲取的不是同一個屬性。
TAG_MERGE
這里還對標(biāo)簽做了二次的判斷,保證標(biāo)簽不會出現(xiàn)在非root元素的位置。 如果不是上述特殊的標(biāo)簽,使用createViewFromTag加載出來view,并用當(dāng)前的attrs加載成LayoutParams設(shè)置給當(dāng)前View,繼續(xù)向下遞歸的同時把view add到parent.
TAG_INCLUDE
<include>標(biāo)簽可以實(shí)現(xiàn)在一個layout中引用另一個layout的布局,這通常適合于界面布局復(fù)雜、不同界面有共用布局的APP中,比如一個APP的頂部布局、側(cè)邊欄布局、底部Tab欄布局、ListView和GridView每一項(xiàng)的布局等,將這些同一個APP中有多個界面用到的布局抽取出來再通過<include>標(biāo)簽引用,既可以降低layout的復(fù)雜度,又可以做到布局重用(布局有改動時只需要修改一個地方就可以了)。
這些類型之外就類似于之前分析過的處理,先調(diào)用createViewFromTag方法創(chuàng)建View,設(shè)置attrs屬性,再調(diào)用遞歸方法rInflateChildren把view的子View add到view上,然后添加到parent上,直到層級遍歷結(jié)束。
下面重點(diǎn)看parseInclude的源碼分析:
parseInclude
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
//-------------------------------------第1部分-------------------------------------//
if (!(parent instanceof ViewGroup)) {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
// 如果有theme屬性,從當(dāng)前View的attrs里面查看是否有theme屬性,如果有,就重新創(chuàng)建ContextThemeWrapper,
// 用當(dāng)前View的theme替換之前ContextThemeWrapper里面的theme
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);//InflateActivityMergeTheme
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
// 查看當(dāng)前view的attrs里面是否有l(wèi)ayout的id,也就是'@layout/xxxx‘,如果沒有就返回0
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
//找不到先找這個layout屬性的值'@layout/xxxx‘,看layout屬性的string是否為空,如果是空就直接拋異常,不為空才去找layoutId
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// 如果取不到,就嘗試去"?attr/"下面找對應(yīng)的屬性。
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());
}
// The layout might be referencing a theme attribute.
if (mTempValue == null) {
mTempValue = new TypedValue();
}
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
layout = mTempValue.resourceId;
}
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
}
//-------------------------------------第2部分-------------------------------------//
final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
(ViewGroup) parent, /*attachToRoot=*/true);
if (precompiled == null) {
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.
}
final String childName = childParser.getName();
if (TAG_MERGE.equals(childName)) {
// 如果是merge標(biāo)簽,不支持屬性的設(shè)置,注意此處直接把parent作為父布局傳入,也就是加載出來的子View直接掛到parent上。
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
// 獲取include設(shè)置的id和visible。也就是說如果include設(shè)置了id和visible,會使用include設(shè)置的這兩個屬性
// 真正view設(shè)置的id和visible會不起作用
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// 先嘗試使用<include >標(biāo)簽的屬性去創(chuàng)建params,判斷的標(biāo)準(zhǔn)是有沒有width/height屬性
// 如果沒有則使用view的屬性去創(chuàng)建params,然后調(diào)用view.setLayoutParams給View設(shè)置屬性
// 換言之,如果<include>設(shè)置了width/height屬性,會整體覆蓋view的屬性,反之則不會。
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);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
// 如果<include>標(biāo)簽設(shè)置了id和visibility屬性則一定會替換里面的id和visibility屬性
// 換言之,<include>標(biāo)簽設(shè)置了id和visibility屬性,里面View的id和visibility會不起作用。
if (id != View.NO_ID) {
view.setId(id);
}
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
group.addView(view);
}
} finally {
childParser.close();
}
}
LayoutInflater.consumeChildElements(parser);
}兩個部分:
- 查找
<include />標(biāo)簽是否有l(wèi)ayout屬性,并應(yīng)用適合的theme屬性 - 判斷是否是
<merge>,不同的方式加載對應(yīng)的view,替換對應(yīng)的屬性
第一部分:查找layout屬性
<include />最重要的就是用來做layout的替換,所以必須設(shè)置一個layout屬性,沒有設(shè)置layout屬性的<include />是沒有意義的,有兩種方式去設(shè)置這個layout屬性: 一種是直接設(shè)置:
<include
layout="@layout/include_test_viewgroup"/>
這種也是我們最常用的方式,這種方式我們稱作①。
第二種方式是自定義一個reference,在attrs中定義,這樣也可以用來實(shí)現(xiàn)重用,比如:
//attrs.xml
<declare-styleable name="TestInclude">
<attr name="theme_layout" format="reference" />
</declare-styleable>
//style.xml
<style name="InflateActivityTheme" parent="AppTheme">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/red</item>
<item name="theme_layout">@layout/include_test_merge</item>
</style>
然后在layout中使用:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/InflateActivityTheme">
<include layout="?attr/theme_layout"/>
</RelativeLayout>
上面這種方式我們稱作②,或者下面這種我們稱作③
<include
layout="?attr/theme_layout"
android:theme="@style/InflateActivityTheme" />
按照這幾種的介紹我們來走一遍上面查找layout的代碼:
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();這是方式②和③的區(qū)別,方式②說明傳過來的context就有theme,方式③表示能從attrs中找到theme屬性,所以hasThemeOverride=true,如果需要覆蓋就用當(dāng)前view的theme重新創(chuàng)建了ContextThemeWrapper。這兩者有一即可。
// 查看當(dāng)前view的attrs里面是否有l(wèi)ayout的id,也就是'@layout/xxxx‘,如果沒有就返回0
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
//找不到先找這個layout屬性的值'@layout/xxxx‘,看layout屬性的string是否為空,如果是空就直接拋異常,不為空才去找layoutId
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// 如果取不到,就嘗試去"?attr/"下面找對應(yīng)的屬性。
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());
}關(guān)于方式①,代碼里其實(shí)寫清楚了,先找@layout/xxx這樣的,如果找不到就到?attr/下面找。
第二部分:加載對應(yīng)的View并替換
這段的代碼其實(shí)看上面代碼里的注釋就好了,很清晰。加載替換的layout有兩種情況:
1.merge標(biāo)簽,我們知道 merge標(biāo)簽用于降低View樹的層次來優(yōu)化Android的布局,所以merge標(biāo)簽并不是一層View結(jié)構(gòu),可以理解成一個占位,遇到merge標(biāo)簽就直接調(diào)用rinflate方法,找到所有的子view掛到parent上就好了,所以給設(shè)置什么屬性,其實(shí)沒什么作用。
2.非merge標(biāo)簽的其他ViewGroup,createViewFromTag加載進(jìn)來對應(yīng)的ViewGroup后
2.1. 嘗試使用<include />的屬性,如果標(biāo)簽沒有設(shè)置width/height這樣的基礎(chǔ)屬性就使用加載進(jìn)來的layout的屬性。
2.2. <include />標(biāo)簽總是起作用的屬性有兩個,一個是id,一個是visibility,如果<include />設(shè)置了這兩個屬性,總是會替換加載的layout的對應(yīng)屬性
設(shè)置完上面的屬性之后,繼續(xù)調(diào)用rInflateChildren去遞歸加載完所有的子view。
其實(shí)這個邏輯很像剛inflate剛開始執(zhí)行時候的邏輯,可以回憶一下之前的代碼。
這里有個小demo來看清這幾個的區(qū)別:
#styles.xml
<style name="InflateActivityTheme" parent="AppTheme">
<item name="colorPrimary">@color/red</item>
<item name="theme_layout">@layout/include_test_merge</item>
</style>
<style name="InflateActivityMergeTheme" parent="AppTheme">
<item name="colorPrimary">@color/green</item>
</style>
<style name="InflateActivityIncludeTheme" parent="AppTheme">
<item name="colorPrimary">@color/blue</item>
</style>總的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimary"
android:theme="@style/InflateActivityTheme">
<include
layout="?attr/theme_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:theme="@style/InflateActivityMergeTheme" />
<include
layout="@layout/include_test_viewgroup"
android:id="@+id/include_1"
android:theme="@style/InflateActivityIncludeTheme" />
<include
layout="@layout/include_test_viewgroup"
android:id="@+id/include_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:theme="@style/InflateActivityIncludeTheme" />
</RelativeLayout>
兩個子View的布局文件如下:
#include_test_merge.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:background="?attr/colorPrimary"
android:gravity="center"
android:text="include merge"
android:textColor="#fff"
android:textSize="12sp" />
</merge>
#include_test_viewgroup.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_playcontroller"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="?attr/colorPrimary"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="include LinearLayout"
android:textColor="#fff"
android:textSize="15sp" />
</LinearLayout>顯示效果圖如下:

大致覆蓋了上面說的幾種include的方式
- theme中設(shè)置layout,
<include />設(shè)置了theme可以不設(shè)置layout屬性 - include子view是
<merge />標(biāo)簽和非<merge />標(biāo)簽的區(qū)別 <include />標(biāo)簽設(shè)置了width/height和其他位置相關(guān)的屬性會使用外面設(shè)置的屬性覆蓋子View的屬性,include_1沒有設(shè)置屬性所以使用的是include_test_viewgroup的屬性,include_2設(shè)置了位置相關(guān)屬性所以使用了設(shè)置的屬性,從實(shí)際顯示效果能看得出來。- 關(guān)于theme的覆蓋,如果子View設(shè)置了theme,會使用子View設(shè)置的theme替換context(父布局RelativeLayout)的theme,根據(jù)
android:background="?attr/colorPrimary"可以看出來
總結(jié)
rinflate是遞歸方法,主要的遞歸判斷條件是XmlPullParser的depth
rinflate中判斷了多種類型,有requestFocus和tag這些特殊標(biāo)簽的處理,View的創(chuàng)建還是會調(diào)用createViewFromTag來處理
如果是include標(biāo)簽會使用parseInclude方法,因?yàn)?code><include />標(biāo)簽的特殊性,會有一些<include />和真實(shí)標(biāo)簽的屬性和theme的判斷和替換
<include />設(shè)置theme就替換掉父布局的theme,兩種方式設(shè)置layout屬性,標(biāo)簽中直接設(shè)置layout或者使用theme中的layout。
<include />標(biāo)簽中設(shè)置了位置屬性會替換子View的屬性,<include />設(shè)置了id和visibility一定會生效。
以上就是源碼分析Android rinflate的使用的詳細(xì)內(nèi)容,更多關(guān)于Android rinflate的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
RxJava之網(wǎng)絡(luò)請求最常見的三種場景
本文想闡述一下當(dāng)你開發(fā)Android應(yīng)用并采用RxJava作為你的架構(gòu),尤其是有關(guān)網(wǎng)絡(luò)請求時最常見的三種場景。這篇文章主要介紹了RxJava之網(wǎng)絡(luò)請求最常見的三種場景,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05
解決webview內(nèi)的iframe中的事件不可用的問題
這篇文章主要介紹了解決webview內(nèi)的iframe中的事件不可用的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android studio 切換flutterSDK之后報錯及解決辦法(推薦)
這篇文章主要介紹了Android studio 切換flutterSDK之后報錯及解決辦法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
Android studio虛擬機(jī)在啟動界面和桌面出現(xiàn)畫面模糊花屏問題的解決方法
這篇文章主要介紹了解決Android studio虛擬機(jī)在啟動界面和桌面出現(xiàn)畫面模糊花屏問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-03-03
PowerManagerService之手動滅屏流程示例分析
這篇文章主要為大家介紹了PowerManagerService之手動滅屏流程的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Android編程之手機(jī)壁紙WallPaper設(shè)置方法示例
這篇文章主要介紹了Android編程之手機(jī)壁紙WallPaper設(shè)置方法,結(jié)合實(shí)例形式分析了Android手機(jī)壁紙WallPaper的相關(guān)設(shè)置與使用技巧,需要的朋友可以參考下2017-08-08

