如何在 NestedScrollView 內遷入 RecyclerView ?
上面的圖,是最終完成的畫面。
至於這個問題就是沒辦法直接隨便在 xml 檔案裡隨便寫寫就可以搞定,如下。主要就是 RecycleView 和 NestedScrollView 的滑動會產生衝突。並非 spec 所要求。
主要就是高度的判定會重疊到,底下的 RecycleView 被 AppBarLayout 蓋住,在 AppBarLayout 展開情況下 , RecycleView 不管怎麼滑動,AppBarLayout 都不為所動,只有當 AppBarLayout 擇疊起來,RecycleView 往下拉到底,才會判定 AppBarLayout 連動到。
因此這裡可以合理推斷 RecycleView 的高度判定是有問題的。從畫面上來看發現 RecycleView 的顯示範圍是在從 AppBarLayout 底部開始算,然後加上 RecycleView 的高度,而 RecycleView 的高度則是 match_parent ,這裡看起來是整個螢幕高度,那就會變成 RecycleView 的底部會超出手機螢幕,這也可以從畫面驗證,AppBarLayout 展開時, RecycleView 無論如何滑動都無法看到 item 9 , 10 ,但是當 AppBarLayout 擇疊時,RecycleView 的範圍判定就是正常的。
因此要修正的點就是重新 measure RecycleView 的範圍,進而修正 NestedScrollView 的行為。(有關 NestedScrollView 這篇寫得不錯,NestedScrollView 出現主要是想要解決何種問題,主要是獨立出 class to handle parent and child touch event ,常搭配 CoordinatorLayout 的 Behavior 完成酷炫功能)
有關 RecycleView 的 wrap_content ,在 measure child 時,會出現 size = 0 的情況。這就照成後續的問題(大神原始問題,已經回報給 google),因此這裡提出重寫 onMeasure 的方法,主要改成用 View.MeasureSpec.UNSPECIFIED 去讓 child 自己決定高度,最後加總得到 RecycleView 的高度。
ps. 上述問題會發生在 android support library 23.2 之前。如果是使用 23.2 (包含)之後的 RecycleView 之後,就可以用另外一套修正方法(跳過下面這段程式碼)
ref:
http://stackoverflow.com/questions/31000081/how-to-use-recyclerview-inside-nestedscrollview
https://github.com/emanuelet/LayoutManagers/blob/master/ExpansiveLayoutManager.java
http://blog.csdn.net/yaochangliang159/article/details/50540276
public class ExpandBarLinearLayoutManager extends LinearLayoutManager { public ExpandBarLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } private int[] mMeasuredDimension = new int[2]; @Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { final int widthMode = View.MeasureSpec.getMode(widthSpec); final int heightMode = View.MeasureSpec.getMode(heightSpec); final int widthSize = View.MeasureSpec.getSize(widthSpec); final int heightSize = View.MeasureSpec.getSize(heightSpec); int width = 0; int height = 0; for (int i = 0; i < getItemCount(); i++) { if (getOrientation() == HORIZONTAL) { measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), heightSpec, mMeasuredDimension); width = width + mMeasuredDimension[0]; if (i == 0) { height = mMeasuredDimension[1]; } } else { measureScrapChild(recycler, i, widthSpec, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension); height = height + mMeasuredDimension[1]; if (i == 0) { width = mMeasuredDimension[0]; } } } switch (widthMode) { case View.MeasureSpec.EXACTLY: width = widthSize; case View.MeasureSpec.AT_MOST: case View.MeasureSpec.UNSPECIFIED: } switch (heightMode) { case View.MeasureSpec.EXACTLY: height = heightSize; case View.MeasureSpec.AT_MOST: case View.MeasureSpec.UNSPECIFIED: } setMeasuredDimension(width, height); } private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] measuredDimension) { View view = recycler.getViewForPosition(position); recycler.bindViewToPosition(view, position); if (view != null) { RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams(); int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, getPaddingLeft() + getPaddingRight(), p.width); int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(), p.height); view.measure(childWidthSpec, childHeightSpec); measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin; measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin; recycler.recycleView(view); } } }
然後一定要幫 RecycleView 設定 setNestedScrollingEnabled(false)
recycleView.setLayoutManager(new ExpandBarLinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); recycleView.setNestedScrollingEnabled(false);
然後如果直接使用 23.2 之後的 RecycleView 的人, google 已經修正了,只需要
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); // 其實默認 auto measure is true. linearLayoutManager.setAutoMeasureEnabled(true); recycleView.setLayoutManager(linearLayoutManager); // 這行不能少 recycleView.setNestedScrollingEnabled(false);
這裡我看原始碼,這一段就是修正 code ,還蠻直覺,並非直接強制使用 View.MeasureSpec.UNSPECIFIED(這問題還蠻明顯的,這會使得 child view measure 風險變很大,因為無法控制 child view 是適合哪一種),而是採用兩階段 layout ,藉此得到正確的 child size.
if (mLayout.mAutoMeasure) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); if (skipMeasure || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); // if RecyclerView has non-exact width and height and if there is at least one child // which also has non-exact width & height, we have to re-measure. if (mLayout.shouldMeasureTwice()) { mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } }
有興趣的人,可以繼續 trace code ,但我就到此一鞠躬啦。
other reference:
http://www.jianshu.com/p/4535442d568f
http://www.jianshu.com/p/06c0ae8d9a96
http://blog.csdn.net/feiduclear_up/article/details/46514791
http://stackoverflow.com/questions/25702884/add-viewpagerindicator-to-android-studio