NestScroll嵌套滑动

标签(空格分隔): 未分类


引用:Android 嵌套滑动——NestedScrolling完全解析

效果图:
NestScroll嵌套滑动

主要代码:
xml:

<?xml version="1.0" encoding="utf-8"?>
<xiey94.com.nestedscrolling.nest1.NestParent xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <View
        android:layout_width="50dip"
        android:layout_height="50dip"
        android:layout_gravity="center"
        android:layout_marginTop="100dip"
        android:background="#f0f" />

    <xiey94.com.nestedscrolling.nest1.NestChild
        android:layout_width="50dip"
        android:layout_height="50dip"
        android:layout_gravity="center"
        android:layout_marginTop="200dip"
        android:background="#88ff7f3c" />
</xiey94.com.nestedscrolling.nest1.NestParent>

NestChild:

package xiey94.com.nestedscrolling.nest1;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class NestChild extends View implements NestedScrollingChild {

    public static final String TAG = "cctw";
    private NestedScrollingChildHelper childHelper;

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

    public NestChild(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //生成辅助类,并传入当前控件
        childHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

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

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

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        childHelper.setNestedScrollingEnabled(enabled);
    }

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

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

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        //滚动之后将剩余滑动传给父类
        return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        //子View滚动之前将滑动距离传给父类
        return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

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

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

    private int mOldY;
    private int[] mConsumed = new int[2];
    private int[] mOffset = new int[2];

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //启动滑动,传入方向
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                //记录y值
                mOldY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int y = (int) event.getRawY();
                //计算y值得偏移量
                int offsetY = y - mOldY;
                //通知父类,如果返回true,表示父类消耗了触摸
                if (dispatchNestedPreScroll(0, offsetY, mConsumed, mOffset)) {
                    offsetY -= mConsumed[1];
                }
                int unConsumed = 0;
                float targetY = getTranslationY() + offsetY;
                if (targetY > -40 && targetY < 40) {
                    setTranslationY(targetY);
                } else {
                    unConsumed = offsetY;
                    offsetY = 0;
                }
                //滚动完成之后,通知当前滑动的状态
                dispatchNestedScroll(0, offsetY, 0, unConsumed, mOffset);
                mOldY = y;
                break;
            case MotionEvent.ACTION_UP:
                //滑动结束
                stopNestedScroll();
                break;
            default:
                break;
        }

        return true;
    }
}

NestParent:

package xiey94.com.nestedscrolling.nest1;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

public class NestParent extends LinearLayout implements NestedScrollingParent {
    public static final String TAG = "ccer";
    NestedScrollingParentHelper parentHelper;

    public NestParent(Context context) {
        this(context, null);
    }

    public NestParent(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

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

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        //child 嵌套滑动的子控件(当前控件的子控件),target,手指触摸的控件
        return true;
    }

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

    @Override
    public void onStopNestedScroll(View child) {
        parentHelper.onStopNestedScroll(child);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        getChildAt(0).setTranslationY(getChildAt(0).getTranslationY() + dyUnconsumed);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        //开始滑动之前
        super.onNestedPreScroll(target, dx, dy, consumed);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

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

说一下过程:
一开始在构造函数里面初始化Helper,然后设置可以嵌套滑动;

public NestChild(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    //生成辅助类,并传入当前控件
    childHelper = new NestedScrollingChildHelper(this);
    setNestedScrollingEnabled(true);
}

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    //child 嵌套滑动的子控件(当前控件的子控件),target,手指触摸的控件
    return true;
}

然后开始嵌套滑动,并指定滑动的方向为竖直方向;并记录当前下手的y值

case MotionEvent.ACTION_DOWN:
    //启动滑动,传入方向
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    //记录y值
    mOldY = (int) event.getRawY();
    break;

移动的时候记录y轴的偏移量;

int y = (int) event.getRawY();
//计算y值得偏移量
int offsetY = y - mOldY;

然后将偏移量传给dispatchNestedPreScroll方法中,回调父类的onNestedPreScroll;

//通知父类,如果返回true,表示父类消耗了触摸
if (dispatchNestedPreScroll(0, offsetY, mConsumed, mOffset)) {
    offsetY -= mConsumed[1];
}

在我们这个demo中,父类在这个回调并没有做 任何操作,所以offsetY这个偏移量并没有消耗;

累积偏移量,

float targetY = getTranslationY() + offsetY;

如果偏移量在-40到40之间则直接平移,否则则统计未消耗的距离,并将偏移量置为0

int unConsumed = 0;
if (targetY > -40 && targetY < 40) {
    setTranslationY(targetY);
} else {
    unConsumed = offsetY;
    offsetY = 0;
}

调用dispatchNestedScroll将未消耗的距离传递给父View的onNestedScroll

//滚动完成之后,通知当前滑动的状态
dispatchNestedScroll(0, offsetY, 0, unConsumed, mOffset);
mOldY = y;
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    getChildAt(0).setTranslationY(getChildAt(0).getTranslationY() + dyUnconsumed);
}

在这里消耗子View未消耗的距离;

最后结束嵌套滑动

case MotionEvent.ACTION_UP:
    //滑动结束
    stopNestedScroll();
    break;

这就是 全部的过程

再来看看另一个例子:

引用:android NestedScroll嵌套滑动机制完全解析-原来如此简单(修正自己的一个错误说法)

先贴一张图看看效果:
NestScroll嵌套滑动

这里先贴一下代码:
MyNestedScrollingParent

public class MyNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
    private NestedScrollingParentHelper mParentHelper;
    private ImageView iv;
    private TextView tv;
    private MyNestedScrollingChild nsv;
    private int ivHeight, tvHeight;

    public MyNestedScrollingParent(Context context) {
        super(context);
        init();
    }

    public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        iv = (ImageView) getChildAt(0);
        tv = (TextView) getChildAt(1);
        nsv = (MyNestedScrollingChild) getChildAt(2);
        iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ivHeight = iv.getMeasuredHeight();
            }
        });
        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                tvHeight = tv.getMeasuredHeight();
            }
        });

    }

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

    /**
     * @param child
     * @param target
     * @param nestedScrollAxes 嵌套滑动的坐标系,也就是用来判断是X轴滑动还是Y轴滑动,返回true或者false,返回false就没得玩了
     * @return 如果要接受嵌套滑动操作,就返回true吧
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
        return true;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View var1, @NonNull View var2, int var3) {
        mParentHelper.onNestedScrollAccepted(var1, var2, var3);
    }

    @Override
    public void onStopNestedScroll(@NonNull View var1) {
        mParentHelper.onStopNestedScroll(var1);
    }

    @Override
    public void onNestedScroll(@NonNull View var1, int var2, int var3, int var4, int var5) {
    }

    /**
     * @param child
     * @param dx
     * @param dy
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(@NonNull View child, int dx, int dy, @NonNull int[] consumed) {
        if (showImage(dy) || hideImage(dy)) {
            consumed[1] = dy;//完全消费有y轴的滑动
            scrollBy(0, dy);
        }
    }

    @Override
    public boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4) {
        return super.onNestedFling(var1, var2, var3, var4);
    }

    @Override
    public boolean onNestedPreFling(@NonNull View var1, float var2, float var3) {
        return super.onNestedPreFling(var1, var2, var3);
    }

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

    /**
     * 滑动控制在图片的高度
     *
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > ivHeight) {
            y = ivHeight;
        }
        super.scrollTo(x, y);
    }

    /**
     * 往上往下滑,当Child滑动完了,只要没有滑动到起始位置(getScrollY==0),就一直往下滑,当滑动到起始位置时,滑动事件再传给Childs
     *
     * @param dy
     * @return
     */
    private boolean showImage(int dy) {
        if (dy < 0) {
            if (getScrollY() > 0 && nsv.getScrollY() == 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * 往上滑是隐藏图片
     *
     * @param dy >0是往上滑动,<0是往下滑动
     *           getScrollY()往上滑动值越大
     * @return 往上滑,当滑动的距离超过图片的高度时,则将滑动事件传给Child
     */
    private boolean hideImage(int dy) {
        if (dy > 0) {
            if (getScrollY() < ivHeight) {
                return true;
            }
        }
        return false;
    }
}

MyNestedScrollingChild

public class MyNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mChildHelper;
    private final int[] mScrollOffset = new int[2];
    private final int[] mScrollConsumed = new int[2];
    private int mLastTouchX, mLastTouchY;
    private int showHeight;

    public MyNestedScrollingChild(Context context) {
        super(context);
        init();
    }

    public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mChildHelper = new NestedScrollingChildHelper(this);
        mChildHelper.setNestedScrollingEnabled(true);
    }

    @Override
    public void setNestedScrollingEnabled(boolean var1) {
        mChildHelper.setNestedScrollingEnabled(var1);
    }

    /**
     * 允许嵌套滑动
     * @return
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return true;
    }

    @Override
    public boolean startNestedScroll(int var1) {
        return mChildHelper.startNestedScroll(var1);
    }

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

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

    @Override
    public boolean dispatchNestedScroll(int var1, int var2, int var3, int var4, @Nullable int[] var5) {
        return mChildHelper.dispatchNestedScroll(var1, var2, var3, var4, var5);
    }

    @Override
    public boolean dispatchNestedPreScroll(int var1, int var2, @Nullable int[] var3, @Nullable int[] var4) {
        return mChildHelper.dispatchNestedPreScroll(var1, var2, var3, var4);
    }

    @Override
    public boolean dispatchNestedFling(float var1, float var2, boolean var3) {
        return mChildHelper.dispatchNestedFling(var1, var2, var3);
    }

    @Override
    public boolean dispatchNestedPreFling(float var1, float var2) {
        return mChildHelper.dispatchNestedPreFling(var1, var2);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchY = (int) (event.getRawY() + 0.5f);
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                startNestedScroll(nestedScrollAxis);
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int) (event.getX() + 0.5f);
                int y = (int) (event.getRawY() + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                mLastTouchY = y;
                mLastTouchX = x;
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dy -= mScrollConsumed[1];
                    if (dy == 0) {
                        return true;
                    }
                } else {
                    scrollBy(0, dy);
                }
                break;
        }
        return true;
    }

    @Override
    public void scrollTo(int x, int y) {
        int mh = getMeasuredHeight();
        int maxY = mh - showHeight;
        if (y < 0) {
            y = 0;
        }
        if (y > maxY) {
            y = maxY;
        }
        super.scrollTo(x, y);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (showHeight <= 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            showHeight = getMeasuredHeight();
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(1000000, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

xml文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <xiey94.com.nestedscrolling.MyNestedScrollingParent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

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

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dip"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="@string/app_name"
            android:textColor="#fff"
            android:textSize="20sp" />

        <xiey94.com.nestedscrolling.MyNestedScrollingChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/test"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="16sp" />
        </xiey94.com.nestedscrolling.MyNestedScrollingChild>
    </xiey94.com.nestedscrolling.MyNestedScrollingParent>

</RelativeLayout>

布局上就是一个父布局,第一个Child是ImageView,第二个Child是TextView,第三个Child是我们写的子View
效果就是当网上滑动时,parent整体往上滑,直到整个ImageView区域全部隐藏,然后我们写的字View开始往上滑动;反之,往下滑,子View先滑动到顶部,然后parent整体下滑,直到整个ImageView完全显示;

流程:

也是指定可以嵌套滑动:

private void init() {
    mChildHelper = new NestedScrollingChildHelper(this);
    mChildHelper.setNestedScrollingEnabled(true);
}

@Override
public void setNestedScrollingEnabled(boolean var1) {
    mChildHelper.setNestedScrollingEnabled(var1);
}

@Override
public boolean isNestedScrollingEnabled() {
    return true;
}

指定方向开始滑动

case MotionEvent.ACTION_DOWN:
    mLastTouchY = (int) (event.getRawY() + 0.5f);
    int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
    startNestedScroll(nestedScrollAxis);
    break;

计算距离差

int x = (int) (event.getX() + 0.5f);
int y = (int) (event.getRawY() + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;

将产生的距离差传给dispatchNestedPreScroll,回调父View的onNestedPreScroll
看父类消耗的距离,初始往上滑的时候,如果滑动的距离小于图片的高度则父类消耗掉;

    /**
     * 往上滑是隐藏图片
     *
     * @param dy >0是往上滑动,<0是往下滑动
     *           getScrollY()往上滑动值越大
     * @return 往上滑,当滑动的距离超过图片的高度时,则将滑动事件传给Child
     */
    private boolean hideImage(int dy) {
        if (dy > 0) {
            if (getScrollY() < ivHeight) {
                return true;
            }
        }
        return false;
    }

或者往下滑,如果子View滑到了顶部并且父类滑动的距离要大于0并且小于图片的高度则显示图片,也是父View消耗;

    /**
     * 从上往下滑,当Child滑动完了,只要没有滑动到起始位置(getScrollY==0),就一直往下滑,当滑动到起始位置时,滑动事件再传给Childs
     *
     * @param dy
     * @return
     */
    private boolean showImage(int dy) {
        if (dy < 0) {
            if (getScrollY() > 0 && nsv.getScrollY() == 0) {
                return true;
            }
        }
        return false;
    }

将父类可以消耗的距离全部消耗

    @Override
    public void onNestedPreScroll(@NonNull View child, int dx, int dy, @NonNull int[] consumed) {
        if (showImage(dy) || hideImage(dy)) {
            consumed[1] = dy;//完全消费有y轴的滑动
            scrollBy(0, dy);
        }
    }

父View滑动的距离固定在图片的高度

    /**
     * 滑动控制在图片的高度
     *
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > ivHeight) {
            y = ivHeight;
        }
        super.scrollTo(x, y);
    }

否则就是子View消耗

scrollBy(0, dy);

子View滑动的距离也指定

    @Override
    public void scrollTo(int x, int y) {
        int mh = getMeasuredHeight();
        int maxY = mh - showHeight;
        if (y < 0) {
            y = 0;
        }
        if (y > maxY) {
            y = maxY;
        }
        super.scrollTo(x, y);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (showHeight <= 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            showHeight = getMeasuredHeight();
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(1000000, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

代码主要就这些;

嵌套滑动一共涉及到了四个类:
NestedScrollingChild、NestedScrollingChildHelper、
NestedScrollingParent、NestedScrollingParentHelper;

NestedScrollingChild是一个接口,主要里面是:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package android.support.v4.view;

import android.support.annotation.Nullable;

public interface NestedScrollingChild {
    void setNestedScrollingEnabled(boolean var1);

    boolean isNestedScrollingEnabled();

    boolean startNestedScroll(int var1);

    void stopNestedScroll();

    boolean hasNestedScrollingParent();

    boolean dispatchNestedScroll(int var1, int var2, int var3, int var4, @Nullable int[] var5);

    boolean dispatchNestedPreScroll(int var1, int var2, @Nullable int[] var3, @Nullable int[] var4);

    boolean dispatchNestedFling(float var1, float var2, boolean var3);

    boolean dispatchNestedPreFling(float var1, float var2);
}

目前这些方法我们基本上都见到过,就一个没见到

boolean hasNestedScrollingParent();

但是他在Helper中间接的使用了,所以目前这个接口方法我们都见过,而且

boolean dispatchNestedFling(float var1, float var2, boolean var3);

boolean dispatchNestedPreFling(float var1, float var2);

除了这俩,在父View中的回调也都用过;所以也就没有特别的地方了;

再来看看NestedScrollingParent

同理他也是接口

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package android.support.v4.view;

import android.support.annotation.NonNull;
import android.view.View;

public interface NestedScrollingParent {
    boolean onStartNestedScroll(@NonNull View var1, @NonNull View var2, int var3);

    void onNestedScrollAccepted(@NonNull View var1, @NonNull View var2, int var3);

    void onStopNestedScroll(@NonNull View var1);

    void onNestedScroll(@NonNull View var1, int var2, int var3, int var4, int var5);

    void onNestedPreScroll(@NonNull View var1, int var2, int var3, @NonNull int[] var4);

    boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4);

    boolean onNestedPreFling(@NonNull View var1, float var2, float var3);

    int getNestedScrollAxes();
}

可以看出,这些方法也都用过,也没有特殊的地方

对比一下:

子View 父View 作用
setNestedScrollingEnabled onNestedScrollAccepted 是否可嵌套滑动
isNestedScrollingEnabled 是否可嵌套滑动
startNestedScroll onStartNestedScroll 开始嵌套滑动,子View回调父View
dispatchNestedPreScroll onNestedPreScroll 嵌套滑动前的准备工作,子View回调父View
dispatchNestedScroll onNestedScroll 嵌套滑动,子View回调父View
dispatchNestedPreFling onNestedPreFling Fling前的准备工作,子View回调父View
dispatchNestedFling onNestedFling Fling,子View回调父View
stopNestedScroll onStopNestedScroll 结束滑动,子View回调父View
getNestedScrollAxes 滑动方向
hasNestedScrollingParent 是否有可以嵌套滑动的父View

一般的触摸消息的分发都是从外向内的,由外层的ViewGroup的dispatchTouchEvent方法调用到内层的View的dispatchTouchEvent方法.
而NestedScroll提供了一个反向的机制,内层的view在接收到ACTION_MOVE的时候,将滚动消息先传回给外层的ViewGroup,看外层的ViewGroup是不是需要消耗一部分的移动,然后内层的View再去消耗剩下的移动.内层view可以消耗剩下的滚动的一部分,如果还没有消耗完,外层的view可以再选择把最后剩下的滚动消耗掉.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package android.support.v4.view;

import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;

public class NestedScrollingParentHelper {
    private final ViewGroup mViewGroup;
    private int mNestedScrollAxes;

    public NestedScrollingParentHelper(@NonNull ViewGroup viewGroup) {
        this.mViewGroup = viewGroup;
    }

    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
        this.onNestedScrollAccepted(child, target, axes, 0);
    }

    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        this.mNestedScrollAxes = axes;
    }

    public int getNestedScrollAxes() {
        return this.mNestedScrollAxes;
    }

    public void onStopNestedScroll(@NonNull View target) {
        this.onStopNestedScroll(target, 0);
    }

    public void onStopNestedScroll(@NonNull View target, int type) {
        this.mNestedScrollAxes = 0;
    }
}

NestedScrollingParentHelper 里面的东西实在是少,本质上就是一个set/get的实体;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package android.support.v4.view;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewParent;

public class NestedScrollingChildHelper {
    private ViewParent mNestedScrollingParentTouch;
    private ViewParent mNestedScrollingParentNonTouch;
    private final View mView;
    private boolean mIsNestedScrollingEnabled;
    private int[] mTempNestedScrollConsumed;

    //把当前子View传进去,赋值
    public NestedScrollingChildHelper(@NonNull View view) {
        this.mView = view;
    }

    //设置可以嵌套滑动
    public void setNestedScrollingEnabled(boolean enabled) {
        if (this.mIsNestedScrollingEnabled) {
            ViewCompat.stopNestedScroll(this.mView);
        }

        this.mIsNestedScrollingEnabled = enabled;
    }

    //get
    public boolean isNestedScrollingEnabled() {
        return this.mIsNestedScrollingEnabled;
    }

    //get
    public boolean hasNestedScrollingParent() {
        return this.hasNestedScrollingParent(0);
    }

    //get
    public boolean hasNestedScrollingParent(int type) {
        return this.getNestedScrollingParentForType(type) != null;
    }

    //get
    public boolean startNestedScroll(int axes) {
        return this.startNestedScroll(axes, 0);
    }

    //
    public boolean startNestedScroll(int axes, int type) {
        //验证一个滑动流程只可以startNestedScroll一次
        if (this.hasNestedScrollingParent(type)) {
            return true;
        } else {
            //是否已经启动嵌套滑动
            if (this.isNestedScrollingEnabled()) {
                ViewParent p = this.mView.getParent();

                for(View child = this.mView; p != null; p = p.getParent()) {
                    //判断父view 是否实现NestedScrollingParent 且接口的onStartNestedScroll方法返回true
                    if (ViewParentCompat.onStartNestedScroll(p, child, this.mView, axes, type)) {
                        this.setNestedScrollingParentForType(type, p);
                        //调用NestedScrollingParent 的onNestedScrollAccepted回调方法
                        ViewParentCompat.onNestedScrollAccepted(p, child, this.mView, axes, type);
                        return true;
                    }

                    if (p instanceof View) {
                        child = (View)p;
                    }
                }
            }

            return false;
        }
    }

    //get
    public void stopNestedScroll() {
        this.stopNestedScroll(0);
    }
    
    //停止
    public void stopNestedScroll(int type) {
        ViewParent parent = this.getNestedScrollingParentForType(type);
        if (parent != null) {
            ViewParentCompat.onStopNestedScroll(parent, this.mView, type);
            this.setNestedScrollingParentForType(type, (ViewParent)null);
        }

    }
    
    //get
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return this.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, 0);
    }
    
    分发滑动事件
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
        //判断是否启动嵌套滑动
        if (this.isNestedScrollingEnabled()) {
            //判断是否拥有实现了嵌套滑动机制的parent  View
            ViewParent parent = this.getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }
            
            //如果消耗的和未消耗的具体有一个不为0,则计算处理消耗
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                //计算出偏移量
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
                
                //滑动消耗
                ViewParentCompat.onNestedScroll(parent, this.mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
                //计算出滑动后剩余的偏移量
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }

                return true;
            }
            
            //未消耗,则偏移量为0
            if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }

        return false;
    }
    
    //get
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        return this.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, 0);
    }
    
    //滑动前的消耗
    //offsetInWindow 记录屏幕坐标偏移量
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        //是否可以嵌套滑动
        if (this.isNestedScrollingEnabled()) {
            //是否有实现了嵌套滑动的parent View
            ViewParent parent = this.getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }
            
            //如果移动的距离不为0,则计算消耗
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                //计算偏移量
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
                
                //如果consumed为null则临时创建一个新的
                if (consumed == null) {
                    if (this.mTempNestedScrollConsumed == null) {
                        this.mTempNestedScrollConsumed = new int[2];
                    }

                    consumed = this.mTempNestedScrollConsumed;
                }

                consumed[0] = 0;
                consumed[1] = 0;
                //滑动
                ViewParentCompat.onNestedPreScroll(parent, this.mView, dx, dy, consumed, type);
                //剩余偏移量
                if (offsetInWindow != null) {
                    this.mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                
                //判断是否有消耗,有消耗则返回true
                return consumed[0] != 0 || consumed[1] != 0;
            }

            if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }

        return false;
    }
    
    //分发fling
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        if (this.isNestedScrollingEnabled()) {
            ViewParent parent = this.getNestedScrollingParentForType(0);
            if (parent != null) {
                return ViewParentCompat.onNestedFling(parent, this.mView, velocityX, velocityY, consumed);
            }
        }

        return false;
    }
    
    //分发预fling
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        if (this.isNestedScrollingEnabled()) {
            ViewParent parent = this.getNestedScrollingParentForType(0);
            if (parent != null) {
                return ViewParentCompat.onNestedPreFling(parent, this.mView, velocityX, velocityY);
            }
        }

        return false;
    }

    public void onDetachedFromWindow() {
        ViewCompat.stopNestedScroll(this.mView);
    }

    public void onStopNestedScroll(@NonNull View child) {
        ViewCompat.stopNestedScroll(this.mView);
    }
    
    //get;   type :0是手动触发;1是非手动触发;
    private ViewParent getNestedScrollingParentForType(int type) {
        switch(type) {
        case 0:
            return this.mNestedScrollingParentTouch;
        case 1:
            return this.mNestedScrollingParentNonTouch;
        default:
            return null;
        }
    }

    //set
    private void setNestedScrollingParentForType(int type, ViewParent p) {
        switch(type) {
        case 0:
            this.mNestedScrollingParentTouch = p;
            break;
        case 1:
            this.mNestedScrollingParentNonTouch = p;
        }

    }
}

案例:
仿美团详情滑动界面,并兼容NestedScroll嵌套
Android NestedScroll嵌套滑动机制解析

2018-12-17 补充

果然是学功夫要易于造功夫

在参考了这么多案例之后,又简单的摸清了原理之后,我就想创造一个案例:

预览图,我想要的效果就是当列表下拉时,头出现,列表上拉时,头隐藏

NestScroll嵌套滑动

这个效果应该大家都见过,在Toolbar和CoordinatorLayout等交互的时候就可以设置

这里主要体现了Parent

<?xml version="1.0" encoding="utf-8"?>
<xiey94.com.nestedscrolling.nest2.NestParent2 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <xiey94.com.nestedscrolling.nest2.NestTextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="#88ff7f3c"
        android:padding="5dip"
        android:text="@string/test_1"
        android:textColor="#fff"
        android:textSize="16sp" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" />

</xiey94.com.nestedscrolling.nest2.NestParent2>

这个NestTextView暂时可以忽略,就当他是一个普通的TextView

package xiey94.com.nestedscrolling.nest2;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;

public class NestParent2 extends LinearLayout implements NestedScrollingParent {

    public static final String TAG = "cctw";
    private NestedScrollingParentHelper parentHelper;

    private View child0, child1;
    //获取第一个View的高度
    private int child0H;
    private int bottom;

    public NestParent2(Context context) {
        this(context, null);
    }

    public NestParent2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestParent2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        child0 = getChildAt(0);
        child1 = getChildAt(1);
        child0.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                child0H = child0.getMeasuredHeight();
                bottom = child1.getBottom();
            }
        });
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }

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

    @Override
    public void onStopNestedScroll(View child) {
        parentHelper.onStopNestedScroll(child);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0 && getScrollY() < child0H) {
            //往上滑;隐藏第一个View
//            consumed[1] = dy;
            scrollBy(0, dy);
            child1.layout(child1.getLeft(), child1.getTop(), child1.getRight(), bottom + getScrollY());
        } else if (dy < 0 && getScrollY() > -child0H) {
            //往下滑;显示第一个View
//            consumed[1] = dy;
            scrollBy(0, dy);
            child1.layout(child1.getLeft(), child1.getTop(), child1.getRight(), bottom + getScrollY());
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

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

    //scrollBy调用的就是scrollTo;这里限制头的滑动范围
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > child0H) {
            y = child0H;
        }
        super.scrollTo(x, y);
    }

    private int abs(int x) {
        return Math.abs(x);
    }

}

参考:

NestedScrollingChild
NestedScrollingChild2
NestedScrollingChild3
NestedScrollingParent
NestedScrollingParent2
NestedScrollingParent3
android NestedScroll嵌套滑动机制完全解析-原来如此简单(修正自己的一个错误说法)
Android 嵌套滑动——NestedScrolling完全解析
安卓嵌套滚动NestedScroll了解一下
仿美团详情滑动界面,并兼容NestedScroll嵌套
Android NestedScroll嵌套滑动机制解析
Android NestedScrolling全面解析 - 带你实现一个支持嵌套滑动的下拉刷新(上篇)
Android 嵌套滑动机制(NestedScrolling)
从源码角度分析嵌套滑动机制NestedScrolling
优雅的嵌套滑动解决方式-NestedScroll
Android NestedScrolling机制完全解析 带你玩转嵌套滑动
嵌套滚动利器–NestedScrolling机制


相关文章:

  • 2021-12-24
  • 2021-12-16
  • 2022-12-23
  • 2022-12-23
  • 2021-12-05
  • 2021-04-26
  • 2022-01-08
  • 2022-12-23
猜你喜欢
  • 2021-12-31
  • 2021-11-16
  • 2021-05-19
  • 2021-12-05
  • 2021-06-13
  • 2021-11-16
  • 2021-12-18
相关资源
相似解决方案