原始碼基於安卓8.0分析結果
- 在這篇文章Android原始碼分析(Activity.setContentView原始碼解析),分析得出,底層走的就是
LayoutInflater.from(this),inflate(),
如果inflate()
傳入的view的話,就呼叫兩次LayoutInflater.from(this),inflate(),
,如果是inflate()
傳入的resId(佈局的id)的話,就呼叫三次LayoutInflater.from(this),inflate(),
,具體請看上篇文章。 - 怎麼使用,一共有三種的方式,主要看下這種方式
LayoutInflater inflater1 = getLayoutInflater();
,其實呼叫的就是Activity的getLayoutInflater()方法。
LayoutInflater inflater1 = getLayoutInflater();//呼叫Activity的getLayoutInflater()
LayoutInflater inflater2 = LayoutInflater.from(this);
LayoutInflater inflater3 = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
複製程式碼
- Activity的getLayoutInflater()方法。
/**
* Convenience for calling
* {@link android.view.Window#getLayoutInflater}.
*/
@NonNull
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
複製程式碼
- 通過Android原始碼分析(事件傳遞)這邊文章,我們知道,
Window類
只有唯一的子類PhoneWindow
。所以主要的是去看PhoneWindow
類
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
複製程式碼
LayoutInflater inflater2 = LayoutInflater.from(this);
的實現的方式
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
複製程式碼
- 以上三種方法,最終還是呼叫了
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
獲取LayoutInflater例項物件。 - inflate的方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
複製程式碼
- 到這裡來
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
,個人的對這個方法的理解是:把佈局檔案填充成View,如果傳入的root
不為null
,並且attachToRoot
為true
,就把view填充到root上,相反就不新增上來!
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
//這裡通過底層的方法,得到一個XmlResourceParser物件
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製程式碼
inflate(parser, root, attachToRoot)
,特別關心這個方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//底層的方法,不知道原理
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
// TODO: 2018/6/5 我沒有搞明白 為啥整個值在這裡做什麼的, 在這個方法裡面都沒有使用
// from傳入的Context
final Context inflaterContext = mContext;
// 判斷parser是否是AttributeSet,如果不是則用XmlPullAttributes去包裝一下。
final AttributeSet attrs = Xml.asAttributeSet(parser);
// 儲存之前的Context
Context lastContext = (Context) mConstructorArgs[0];
// 賦值為傳入的Context
mConstructorArgs[0] = inflaterContext;
// 預設返回的是傳入的Parent
View result = root;
try {
// Look for the root node.
int type;// 迭代xml中的所有元素,挨個解析
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// //如果沒找到有效的開始標籤則丟擲InflateException
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//// 如果根節點是“merge”標籤
if (TAG_MERGE.equals(name)) {
// 根節點為空或者不新增到根節點上,則丟擲異常。
// 因為“merge”標籤必須是要被新增到父節點上的,不能獨立存在。
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 遞迴例項化root(也就是傳入Parent)下所有的View
// 如果xml中的節點是merge節點,則呼叫rInflate()--方法 // 遞迴例項化根節點的子View
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//通過View的父View,View的名稱、attrs屬性例項化View(內部呼叫onCreateView()和createView())。
// TODO: 2018/6/6 // 例項化根節點的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 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);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
// 遞迴例項化跟節點的子View
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.\
if (root != null && attachToRoot) {
root.addView(temp, params); // TODO: 2018/6/6 返回父View
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
// TODO: 2018/6/6 父View是空或者不把填充的View新增到父View)
if (root == null || !attachToRoot) {
result = temp; // TODO: 2018/6/6 返回根節點View
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
//不要在上下文中保留靜態引用。
// 把這之前儲存的Context從新放回全域性變數中。
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
複製程式碼
- 注意其中有個判斷
TAG_MERGE.equals(name))
,這裡就是merge標籤的開始,以下的問題,都帶著這個疑問往下面看。
if (TAG_MERGE.equals(name)) {
// 根節點為空或者不新增到根節點上,則丟擲異常。
// 因為“merge”標籤必須是要被新增到父節點上的,不能獨立存在。
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 遞迴例項化root(也就是傳入Parent)下所有的View
// 如果xml中的節點是merge節點,則呼叫rInflate()--方法 // 遞迴例項化根節點的子View
rInflate(parser, root, inflaterContext, attrs, false);
}
複製程式碼
-
解析到
merge
標籤的話,就會進入到這個方法rInflate(parser, root, inflaterContext, attrs, false);
:遞迴例項化根節點的子View,在這裡說明一點的是這個方法傳入的root
會不會為null
.這篇文章說過Android原始碼分析(Activity.setContentView原始碼解析),肯定不會為null,一般是我們自己去使用的時候,有可能為null,具體的差別可以參考這篇文章Android LayoutInflater深度解析 給你帶來全新的認識 -
關於
rInflate(parser, root, inflaterContext, attrs, false);
方法:遞迴方法用於降序XML層次結構並例項化檢視,例項化它們的子節點,然後呼叫onFinishInflate(),
, 完成填充
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;
// 迭代xml中的所有元素,挨個解析
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();
//1、解析請求焦點 ,這個控制元件一直有焦點 requestFocus
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)) {// 如果xml中的節點是include節點,則呼叫parseInclude方法
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
//解析include標籤
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// TODO: 2018/5/23 merge 作為佈局裡面的元素 會報錯的哦 ,注意哦
//想象下 兩個merge 標籤重合在一起
throw new InflateException("<merge /> must be the root element");
} else {
// TODO: 2018/5/23 其實就是如果是merge標籤,
// TODO: 2018/5/23 那麼直接將其中的子元素新增到merge標籤parent中,這樣就保證了不會引入額外的層級。
// 1、我們的例子會進入這裡 // 通過View的名稱例項化View
final View view = createViewFromTag(parent, name, context, attrs);
// 2、獲取merge標籤的parent
final ViewGroup viewGroup = (ViewGroup) parent;
// 3、獲取佈局引數
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 4、遞迴解析每個子元素
rInflateChildren(parser, view, attrs, true);
// 5、將子元素直接新增到merge標籤的parent view中
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
/**
* onFinishInflate() View 中的一個空實現,標記完全填充完成了
* The method onFinishInflate() will be called after all children have been added.
* 以後是所有的孩子呼叫完成了,之後就呼叫這個方法
*/
if (finishInflate) {
parent.onFinishInflate();
}
}
複製程式碼
- 通過對上面的方法分析,還打了斷點,打斷點注意一點,用模擬器,基本上手機廠商都改了
frameWork
層的程式碼,打出來是亂的。程式碼會走到這裡來,看程式碼的分析,就把新的View,通過ViewGroup.addView()
進去了,這也是merge便籤的原理:merge標籤可以自動消除當一個佈局插入到另一個佈局時產生的多餘的View Group,也可用於替換FrameLayout
// TODO: 2018/5/23 其實就是如果是merge標籤,
// TODO: 2018/5/23 那麼直接將其中的子元素新增到merge標籤parent中,這樣就保證了不會引入額外的層級。
// 1、我們的例子會進入這裡 // 通過View的名稱例項化View
final View view = createViewFromTag(parent, name, context, attrs);
// 2、獲取merge標籤的parent
final ViewGroup viewGroup = (ViewGroup) parent;
// 3、獲取佈局引數
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 4、遞迴解析每個子元素
rInflateChildren(parser, view, attrs, true);
// 5、將子元素直接新增到merge標籤的parent view中
viewGroup.addView(view, params);
複製程式碼
merge
的原理我們清楚了,分析另外一條線,沒有merge
標籤,就會走到else的方法來。
// 如果根節點是“merge”標籤
if (TAG_MERGE.equals(name)) {
} else {
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
// 遞迴例項化跟節點的子View start inflating children
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
if (root != null && attachToRoot) {
root.addView(temp, params); // TODO: 2018/6/6 返回父View
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
// TODO: 2018/6/6 父View是空或者不把填充的View新增到父View)
if (root == null || !attachToRoot) {
result = temp; // TODO: 2018/6/6 返回根節點View
}
複製程式碼
- 1、這個方法
createViewFromTag(View parent, String name, Context context, AttributeSet attrs)
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
複製程式碼
- 最終呼叫的是這個方法
createViewFromTag(parent, name, context, attrs, false);
這個方法 - ,什麼時候會走到下面來,
name.equals("view")
成立的情況,我記得以前我為了在佈局中加了一根線就是這樣寫的,寫快了,寫成小寫的了view
,應該寫成大寫View
.
<view
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
複製程式碼
小寫的view:我自己打的斷點發現,如果根標籤是view的,注意是小寫的,view,這個name會為null ,接著下面就會出現空指標的異常---> 我很聰明 很牛逼
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
//根據我的斷點的話,只要進來的話,這個那麼就會為null
name = attrs.getAttributeValue(null, "class");
}
複製程式碼
// Apply a theme wrapper, if allowed and one is specified.
//如果允許的話,應用一個主題包裝器,並指定一個。
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
複製程式碼
name ==null,就會丟擲ava.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
,也就是這個原因,所以寫程式碼還是要仔細,仔細!!
在這裡有個很有意思的佈局,開發中很少使用到,而且也不清楚,作者為啥寫,姑且這樣理解Let's party like it's 1995! 寫這個程式碼的哥們,在1995年經歷了一件很開心的事情
//如果有這個標籤的話,就出現這個佈局,閃動的佈局
if (name.equals(TAG_1995)) {
// Let's party like it's 1995! 寫這個程式碼的哥們,在1995年經歷了一件很開心的事情
return new BlinkLayout(context, attrs);
}
複製程式碼
BlinkLayout
使用的方式,blink 翻譯過來會一閃一閃的
<blink
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:text="blink單詞也是這個意思.閃爍頻率500毫秒" />
</blink>
複製程式碼
BlinkLayout
程式碼如下, BLINK_DELAY
這個是閃動的時間間隔,如果開發中需要,改動這個時間就行了,感覺這個佈局也沒啥用!
public class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;
private boolean mBlink;
private boolean mBlinkState;
private final Handler mHandler;
public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO: 2018/5/22 對handler 原始碼比較熟悉的話,這裡就是Handler另外的一種使用的方法
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
mBlinkState = !mBlinkState;
makeBlink();
}
//invalidate()是用來重新整理View的,必須是在UI執行緒中進行工作。
// 比如在修改某個view的顯示時,呼叫invalidate()才能看到重新繪製的介面
invalidate();
return true;
}
return false;
}
});
}
private void makeBlink() {
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mBlink = true;
mBlinkState = true;
makeBlink();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mBlink = false;
mBlinkState = true;
mHandler.removeMessages(MESSAGE_BLINK);
}
@Override
protected void dispatchDraw(Canvas canvas) {
//根據這個狀態是否去dispatchDraw
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
複製程式碼
下面是後續的程式碼,通過工廠建立了view。去定位過,在public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2
,很多的地方,如果需要研究的,還需要更深層的原始碼理解,這裡就先mark下,只需要知道view是通過工廠來得到的
try {
View view;
if (mFactory2 != null) {
//各個工廠先onCreateView()
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
複製程式碼
- 2、關於這個方法
rInflateChildren(parser, temp, attrs, true);
:遞迴例項化跟節點的子View.其實呼叫的還是這個方法rInflate(parser, root, inflaterContext, attrs, false);
,文章的前部分貼出了這部分的程式碼
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
複製程式碼
主要是為了分析幾個Tag
private static final String TAG_MERGE = "merge";
private static final String TAG_INCLUDE = "include";
private static final String TAG_1995 = "blink";
private static final String TAG_REQUEST_FOCUS = "requestFocus";
private static final String TAG_TAG = "tag";
private static final String ATTR_LAYOUT = "layout";
複製程式碼
requestFocus:解析請求焦點 ,這個控制元件一直有焦點 requestFocus
,一般作用於 EditText
,需要長期獲取焦點的控制元件,但是真正的實現的方法,我們都是程式碼去實現了,很少這樣使用,這裡就說明一下,有這樣的使用的方法!
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
}
複製程式碼
<EditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<!-- 當前控制元件處於焦點狀態 -->
<requestFocus />
</EditText>
複製程式碼
tag:在包含的檢視上設定鍵標記,其實就是給view設定上了一個tag標籤
if (TAG_TAG.equals(name)) {
//在包含的檢視上設定鍵標記。
parseViewTag(parser, parent, attrs);
}
final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
final CharSequence value = ta.getText(R.styleable.ViewTag_value);
view.setTag(key, value);
複製程式碼
merge:在這裡如果解析到了merge
標籤的話,這裡就直接丟擲異常了,必須是根元素!兩種情況下,一種是套用了兩個merge標籤,或者是一個ViewGruop標籤巢狀了一個merge,都會丟擲異常
if (TAG_MERGE.equals(name)) {
// TODO: 2018/5/23 merge 作為佈局裡面的元素 會報錯的哦 ,注意哦
//想象下 兩個merge 標籤重合在一起
throw new InflateException("<merge /> must be the root element");
}
複製程式碼
include:標籤,在這裡如果include
作為了一個根元素,也會丟擲異常,當然,如果在程式碼中見到別人那麼用的話,建議自己去面壁思過會!
if (TAG_INCLUDE.equals(name)) {// 如果xml中的節點是include節點,則呼叫parseInclude方法
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
//解析include標籤
parseInclude(parser, context, parent, attrs);
}
複製程式碼
parseInclude(parser, context, parent, attrs);
,這個方法也就是include
必須走的方法,搞明白的話,就可以搞明白原理,而且在特殊的情況下,include
標籤會和 merge
標籤一起使用。這個方法也可以解釋include
標籤的原理!
- 可以從這裡可以看到,
include
只能在ViewGroup
中使用,在 'View'中使用的話,會報錯
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
if (parent instanceof ViewGroup) {
// Apply a theme wrapper, if requested. This is sort of a weird
// edge case, since developers think the <include> overwrites
// values in the AttributeSet of the included View. So, if the
// included View has a theme attribute, we'll need to ignore it.
//如果VIEW檢視有一個主題屬性,我們需要忽略它。
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();
//如果佈局指向主題屬性,我們必須
//按摩該值以從中獲取資源識別符號。
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
// TODO: 2018/6/6 include標籤中沒有設定layout屬性,會丟擲異常
if (value == null || value.length() <= 0) {//
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// Attempt to resolve the "?attr/name" string to an attribute
// within the default (e.g. application) package.
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.");
} else {
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {// 獲取屬性集,即在include標籤中設定的屬性
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(childParser.getPositionDescription() +
": No start tag found!");
}
// 1、解析include中的第一個元素
final String childName = childParser.getName();
// 如果第一個元素是merge標籤,那麼呼叫rInflate函式解析
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
//2、我們例子中的情況會走到這一步,首先根據include的屬性集建立被include進來的xml佈局的根view
// 這裡的根view對應為my_title_layout.xml中的RelativeLayout
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
//這裡就是設定的<include>佈局設定的id ,如果include設定了這個id ,那麼這個id就不等於 View.No_ID=-1
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
// 獲3、取佈局屬性
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
// 被inlcude進來的根view設定佈局引數
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// 4、Inflate all children. 解析所有子控制元件
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
// 5、將include中設定的id設定給根view,因此實際上my_title_layout.xml中
// 的RelativeLayout的id會變成include標籤中的id,include不設定id,那麼也可以通過relative的找到.
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;
}
// 6、將根view新增到父控制元件中
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
//預設的可見性使得LayoutInflater_Delegate代表可以呼叫它。
LayoutInflater.consumeChildElements(parser);
複製程式碼
- 分段分析上面的那段程式碼, 1、如果VIEW檢視有一個主題屬性,我們需要忽略它。
//如果VIEW檢視有一個主題屬性,我們需要忽略它。
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();
複製程式碼
2、如果佈局指向主題屬性,我們必須從該值以從中獲取資源識別符號!
//如果佈局指向主題屬性,我們必須
//按摩該值以從中獲取資源識別符號。
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
複製程式碼
3、在這裡出現value == null || value.length() <= 0
的話,就會丟擲異常,也就是說include
標籤中沒有設定layout
屬性,會丟擲異常。
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
// TODO: 2018/6/6 include標籤中沒有設定layout屬性,會丟擲異常
if (value == null || value.length() <= 0) {//
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// Attempt to resolve the "?attr/name" string to an attribute
// within the default (e.g. application) package.
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());
}
複製程式碼
3、解析include
中的第一個元素,如果這裡第一個childeName
是merge
,那麼就是 include
和merge
一起使用的情況,呼叫的方法是rInflate(childParser, parent, context, childAttrs, false);
和上面一樣,不會生成多餘的層級
// 1、解析include中的第一個元素
final String childName = childParser.getName();
// 如果第一個元素是merge標籤,那麼呼叫rInflate函式解析
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
}
複製程式碼
4、首先根據include的屬性集建立被include進來的xml佈局的根view,這個view
其實就include
進來的根ViewGroup
,因為在前面已經把merge
的情況過濾掉了,
else {
//2、我們例子中的情況會走到這一步,首先根據include的屬性集建立被include進來的xml佈局的根view
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
複製程式碼
5、特別關心這個final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
函式,其實就是給include
標籤設是否設定了id
,就好像下面的佈局一樣,如果設定了這個android:id="@+id/my_title_ly"
<include
android:id="@+id/my_title_ly"
android:layout_width="match_parent"
android:layout_height="200dp"
layout="@layout/my_include_layout" />
複製程式碼
這個是上面得include
的佈局my_include_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/my_title_parent_id"
android:layout_height="wrap_content" >
....
</RelativeLayout>
複製程式碼
下面的這段程式碼就會執行,這也就解釋了:安卓include
標籤的找不到id的問題,因為你在include
設定了這個id
屬性,那麼你找這個my_title_parent_id
這個id,findViewById(R.id.my_title_parent_id)
肯定也會找出來為null!
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
if (id != View.NO_ID) {
// 5、將include中設定的id設定給根view,因此實際上my_title_layout.xml中
// 的RelativeLayout的id會變成include標籤中的id,include不設定id,那麼也可以通過relative的找到.
view.setId(id);
}
}
複製程式碼
為什麼findViewById(R.id.my_title_parent_id)
會找出來為null,簡單敘述下就是在Activity
中呼叫方法其實是Window
的findViewById
,getDecorView().findViewById(id)
,也就是PhoneWindow
裡面的DecorView
的findViewById
,也就是View
的findViewById
;可以看這邊文章Android原始碼分析(事件傳遞)涉及到這幾個類了。如下面的方法,這個View 有id,找id的話就會走findViewTraversal
這個方法,找的id和setId
不一樣的話,就會返回null,這就是找不到 id
的原因。這個原因和ViewStub
標籤中設定了inflatedId
一樣的效果,下篇文章會講到,非常有意思!已經寫好Android原始碼分析(ViewStub原始碼解析)
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
return null;
}
複製程式碼
View中設定id
的方法
public void setId(@IdRes int id) {
mID = id;
if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
mID = generateViewId();
}
}
複製程式碼
6、關於final int visibility = a.getInt(R.styleable.Include_visibility, -1);
這行程式碼,也就是說我們設定了include
標籤'visibility'其實也就是設定layout
的visibility
的屬性!
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
複製程式碼
7、將根view新增到父控制元件中,最後都會走到這裡來,只不過使用了merge
是不會產生多餘的層級的,但是使用的時候有場景的要求!
group.addView(view);
複製程式碼
-
最後做了一張圖,謝謝
-
說明幾點
- 佈局轉化為View,關鍵方法就是
LayoutInflater.from(this),inflate()
PhoneWindow
這個類很關鍵- 如果需要打斷點,建議使用模擬器,手機的廠商基本上改動過
framework
的程式碼,打出來不準確 merge
原理就是通過addView(view)
,從而減少了層級BlinkLayout
這個是LayoutInflater
的內部類,感覺是谷歌工程師皮了一下
- 佈局轉化為View,關鍵方法就是
-
include
只能在ViewGroup中使用,在 'View'結點中使用的話,會報錯