文档章节

NestedScrollView分析

街角的小丑
 街角的小丑
发布于 2017/05/03 16:07
字数 2640
阅读 47
收藏 0
点赞 0
评论 0

前言

    级联滑动在现在的app设计中越来越常见,在android的老版本中,并没有添加对级联滑动的支持,但是如今,几乎所有view都会默认实现了级联滑动的功能。

    除了在源码中实现了级联滑动,为了兼容,android还在support中添加了级联滑动的接口,实际上实现方案和源码相同。为了方便分析,我们就从support入手,来看一下级联滑动的实现。

NestedScrollingChild

public interface NestedScrollingChild {
    // 参数enabled:true表示view使用嵌套滚动,false表示禁用.
    public void setNestedScrollingEnabled(boolean enabled);

    public boolean isNestedScrollingEnabled();

    // 参数axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和
    // ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动)
    // 返回值:true表示本次滚动支持嵌套滚动,false不支持
    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

    public boolean hasNestedScrollingParent();

    // 参数dxConsumed: 表示view消费了x方向的距离长度
    // 参数dyConsumed: 表示view消费了y方向的距离长度
    // 参数dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费
    // 参数dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费
    // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    // 参数dx: 表示view本次x方向的滚动的总距离长度
    // 参数dy: 表示view本次y方向的滚动的总距离长度
    // 参数consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向
    // 参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
    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);
}

    来说 NestedScrollingChild。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild 接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。

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

NestedScrollingParent

public interface NestedScrollingParent {

      // 参数child:ViewParent包含触发嵌套滚动的view的对象
      // 参数target:触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的
      // 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

    // 参数target:同上
    // 参数dxConsumed:表示target已经消费的x方向的距离
    // 参数dyConsumed:表示target已经消费的x方向的距离
    // 参数dxUnconsumed:表示x方向剩下的滑动距离
    // 参数dyUnconsumed:表示y方向剩下的滑动距离
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    // 参数dx:表示target本次滚动产生的x方向的滚动总距离
    // 参数dy:表示target本次滚动产生的y方向的滚动总距离
    // 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
    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();
}

    作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现 NestedScrollingParent,这个接口方法和 NestedScrollingChild 大致有一一对应的关系。同样,也有一个 NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。

从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()

每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。

Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。

最后,滑动结束,调用 onStopNestedScroll() 表示本次处理结束。

其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。

NestedScrollView

    NestedScrollView 作为级联滑动最典型的实现,我们可以通过它来详细了解这中间的流程。

public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
        NestedScrollingChild, ScrollingView {
..........
}

    首先该类的定义,它同时继承了Parent和Child,原因在于NestedScrollView中嵌套NestedScrollView是非常多见的,这个时候它既是child也是parent。这会对我们代码分析造成一定障碍,但是影响并不会很大。

    忽略构造,初始化等过程,我们此文的目标是滚动,所以直接来看第一个和滚动相关的方法

    第一段代码实际上并不是最开始起作用的,因为我们第一个事件一定是DOWN,所以需要先看一下对于DOWN的处理

case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                /*
                 * If being flinged and user touches the screen, initiate drag;
                 * otherwise don't. mScroller.isFinished should be false when
                 * being flinged. We need to call computeScrollOffset() first so that
                 * isFinished() is correct.
                */
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
            }

    如果点击的位置不在child中,我们intercept方法直接返回false,表示不拦截事件(这个时候就算我们不拦截,其实也会传入onTouch的吧)。

    如果当前正在滚动(正在滚动,又遇到手指按下……可能的情况就是在flinged的时候按下)那么isBeingDragged为true表示会拦截后面的事件,否则为false不拦截。然后调用startNestedScroll。

    疑问,如果该scrollview是parent的,那么它调用这个方法会不会出问题?来看下代码:

public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //递归查找parent,如果parent的onStartNestedScroll返回true,表示找到了父NestedScrolling
            // 并且调用它的onNestedScrollAccepted方法
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    这是childHelper的最终调用。如果已经有NestedScrollingParent了,那么直接返回true。如果该view允许级联滑动(NestedScrollView在初始化时就设置为允许级联了。)版本寻找它的父NestedScrolling。方法就是递归调用父控件的onStartNestedScroll(与当前版本相关,所以用到了ViewParentCompat.)

    在NestedScrollView嵌套的环境下,实际上就是调用父NestedScrollView的onStartNestedScroll方法

 @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    支持y轴,返回true。然后再调用onNestedScrollAccepted方法。一般情况下,我们只会调用parentHelper的对应方法(实际上就是记录一下滚动轴,没有其他操作),但是由于NestedScrollView同事继承了parent和child,所以会继续调用startNestedScroll用于多层(大于两层)scrollView的级联。

    假设假设子view不会拦截onTouch事件,所以还是会回传到child的onTouch方法中。onTouch方法中关于DOWN事件的操作我们本篇不关心,无非是停止滚动动画之类的,我们关心的是,onTouch方法会返回true!也就是child的onIntercept方法将不再调用。

    接下去就是Move事件了

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
..........
}

   第一关,不是isBeingDragged,所以忽略,继续往下走。

final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop
                        && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                    ...
                }

    至少在NestedScrollView嵌套的情况下

    (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0

    永远不成立,所以这段代码且略过。

    所以接下去还是child的onTouch方法。

case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                ....
                break;

    首先会调用dispatchNestedPreScroll方法,该方法意义在开篇有说。在NestedScrollView中,该方法仅仅只是级联调用,在本例情况下(双NestedScrollView嵌套)实际上最终并没有什么效果,返回false。

if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    // Calling overScrollByCompat will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                        ensureGlows();
                        final int pulledToY = oldY + deltaY;
                        if (pulledToY < 0) {
                            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                    ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {
                            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                    1.f - ev.getX(activePointerIndex)
                                            / getWidth());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    }
                }

    如果大于一个滚动阈值,那么就会进入下面操作了。

    requestDisallowInterceptTouchEvent,表示禁用父控件的onIntercept方法。所以最后的事件实际上只有最底层的NestedScrollView能够去处理,parent连onIntercept都不会调用(需要主要,如果当前的点击不是发生在子NestedScrollView的控件上的话实际上就不会涉及级联滚动的,相当于单层scrollview而已)。

    并且把isBegingDragged设置为true,表示需要开始滚动了。

    主要回去调用dispatchNestedScroll方法,通知父控件消费滚动距离。

 

© 著作权归作者所有

共有 人打赏支持
街角的小丑
粉丝 1
博文 86
码字总数 136018
作品 0
杭州
NestedScrollView 初体验

出现的原因: 一般情况下,scrollview的内部或者外部无法添加另一个scrollview “ It ( NestedScrollView ) can be used as both parent or child ScrollView . ”——网络博客的解释 “Nes...

Freewheel ⋅ 2016/04/04 ⋅ 0

NestedScrollView嵌套RecyclerView最后一条item显示不全

NestedScrollView嵌套RecyclerView最后一条item显示不全 首先要在最外层的NestedScrollView配置属性 android:fillViewport="true":...

zhangphil ⋅ 05/07 ⋅ 0

关于使用android,drawerLayout的事件

根布局是drawerLayout, 菜单是fragment 其中头是一个relativeLayout,menuItem是textView(clickable= true),整个菜单都是不能滑动的 内容区域是CoordinatorLayout 里面包含了一个可伸缩的a...

pokerWu ⋅ 2015/12/04 ⋅ 0

Android NestedScrollView嵌套多个RecyclerView的问题

最近在做一个项目,首页需要多个RecyclerView,所以我使用NestedScrollView来嵌套,如下所示。 布局文件的结构大概是这样: 在代码中,进行了如下设置: 最后代码运行,在Android5.0, 6.0都没...

ListerCi ⋅ 2017/09/04 ⋅ 0

Android NestedScrollView/ScrollView包裹ViewPager自适应高度

Android NestedScrollView/ScrollView包裹ViewPager自适应高度 当Android的NestedScrollView/ScrollView这类滚动View包裹ViewPager时候,ViewPager中的Fragment包含的又是一系列高度值不固定...

zhangphil ⋅ 05/12 ⋅ 0

Andorid - Material Design之CoordinatorLayout

老婆保佑,代码无BUG 前言 这是MD 系列的最后一篇文章,文末,会将代码上传,有兴趣的小伙伴可以一起参考 目录 与FloatingActionButton联动 与AppBarLayout layout_scrollFlags属性讲解 Coll...

Allens_Jiang ⋅ 01/03 ⋅ 0

CoordinatorLayout + AppBarLayout + ToolBar

主要参考了 http://blog.csdn.net/leejizhou/article/details/50533020, 然后做了 一个自己的例子 首先是引入sdk compile 'com.android.support:appcompat-v7:23.1.1'compile 'com.android.s......

新年 ⋅ 2016/01/18 ⋅ 0

Android Scrollview嵌套FrameLayout嵌套RecyclerView 显示不全、滑动粘滞

标题可能说不清楚这么hehe的界面布局,如图(这里找来类似的界面布局 : 网易1元XX截图): 绿色框选部分是ScrollView,红色框选部分是一个FrameLayout,里面放的是只有RecyclerView的Fragm...

yaly ⋅ 2016/12/22 ⋅ 0

可折叠-上下左右都可滑动-同时具备上拉加载下拉刷新

首先,上下滑动和左右滑动很好解决,ViewPager+TabLayout+ListView就能很好的实现。 最主要的是折叠 + 加载刷新: 一.部局折叠 我们可以使用CoordinatorLayout来实现,它主要的作用是 使用:...

王先森oO ⋅ 05/28 ⋅ 0

关于下拉刷新项目中所需要的功能(无痕过渡、loadingview出现方式、边界回弹)

PullRefreshLayout 首先吐槽一下现在流行的刷新库,一个字大,包涵个人很多集成到项目中不需要的类,也很难找到很满意的效果(无痕过渡,回弹的效果不够真实),所以自己自己动手丰衣足食,撸一...

北纬34点8度 ⋅ 2017/07/08 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Day 17 vim简介与一般模式介绍

vim简介 vi和Vim的最大区别就是编辑一个文件时vi不会显示颜色,而Vim会显示颜色。显示颜色更便于用户编辑,凄然功能没有太大的区别 使用 yum install -y vim-enhanced 安装 vim的三种常用模式...

杉下 ⋅ 52分钟前 ⋅ 0

【每天一个JQuery特效】根据可见状态确定是否显示或隐藏元素(3)

效果图示: 主要代码: <!DOCTYPE html><html><head><meta charset="UTF-8"><title>根据可见状态确定 是否显示或隐藏元素</title><script src="js/jquery-3.3.1.min.js" ty......

Rhymo-Wu ⋅ 今天 ⋅ 0

OSChina 周四乱弹 —— 初中我身体就已经垮了,不知道为什么

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @加油东溪少年 :下完这场雨 后弦 《下完这场雨》- 后弦 手机党少年们想听歌,请使劲儿戳(这里) @马丁的代码 :买了日本 日本果然赢了 翻了...

小小编辑 ⋅ 今天 ⋅ 12

浅谈springboot Web模式下的线程安全问题

我们在@RestController下,一般都是@AutoWired一些Service,由于这些Service都是单例,所以并不存在线程安全问题。 由于Controller本身是单例模式 (非线程安全的), 这意味着每个request过来,...

算法之名 ⋅ 今天 ⋅ 0

知乎Java数据结构

作者:匿名用户 链接:https://www.zhihu.com/question/35947829/answer/66113038 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 感觉知乎上嘲讽题主简...

颖伙虫 ⋅ 今天 ⋅ 0

Confluence 6 恢复一个站点有关使用站点导出为备份的说明

推荐使用生产备份策略。我们推荐你针对你的生产环境中使用的 Confluence 参考 Production Backup Strategy 页面中的内容进行备份和恢复(这个需要你备份你的数据库和 home 目录)。XML 导出备...

honeymose ⋅ 今天 ⋅ 0

JavaScript零基础入门——(九)JavaScript的函数

JavaScript零基础入门——(九)JavaScript的函数 欢迎回到我们的JavaScript零基础入门,上一节课我们了解了有关JS中数组的相关知识点,不知道大家有没有自己去敲一敲,消化一下?这一节课,...

JandenMa ⋅ 今天 ⋅ 0

火狐浏览器各版本下载及插件httprequest

各版本下载地址:http://ftp.mozilla.org/pub/mozilla.org//firefox/releases/ httprequest插件截至57版本可用

xiaoge2016 ⋅ 今天 ⋅ 0

Docker系列教程28-实战:使用Docker Compose运行ELK

原文:http://www.itmuch.com/docker/28-docker-compose-in-action-elk/,转载请说明出处。 ElasticSearch【存储】 Logtash【日志聚合器】 Kibana【界面】 答案: version: '2'services: ...

周立_ITMuch ⋅ 今天 ⋅ 0

使用快嘉sdkg极速搭建接口模拟系统

在具体项目研发过程中,一旦前后端双方约定好接口,前端和app同事就会希望后台同事可以尽快提供可供对接的接口方便调试,而对后台同事来说定好接口还仅是个开始、设计流程,实现业务逻辑,编...

fastjrun ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部