前言
這個問題很早之前就碰到過,後來通過google找到了解決辦法,也就沒有去管它了,直到最近有朋友問到這個問題,感覺很熟悉卻又說不出具體原因,是以,就想通過源碼分析一下。順便做個總結,避免以後出現類似的問題。

封面.png
問題複現
為什麼發現了這個問題呢?是當時要寫一個清單,清單本來很簡單,一行顯示一個文本,實作起來也很容易,一個RecyclerView就搞定。
Activity以及Adapter代碼如下:
private void initView() {
mRecyclerView = (RecyclerView) findViewById(R.id.rv_inflate_test);
RVAdapter adapter = new RVAdapter();
adapter.setData(mockData());
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
private List mockData(){
List datas = new ArrayList<>();
for(int i=0;i<100;i++){
datas.add("這是第"+i+ "個item ");
}
return datas;
}
public static class RVAdapter extends RecyclerView.Adapter{
private List mData;
public void setData(List data) {
mData = data;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new InflateViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
InflateViewHolder viewHolder = (InflateViewHolder) holder;
((InflateViewHolder) holder).mTextView.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0:mData.size();
}
public static class InflateViewHolder extends RecyclerView.ViewHolder{
private TextView mTextView;
public InflateViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.text_item);
}
}
}
然後RecyclerView的item布局檔案如下:
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/text_item"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="#AA47BC"
android:gravity="center"
/>
代碼很簡單,就是一個RecyclerView 顯示一個簡單的清單,一行顯示一個文本。寫完代碼運作看一下效果:

運作效果一看,這是什麼鬼?右邊空出來這麼大一塊?一看就覺得是item的布局寫錯了,難道item的寬寫成wrap_content? 那就去改一下嘛。進入item布局一看:

不對啊,明明布局的寬寫的是match_parent,為什麼運作的結果就是包裹内容的呢?然後就想着既然LinearLayout作為根布局寬失效了,那就換其他幾種布局方式試一下呢?
根布局換為FrameLayout,其他不變:

運作效果如下:

效果和LinearLayout一樣,還是不行,那再換成RelativeLayout試一下:

看一下運作效果:

換成RelativeLayout後,運作的效果,好像就是我們想要的了,曾經一度以後隻要将跟布局換成RelativeLayout,就沒有寬高失效的問題了。為了驗證這個問題,我改變了高度再來測試,如下:
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_red_light"
>
android:id="@+id/text_item"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="#AA47BC"
android:gravity="center"
/>
将布局的寬和高固定一個确定的值200dp,然後再來看一下運作效果。

如上,并沒有什麼卵用,寬和高都失效了。然後又在固定寬高的情況下将布局換為原來的LinearLayout和FrameLayout,效果和前面一樣,包裹内容。
是以,不管用什麼布局作為根布局都會出現寬高失效的問題,那就得另找原因。到底是什麼原因呢?想到以前寫了這麼多的清單,也沒有出現寬高失效的問題啊?于是就去找以前的代碼來對比一下:
通過對比,發現寬高失效與不失效的差別在與Adapter中建立ViewHolder是加載布局的方式不同:
LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)
以上這種加載方式Item寬高失效。
LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
以上這種方式加載布局item不會出現寬高失效。,效果如下(寬和高都為200dp):

問題我們算是定位到了,就是加載布局的方式不一樣,那麼這兩種加載布局的寫法到底有什麼差別呢?這個我們就需要去深入了解inflate這個方法了
inflate 加載布局幾種寫法的差別
上面我們定位到了RecyclerView item 布局寬高失效的原因在于使用inflate 加載布局時的問題,那麼我們就看一下inflate這個方法:

從上圖可以看到 inflate 方法有四個重載方法,有兩個方法第一個參數接收的是一個布局檔案id,另外兩個接收的是XmlPullParse,看源碼就知道,接收布局檔案的inflate方法裡面調用的是接收XmlPullParse的方法。

是以,我們一般隻調用接收布局檔案ID的inflate方法。兩個重載方法的差別在于有無第三個參數attachToRoot, 而從源碼裡裡面可以看到,兩個參數的方法最終調用的是三個參數的inflate方法:

第三個參數的值是根據第二個參數的值來判斷的。
是以我們隻需要分析一下三個參數的inflate方法,看一下這個方法的定義:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
解釋:從指定的xml資源檔案加載一個新的View,如果發生錯誤會抛出InflateException異常。
參數解釋:
resource:加載的布局檔案資源id,如:R.layout.main_page。
root:如果attachToRoot(也就是第三個參數)為true, 那麼root就是為新加載的View指定的父View。否則,root隻是一個為傳回View層級的根布局提供LayoutParams值的簡單對象。
attachToRoot: 新加載的布局是否添加到root,如果為false,root參數僅僅用于為xml根布局建立正确的LayoutParams子類(列如:根布局為LinearLayout,則用LinearLayout.LayoutParam)。
了解了這幾個參數的意義後,我們來看一下前面提到的兩種寫法
第一種:root 為null
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)
這可能是我們用得比較多的一種方式,直接提供一個布局,傳回一個View,根據上面的幾個參數解釋就知道,這種方式,沒有指定新加載的View添加到哪個父容器,也沒有root提供LayoutParams布局資訊。這個時候,如果調用view.getLayoutParams() 傳回的值為null。通過上面的測試,我們知道這種方式會導緻RecyclerView Item 布局寬高失效。具體原因稍後再分析。
第二種:root不為null,attachToRoot為false
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
這種方式加載,root不為null,但是attachToRoot 為 false,是以,加載的View不會添加到root,但是會用root生成的LayoutParams資訊。這種方式就是上面我們說的 RecyclerView Item 寬高不會失效的加載方式。
那麼為什麼第一種加載方式RecyclerView Item 布局寬高會失效?而第二種加載方式寬高不會失效呢?我們接下來從原來來分析一下。
源碼分析寬高失效原因
1,首先我們來分析一下inflate 方法的源碼:
....
//前面省略
//result是最終傳回的View
View result = root;
try {
...
// 省略部分代碼
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(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 重點就在這個else代碼塊裡了
//解釋1:首先建立了xml布局檔案的根View,temp View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 解釋2:判斷root是否為null,不為null,就通過root生成LayoutParams
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);
// 解釋3:如果在root不為null, 并且attachToRoot為false,就為temp View(也就是通過inflate加載的根View)設定LayoutParams.
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");
}
//解釋4:加載根布局temp View 下面的子View
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
//解釋5: 注意這一步,root不為null ,并且attachToRoot 為true時,才将從xml加載的View添加到root.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 解釋6:最後,如果root為null,或者attachToRoot為false,那麼最終inflate傳回的值就是從xml加載的View(temp),否則,傳回的就是root(temp已添加到root)
if (root == null || !attachToRoot) {
result = temp;
}
}
}
...
//省略部分代碼
return result;
}
從上面這段代碼就能很清楚的說明前面提到的兩種加載方式的差別了。
第一種加載方式 root為 null :源碼中的代碼在 解釋1 和 解釋6 直接傳回的就是從xml加載的temp View。
第二種加載方式 root不為null ,attachToRoot 為false: 源碼中在 解釋3 和解釋5 ,為temp 設定了通過root生成的LayoutParams資訊,但是沒有add 添加到root 。
2,RecyclerView 部分源碼分析
分析了inflate的源碼,那麼接下來我們就要看一下RecyclerView 的源碼了,看一下是怎麼加載item 到 RecyclerView 的。由于RecyclerView的代碼比較多,我們就通過關鍵字來找,主要找holer.itemView ,加載的布局就是ViewHolder中的itemView.
通過源碼我們找到了一個方法tryGetViewHolderForPositionByDeadline,其中有一段代碼如下:
//1,重點就在這裡了,擷取itemView 的LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
// 2,如果itemView擷取到的LayoutParams為null,就生成預設的LayoutParams
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
其實重點就在這個方法裡面了,看一下我注釋的兩個地方,先擷取itemView的LayoutParams,如果擷取到的LayoutPrams為null 的話,那麼就生成預設的LayoutParams。我們看一下生成預設LayoutParams的方法generateDefaultLayoutParams:
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
if (mLayout == null) {
throw new IllegalStateException("RecyclerView has no LayoutManager");
}
return mLayout.generateDefaultLayoutParams();
}
注意,裡面又調用了mLayout的generateDefaultLayoutParams方法,這個mLayout其實就是RecyclerView 的布局管理器LayoutManager.


可以看到generateDefaultLayoutParams是一個抽象方法,具體的實作由對應的LayoutManager實作,我們用的是LinearLayoutManager,是以我們看一下LinearLayoutManager 的實作。
@Override
public LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
卧槽,看到這兒大概就明白了item布局的寬高為什麼會失效了,如果使用了預設生成LayoutParams這個方法,寬高都是WRAP_CONTENT。也就是說不管外面你的item根布局 寬高寫的多少最終都是包裹内容。
那麼前面說的兩種方式哪一種用了這個方法呢?其實按照前面的分析和前面的結果來看,我們推測第一種加載方式(root為null)使用了這個方法,而第二種加載方式(root不為null,attachToRoot為false)則沒有使用這個方法。是以我們斷點調試看一下:
第一種加載方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

通過斷點調試如上圖,從itemView 中擷取的layoutParams為null,是以會調用generateDefaultLayoutParams方法。是以會生成一個寬高都是wrap_content的LayoutParams,最後導緻不管外面的item根布局設定的寬高是多少都會失效。
第二種加載方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
斷點調試如下圖:

從上圖可以看出,這種加載方式從itemView是可以擷取LayoutParams的,為RecyclerView的LayoutParams,是以就不會生成預設的LayoutParams,布局設定的寬高也就不會失效。
總結
本文了解了infalte 加載布局的幾種寫法,也解釋了每個參數的意義。最後通過源碼解釋了兩種加載布局的方式在RecyclerView 中為什麼一種寬高會失效,而另一種則不會失效。是以在使用RecyclerView寫清單的時候,我們應該使用item布局不會失效的這種方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
可能有的同學會問,如果加載布局時第三個參數設定為true呢?結果會一樣嗎?你會發現,一運作就會崩潰

為什麼呢?因為相當于 addView 了兩次.RecyclerView中不應該這樣使用。
好了,以上就是全部内容,如有問題,歡迎指正。