Android 利用NestedScrolling机制为RecyclerView添加Header

原创
2020/08/23 08:52
阅读数 422

一、关于NestedScrolling

NestedScrolling机制主要是能够让父View和子View在滚动时互相协调配合。其中有两个重要的类,分别是:

接口类
NestedScrollingParent(最新:NestedScrollingParent2) - 代表类:NestedScrollView
NestedScrollingChild(最新:NestedScrollingChild2)   - 代表类:RecyclerView

帮助类
NestedScrollingChildHelper 
NestedScrollingParentHelper(用处不是很大)
父类继承NestedScrollingParent接口,而子类继承NestedScrollingChild接口,同时让父类包含子类,而不是自接父子关系,就搭起了NestedScrollingParent机制的基本骨架。

其主要流程是:

  1. 子类滑动,把滑动产生的事件和参数传给父类
  2. 父类根据子类传过来的参数偏移量、滑动方向等参数、当前滑动View等判断是否关注此事件,如果不关注,那么父View不会参与子View的滑动
  3. 父View关注了子View的滑动,子View通过PreScroll/PreFling会优先让父view消费事件,其实本质是子View的回调
  4. 接着,子View消费“剩余事件“(父View不一定消费掉所有事件,比如某时刻的偏移数据)
  5. 流程终止,子view通知父view本次滑动任务完成
public interface NestedScrollingChild {

    /**
     * 设置嵌套滑动是否能用
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 判断嵌套滑动是否可用
     */
    @Override
    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动
     *
     * @param axes 表示方向轴,有横向和竖向
     */
    @Override
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滑动
     */
    @Override
    public void stopNestedScroll();

    /**
     * 判断是否有父View 支持嵌套滑动
     */
    @Override
    public boolean hasNestedScrollingParent() ;

    /**
     * 滑行时调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @param consumed 是否被消费
     * @return  true if the nested scrolling parent consumed or otherwise reacted to the fling
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ;

    /**
     * 进行滑行前调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @return true if a nested scrolling parent consumed the fling
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) ;

    /**
     * 子view处理scroll后调用
     * @param dxConsumed x轴上被消费的距离(横向)
     * @param dyConsumed y轴上被消费的距离(竖向)
     * @param dxUnconsumed x轴上未被消费的距离
     * @param dyUnconsumed y轴上未被消费的距离
     * @param offsetInWindow 子View的窗体偏移量
     * @return  true if the event was dispatched, false if it could not be dispatched.
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ;

    /**
     * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
     * @param dx  x轴上滑动的距离
     * @param dy  y轴上滑动的距离
     * @param consumed 父view消费掉的scroll长度
     * @param offsetInWindow   子View的窗体偏移量
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);

}

 二、案例展示

我们之前使用ViewDragerHelper实现的《自定义上滑组件NestedScrollLayout》,其实可以通过NestedScrolling机制实现,NestedScrolling相比ViewDragHelper,ViewDragHelper联动使用不当可能产生丢帧问题,联动机制非常强大,相比ViewDragHelper,可减少很多事件处理,让联动变的更简单。当然如果是单View非联动操作ViewDragHelper更有优势,具体问题具体解决。

我们通过本文开头展示效果,给RecyclerView增加一个的Header,这个Header不是通过Adapter实现,而是通过联动效果效果实现。

public class NestedScrollChildLayout extends FrameLayout implements NestedScrollingParent2 {
    private final int mFlingVelocity;
    private  int mOverScrollExtends;
    private float startEventX = 0;
    private float startEventY = 0;
    private float mSlopTouchScale = 0;
    private boolean isTouchMoving = false;
    private View mHeaderView = null;
    private View mBodyView = null;
    private View mVerticalScrollView = null;
    private VelocityTracker mVelocityTracker;
    public NestedScrollChildLayout(@NonNull Context context) {
        this(context, null);
    }
    public NestedScrollChildLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public NestedScrollChildLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();
        mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        setClickable(true);
    }

    public void setOverScrollExtends(int overScrollExtends) {
        this.mOverScrollExtends = overScrollExtends;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int overScrollExtent = overScrollExtent();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                                + 0, lp.width);
                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
                                + 0, height-overScrollExtent);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

    }

    public boolean canScrollVertically(int direction) {
        final int offset = computeVerticalScrollOffset();
        final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range;
        }
    }

    @Override
    protected int computeVerticalScrollRange() {
        int childCount = getChildCount();
        if (childCount == 0) return super.computeVerticalScrollRange();
        int range = getPaddingBottom() + getPaddingTop();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            range += child.getHeight() + lp.bottomMargin + lp.topMargin;
        }
        if (range < getHeight()) {
            return super.computeVerticalScrollRange();
        }
        return range;
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mHeaderView = getChildView(LayoutParams.TYPE_HEAD);
        mBodyView = getChildView(LayoutParams.TYPE_BODY);
        int childLeft = getPaddingLeft();
        int childTop = getPaddingTop();
        if (mHeaderView != null) {
            LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();
            mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());
            childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        }
        if (mBodyView != null) {
            LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();
            mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());
        }
    }
    protected int overScrollExtent() {
        return  mOverScrollExtends;
    }
    private View getHeaderView() {
        return mHeaderView;
    }

    private View getBodyView() {
        return mBodyView;
    }

    private View findTouchView(float currentX, float currentY) {

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);

            float childX = (child.getX() - getScrollX());
            float childY = (child.getY() - getScrollY());

            if (currentX < childX || currentX > (childX + child.getWidth())) {
                continue;
            }
            if (currentY < childY || currentY > (childY + child.getHeight())) {
                continue;
            }
            return child;
        }
        return null;
    }

    private boolean hasHeader() {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
            if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {
                return true;
            }
        }
        return false;
    }
    public View getChildView(int layoutType) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
            if (lp.childLayoutType == layoutType) {
                return getChildAt(i);
            }
        }
        return null;
    }

    private boolean hasBody() {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
            if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void addView(View child) {
        assertLayoutType(child);
        super.addView(child);
    }
    private void assertLayoutType(View child) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        assertLayoutParams(lp);
    }
    private void assertLayoutParams(ViewGroup.LayoutParams lp) {

        if (hasHeader() && hasBody()) {
            throw new IllegalStateException("header and body has already existed");
        }
        if (hasHeader()) {
            if (!(lp instanceof LayoutParams)) {
                throw new IllegalStateException("header should keep only one");
            }
            if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {
                throw new IllegalStateException("header should keep only one");
            }
        }
        if (hasBody()) {
            if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {
                throw new IllegalStateException("header should keep only one");
            }
        }
    }
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        assertLayoutParams(params);
        super.addView(child, index, params);
    }
    @Override
    public void addView(View child, int index) {
        assertLayoutType(child);
        super.addView(child, index);
    }
    @Override
    public void addView(View child, int width, int height) {
        assertLayoutParams(new LinearLayout.LayoutParams(width, height));
        super.addView(child, width, height);
    }
    @Override
    public void onViewAdded(View child) {
        super.onViewAdded(child);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.childLayoutType != LayoutParams.TYPE_BODY) {
            return;
        }
        if (!(child instanceof NestedScrollingChild) &&  !(child instanceof ScrollFlingChild)) {
            throw new RuntimeException("body must be 'view implemention NestedScrollingChild or ScrollFlingChild '");
        }
    }
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
    @Override
    protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new LayoutParams(lp);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        if (axes == SCROLL_AXIS_VERTICAL) {
            //只关注垂直方向的移动
            int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
            int offset = computeVerticalScrollOffset();
            if (offset <= maxOffset) {
                mVerticalScrollView = target;
                return true;
            }
        }
        return false;
    }

    @Override
    protected int computeVerticalScrollExtent() {
        int computeVerticalScrollExtent = super.computeVerticalScrollExtent();
        return computeVerticalScrollExtent ;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {

    }
    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        if (mVerticalScrollView == target) {
            Log.d("onNestedScroll", "::::onStopNestedScroll vertical");
        }
    }
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {

    }
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
        int scrollRange = computeVerticalScrollRange();
        if (scrollRange <= getHeight()) {
            return;
        }
        if (target == null) return;
        if (mVerticalScrollView != target) {
            return;
        }
        int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
        int scrollOffset = computeVerticalScrollOffset();
        handleVerticalNestedScroll(dx, dy, consumed, maxOffset, scrollOffset);

    }

    private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed, int maxOffset, int scrollOffset) {
        if (dy == 0) return;
        if (!checkScrollableStationTop(mVerticalScrollView)) {
            return;
        }
        int dyOffset = dy;
        int targetOffset = scrollOffset + dy;
        if (targetOffset >= maxOffset) {
            dyOffset = maxOffset - scrollOffset;
        }
        if (targetOffset <= 0) {
            dyOffset = 0 - scrollOffset;
        }
        if (!canScrollVertically(dyOffset)) {
            return;
        }
        consumed[1] = dyOffset;
        Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
        scrollBy(0, dyOffset);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int scrollRange = computeVerticalScrollRange();
        if (scrollRange <= getHeight()) {
            return super.dispatchTouchEvent(event);
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mVelocityTracker.addMovement(event);
                startEventX = event.getX();
                startEventY = event.getY();
                isTouchMoving = false;
                break;
            case MotionEvent.ACTION_MOVE:
                float currentX = event.getX();
                float currentY = event.getY();
                float dx = currentX - startEventX;
                float dy = currentY - startEventY;
                if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
                    startEventX = currentX;
                    startEventY = currentY;
                    break;
                }
                View touchView = null;
                int offset = (int) -dy;
                if (Math.abs(dy) >= mSlopTouchScale) {
                    touchView = findTouchView(currentX, currentY);
                    isTouchMoving = touchView != null && touchView == getHeaderView();
                }
                if (offset != 0 && !canScrollVertically(offset)) {
                    isTouchMoving = false;
                }
                startEventX = currentX;
                startEventY = currentY;
                if (!isTouchMoving) {
                    break;
                }
                mVelocityTracker.addMovement(event);
                int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
                int scrollOffset = computeVerticalScrollOffset();
                int targetOffset = scrollOffset + offset;
                if (targetOffset >= maxOffset) {
                    offset = maxOffset - scrollOffset;
                }
                if (targetOffset <= 0) {
                    offset = 0 - scrollOffset;
                }
                if (offset == 0) {
                    break;
                }

                scrollBy(0, offset);
                Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
                super.dispatchTouchEvent(event);
                return true;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                mVelocityTracker.addMovement(event);
                if (isTouchMoving) {
                    isTouchMoving = false;
                    mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
                    startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
        }

        return super.dispatchTouchEvent(event);
    }

    private void startFling(VelocityTracker velocityTracker, int x, int y) {
        int xVolecity = (int) velocityTracker.getXVelocity();
        int yVolecity = (int) velocityTracker.getYVelocity();
        if (mVerticalScrollView instanceof NestedScrollingChild) {
            Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);
            ((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);
        }
        if (mVerticalScrollView instanceof ScrollFlingChild) {
            ((ScrollFlingChild) mVerticalScrollView).startFling(xVolecity, yVolecity);
        }
    }
    private boolean checkScrollableStationTop(View view) {
        if (view instanceof RecyclerView) {
            //显示区域最上面一条信息的position
            RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
            if (manager == null) {
                return true;
            }
            if (manager.getChildCount() == 0) {
                return true;
            }
            int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();
            return scrollOffset <= 0;
        }
        if (view instanceof NestedScrollingChild) {
            return view.canScrollVertically(-1);
        }
        if ((view instanceof View) && !(view instanceof ViewGroup)) {
            return true;
        }

        throw new IllegalArgumentException("不支持非NestedScrollingChild子类ViewGroup");
    }

    public static class LayoutParams extends FrameLayout.LayoutParams {
        public final static int TYPE_HEAD = 0;
        public final static int TYPE_BODY = 1;
        private int childLayoutType = TYPE_HEAD;

        public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
            super(c, attrs);
            if (attrs == null) return;
            final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedScrollChildLayout);
            childLayoutType = a.getInt(R.styleable.NestedScrollChildLayout_layoutScrollNestedType, 0);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }
        public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
            super(source);
        }
        public LayoutParams(@NonNull MarginLayoutParams source) {
            super(source);
        }

    }
    public interface ScrollFlingChild {
        public void startFling(int xVolecity, int yVolecity);
    }
}

子View属性参数

 <declare-styleable name="NestedScrollChildLayout">
        <attr name="layoutScrollNestedType" format="flags">
            <flag name="Head" value="0"/>
            <flag name="Body" value="1"/>
        </attr>
</declare-styleable>

 

三、布局使用

<com.cn.scrolllayout.view.NestedScrollChildLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/head"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:nestedChildLayoutType="Header"
        >
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/img_sample_panda"
            android:scaleType="centerCrop"
            />
    </LinearLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/body"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:nestedChildLayoutType="Body"
        android:background="@color/colorPrimary"
        />

</com.cn.scrolllayout.view.NestedScrollChildLayout>

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部