时间:2022-04-04 07:35:17 | 栏目:Android代码 | 点击:次
本文主要探讨以下几个问题:
不知道大家有没有注意过淘宝APP首页的二级联动,滑动的商品的时候上面类别也会滑动,滑动过程中类别模块停了商品还能继续滑动。也就是说滑动的是view,ViewGroup也会跟着滑动。如果用事件分发机制处理也能处理,但会及其麻烦。那用NestedScroll会咋样?
假设布局如下
RecyclerView 实现了 NestedScrollingChild 接口,NestedScrollView 实现了 NestedScrollingParent,这是实现嵌套布局的基础
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView
滑动屏幕时 RecyclerView 收到滑动事件,在 ACTION_DOWN 时
// RecyclerView.java onTouchEvent函数 case MotionEvent.ACTION_DOWN: { mScrollPointerId = e.getPointerId(0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } // startNestedScroll(nestedScrollAxis, TYPE_TOUCH); } break;
继续深入
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { if (hasNestedScrollingParent(type)) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { setNestedScrollingParentForType(type, p); ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
递归寻找NestedScrollingParent,然后回调 onStartNestedScroll 和 onNestedScrollAccepted 。onStartNestedScroll 决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;按下时确定其嵌套的父布局以及是否能收到后续事件。再看ACTION_MOVE事件
case MotionEvent.ACTION_MOVE: { if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); } } break;
ACTION_MOVE 中调用了 dispatchNestedPreScroll 。dispatchNestedPreScroll 中会回调 onNestedPreScroll 方法,内部的 scrollByInternal 中还会回调 onNestedScroll 方法
整个流程如下
onNestedPreScroll中,我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部View已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是我们的NestedScrollView 滑动。
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { // 向上滑动。若当前topview可见,需要将topview滑动至不可见 boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight(); if (hideTop) { scrollBy(0, dy); // 这个是被消费的距离,如果没有会被重复消费现象是父布局与子布局同时滑动,滑动的距离被消费两次 consumed[1] = dy; } }
整体代码如下
public class NestedScrollLayout extends NestedScrollView { private View topView; private ViewGroup contentView; private static final String TAG = "NestedScrollLayout"; public NestedScrollLayout(Context context) { this(context, null); init(); } public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); init(); } public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); init(); } public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); init(); } private FlingHelper mFlingHelper; int totalDy = 0; /** * 用于判断RecyclerView是否在fling */ boolean isStartFling = false; /** * 记录当前滑动的y轴加速度 */ private int velocityY = 0; private void init() { mFlingHelper = new FlingHelper(getContext()); setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { if (isStartFling) { totalDy = 0; isStartFling = false; } if (scrollY == 0) { Log.e(TAG, "TOP SCROLL"); // refreshLayout.setEnabled(true); } if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) { Log.e(TAG, "BOTTOM SCROLL"); dispatchChildFling(); } //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移 totalDy += scrollY - oldScrollY; } }); } private void dispatchChildFling() { if (velocityY != 0) { Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY); if (splineFlingDistance > totalDy) { childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy))); } } totalDy = 0; velocityY = 0; } private void childFling(int velY) { RecyclerView childRecyclerView = getChildRecyclerView(contentView); if (childRecyclerView != null) { childRecyclerView.fling(0, velY); } } @Override public void fling(int velocityY) { super.fling(velocityY); if (velocityY <= 0) { this.velocityY = 0; } else { isStartFling = true; this.velocityY = velocityY; } } @Override protected void onFinishInflate() { super.onFinishInflate(); topView = ((ViewGroup) getChildAt(0)).getChildAt(0); contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白 super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams lp = contentView.getLayoutParams(); lp.height = getMeasuredHeight(); contentView.setLayoutParams(lp); } /** * 解决滑动冲突:RecyclerView在滑动之前会问下父布局是否需要拦截,父布局使用此方法 */ @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { Log.e("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight()+"::dy::"+dy); // 向上滑动。若当前topview可见,需要将topview滑动至不可见 boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight(); if (hideTop) { scrollBy(0, dy); // 这个是被消费的距离,如果没有会被重复消费,现象是父布局与子布局同时滑动 consumed[1] = dy; } } private RecyclerView getChildRecyclerView(ViewGroup viewGroup) { for (int i = 0; i < viewGroup.getChildCount(); i++) { View view = viewGroup.getChildAt(i); if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) { return (RecyclerView) viewGroup.getChildAt(i); } else if (viewGroup.getChildAt(i) instanceof ViewGroup) { ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i)); if (childRecyclerView instanceof RecyclerView) { return (RecyclerView) childRecyclerView; } } continue; } return null; } }
总结:嵌套布局要注意的有几个方面