ScrollView 属性设置
android:fillViewport="true"
RecyclerView属性设置
android:layout_height="wrap_content"
设置RecyclerView的LinearLayoutManager
LinearLayoutManager mLayoutManager =newJSFullyLinearLayoutManager(getActivity());
rvRecycler.setLayoutManager(mLayoutManager);
支持RecyclerView嵌套ScrollView的自定义LinearLayoutManager
public class JSFullyLinearLayoutManager extends LinearLayoutManager {
private static booleancanMakeInsetsDirty=true;
private staticFieldinsetsDirtyField=null;
private static final intCHILD_WIDTH=0;
private static final intCHILD_HEIGHT=1;
private static final intDEFAULT_CHILD_SIZE=100;
private final int[]childDimensions=new int[2];
private finalRecyclerViewview;
private intchildSize=DEFAULT_CHILD_SIZE;
private booleanhasChildSize;
private intoverScrollMode= ViewCompat.OVER_SCROLL_ALWAYS;
private finalRecttmpRect=newRect();
publicJSFullyLinearLayoutManager(Context context) {
super(context);
this.view=null;
}
publicJSFullyLinearLayoutManager(Context context, intorientation, booleanreverseLayout) {
super(context,orientation,reverseLayout);
this.view=null;
}
publicJSFullyLinearLayoutManager(RecyclerView view) {
super(view.getContext());
this.view= view;
this.overScrollMode= ViewCompat.getOverScrollMode(view);
}
publicJSFullyLinearLayoutManager(RecyclerView view, intorientation, booleanreverseLayout) {
super(view.getContext(),orientation,reverseLayout);
this.view= view;
this.overScrollMode= ViewCompat.getOverScrollMode(view);
}
public voidsetOverScrollMode(intoverScrollMode) {
if(overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS|| overScrollMode > ViewCompat.OVER_SCROLL_NEVER)
throw newIllegalArgumentException("Unknown overscroll mode: "+ overScrollMode);
if(this.view==null)throw newIllegalStateException("view == null");
this.overScrollMode= overScrollMode;
ViewCompat.setOverScrollMode(view,overScrollMode);
}
public static intmakeUnspecifiedSpec() {
returnView.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);
}
@Override
public voidonMeasure(RecyclerView.Recycler recycler,RecyclerView.State state, intwidthSpec, intheightSpec) {
final intwidthMode = View.MeasureSpec.getMode(widthSpec);
final intheightMode = View.MeasureSpec.getMode(heightSpec);
final intwidthSize = View.MeasureSpec.getSize(widthSpec);
final intheightSize = View.MeasureSpec.getSize(heightSpec);
final booleanhasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final booleanhasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final booleanexactWidth = widthMode == View.MeasureSpec.EXACTLY;
final booleanexactHeight = heightMode == View.MeasureSpec.EXACTLY;
final intunspecified =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 booleanvertical = getOrientation() ==VERTICAL;
initChildDimensions(widthSize,heightSize,vertical);
intwidth =0;
intheight =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 intstateItemCount = state.getItemCount();
final intadapterItemCount = 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(inti =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 booleanfit = (vertical && (!hasHeightSize || height < heightSize))
|| (!vertical && (!hasWidthSize || width < widthSize));
ViewCompat.setOverScrollMode(view,fit ? ViewCompat.OVER_SCROLL_NEVER: ViewCompat.OVER_SCROLL_ALWAYS);
}
}
private voidlogMeasureWarning(intchild) {
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 voidinitChildDimensions(intwidth, intheight, booleanvertical) {
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 voidsetOrientation(intorientation) {
// 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 voidclearChildSize() {
hasChildSize=false;
setChildSize(DEFAULT_CHILD_SIZE);
}
public voidsetChildSize(intchildSize) {
hasChildSize=true;
if(this.childSize!= childSize) {
this.childSize= childSize;
requestLayout();
}
}
private voidmeasureChild(RecyclerView.Recycler recycler, intposition, intwidthSize, intheightSize, int[] dimensions) {
finalView 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;
}
finalRecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
final inthPadding = getPaddingLeft() + getPaddingRight();
final intvPadding = getPaddingTop() + getPaddingBottom();
final inthMargin = p.leftMargin+ p.rightMargin;
final intvMargin = 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 inthDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
final intvDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
final intchildWidthSpec =getChildMeasureSpec(widthSize,hPadding + hMargin + hDecoration,p.width,canScrollHorizontally());
final intchildHeightSpec =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 voidmakeInsetsDirty(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 voidonMakeInsertDirtyFailed() {
canMakeInsetsDirty=false;
if(BuildConfig.DEBUG) {
Log.w("LinearLayoutManager","Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
}
}
}