NestedScrolling机制

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/lylodyf/article/details/84715807

简介

NestedScrolling机制能够让父View和子View在滚动式进行配合,其基本流程如下:

  • 当子view开始滚动之前,可以通知父View,让其先于自己进行滚动;
  • 子View自己进行滚动;
  • 子view滚动之后,还可以通知父view继续滚动

嵌套滑动的基本原理是在子控件接收到滑动一段距离的请求时, 先询问父控件是否要滑动, 如果父控件滑动了父控件就通知子控件它消耗了一部分滑动距离, 子控件就只处理剩下的滑动距离, 然后子控件滑动完毕后再把剩余的滑动(如果还有)距离传给父控件.

父view要实现NestedScrollingParent接口,而子View需要实现NsetScrolling从rollingChild接口,在这套机制中子View是发起者,父view是接受回调并做出响应的。

主要的类和接口

两个接口NestedScrollingChild和NestedScrollingParent
两个辅助类NestedScrollingChildHelper和NestedScrollingParentHelper

以上四个类都是在support-v4包中,SDK21之后的View是默认实现了的

NestedScrollingChild

  • void setNestedScrollingEnabled(boolean enabled);
  • boolean isNestedScrollingEnabled();
  • boolean startNestedScroll(@ScrollAxis int axes);
    起始方法, 主要作用是找到合适的能接受nestedScroll的parent
  • void stopNestedScroll();
    结束方法, 主要作用就是清空嵌套滑动的相关状态
  • boolean hasNestedScrollingParent();
  • boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
    在子view自己进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
      前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
  • boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
     在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
    此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
  • boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
  • boolean dispatchNestedPreFling(float velocityX, float velocityY);

NestedScrollingParent

//当开启、停止嵌套滚动时被调用
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
//当触摸嵌套滚动时被调用
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//当惯性嵌套滚动时被调用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

对应关系

子view 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

流程分析

  1. 首先在滚动之前,子View需要找到一个支持NestedScrollingParent的父View,告知父View我准备开始和你一起处理滑动事件了。我们在onTouchEvent的ACTION_DOWN中调用public boolean startNestedScroll(int axes),父View就会被回调public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)
  • axes表示滑动的方向
  • 调用startNestedScroll会调用父View的onStartNestedScroll和onNestedScrollAccepted是因为在NestedScrollingChildHelper中调用的
  • onStartNestedScroll返回true,才会调用onNestedScrollAccepted,在onNestedScrollAccepted中一般做一些准备工作
  1. 在子View每次滑动之前我们需要把滑动的信息传递个父View进行处理,一般情况下是在ACTION_MOVE中调用public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow),然后父View就会被回调public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
  • dx,dy表示滑动的距离
  • consumed用于存储父View消耗的距离
  • offsetInWindow表示方法调用前后子View的偏移量
  1. 当滑动结束,子View 调用public void stopNestedScroll()告知父View本次滑动结束,父View回调public void onStopNestedScroll(View target),在这里面做一些停止的逻辑

实例

首先定义Child

/**
 * Created by LiZhiyu on 2018/6/28.
 */
public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消费
    private int lastY;
    private int showHeight;


    public MyNestedScrollChild(Context context) {
        super(context);
    }

    public MyNestedScrollChild(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //第一次测量,因为布局文件中高度是wrap_content,因此测量模式为atmost,即高度不超过父控件的剩余空间
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        showHeight = getMeasuredHeight();

        //第二次测量,对稿哦度没有任何限制,那么测量出来的就是完全展示内容所需要的高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                break;
            //移动
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                //找到支持NestedScrollParent父View返回true
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑动的父类,父类进行了一系列的滑动,并且把消耗的距离写入consumed
                {       //dispatchNestedPreScroll询问父View是否需要滚动
                    //startNestedScroll是否找到能够接受的父view
                    //获取子View的滑动距离
                    int remain = dy - consumed[1];//减去父view消耗的距离|
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }
                } else {//没有找到对应的父Viw则全部由子View处理
                    scrollBy(0, -dy);
                }
                break;
        }

        return true;
    }

    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        int maxY = getMeasuredHeight() - showHeight;
        if (y > maxY) {
            y = maxY;
        }
        if (y < 0) {
            y = 0;
        }
        super.scrollTo(x, y);
    }

    //初始化helper对象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }

    //实现一下接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }
}

parent

/**
 * Created by LiZhiyu on 2018/6/28.
 */
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private ImageView img;
    private TextView tv;
    private MyNestedScrollChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;

    public MyNestedScrollParent(Context context) {
        super(context);
    }

    public MyNestedScrollParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    //获取子view
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        img = (ImageView) getChildAt(0);
        tv = (TextView) getChildAt(1);
        myNestedScrollChild = (MyNestedScrollChild) getChildAt(2);
        img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (imgHeight <= 0) {
                    imgHeight = img.getMeasuredHeight();
                }
            }
        });
        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (tvHeight <= 0) {
                    tvHeight = tv.getMeasuredHeight();
                }
            }
        });
    }

    //在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof MyNestedScrollChild) {
            return true;
        }
        return false;
    }


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    //先于child滚动
    //前3个为输入参数,最后一个是输出参数
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动
            scrollBy(0, -dy);//滚动
            consumed[1] = dy;//告诉child我消费了多少
        }
    }

    //后于child滚动
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    //返回值:是否消费了fling
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    //下拉的时候是否要向下滚动以显示图片
    public boolean showImg(int dy) {
        if (dy > 0) {
            if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
                return true;
            }
        }

        return false;
    }

    //上拉的时候,是否要向上滚动,隐藏图片
    public boolean hideImg(int dy) {
        if (dy < 0) {
            if (getScrollY() < imgHeight) {
                return true;
            }
        }
        return false;
    }

    //scrollBy内部会调用scrollTo
    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > imgHeight) {
            y = imgHeight;
        }

        super.scrollTo(x, y);
    }
}

布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.nestedscrolling.test1.Test1Activity">

    <com.lzy.testproject.ui.nestedscrolling.test1.MyNestedScrollParent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#f0f"
            android:text="上面的图片会被隐藏,而这个文字不会被隐藏" />

        <com.lzy.testproject.ui.nestedscrolling.test1.MyNestedScrollChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="123\n456\n789\n111\n222\n333\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n99\n11\n22\n33\n44\n55\n66\n77\n88\n99\n77\n88\n88\n8\n88\n88\n"
                android:textColor="#f0f"
                android:textSize="20sp" />
        </com.lzy.testproject.ui.nestedscrolling.test1.MyNestedScrollChild>
    </com.lzy.testproject.ui.nestedscrolling.test1.MyNestedScrollParent>
</android.support.constraint.ConstraintLayout>

猜你喜欢

转载自blog.csdn.net/lylodyf/article/details/84715807
今日推荐