Material Design

android ui 学习系列 -自定义Behavior (1)

2017-11-07  本文已影响228人  前行的乌龟

写在开头

不吐不快啊,关于自定义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种角色分别抽象除了接口:

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


NestedScrollView.png

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

child 的滚动逻辑如下:

滚动的简单逻辑顺序:

从事件分发的角度来看:

parent 的滚逻辑如下:

其实不写 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 提供了几个重要的方法:

/**
     * 表示是否给应用了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);
    }

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

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

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


结尾

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

写几个涉及的单词:


参考文档:

上一篇 下一篇

猜你喜欢

热点阅读