LayoutInflater
LayoutInflatre
能將一個xml
文件解析成對應的View
并構建對應的View
的關系結構靴寂。使用這個類在需要的時候才解析一個布局文件匾荆,來避免一開始就加載xml布局文件造成資源浪費然走。
1. 使用方法
LayoutInflater inflater = LayoutInflater.from(this);
View inflatedLayout = inflater.inflate(R.layout.inflate_layout, mParent, false);
首先通過靜態(tài)方法傳入一個上下文對象獲取一個LayoutInflater
實例,然后調用inflate
方法來解析這個布局文件阱佛。這個方法傳入三個參數(shù):
-
resource
:要解析的資源文件 -
root
:解析出來的View
的要添加到哪個容器中,相當于這個資源文件的上一層所表示的View
-
attachToRoot
:是否將解析出來的View
作為子View
添加到上一個參數(shù)傳入的View
容器中
2. 源碼分析
2.1
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
先判斷是否已經(jīng)解析過該文件余耽,如果已經(jīng)解析過直接返回解析出來的View
,否則就將資源文件準換成一個XmlResourceParser
對象辩蛋,然后調用inflate
方法呻畸;
2.2
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
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");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
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.
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);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
這個方法用于對不同的標簽類型來執(zhí)行不同的解析策略,最終把返回一個解析出來的View
.在解析過程中主要由以下幾個步驟:
把解析出來的
View
添加到父View
中,并設置LayoutParam解析完成后調用
onFinishInflate
方法悼院,通知已經(jīng)解析完成返回最終解析出來的最頂層的父
View
,如果inflate
參數(shù)傳入的root
不為空且attachToRoot
為true
伤为,返回的View
就是傳入的root
,否則就是解析出來的頂層的view
-
如果
inflate
方法傳入的root
不為空attachToRoot
為true
或不傳入,就會把解析出來的View
作為root
下的一個子View
.總結
從上面的流程中看出在
inflate
階段就會創(chuàng)建布局文件中的所有View
實例,并按照層級關系組織好View
之間的布局關系据途。最后返回一個這個布局文件的根View
,通過這個View
就可以遍歷所有的View
绞愚。3. findViewById
從上面的分析中可以看出在
inflate
階段就會實例化布局文件中的所有View
, 那通過findViewById
是怎么拿到對應的View
的。public final <T extends View> T findViewById(@IdRes int id) { if (id == NO_ID) { return null; } return findViewTraversal(id); }
這個方法是在
View.java
這個類中定義的final方法颖医,這個方法會調用另外一個findViewTraversal
方法protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } return null; }
這個方法是一個可以被重寫的方法位衩,在
ViewGourp
中重寫了這個方法,在調用的時候如果當前的View
不是一個ViewGroup
熔萧,就會執(zhí)行上面的邏輯糖驴,判斷傳入的id是不是等于自己的id僚祷,如果是就返回本View
否則就返回null
;如果是一個
ViewGroup
就執(zhí)行下面的邏輯/** * {@hide} */ @Override protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return (T) v; } } } return null; }
在
ViewGroup
中檢查Id是否和自己相等贮缕,如果不等會遍歷自己的所有子View/ViewGroup
辙谜,然后依次調用他們的findViewById
方法,如果子View是一個View
就會判斷id是否相等感昼,如果相等就返回這個View
,如果是ViewGroup
就遞歸的調用這個方法來找子View
中是否存在相同id的View
装哆,如果遍歷完后也沒有就說明這個根節(jié)點的View
下是沒有這個id的View
。從這里可以看出通過findViewById
這個方法查找某個View定嗓,這個View必須要是自己的子View
,否則就會找不到烂琴,并不是在任意的View
上調用findViewById
方法來查找任意的View
都能找到。3. 與inflate相關的幾個問題
3.1
ResyclerView蜕乡、ListView
中的子項設置寬高屬性失效在使用
inflate
來加載ItemView子項的布局文件時,如果在inlfate參數(shù)中參數(shù)的root
為null
奸绷,那布局文件中設置的寬高可能就會失效。因為當傳入的root
參數(shù)為null時层玲,就不會給這個布局文件的中的rootView
設置layoutParams
号醉。代碼:
// Temp is the root view that was found in the xml 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); } }
看到這有當
rootView
不為null
,并且attachToRoot
為false
時才會設置布局文件的layoutParam
辛块。否則layoutParam
就位null
畔派。inflate
完成后當RecyclerView
創(chuàng)建ViewHolder
時發(fā)現(xiàn)如果當前View
的LayoutParam
為空時就是設置一個默認的LayoutParam
。代碼:
//從ViewAdapter中創(chuàng)建一個ViewHolder holder = mAdapter.createViewHolder(RecyclerView.this, type); //設置LayoutParam final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams; if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; }
獲取創(chuàng)建的
ViewHolder
的LayoutParam
润绵,如果為空就生成一個默認的LayoutManager
线椰,實現(xiàn)的生成的方法在LayoutManager
中定義,在LinearLayoutManager
的實現(xiàn)為@Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { if (mOrientation == HORIZONTAL) { return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); } else { return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }
可以看到
LineaLayoutManager
中會生成wrap_content
的參數(shù)值尘盼,也就是所有的ViewHolder
的布局寬高都是wrap_content
的憨愉。3.2
RecyclerView inflate
第三個參數(shù)傳true
報錯報錯的信息:
throw new IllegalStateException("ViewHolder views must not be attached when" + " created. Ensure that you are not passing 'true' to the attachToRoot" + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
從
inflate
源碼中可以看出,但傳入的不為空且attachToRoot
為true
時卿捎,inflate
完成返回的為傳入的 Root 配紫,holder = mAdapter.createViewHolder(RecyclerView.this, type)
在創(chuàng)建一個ViewHodler
時傳入的root就是當前的ResyclerView
對象,所以午阵,如果inflate時傳入true
躺孝,那么返回的還是RecyclerView
對象,這樣在LayoutManager在調用下面的方法獲取一個View時拿到還是當前的RecyclerView
底桂。----疑問:拿到RecyclerView也是可以獲取到里面的ViewHolder的植袍,為什么不可以呢。是不是無法確定哪個ViewHolder是新增的
@NonNull public View getViewForPosition(int position) { return getViewForPosition(position, false); }