android ui 学习系列 -自定义Behavior (1) - 相关原理

写在开头

不吐不快啊,关于自定义Behavior 这个东东真是让我挠头啊,N 多回调方法,看了很多资料,这 N 多回调方法差不多清楚了,但是又出来一个 view 依赖绑定,概念有依赖和依赖的 view,到底谁提供滚动事件,谁来消费滚动事件,这个仔细看了那是很多资料才算是明白。那么问题又来了,2个 view 要建立依赖关系,才能实现滚动联动,那么为啥我写一个自定义 Behavior 消费滚动事件了,我这个自定义 Behavior 没有和任何 view 实现依赖绑定啊......

这里面一堆的相关概念和原理,真是让人挠头啊,真是费了不少时间找资料,才总算是搞明白了,在这里说一句 : 真 NM 费劲 !!!

在这里非常感谢这篇文章:

把 5.0 的 nestedScrolling 嵌套滚动和Behavior解释的很清楚。下面的内容我也是把文章里面 大段的理论简单描述一下,便于理解,还是推荐大家去详细看这篇文章,想要搞懂 5.0 的 nestedScrolling 嵌套滚动,非这篇博文不行。


NestedScrollingParent # NestedScrollingChild

在以前,我们要实现控件滚动间的联动,只能去监听滚动控件,在这个滚动控件上注册监听器,或者是写一个自定义 view,在滚动事件里去操作其他的 view,实现状态变换。这样呢滚动效果代码就和具体的业务代码放在一起了,无法分离,自定义 view 的方式也是很不灵活,所以呢随着 5.0的推出,在 MD 中 google 推出了一套新的滚动监听套件,就是标题中的 NestedScrollingParent / NestedScrollingChild

对于控件滚动间的联动效果来说,分2种角色:一种是产生发送滚动事件,另一种是消费滚动事件,因此 google 对这2种角色分别抽象除了接口:

  • NestedScrollingChild :产生发送滚动事件
  • NestedScrollingParent :消费滚动事件

其中分别有 NestedScrollingParentHelper / NestedScrollingChildrenHelper辅助类来帮助处理的大部分逻辑


NestedScrollView.png

借着这张图我们可以看到这2个接口全部信息和所有回调方法。

child 的滚动逻辑如下:

滚动的简单逻辑顺序:

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

从事件分发的角度来看:

  • case MotionEvent.ACTION_DOWN:
    • child 先回调 startNestedScroll 方法,里面传入的是滚动方向,startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
    • startNestedScroll 方法又会去询问 Parent 的 onStartNestedScroll / onNestedScrollAccepted 方法,只要 Parent 愿意优先处理这次的滑动事件,在结束的时候 Parent 还会收到 onStopNestedScroll 回调
  • case MotionEvent.ACTION_MOVE:
    • 在消费滚动事件之前,会提供一个让 Parent 实现联合滚动的机会,因此在 child 滚动之前, Parent 可以消费一部分或者全部的滑动事件,注意若是 parent 先消费了部分滚动数值, child 是无效再去消费这部分滚动数值的
      • child 调用 dispatchNestedPreScroll 方法
      • dispatchNestedPreScroll 方法会调用 parent 的 onNestedPreScroll
    • 在 child 消费完滚动数值后,会再去询问 parent 还要不要滚动
      • child 调用 dispatchNestedScroll 方法
      • dispatchNestedScroll 方法会调用 parent 的 onNestedScroll 方法
  • case MotionEvent.ACTION_CANCEL | MotionEvent.ACTION_UP:
    • 在滚动结束后,会分别调用 child 和 parent 的 stopNestedScroll方法
      • stopNestedScroll();

parent 的滚逻辑如下:

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

  • 每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll() ,这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中消费掉 Child 的 Scroll 事件,也就是优先于 Child 滑动

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

  • 最后,滑动结束 Child 调用 stopNestedScroll,回调 Parent 的 onStopNestedScroll() 表示本次处理结束

其实不写 parent 的逻辑思路 ,单看 child 的也能知道的。

总之我写的比较简单,这样便于理解,想看详细的去看上面贴出的地址,那篇文章写的很详细。这就是 Google 新的嵌套滚动的核心,在具体滚动控件消费滚动数值的前后都去问问有谁需要消费滚动数值,这样就实现了联动的效果。你可以选择我们先消费滚动事件人,然后具体的滚动控件再滚动剩下的值。或者选择跟随滚动控件滚动之后再消费滚动值。

Behavior 扮演的角色

上面说了 google 的 nestedScrolling 嵌套滚动的实现思路,那么 Behavior 具体在这期中是个什么位置呢,为啥我们要使用 Behavior 呢

从 MD 的控件使用思路上来看,NestedScrollView / RecyclerView 实现了 NestedScrollingChild 接口,发送滚动事件。CoordinatorLayout 一定要作跟布局使用,CoordinatorLayout 实现了 NestedScrollingParent 接口,他遍历所有的直接子 view,找到期中实现了 NestedScrollingChild 接口的可滚动 view, 然后把自己 set 进去,实现和可滚动控件的绑定,进而可以作为跟容器分发滚动事件,至于如何分发滚动事件,这里就用到 Behavior 了。CoordinatorLayout 本身并不直接实现 NestedScrollingChild 接口,而是把相关方法再次抽象成一个 Behavior 接口抛出去,交给需要的直接子 view 去实现,自己作为 Behavior 接口的代理,NestedScrollingParent 的每个方法 CoordinatorLayout 都会遍历所有直接子 view 获取其中的 Behavior ,执行对应的方法,从而实现在跟节点上分发滚动事件。

我们来看一个 CoordinatorLayout 具体的方法:

   // 参数child:当前实现`NestedScrollingParent`的ViewParent包含触发嵌套滚动的直接子view对象
   // 参数target:触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的
   // 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.垂直或水平方法
   //返回参数代表当前ViewParent是否可以触发嵌套滚动操作
   //CoordiantorLayout的实现上是交由子View的Behavior来决定,并回调了各个acceptNestedScroll方法,告诉它们处理结果
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
                handled |= accepted;
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

可以很明显的看到 CoordiantorLayout 就是遍历了所有的字节子 view,获取到子View的Behavior 来回调了相关方法。

继续看图:


NestedScroll2.png

Behavior 方法大全

从上面我们知道了 Behavior 实现的都是 CoordinatorLayout 抛出来的 NestedScrollingParent 接口的具体实现,当然肯定发还有其他的一些方法,这里我们先来看一看,最好结合上面我们讲的 parent 的逻辑顺序来看,你会发现简单好理解多了

Behavior 提供了几个重要的方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
  • onLayoutChild
/**
     * 表示是否给应用了Behavior 的View 指定一个依赖的布局,通常,当依赖的View 布局发生变化时
     * 不管被被依赖View 的顺序怎样,被依赖的View也会重新布局
     * @param parent
     * @param child 绑定behavior 的View
     * @param dependency   依赖的view
     * @return 如果child 是依赖的指定的View 返回true,否则返回false
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 当被依赖的View 状态(如:位置、大小)发生变化时,这个方法被调用
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

    /**
     *  当coordinatorLayout 的子View试图开始嵌套滑动的时候被调用。当返回值为true的时候表明
     *  coordinatorLayout 充当nested scroll parent 处理这次滑动,需要注意的是只有当返回值为true
     *  的时候,Behavior 才能收到后面的一些nested scroll 事件回调(如:onNestedPreScroll、onNestedScroll等)
     *  这个方法有个重要的参数nestedScrollAxes,表明处理的滑动的方向。
     *
     * @param coordinatorLayout 和Behavior 绑定的View的父CoordinatorLayout
     * @param child  和Behavior 绑定的View
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes 嵌套滑动 应用的滑动方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 嵌套滚动发生之前被调用
     * 在nested scroll child 消费掉自己的滚动距离之前,嵌套滚动每次被nested scroll child
     * 更新都会调用onNestedPreScroll。注意有个重要的参数consumed,可以修改这个数组表示你消费
     * 了多少距离。假设用户滑动了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,
     * 这样coordinatorLayout就能知道只处理剩下的10px的滚动。
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx  用户水平方向的滚动距离
     * @param dy  用户竖直方向的滚动距离
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    /**
     * 进行嵌套滚动时被调用
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed target 已经消费的x方向的距离
     * @param dyConsumed target 已经消费的y方向的距离
     * @param dxUnconsumed x 方向剩下的滚动距离
     * @param dyUnconsumed y 方向剩下的滚动距离
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     *  嵌套滚动结束时被调用,这是一个清除滚动状态等的好时机。
     * @param coordinatorLayout
     * @param child
     * @param target
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
    }

    /**
     * onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个
     * 方法里做一些准备工作,如一些状态的重置等。
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     */
    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 用户松开手指并且会发生惯性动作之前调用,参数提供了速度信息,可以根据这些速度信息
     * 决定最终状态,比如滚动Header,是让Header处于展开状态还是折叠状态。返回true 表
     * 示消费了fling.
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX x 方向的速度
     * @param velocityY y 方向的速度
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    //可以重写这个方法对子View 进行重新布局
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        return super.onLayoutChild(parent, child, layoutDirection);
    }

或者看这个理解,这个好理解:

  • NestedScrollingParent : 下文简称 NSP

  • NestedScrollingChild : 下文简称 NSC

  • onStartNestedScroll
    用户按下手指时触发,询问 NSP 是否要处理这次滑动操作,如果返回 true 则表示“我要处理这次滑动”,如果返回 false 则表示“我不 care 你的滑动,你想咋滑就咋滑”,后面的一系列回调函数就不会被调用了。它有一个关键的参数,就是滑动方向,表明了用户是垂直滑动还是水平滑动,本例子只需考虑垂直滑动,因此判断滑动方向为垂直时就处理这次滑动,否则就不 care

  • onNestedScrollAccepted
    当 NSP 接受要处理本次滑动后,这个回调被调用,我们可以做一些准备工作,比如让之前的滑动动画结束。

  • onNestedPreScroll
    当 NSC 即将被滑动时调用,在这里你可以做一些处理。值得注意的是,这个方法有一个参数 int[] consumed,你可以修改这个数组来表示你到底处理掉了多少像素。假设用户滑动了 100px,你做了 90px 的位移,那么就需要把 consumed[1] 改成 90(下标 0、1 分别对应 x、y 轴),这样 NSC 就能知道,然后继续处理剩下的 10px。

  • onNestedScroll
    上一个方法结束后,NSC 处理剩下的距离。比如上面还剩 10px,这里 NSC 滚动 2px 后发现已经到头了,于是 NSC 结束其滚动,调用该方法,并将 NSC 处理剩下的像素数作为参数(dxUnconsumed、dyUnconsumed)传过来,这里传过来的就是 8px。参数中还会有 NSC 处理过的像素数(dxConsumed、dyConsumed)。这个方法主要处理一些越界后的滚动

  • onNestedPreFling
    用户松开手指并且会发生惯性滚动之前调用。参数提供了速度信息,我们这里可以根据速度,决定最终的状态是展开还是折叠,并且启动滑动动画。通过返回值我们可以通知 NSC 是否自己还要进行滑动滚动,一般情况如果面板处于中间态,我们就不让 NSC 接着滚了,因为我们还要用动画把面板完全展开或者完全折叠。

  • onStopNestedScroll

一切滚动停止后调用,如果不会发生惯性滚动,fling 相关方法不会调用,直接执行到这里。这里我们做一些清理工作,当然有时也要处理中间态问题。

自定义 Behavior 可以分2种实现思路:

  • 某个 view 监听另一个 view 的状态变化,例如大小、位置、显示状态等
    这情况需要重写 layoutDependsOn 和 onDependentViewChanged 方法
  • 某个 view 监听 CoordinatorLayout 内的 NestedScrollingChild 的接口实现类的滑动状态
    这情况需要重写 onStartNestedScroll 和 onNestedPreScroll 系列方法,这就是NestedScrollingParent 的思路范围了

结尾

说到这里基本 nestedScrolling 嵌套滚动原理和 自定义Behavior 的原理接都清楚了,剩下的我们去多多实践才能灵活的掌握。文章开头的文章中,里面的例子有些难,不建议易上手就去看那个例子。

写几个涉及的单词:

  • ScrollAxes 滚动方向
  • NestedFling 快速滚动,一般指手指已经离开屏幕,但是屏幕还在快速滚动的状态

参考文档:

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

推荐阅读更多精彩内容