美文网首页
[Android][NestedScrollingParent和

[Android][NestedScrollingParent和

作者: lgy_gg | 来源:发表于2019-05-07 18:38 被阅读0次

原文地址:http://www.lgygg.wang/lgyblog/2019/05/07/androidnestedscrollingparent%E5%92%8Cnestedscrollingchild%E4%BD%BF%E7%94%A8%E8%AF%A6%E8%A7%A3/

1.NestedScrollingParent和NestedScrollingChild的作用

其实NestedScrolling就是父View和子View之间的一套滑动交互机制。简单点来说就是要求子View在准备滑动之前将滑动的细节信息传递给父View,父View可以决定是否部分或者全部消耗掉这次滑动,并使用消耗掉的值在子View滑动之前做自己想做的事情,子View会在父View处理完后收到剩余的没有被父View消耗掉的值,然后再根据这个值进行滑动。滑动完成之后如果子View没有完全消耗掉这个剩余的值就再告知一下父View,我滑完了,但是还有剩余的值你还要不要?

NestedScrollingChild的话,Lollipop及以上版本可滑动的View如ScrollView、ListView已经按此交互流程为我们处理好了一切,如果你需要自定义View或者要兼容Lollipop以下版本就需要自己实现上述所有逻辑,当然更好的办法是使用support包里为我们提供的,比如NestedScrollView,RecyclerView等。 NestedScrollingParent就需要我们自己去实现了,毕竟父View要实现什么酷炫的效果还是需要我们去定义的,当然,support包中也有一系列为我们准备好的Parent,就是design包中的CoordinatorLayout。

2.分析

image.png

图中的onXXX开头的方法都是父View的方法。

下面分析嵌套滚动的实现方法:

1)子View的实现

(1)创建一个final的NestedScrollingChildHelper

如果允许子View和父View进行嵌套滑动交互,那么不要忘了调用setNestedScrollingEnabled方法,设置为true,如果没有设置为true,那么子View调用startNestedScroll方法的时候,父View是不会有任何反应的。下面的代码中,在子View的构造方法里调用了setNestedScrollingEnabled(true),其实在子View创建实例对象后再调用setNestedScrollingEnabled(true)也没有问题。

 //1(步骤一)创建一个final的NestedScrollingChildHelper
private final NestedScrollingChildHelper mChildHelper;

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

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

public ChildView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mChildHelper = new NestedScrollingChildHelper(this);
    setNestedScrollingEnabled(true);
}

(2)调用NestedScrollingChildHelper对象的同名方法

在子View里实现NestedScrollingChild的接口,并调用NestedScrollingChildHelper同名方法。如下方法:

public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);

操作如下:

@Override
public void setNestedScrollingEnabled(boolean enabled) {
    mChildHelper.setNestedScrollingEnabled(enabled);
    showLog("----------------setNestedScrollingEnabled");
}

@Override
public boolean isNestedScrollingEnabled() {
    showLog("----------------isNestedScrollingEnabled");
    return mChildHelper.isNestedScrollingEnabled();
}

@Override
public boolean startNestedScroll(int axes) {
    showLog("----------------startNestedScroll");
    return mChildHelper.startNestedScroll(axes);
}

/**
 * 当抬起手势的时候,默认会调用stopNestedScroll
 */
@Override
public void stopNestedScroll() {
    mChildHelper.stopNestedScroll();
    showLog("----------------stopNestedScroll");
}

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

/**
 * 当子类调用dispatchNestedScroll分发消费距离给父View的时候,
 * 会触发父View的onNestedScroll方法
 * @param dxConsumed
 * @param dyConsumed
 * @param dxUnconsumed
 * @param dyUnconsumed
 * @param offsetInWindow
 * @return
 */
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    showLog("----------------dispatchNestedScroll");
    return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}

/**
 * 一般在子类里的onTouchEvent方法里调用,当子类调用dispatchNestedPreScroll分发消费距离给父View的时候,
 * 会触发父View的onNestedPreScroll方法,如果父View要消费,那么子View则不进行任何滚动操作,
 * 如果父View不消费,子View自己消费滚动操作
 * @param dx
 * @param dy
 * @param consumed
 * @param offsetInWindow
 * @return
 */
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    showLog("----------------dispatchNestedPreScroll");
    return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

/**
 * 当子View ACTION_UP时可能伴随着fling的产生,如果产生了fling,就需要子View在stopNestedScroll
 * 前调用public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY)和
 * public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed),
 * 父View对应的会被回调public boolean onNestedPreFling(View target, float velocityX, float velocityY)和
 * public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
 * @param velocityX
 * @param velocityY
 * @param consumed
 * @return
 */
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    showLog("----------------dispatchNestedFling");
    return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    showLog("----------------dispatchNestedPreFling");
    return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
(3)在onTouchEvent里定义手势操作,决定谁消费滚动距离
    float moveYs;//移动手势其点
    boolean isStartNestedScrloll = false;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float downY;
        float downX;
        float moveY;
//        float moveYs = 0;//移动手势其点
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getRawX();
                //moveYs定义必须放到onTouchEvent外边,否则moveYs拿到的是默认值,
                // 因为MotionEvent.ACTION_DOWN和MotionEvent.ACTION_MOVE手势的触发都是会重新调用onTouchEvent
                //这就导致moveYs = 0
                moveYs = downY = event.getRawY();
                isStartNestedScrloll = startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                showLog( "ACTION_DOWN x:"+downX+" y:" + downY);
                break;
            case MotionEvent.ACTION_MOVE:
                moveY = event.getRawY();
                float dy = moveY - moveYs;
                moveYs = moveY;
                //如果开始嵌套滚动且分发给父View的距离被父View消耗的话,那么子View不做任何操作,否则子View自己进行消化,执行scrollBy
                if (!(isStartNestedScrloll&&dispatchNestedPreScroll(0,(int)dy, consumed, offset))) {
                    showLog( "ACTION_DOWN dy:"+dy);
                    scrollBy(0, -(int)dy);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        super.onTouchEvent(event);
        return true;
    }

2)父View的实现

(1)创建一个final的NestedScrollingParentHelper
//1(步骤一)创建一个final的NestedScrollingParentHelper
private final NestedScrollingParentHelper mParentHelper;
public ParentLayout(Context context) {
    this(context, null);
}

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

public ParentLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mParentHelper = new NestedScrollingParentHelper(this);
}
(2)调用NestedScrollingParentHelper对象的同名方法

在父View里实现NestedScrollingParent的接口,并调用NestedScrollingParentHelper同名方法。如下方法:

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 onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();

其中,onNestedScrollAccepted和onStopNestedScroll方法需要调用NestedScrollingParentHelper的同名方法。

/*
 * 如果父View的onStartNestedScroll方法返回值是true,那么onNestedScrollAccepted方法将会被调用
 *
 */
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    showLog("----------------onNestedScrollAccepted");
}

/**
 * 当允许和父View进行嵌套滚动交互,且onStartNestedScroll返回true的时候,
 * 当子View抬起手势的时候,默认会调用onStartNestedScroll
 * 当子View抬起手势的时候的日志:
 * 05-07 11:05:30.480 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onStartNestedScroll
 * 05-07 11:05:30.480 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onNestedScrollAccepted
 * 05-07 11:05:30.530 16320-16320/com.tan.lgy.realizeas E/ParentLayout: ----------------onStopNestedScroll
 * @param target
 */
@Override
public void onStopNestedScroll(View target) {
    mParentHelper.onStopNestedScroll(target);
    showLog("----------------onStopNestedScroll");
}
(3)复写onStartNestedScroll方法
//(步骤二)调用NestedScrollingParentHelper对象的同名方法。
/*
 * 当子View调用startNestedScroll方法的时候,如果子View调用setNestedScrollingEnabled(true),
 * 允许和父View进行嵌套滚动交互(默认是false),那么父View会回调onStartNestedScroll方法
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    showLog("----------------onStartNestedScroll");
    if (target instanceof ChildView) {
        return true;
    }
    return false;
}
(4)复写onNestedPreScroll,onNestedScroll,onNestedFling或onNestedPreFling方法

这里需要复写onNestedPreScroll,onNestedScroll,onNestedFling或onNestedPreFling里的哪个方法,完全是根据需求来决定:

如果子View调用了dispatchNestedPreScroll方法,那么父View对应的就需要实现onNestedPreScroll方法;

如果子View调用了dispatchNestedScroll方法,那么父View对应的就需要实现onNestedScroll方法;

如果子View调用了dispatchNestedFling方法,那么父View对应的就需要实现onNestedFling方法;

如果子View调用了dispatchNestedPreFling方法,那么父View对应的就需要实现onNestedPreFling方法。

如下,我再子View里调用了dispatchNestedPreScroll方法,那么在父View里就需要复写onNestedPreScroll。在这个方法里,决定了父View要不要消费子View的滚动距离,通过consumed数组来告诉子View, consumed[0]表示x轴上的距离,consumed[1]表示y轴上的距离。

/**
 * 当子类调用dispatchNestedPreScroll分发消费距离给父View的时候,
 * 会触发父View的onNestedPreScroll方法,如果父View要消费,
 * 那么子View则不进行任何滚动操作,如果父View不消费,子View自己消费滚动操作
 * @param target
 * @param dx
 * @param dy
 * @param consumed
 */
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    showLog("----------------onNestedPreScroll");
    if (showImg(dy) || hideImg(dy)) {//如果需要显示或隐藏图片,即需要自己(parent)滚动
        scrollBy(0, -dy);//滚动
        consumed[1] = dy;//告诉child我消费了多少
    }
}
(5)限制父View的活动范围

如下,会将父View的y轴方向滚动范围限制在0 – imgHeight里,

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

3.总结

1) Lollipop及以上版本的所有View都已经支持了NestedScrolling,Lollipop之前版本可以通过Support包进行向前兼容,NestedScrollingParent和NestedScrollingChild都是接口,分别对应ViewGroup和View中新增的方法,在Lollipop以下版本中,我们需要手动添加这两个接口的实现。

2) 当子View调用startNestedScroll方法的时候,如果子View调用setNestedScrollingEnabled(true), 允许和父View进行嵌套滚动交互(默认是false),那么父View会回调onStartNestedScroll方法。如果父View的onStartNestedScroll方法返回值是true,那么父类的onNestedScrollAccepted方法将会被调用。

3) 如果子View调用了dispatchNestedPreScroll方法,那么父View对应的就需要实现onNestedPreScroll方法。

4) 如果子View调用了dispatchNestedScroll方法,那么父View对应的就需要实现onNestedScroll方法。

5) 如果子View调用了dispatchNestedFling方法,那么父View对应的就需要实现onNestedFling方法。

6) 如果子View调用了dispatchNestedPreFling方法,那么父View对应的就需要实现onNestedPreFling方法。

4.参考文章

https://www.jianshu.com/p/f09762df81a5

相关文章

网友评论

      本文标题:[Android][NestedScrollingParent和

      本文链接:https://www.haomeiwen.com/subject/ocbyoqtx.html