如何在 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


