RecyclerView Item 布局寬高無效問題探究
前言
這個問題很早之前就碰到過,后來通過google找到了解決辦法,也就沒有去管它了,直到最近有朋友問到這個問題,感覺很熟悉卻又說不出具體原因,因此,就想通過源碼分析一下。順便做個總結,避免以后出現類似的問題。
問題復現
為什么發現了這個問題呢?是當時要寫一個列表,列表本來很簡單,一行顯示一個文本,實現起來也很容易,一個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方法,看一下這個方法的定義:
/**
? ? * Inflate a new view hierarchy from the specified xml resource. Throws
? ? * {@link InflateException} if there is an error.
? ? *
* @param resource ID for an XML layout resource to load (e.g.,
*? ? ? ? R.layout.main_page
)
* @param root Optional view to be the parent of the generated hierarchy (if
*? ? ? ? attachToRoot is true), or else simply an object that
*? ? ? ? provides a set of LayoutParams values for root of the returned
*? ? ? ? hierarchy (if attachToRoot is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
*? ? ? ? the root parameter? If false, root is only used to create the
*? ? ? ? correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
*? ? ? ? attachToRoot is true, this is root; otherwise it is the root of
*? ? ? ? the inflated XML file.
*/
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 的實現。
? /**
? ? * {@inheritDoc}
? ? */
? ? @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中不應該這樣使用。
? ? ? ?
? ? ?
? ? ? ? ? ?
? ? ? ? ? ?
? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ?
? ? ? ? ?
? ? ? ?
? ? ? ?
? ?