Android嵌套滑动逻辑浅析

栏目: Android · 发布时间: 5年前

内容简介:嵌套滑动一直是Android中比较棘手的问题, 根本原因是Android的事件分发机制导致的.导致嵌套滑动难处理的关键原因在于不过这个问题终于在LOLLIPOP(SDK21)之后终于有了官方的解决方法, 就是嵌套滑动机制. 在分析具体的代码逻辑之前, 下面先简单介绍下嵌套滑动的一些基本知识.嵌套滑动机制可以理解为一个约定, 原生的支持嵌套滑动的控件都是依据这个约定来实现嵌套滑动的, 例如CoordinatorLayout, 所以如果你自定义的控件也遵守这个约定, 那么就可以跟原生的控件进行嵌套滑动了.

问题分析

嵌套滑动一直是Android中比较棘手的问题, 根本原因是Android的事件分发机制导致的.导致嵌套滑动难处理的关键原因在于 当子控件消费了事件, 那么父控件就不会再有机会处理这个事件了 , 所以一旦内部的滑动控件消费了滑动操作, 外部的滑动控件就再也没机会响应这个滑动操作了.

嵌套滑动

不过这个问题终于在LOLLIPOP(SDK21)之后终于有了官方的解决方法, 就是嵌套滑动机制. 在分析具体的代码逻辑之前, 下面先简单介绍下嵌套滑动的一些基本知识.

嵌套滑动机制可以理解为一个约定, 原生的支持嵌套滑动的控件都是依据这个约定来实现嵌套滑动的, 例如CoordinatorLayout, 所以如果你自定义的控件也遵守这个约定, 那么就可以跟原生的控件进行嵌套滑动了.

基本原理

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

通过这样的嵌套滑动机制, 在一次滑动操作过程中

父控件和子控件都有机会对滑动操作作出响应, 尤其父控件能够分别 在子控件处理滑动距离之前和之后 对滑动距离进行响应.

这解决了事件分发机制缺点引起的问题.

版本之别

在看具体的代码之前先说下嵌套滑动相关方法的一些我认为值得注意的地方.

LOLLIPOP(SDK21)之后

为什么说这个是官方的解决方法? 因为

嵌套滑动的相关逻辑作为 普通方法 直接写进了最新的(SDK21之后) ViewViewGroup 类.

普通方法是指这个方法不是继承自接口或者其他类, 例如View#dispatchNestedScroll, 可以看到官方标注了 Added in API level 21 标示, 也就是说这是在SDK21版本之后添加进去的一个普通方法.

向前兼容

而SDK21之前的版本

官方在 android.support.v4 兼容包中提供了两个接口 NestedScrollingChildNestedScrollingParent , 还有两个辅助类 NestedScrollingChildHelperNestedScrollingParentHelper 来帮助控件实现嵌套滑动.

这个兼容的原理很简单

两个接口 NestedScrollingChildNestedScrollingParent 分别定义上面提到的 ViewViewParent 新增的普通方法

在嵌套滑动中会要求控件要么是继承于SDK21之后的 ViewViewGroup , 要么实现了这两个接口, 这是控件能够进行嵌套滑动的前提条件.

那么怎么知道调用的方法是控件自有的方法, 还是接口的方法? 在代码中是通过 ViewCompatViewParentCompat 类来实现.

ViewCompatViewParentCompat 通过当前的 Build.VERSION.SDK_INT 来判断当前版本, 然后选择不同的实现类, 这样就可以根据版本选择调用的方法.

例如如果版本是SDK21之前, 那么就会判断控件是否实现了接口, 然后调用接口的方法, 如果是SDK21之后, 那么就可以直接调用对应的方法.

辅助类

除了接口兼容包还提供了 NestedScrollingChildHelperNestedScrollingParentHelper 两个辅助类, 这两个辅助类实际上就是对应 ViewViewParent 中新增的普通方法, 代码就不贴了, 简单对比下就可以发现, 对应方法实现的逻辑基本一样, 所以

只要在接口方法内对应调用辅助类的方法就可以兼容嵌套滑动了.

例如在 NestedScrollingChild#startNestedScroll 方法中调用 NestedScrollingChildHelper#startNestedScroll .

题外话: 这里实际用了代理模式来让SDK21之前的控件具有了新增的方法.

默认处理逻辑

虽然 ViewViewGroup (SDK21之后)本身就具有嵌套滑动的相关方法, 但是默认情况是是不会被调用, 因为 ViewViewGroup 本身不支持滑动, 所以

本身不支持滑动的控件即使有嵌套滑动的相关方法也不能进行嵌套滑动.

上面已经说到要让控件支持嵌套滑动

  • 首先要控件类具有嵌套滑动的相关方法, 要么仅支持SDK21之后版本, 要么实现对应的接口, 为了兼容低版本, 更常用到的是后者.

  • 因为默认的情况是不会支持滑动的, 所以控件要在合适的位置主动调起嵌套滑动的方法.

接下来通过分析相对简单的支持嵌套滑动的容器 NestedScrollView 来了解下怎样主动调起嵌套滑动的方法, 以及嵌套滑动的具体逻辑.

相关方法

先简单看看相关方法的作用, 更具体的说明建议看源码注释中的方法说明.

注意 : 下文分析用内控件表示两层嵌套中的子控件, 外控件表示嵌套中的父控件.

NestedScrollingChild

startNestedScroll : 起始方法, 主要作用是找到接收滑动距离信息的外控件.

dispatchNestedPreScroll : 在内控件 处理滑动前 把滑动信息分发给外控件.

dispatchNestedScroll : 在内控件 处理完滑动后 把剩下的滑动距离信息分发给外控件.

stopNestedScroll : 结束方法, 主要作用就是清空嵌套滑动的相关状态

setNestedScrollingEnabledisNestedScrollingEnabled : 一对get&set方法, 用来判断控件是否支持嵌套滑动.

dispatchNestedPreFlingdispatchNestedFling : 跟Scroll的对应方法作用类似, 不过分发的不是滑动信息而是Fling信息.(这个Fling好难翻译.. =。=)本文主要关注滑动的处理, 所以后续不分析这两个方法.

从方法名就可以看出

内控件是嵌套滑动的发起者.

NestedScrollingParent

因为内控件是发起者, 所以外控件的大部分方法都是被内控件的对应方法回调的.

onStartNestedScroll : 对应 startNestedScroll , 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.

onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调, 可以让外控件针对嵌套滑动做一些前期工作.

onNestedPreScroll : 关键方法, 接收内控件 处理滑动前 的滑动距离信息, 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.

onNestedScroll : 关键方法, 接收内控件 处理完滑动后 的滑动距离信息, 在这里外控件可以选择是否处理剩余的滑动距离.

onStopNestedScroll : 对应 stopNestedScroll , 用来做一些收尾工作.

getNestedScrollAxes : 返回嵌套滑动的方向, 区分横向滑动和竖向滑动, 作用不大

onNestedPreFlingonNestedFling : 同上略

外控件通过 onNestedPreScrollonNestedScroll 来接收内控件响应滑动前后的滑动距离信息.

再次指出, 这两个方法是实现嵌套滑动效果的关键方法.

从NestedScrollView看嵌套机制

说完上面一大通, 终于可以开始分析源码来了解嵌套滑动机制起作用的具体逻辑了.

NestedScrollView 简单地说就是支持嵌套滑动的 ScrollView , 内部逻辑简单, 而且它既可以是内控件, 也可以是外控件, 所以选择分析它来了解嵌套滑动机制.

注意 : 因为 NestedScrollingChildHelperNestedScrollingParent 这两个辅助类的实现跟 ViewViewGroup 中的对应方法是一样的, 而且 ViewViewGroup 的源码没有使用兼容类, 所以下面 分析相关方法的时候源码都使用 ViewViewGroup 中的代码 .

上面已经说了嵌套滑动是从 startNestedScroll 开始, 所以先看看哪里调用了这个方法, 在源码里一搜就能知道有两个地方调用了这个方法.

  • onInterceptTouchEventACTION_DOWN 的情况

  • onTouchEventACTION_DOWN 的情况

因为 ACTION_DOWN 是滑动操作的开始事件, 所以当接收到这个事件的时候尝试找对应的外控件. 只有找到了外控件才有后续的嵌套滑动的逻辑发生.

关于 NestedScrollView 在这里的实现其实有个奇怪的地方, 提出一个问题, 不感兴趣的可以直接跳过这段.

  • 既然内控件是发起者, 为什么要在 onInterceptTouchEvent 也调用 startNestedScroll 呢?

因为事件传递的时候会先执行外控件的 onInterceptTouchEvent , 也就是说第一个执行 startNestedScroll 的是最外层的 NestedScrollView , 即使它找到了对应的外控件后续如果有子控件消费了这个事件, 也就是说不执行 onTouchEvent 方法, 那么找到外控件也没用的, 不清楚设计者的意图.

接着我们看 startNestedScroll 是如何找对应的外控件的, 因为 NestedScrollView#startNestedScroll 调用了辅助方法的 startNestedScroll , 所以下面直接贴 View#startNestedScroll .

// View.javapublic 
boolean startNestedScroll(int axes) { 
    // ... 
    if (isNestedScrollingEnabled()) { 
        ViewParent p = getParent(); 
        View child = this; 
        while (p != null) { 
            try { 
                // 关键代码 
                if (p.onStartNestedScroll(child, this, axes)) { 
                    mNestedScrollingParent = p; 
                    p.onNestedScrollAccepted(child, this, axes); 
                    return true; 
                }
            } catch (AbstractMethodError e) { 
                // ... 
            } 
            if (p instanceof View) { 
                child = (View) p; 
            } 
            p = p.getParent(); 
        } 
    } 
    return false;
}复制代码

非常简单的逻辑遍历父控件, 调用父控件的 onStartNestedScroll , 返回 true 表示找到了对应的外控件, 找到外控件后马上调用 onNestedScrollAccepted

从这里可以知道

外控件不一定是内控件的直接父控件, 但一定是 最近 的符合条件的外控件.

还可以确定了上面关于 onStartNestedScroll 的方法说明, 返回 true 表示接收内控件的滑动信息.对于 NestedScrollView#onStartNestedScroll 内部逻辑很简单, 只要是竖直滑动方向就返回 true , 所以可以知道

NestedScrollView 不支持横向嵌套滑动.

接着被调用的是 onNestedScrollAccepted , 看 NestedScrollView#onNestedScrollAccepted

// NestedScrollView.java
@Overridepublic void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}复制代码

辅助类的方法很简单, 就是记录当前的滑动方向, 在这里 NestedScrollView 又调用 startNestedScroll 来找它自己的外控件, 这是为了连续嵌套 NestedScrollView , 不过这是 NestedScrollView 自己的实现, 不管它.

找到了外控件后 ACTION_DOWN 事件就没嵌套滑动的事了, 要滑动肯定会在 onTouchEvent 中处理 ACTION_MOVE 事件, 接着我们看 ACTION_MOVE 事件是怎样处理的.

// NestedScrollView#onTouchEvent
case MotionEvent.ACTION_MOVE: 
    // ... 
    final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); 
    int deltaY = mLastMotionY - y; 
    // 让外控件先处理滑动距离 
    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 
        deltaY -= mScrollConsumed[1];// 消耗滑动距离 
        // ... 
    } 
    // ... 
    if (mIsBeingDragged) { 
        // ... 
        // 内控件处理滑动距离 
        if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 
                      0, true) && !hasNestedScrollingParent()) { 
            // ... 
        } 

        final int scrolledDeltaY = getScrollY() - oldY; 
        final int unconsumedY = deltaY - scrolledDeltaY; 
        if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { 
            // ... 
        } 
        // ... 
    } 
    break;复制代码

这部分是 NestedScrollView 能够处理嵌套滑动的关键代码了, 其他能够嵌套滑动的控件也应该在 ACTION_MOVE 中类似地处理滑动距离.

先计算出本次滑动距离 deltaY , 这里有个小细节

deltaY 等于上一次的Y坐标减去这次的Y坐标, 这意味着在相关方法中接收到的滑动距离参数中, 滑动距离 > 0表示手指向下滑动, 反之表示手指向上滑动. 这是因为在屏幕中Y轴正方向是向下的.

得到滑动距离 deltaY 后, 先把它传给 dispatchNestedPreScroll , 然后在结果返回 true 的时候, delta 会减去 mScrollConsumed[1] .

接着看 dispatchNestedPreScroll 干了什么

// View.java
public boolean dispatchNestedPreScroll(int dx, int dy,
                     @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
    // ... 忽略状态判断 
    consumed[0] = 0; 
    consumed[1] = 0; 
    mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); 
    return consumed[0] != 0 || consumed[1] != 0; 
    // 其他情况返回false
}复制代码

忽略条件判断和 offsetInWindow 的相关处理, 先指出 consumed 就是上一步分析中的 mScrollConsumed , dy 就是 deltaY .

因为 dispatchNestedPreScroll 的工作就是把滑动距离在内控件处理前分发给外控件, 所以这里的关键代码也很简单, 就是直接把相关的参数传给外控件的 onNestedPreScroll , 然后只要外控件消耗了滑动距离(不论横向还是竖向), 就会返回 true

所以

外控件如果想在内控件之前消耗滑动距离仅需要在 onNestedPreScroll 把消耗的值放到数组中返回给内控件.

onNestedPreScroll 是决定外控件的嵌套滑动逻辑的关键方法, 在不同的控件中应该是根据需要有不同的实现的 , 而在 NestedScrollView 中就是直接询问它自己的外控件是否消耗滑动距离, 实现比较简单就不贴代码了.

在这里提醒下, 在我们自己修改嵌套滑动逻辑的时候需要 注意滑动距离的正负号和内控件处理 consumed 数组的方式 . 不过这些都是些数字游戏, 不细说了.

好了, 现在外控件已经比内控件先处理了滑动距离了, 如果外控件没有完全消耗掉所有滑动距离, 这时该内控件处理剩下的滑动距离了, 不同的控件有不同的滑动实现, 在 NestedScrollView 中通过 NestedScrollView#overScrollByCompat 来进行滑动, 并且滑动结束后通过比对滑动前后的 scrollY 值得到了内控件消耗的滑动距离, 然后得到剩下的滑动距离, 最后传给 dispatchNestedScroll .

dispatchNestedScroll 的逻辑跟 dispatchNestedPreScroll 几乎一样, 区别是它调用了外控件的 onNestedScroll , 因为到这里已经是处理滑动距离最后的机会了, 所以 onNestedScroll 不会再影响内控件的处理逻辑了.

到这里 ACTION_MOVE 事件就分析完毕了.

最后就是 stopNestedScroll 了, 代码就不贴了, 调用这个方法基本是新的滑动操作开始前, 或者滑动操作结束/取消, 代码逻辑就是进行一些变量的重置工作和调用 onStopNestedScroll , 而 onStopNestedScroll 也类似.

整个嵌套滑动的基本逻辑就是这样. 注意这里虽然分析的是 NestedScrollView , 但这代表了嵌套滑动的"约定"处理方式, 虽然不同的控件实际的实现会有不同不过应该遵循基本方法的调用顺序, 确保参数的含义和参数的处理方式.

总结

  • 如果要支持嵌套滑动, 内控件和外控件要支持对应的方法, 为了兼容低版本一般通过实现 NestedScrollingChildNestedScrollingParent 接口以及使用 NestedScrollingChildHelperNestedScrollingParent 辅助类.

  • 具体嵌套滑动逻辑主要是在 onNestedPreScrollonNestedScroll 方法中.

  • 父控件通过给数组赋值来把消耗的滑动距离传递给内控件.

感谢原创作者的独到的剖析!!

本文转载自:http://www.apkbus.com/blog-977752-79583.html


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

突破——程序员如何练就领导力

突破——程序员如何练就领导力

刘朋 / 电子工业出版社 / 2018-8-31 / 55.00元

内容简介: 在今日中国如雨后春笋般出现的各种新兴的互联网和软件公司中,有越来越多的技术达人凭借在技术上的优异表现而被晋升为技术团队的管理者和领导者。然而,从技术到管理——从单枪匹马的个人贡献者到一呼百应的技术团队领导者——注定是“惊险的一跃”。对于刚走上技术团队管理岗位的技术专家,你一定遇到过和本书作者当年一样的各种困惑和不适“症状”: ——我能处理好人“机”关系,但是如何处理好人际关......一起来看看 《突破——程序员如何练就领导力》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器