通俗易懂的小例子来演示如何使用NestedScroll

写在前面

最近遇到了一个问题,在SwipeRefreshLayout中,有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题,而且跟嵌套滑动似乎很有关联,我们看,public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild,可以看出SwipeRefreshLayout 即实现了NestedScrollingParent也实现了NestedScrollingChild,那先从这个角度着手,看看NestedScroll是个什么玩意儿。

学习一个

先来看看这两篇文章

这里摘抄几句关于NestedScrollingChild比较重要的:

需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()

关于NestedScrollingParent的:

从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll()回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()
每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
最后,滑动结束,调用 onStopNestedScroll()表示本次处理结束。

下面的内容是假定大家已经把上面两篇文章看完了。

我的例子

其实上面两篇文章已经写明白了,但有点不足的是,没有一个通俗易懂的例子来演示。所以如果各位还不是太清楚的话,可以通过下面的例子来理解。

先来看一个图。


嵌套滑动效果

这是一整次的滑动,橙色的为子View,蓝色的为父View。我们将子View往上滑的时候,先是父View带着子View一起向上滑动,等父View到了顶之后,子View开始滑动。

大概的原理是,滑动事件在子View中的时候,先让父View进行滑动的处理,然后子View去处理未被父View消费的距离。

在代码中是这么处理的。
1. 首先,子View是肯定需要实现NestedScrollingChild的,然后重写onTouchEvent方法,。。。
2.

得,不解释了。Talk is plain. Show you the codes.

下面是子View的实现。


public class NestedChildView extends View implements NestedScrollingChild {

    public static final String TAG = "NestedChildView";

    private final NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
    private float downY;

    private int[] consumed = new int[2];
    private int[] offsetInWindow = new int[2];


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


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

    }

    private void init() {
        setNestedScrollingEnabled(true);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int actionMasked = MotionEventCompat.getActionMasked(event);

        // 取第一个接触屏幕的手指Id
        final int pointerId = MotionEventCompat.getPointerId(event, 0);
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:

                // 取得当前的Y,并赋值给lastY变量
                downY = getPointerY(event, pointerId);
                // 找不到手指,放弃掉这个触摸事件流
                if (downY == -1) {
                    return false;
                }

                // 通知父View,开始滑动
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            case MotionEvent.ACTION_MOVE:

                // 获得当前手指的Y
                final float pointerY = getPointerY(event, pointerId);

                // 找不到手指,放弃掉这个触摸事件流
                if (pointerY == -1) {
                    return false;
                }

                // 计算出滑动的偏移量
                float deltaY = pointerY - downY;

                Log.d(TAG, String.format("downY = %f",deltaY));

                Log.d(TAG, String.format("before dispatchNestedPreScroll, deltaY = %f", deltaY));

                // 通知父View, 子View想滑动 deltaY 个偏移量,父View要不要先滑一下,然后把父View滑了多少,告诉子View一下
                // 下面这个方法的前两个参数为在x,y方向上想要滑动的偏移量
                // 第三个参数为一个长度为2的整型数组,父View将消费掉的距离放置在这个数组里面
                // 第四个参数为一个长度为2的整型数组,父View在屏幕里面的偏移量放置在这个数组里面
                // 返回值为 true,代表父View有消费任何的滑动.
                if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {

                    // 偏移量需要减掉被父View消费掉的
                    deltaY -= consumed[1];
                    Log.d(TAG, String.format("after dispatchNestedPreScroll , deltaY = %f", deltaY));

                }

                // 上面的 (int)deltaY 会造成精度丢失,这里把精度给舍弃掉
                if(Math.floor(Math.abs(deltaY)) == 0) {
                    deltaY = 0;
                }

                // 这里移动子View,下面的min,max是为了控制边界,避免子View越界
                setY(Math.min(Math.max(getY() + deltaY, 0), ((View) getParent()).getHeight() - getHeight()));


                break;
        }
        return true;
    }

    /**
     * 这个方法通过pointerId获取pointerIndex,然后获取Y
     *
     */
    private float getPointerY(MotionEvent event, int pointerId) {
        final int pointerIndex = MotionEventCompat.findPointerIndex(event, pointerId);
        if (pointerIndex < 0) {
            return -1;
        }
        return MotionEventCompat.getY(event, pointerIndex);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        Log.d(TAG, String.format("setNestedScrollingEnabled , enabled = %b", enabled));
        childHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        Log.d(TAG, "isNestedScrollingEnabled");
        return childHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        Log.d(TAG, String.format("startNestedScroll , axes = %d", axes));
        return childHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        Log.d(TAG, "stopNestedScroll");
        childHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        Log.d(TAG, "hasNestedScrollingParent");
        return childHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        final boolean b = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
        Log.d(TAG, String.format("dispatchNestedScroll , dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d, offsetInWindow = %s", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, Arrays.toString(offsetInWindow)));
        return b;
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        final boolean b = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
        Log.d(TAG, String.format("dispatchNestedPreScroll , dx = %d, dy = %d, consumed = %s, offsetInWindow = %s", dx, dy, Arrays.toString(consumed), Arrays.toString(offsetInWindow)));
        return b;
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        Log.d(TAG, String.format("dispatchNestedFling , velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
        return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        Log.d(TAG, String.format("dispatchNestedPreFling , velocityX = %f, velocityY = %f", velocityX, velocityY));
        return childHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

可以看到,NestedScrollingChild接口中的方法,都委托给NestedScrollingChildHelper去实现了,根本就不用我们来做。其实在Lollipop版本以上,View中是有这些方法的,只是我们要兼容Lollipop以下的版本,所以要自己来实现这个接口。

主要的逻辑,就在onTouchEvent方法中了。如果之前有重写过这个方法的经验,其实一点都不复杂。

  1. ACTION_DOWN中,记录了一个按下的位置。
  2. ACTION_MOVE中,计算出偏移量,然后将这个偏移量,通过dispatchNestedPreScroll方法,传递给父View(当然,是需要实现NestedScrollingParent的父View),稍后会贴出父View中,在收到通知后,是怎么处理的。
  3. 如果被有被父View消费,那么偏移量需要减去被父View消费掉的。
  4. 根据偏移量移动子View。

下面看父View是怎么实现的。

public class NestedParentView extends FrameLayout implements NestedScrollingParent {

    public static final String TAG = NestedParentView.class.getSimpleName();

    private NestedScrollingParentHelper parentHelper;

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

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

    {
        parentHelper = new NestedScrollingParentHelper(this);

    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d(TAG, String.format("onStartNestedScroll, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
        return true;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        Log.d(TAG, String.format("onNestedScrollAccepted, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
        parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        Log.d(TAG, "onStopNestedScroll");
        parentHelper.onStopNestedScroll(target);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.d(TAG, String.format("onNestedScroll, dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed));
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // 应该移动的Y距离
        final float shouldMoveY = getY() + dy;

        // 获取到父View的容器的引用,这里假定父View容器是View
        final View parent = (View) getParent();

        int consumedY;
        // 如果超过了父View的上边界,只消费子View到父View上边的距离
        if (shouldMoveY <= 0) {
            consumedY = - (int) getY();
        } else if (shouldMoveY >= parent.getHeight() - getHeight()) {
            // 如果超过了父View的下边界,只消费子View到父View
            consumedY = (int) (parent.getHeight() - getHeight() - getY());
        } else {
            // 其他情况下全部消费
            consumedY = dy;
        }

        // 对父View进行移动
        setY(getY() + consumedY);

        // 将父View消费掉的放入consumed数组中
        consumed[1] = consumedY;

        Log.d(TAG, String.format("onNestedPreScroll, dx = %d, dy = %d, consumed = %s", dx, dy, Arrays.toString(consumed)));
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        Log.d(TAG, String.format("onNestedFling, velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
        return true;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        Log.d(TAG, String.format("onNestedPreFling, velocityX = %f, velocityY = %f", velocityX, velocityY));
        return true;
    }

    @Override
    public int getNestedScrollAxes() {
        Log.d(TAG, "getNestedScrollAxes");
        return parentHelper.getNestedScrollAxes();
    }
}

其实也很清晰,接口NestedScrollingParent部分委托给NestedScrollingParentHelper实现,在本例中,我们重点关注onNestedPreScroll这个方法。这个方法就是在子View中调用dispatchNestedPreScroll之后被调用,除了参数offsetInWindow由Helper类控制,其他的参数都是一样的。

父View获取到子View给的dy之后,看要消费多少,把消费的量设置到consumed数组中即可,很简单。


至此这个小例子就写完了,希望能让大家有所启发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容