CoordinatorLayout系列(二):Behavior的使用

Posted by Xugter on July 10, 2019

准备

上一篇文章介绍了CoordinatorLayout的基本使用方法,网上有很多demo,是基于这些基本使用的官方控件。但是基于这些的方法能实现的效果很有限。要想非常自由地实现各种炫酷效果,还是需要学习Behaivor的使用。

先在这里立个小目标,脱离官方提供的Behavior,自己去实现这些交互。

开始

要实现这个目标,就先需要学习Beahavior怎么使用。Behavior主要功能有三块

1. onLayoutChild和onMeasureChild

跟布局有关的,这部分控制界面位置和大小

2. layoutDependsOn和onDependentViewChanged

跟别的子view的位置互动 layoutDependsOn和onDependentViewChanged

3 onXXXSroll和onXXXFling

跟嵌套滑动有关 onStartNestedScroll onNestedPreScroll onNestedScroll onStopNestedScroll onNestedPreFling onNestedFling

接下来看看具体怎么使用的,这里Demo源码地址都放在了Github

1.onMeasureChild和onLayoutChild

a.onMeasureChild

public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
    return false;
}

这个方法用来计算child的宽高(getMeasuredWidth和getMeasuredHeight),其实这部分对于了解view的绘制是可以直接跳过的,方法和作用都是一样的。这里只是简单的演示一下。

int newHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY);
int newWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(500, View.MeasureSpec.EXACTLY);

如上,我们重新设定parent的宽高是500*300,然后用新的参数测量child

parent.onMeasureChild(child, newWidthMeasureSpec, widthUsed, newHeightMeasureSpec, heightUsed);

最后看日志输出

com.xugter.cooridnatorlayoutstudy I/MeasureBehavior: onMeasureChild==========w=500   h=300

1.pic

就像上面的蓝色的方块,即使我在xml文件设定的宽高是match_parent,也是显示成我们代码里面设置的500*300

<View
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_light"
    app:layout_behavior=".part2.layout.MeasureBehavior" />

b.onLayoutChild

public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
    return false;
}

这个方法是用来放置child的位置的

@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
    child.layout(0, 500, child.getMeasuredWidth(), 500 + child.getMeasuredHeight());
    return true;
}

只要这样设置了后,就会像上图的红色方块,往下移动500

另外简单说一下onInterceptTouchEvent和onTouchEvent这两个功能和view的onInterceptTouchEvent和onTouchEvent是一样的 在这两个方法里面进行一些事件的分发,返回true就可以代替view自己的这两个方法

这个部分的内容其实和我们平时了解到View的绘制和事件分发是一样的。

2.layoutDependsOn和onDependentViewChanged

这个部分是Behavior比较核心的一个功能。

Behavior可以让一个child根据另外的一个child的位移,来改变自己的状态,无论是位置还是透明度等一些属性。

public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
    return false;
}

这个方法主要是决定当前child要根据哪个child(即dependency)来改变自己,可以用dependency的id,tag,类型等一些办法来判定是否是dependency.

类似ScrollingViewBehavior就是根据view的类型来判断的,如下

public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof AppBarLayout;
}
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
    return false;
}

当上面确定好dependency,就可以用这个方法根据dependency位置变化来改变自己的状态了

@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
    int dependBottom = dependency.getBottom();
    child.setY(dependBottom + 50);
    child.setX(dependency.getLeft());
    return true;
}

上面的代码可以让红色方块跟住绿色方块(这样只需要更新位置就好了) 如下图

2.gif

3.onXXXXXXXScroll

这个部分也是Behavior比较核心的一个功能。

关于这部分的原理解释起来会经常涉及到嵌套滑动,在以后会详细讨论。至于onXXXXXXXFling部分跟Scroll流程是类似的就省略掉了。

这里先简单介绍一下嵌套滑动的概念,然后直接讨论怎么使用。 嵌套滑动简单来说就是关于两个可滑动控件打架的故事,即一个可滑动的控件包含另一个可滑动的控件,当用户滑动里面的控件,怎么处理触摸事件的过程。

CoordinatorLayout实现了NestedScrollingParent就能接收实现了NestedScrollingChild接口的child的滑动事件,至于这是为啥。。。那就等到以后讨论到嵌套滑动再说吧。现在要假装强行可以接收到就可以了

CoordinatorLayout接收到滑动事件后就会把事件发送给每个愿意接收这个事件的child 至于怎么确定是否愿意是在这个方法 onStartNestedScroll

public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes) {
    return false;
}

只要返回true就表示这个child愿意接收这个滑动事件

如果愿意接收这个滑动事件,就会在接下来的这两个方法 onNestedPreScroll onNestedScroll 接收到事件,处理自己想要处理的事情,那么为啥有两个方法来接收这个事件呢,这个又跟嵌套滑动的机制有关,再次强行绕过嵌套滑动这个坑。

如果有兴趣了解的话,可以去看这两篇文章,个人感觉这两篇文章写的非常好,基本把事件的分发和嵌套滑动讲的很透彻。 【透镜系列】看穿 > 触摸事件分发 > 【透镜系列】看穿 > NestedScrolling 机制 >

这里先简单介绍一下这两个方法的流程,当一个NestedScrollingChild的childA发生滑动的时候,会先询问有没有childB愿意接收这个滑动事件,愿意的话在onStartNestedScroll返回true,然后把这个事件会发给childB的onNestedPreScroll。

public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
}

在onNestedPreScroll里面,childB有可能会消费这个滑动事件,也有可能一点都不消费,或者消费部分,这个都是通过consumed来传回的。

当childB的onNestedPreScroll结束后,childA根据consumed的值,判断是否还有剩下滑动距离没消费完消。如果还有的话,childA就开始自己的滑动,当然childA依然还是有可能消费不完这次的滑动事件,比如滑到底部了,childA会再次把事件发给childB的onNestedScroll

public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}

如果childB还没消费完这个事件,会在返回到childA,自生自灭。

由于onNestedScroll接收的不是第一个接收到的,所以一般情况下onNestedPreScroll这个用的比较多一点。

demo里面,有个nestscrollview和一个方块 3.gif 运行规则是这个方块会根据nestscrollview的反方向移动,当触顶或者触底的时候,才会轮到nestscrollview自己滚动。consumed控制着方块的消费情况。

public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        Log.d(TAG, "onNestedPreScroll");
        if (child.getY() + dy < 0) {
            //方块到达顶部,滑动距离消费不完
            ViewCompat.offsetTopAndBottom(child, (int) (0 - child.getY()));
            consumed[1] = 0 - (int) child.getY();
        } else if (child.getY() + dy > coordinatorLayout.getHeight() - child.getHeight()) {
            //方块到达底部,滑动距离消费不完
            ViewCompat.offsetTopAndBottom(child, (int) (coordinatorLayout.getHeight() - child.getHeight() - child.getY()));
            consumed[1] = coordinatorLayout.getHeight() - child.getHeight() - (int) child.getY();
        } else {
            //方块消费完全部事件
            ViewCompat.offsetTopAndBottom(child, dy);
            consumed[1] = dy;
        }
    }

从日志情况也可以看出onNestedPreScroll和onNestedScroll的运行关系。 当nestscrollview可以滑动的时候,是不会触发onNestedScroll的,日志如下

2019-09-07 16:15:15.169 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onStartNestedScroll
2019-09-07 16:15:15.170 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedScrollAccepted
2019-09-07 16:15:15.313 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedPreScroll
..............
2019-09-07 16:15:16.094 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedPreScroll
2019-09-07 16:15:16.387 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onStopNestedScroll

当nestscrollview不可滑动了,才会触发onNestedScroll,日志如下

2019-09-07 16:17:50.257 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onStartNestedScroll
2019-09-07 16:17:50.257 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedScrollAccepted
2019-09-07 16:17:50.475 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedPreScroll
2019-09-07 16:17:50.494 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedPreScroll
.............................
2019-09-07 16:17:53.919 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedScroll
2019-09-07 16:17:54.017 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedPreScroll
2019-09-07 16:17:54.017 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onNestedScroll
2019-09-07 16:17:54.018 15774-15774/com.xugter.cooridnatorlayoutstudy D/====ScrollBehavior====: onStopNestedScroll

另外 不知道有没有注意到,demo里面把onNestedScroll里面的每个view都设置了onclick事件,如果不设置onclick事件,这个demo就有问题了。至于为什么,你猜对了,又需要理解嵌套机制。这个小坑,我也懵逼了好久,以后再拎出来讨论吧。

Github