最近公司項目中用到了ScrollView去嵌套recycleView, 最開始我天真的把recycleView直接放入scrollView中,結果可想而知,什么都不顯示,瞬間懵逼,我心想應該是和嵌套ListView差不多吧,看來需要重寫recycleView中onMeasure()方法,
像這樣:
@Override protected void onMeasure(int widthSpec, int heightSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST); super.onMeasure(widthSpec, expandSpec); }
結果:運行還是什么都沒有。
沒辦法上網搜索解決方案,一搜看來網友很多跟我有一樣的需求,網上也有大神提供的解決方案
例如這樣:
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants"> <android.support.v7.widget.RecyclerView android:id="@+id/menuRv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/margin_16" android:layout_marginRight="@dimen/margin_16"/> </RelativeLayout>
我的結果: 運行還是原來一樣沒有任何反應.
注意: 你們可以試試,說是解決在Android6.0顯示不全的問題,如果成功了那祝賀你們,如果沒有成功的話就跟着我往下看。
例如這樣: 重寫LinearLayoutManager,來計算每個item進行顯示
public class FullyLinearLayoutManager extends LinearLayoutManager { private static final String TAG = FullyLinearLayoutManager.class.getSimpleName(); public FullyLinearLayoutManager(Context context) { super(context); } public FullyLinearLayoutManager(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); Log.i(TAG, "onMeasure called. \nwidthMode " + widthMode + " \nheightMode " + heightSpec + " \nwidthSize " + widthSize + " \nheightSize " + heightSize + " \ngetItemCount() " + getItemCount()); int width = 0; int height = 0; for (int i = 0; i < getItemCount(); i++) { measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension); if (getOrientation() == HORIZONTAL) { width = width + mMeasuredDimension[0]; if (i == 0) { height = mMeasuredDimension[1]; } } else { 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) { try { View view = recycler.getViewForPosition(0);// fix // 動態添加時報IndexOutOfBoundsException 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); } } catch (Exception e) { e.printStackTrace(); } finally { } } }
結果:運行成功 好開心,但是發現顯示不完全,item越多這個就無法顯示完全,不是最后一個被擋了,就是明明已經是最后一個item了,可是滾動條還沒有結束,再繼續往下滾,下面沒有數據而是空了一大片白。沒辦法這是算失敗了。
注意: 你們可以試試,說是可以解決item不顯示問題和顯示不全問題,結果我是失敗了,所以你們如果用這個方法成功了那么恭喜,如果沒有請看接下來。
國內目前大多數都是上面幾種解決方案,很不幸我逐一試了都不行,最后一個最有希望結果還是失敗了,沒辦法,只能國外找了終於在國外的找到了解決方法,不知道你們是不是用了這個,反正我是用了這個了,結果成功了,很滿意。
接下來進入正題:
1.廢話不多說先上個xml看看
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F2F2F2" android:orientation="vertical" > <include layout="@layout/include_header_view" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" > <LinearLayout android:id="@+id/li_show_recyclerView" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <TextView android:id="@+id/tvHeaderStormTitle" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="1dp" android:background="@color/white" android:gravity="center_vertical" android:minHeight="40dp" android:paddingLeft="15dp" android:paddingRight="10dp" android:textColor="#808080" android:textSize="16sp" /> <TextView android:id="@+id/tvHeaderStormEditContent" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="1dp" android:background="@color/white" android:padding="5dp" android:textColor="#404040" android:textSize="17sp" /> <android.support.v7.widget.RecyclerView android:id="@+id/id_recyclerview" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" /> </LinearLayout> </ScrollView> </LinearLayout>
看完xml后是不是很失望發現和你們的並沒有什么不一樣 ,但我既然貼了代碼就要說明白,這里1.最好為ScrollView加上android:fillViewport="true"這個屬性。 2.最好將RecyclerView的高度屬性 設成android:layout_height="wrap_content",這2點是建議,
我不知道這兩點會不會影響接下來的事,但最好這樣設置吧,我沒有試過你們可以試試。
2.接下來看java代碼(這里只是截取了用到recycleView的代碼)
FullyLinearLayoutManager mLayoutManager = new FullyLinearLayoutManager(this);
@Override public void initView() { mReplyList = new ArrayList<BeanHeaderStorm>(); mRecyclerView = (RecyclerView) findViewById(R.id.id_recyclerview);// 設置item動畫 mRecyclerView.setItemAnimator(new DefaultItemAnimator()); if (null == mRecyclerAdapter) { //設置布局樣式
mRecyclerView.setLayoutManager(mLayoutManager); //設置分割線 HeaderStormItemDiratcion diraction = new HeaderStormItemDiratcion(1); mRecyclerView.addItemDecoration(diraction); //設置適配器 mRecyclerAdapter = new AdapterHeaderStorm(ActivityHeaderStormShow.this, mReplyList); //設置item點擊事件 mRecyclerAdapter.setOnItemClickLitener(this); mRecyclerView.setAdapter(mRecyclerAdapter); } else { //刷新,這里的適配器是我自己單獨寫的別直接復制這段代碼 mRecyclerAdapter.notifyView(mReplyList); } }
這里看完是不是發現好像還是沒有什么區別,的確是沒有區別,但注意里面紅色代碼標注的地方,沒錯用了名字和前面重寫LinearLayoutManager用的名字一樣,但內部是不一樣的,接下來就來看看這里面到底改變了什么
完全重新重寫的FullyLinearLayoutManager如下:
import java.lang.reflect.Field; import com.loopj.android.http.BuildConfig; import android.content.Context; import android.graphics.Rect; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; /** * 重寫 LinearLayoutManager 為了ScrollView可以顯示RecyclerView 垂直布局 * * @author M.Z */ public class FullyLinearLayoutManager extends LinearLayoutManager { private static boolean canMakeInsetsDirty = true; private static Field insetsDirtyField = null; private static final int CHILD_WIDTH = 0; private static final int CHILD_HEIGHT = 1; private static final int DEFAULT_CHILD_SIZE = 100; private final int[] childDimensions = new int[2]; private final RecyclerView view; private int childSize = DEFAULT_CHILD_SIZE; private boolean hasChildSize; private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS; private final Rect tmpRect = new Rect(); public FullyLinearLayoutManager(Context context) { super(context); this.view = null; } public FullyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); this.view = null; } public FullyLinearLayoutManager(RecyclerView view) { super(view.getContext()); this.view = view; this.overScrollMode = ViewCompat.getOverScrollMode(view); } public FullyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) { super(view.getContext(), orientation, reverseLayout); this.view = view; this.overScrollMode = ViewCompat.getOverScrollMode(view); } public void setOverScrollMode(int overScrollMode) { if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode); if (this.view == null) throw new IllegalStateException("view == null"); this.overScrollMode = overScrollMode; ViewCompat.setOverScrollMode(view, overScrollMode); } public static int makeUnspecifiedSpec() { return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); } @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); final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED; final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED; final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY; final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY; final int unspecified = makeUnspecifiedSpec(); if (exactWidth && exactHeight) { // in case of exact calculations for both dimensions let's use default "onMeasure" implementation super.onMeasure(recycler, state, widthSpec, heightSpec); return; } final boolean vertical = getOrientation() == VERTICAL; initChildDimensions(widthSize, heightSize, vertical); int width = 0; int height = 0; // it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This // happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the // recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never // called whiles scrolling) recycler.clear(); final int stateItemCount = state.getItemCount(); final int adapterItemCount = getItemCount(); // adapter always contains actual data while state might contain old data (f.e. data before the animation is // done). As we want to measure the view with actual data we must use data from the adapter and not from the // state for (int i = 0; i < adapterItemCount; i++) { if (vertical) { if (!hasChildSize) { if (i < stateItemCount) { // we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items // we will use previously calculated dimensions measureChild(recycler, i, widthSize, unspecified, childDimensions); } else { logMeasureWarning(i); } } height += childDimensions[CHILD_HEIGHT]; if (i == 0) { width = childDimensions[CHILD_WIDTH]; } if (hasHeightSize && height >= heightSize) { break; } } else { if (!hasChildSize) { if (i < stateItemCount) { // we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items // we will use previously calculated dimensions measureChild(recycler, i, unspecified, heightSize, childDimensions); } else { logMeasureWarning(i); } } width += childDimensions[CHILD_WIDTH]; if (i == 0) { height = childDimensions[CHILD_HEIGHT]; } if (hasWidthSize && width >= widthSize) { break; } } } if (exactWidth) { width = widthSize; } else { width += getPaddingLeft() + getPaddingRight(); if (hasWidthSize) { width = Math.min(width, widthSize); } } if (exactHeight) { height = heightSize; } else { height += getPaddingTop() + getPaddingBottom(); if (hasHeightSize) { height = Math.min(height, heightSize); } } setMeasuredDimension(width, height); if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) { final boolean fit = (vertical && (!hasHeightSize || height < heightSize)) || (!vertical && (!hasWidthSize || width < widthSize)); ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS); } } private void logMeasureWarning(int child) { if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." + "To remove this message either use #setChildSize() method or don't run RecyclerView animations"); } } private void initChildDimensions(int width, int height, boolean vertical) { if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) { // already initialized, skipping return; } if (vertical) { childDimensions[CHILD_WIDTH] = width; childDimensions[CHILD_HEIGHT] = childSize; } else { childDimensions[CHILD_WIDTH] = childSize; childDimensions[CHILD_HEIGHT] = height; } } @Override public void setOrientation(int orientation) { // might be called before the constructor of this class is called //noinspection ConstantConditions if (childDimensions != null) { if (getOrientation() != orientation) { childDimensions[CHILD_WIDTH] = 0; childDimensions[CHILD_HEIGHT] = 0; } } super.setOrientation(orientation); } public void clearChildSize() { hasChildSize = false; setChildSize(DEFAULT_CHILD_SIZE); } public void setChildSize(int childSize) { hasChildSize = true; if (this.childSize != childSize) { this.childSize = childSize; requestLayout(); } } private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) { final View child; try { child = recycler.getViewForPosition(position); } catch (IndexOutOfBoundsException e) { if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "LinearLayoutManager doesn't work well with animations. Consider switching them off", e); } return; } final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams(); final int hPadding = getPaddingLeft() + getPaddingRight(); final int vPadding = getPaddingTop() + getPaddingBottom(); final int hMargin = p.leftMargin + p.rightMargin; final int vMargin = p.topMargin + p.bottomMargin; // we must make insets dirty in order calculateItemDecorationsForChild to work makeInsetsDirty(p); // this method should be called before any getXxxDecorationXxx() methods calculateItemDecorationsForChild(child, tmpRect); final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child); final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child); final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally()); final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically()); child.measure(childWidthSpec, childHeightSpec); dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin; dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin; // as view is recycled let's not keep old measured values makeInsetsDirty(p); recycler.recycleView(child); } private static void makeInsetsDirty(RecyclerView.LayoutParams p) { if (!canMakeInsetsDirty) { return; } try { if (insetsDirtyField == null) { insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty"); insetsDirtyField.setAccessible(true); } insetsDirtyField.set(p, true); } catch (NoSuchFieldException e) { onMakeInsertDirtyFailed(); } catch (IllegalAccessException e) { onMakeInsertDirtyFailed(); } } private static void onMakeInsertDirtyFailed() { canMakeInsetsDirty = false; if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect"); } } }
忽然一看,我的天是不是改變太大了,對沒錯,這就是終極的解決方案,最上面的國內的重寫代碼思路是對的,但還是處理的不夠全面,而這個應該是一個外國的大牛寫的,寫的很詳細全面,我拿來試了試,果然很完美的解決了我的問題.
我用了評論區2樓的方案寫的demo :DEMO.ZIP
總結:自身的技術還是太嫩了,完全不夠用,自身解決問題能力不行,作為一個伸手黨我們還是別忘了還要真正的去學習別人的思路想法和技術構思,
提問的出處的地址:http://stackoverflow.com/questions/27083091/recyclerview-inside-scrollview-is-not-working