Android中ScrollView中嵌套RecyclerView的完美解决办法
16lz
2021-12-04
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 FullyLinearLayoutManager2 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 FullyLinearLayoutManager2(Context context) { super(context); this.view = null; } public FullyLinearLayoutManager2(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); this.view = null; } public FullyLinearLayoutManager2(RecyclerView view) { super(view.getContext()); this.view = view; this.overScrollMode = ViewCompat.getOverScrollMode(view); } public FullyLinearLayoutManager2(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"); } }}
使用:
recyclerView.setLayoutManager(new FullyLinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL, false));
最后RecyclerView外层嵌套一层
重写ScrollView
import android.content.Context;import android.os.Handler;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.ViewConfiguration;import android.widget.ScrollView;/** * @author 作者 zxb: * @date 创建时间:2016年7月27日 * @Description 带监听滚动的scrollview */public class MyScrollView extends ScrollView {private int downX;private int downY;private int mTouchSlop;private OnScrollListener onScrollListener;/** * 主要是用在用户手指离开MyScrollView,MyScrollView还在继续滑动,我们用来保存Y的距离,然后做比较 */private int lastScrollY;public MyScrollView(Context context) {this(context, null);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}public MyScrollView(Context context, AttributeSet attrs) {this(context, attrs, 0);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}public MyScrollView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}/** * 设置滚动接口 * * @param onScrollListener */public void setOnScrollListener(OnScrollListener onScrollListener) {this.onScrollListener = onScrollListener;}/** * 用于用户手指离开MyScrollView的时候获取MyScrollView滚动的Y距离,然后回调给onScroll方法中 */private Handler handler = new Handler() {public void handleMessage(android.os.Message msg) {int scrollY = MyScrollView.this.getScrollY();// 此时的距离和记录下的距离不相等,在隔5毫秒给handler发送消息if (lastScrollY != scrollY) {lastScrollY = scrollY;handler.sendMessageDelayed(handler.obtainMessage(), 5);}if (onScrollListener != null) {onScrollListener.onScroll(scrollY);}};};/** * 重写onTouchEvent, 当用户的手在MyScrollView上面的时候, * 直接将MyScrollView滑动的Y方向距离回调给onScroll方法中,当用户抬起手的时候, * MyScrollView可能还在滑动,所以当用户抬起手我们隔5毫秒给handler发送消息,在handler处理 * MyScrollView滑动的距离 */@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (onScrollListener != null) {onScrollListener.onScroll(lastScrollY = this.getScrollY());}switch (ev.getAction()) {case MotionEvent.ACTION_UP:handler.sendMessageDelayed(handler.obtainMessage(), 5);break;}return super.onTouchEvent(ev);}/** * * 滚动的回调接口 * * @author xiaanming * */public interface OnScrollListener {/** * 回调方法, 返回MyScrollView滑动的Y方向距离 * * @param scrollY * 、 */public void onScroll(int scrollY);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {int action = e.getAction();switch (action) {case MotionEvent.ACTION_DOWN:downX = (int) e.getRawX();downY = (int) e.getRawY();break;case MotionEvent.ACTION_MOVE:int moveY = (int) e.getRawY();if (Math.abs(moveY - downY) > mTouchSlop) {return true;}}return super.onInterceptTouchEvent(e);}}
更多相关文章
- android 3d页面跳转
- Android(安卓)drawableleft如何设置图片大小
- Android(安卓)ontouch 手速
- Android仿iOS左右滑动开关控件(Android4.0以上适用)
- android用户界面之AlarmManager教程实例汇
- Android端的极光配置
- android用户界面之TabHost教程实例汇总
- android用户界面之AlarmManager教程实例汇
- android GridView 去掉自带点击边框效果和禁止上下滑动