在之前的文章里,我们在源码的基础上学习了CoordinatorLayout的大致执行流程,那么今天我们将来到另一个重要的关键点--自定义Behavior。
本次代码的地址在github上,欢迎大家star、follow
这里再一次啰嗦下什么是Behavior
- Behavior是Android Support Design库中的布局概念。只有CoordinatorLayout的直接子View使用Behavior才有效果。
- 你可以为任何View添加一个Behavior。
- 在新的嵌套滑动机制中,引入了NestedScrollingChild和NestedScrollingParent两个接口,用于协调父子控件滑动状态。CoordinatorLayout实现了NestedScrollingParent接口,实现NestedScrollingChild这个接口的子控件在滑动时会调用NestedScrollingParent接口的相关方法,将事件发给父控件CoordinatorLayout,由CoordinatorLayout决定是否消费当前事件。与此同时,在CoordinatorLayout实现的NestedScrollingParent相关方法中,会分别调用Behavior内部的不同方法。所以说Behavior是一系列回调,让你有机会以非侵入的方式动态的为View添加依赖布局以及处理父布局(CoordinatorLayout)的滑动手势。
盗2张图来体会下
Behavior的功能 Behavior在整套体系中的位置本篇将使用N个Demo来具体说明下如何完成自定义功能
-
BackBehavior 快速返回效果的Behavior,根据AppBarLayout的滚动来控制自定义View的滚动
BackBehavior -
FloatingActionBarBehavior 控制FloatingActionButton滚动的Behavior,根据NestedScrollView的滚动方向来决定是否显示FloatingActionButton
嵌套滑动 -
另外一种嵌套滑动展示
嵌套滑动 -
通过自定义的NestedScrollingParent与NestedScrollingChild实现嵌套滚动
嵌套滑动 -
BottomSheetBehavior
b.gif
由于篇幅有限,这里仅简单的举例子稍微说明下,如果有疑问,请直接在简书中评论区或者在github仓库Issues中给我留言。再啰嗦一句,本次代码的地址在github上,欢迎大家star、follow
通用流程
- 重写构造方法
- 绑定到需要处理的View上
- 事件流
重写构造方法
这个在之前的文章中已经说够了,自定义的Behavior一定要继承CoordinatorLayout.Behavior的2个参数的构造方法
public class BackBehavior extends CoordinatorLayout.Behavior<View> {
public BackBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
绑定到某一个View
这里我们更加常见的是直接在xml里添加,其实除此之外还有另外2种方法。
一个是直接用反射的形式绑定在自定义布局上,这个我们也提到过AppBarLayout就是这样实现的
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout
还有一种就是动态设置
((CoordinatorLayout.LayoutParams) findViewById(R.id.back_bottom_view).getLayoutParams()).setBehavior(new BackBehavior());
再来看看最常见的绑定到xml上
<View xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="50dip"
android:layout_gravity="bottom"
app:layout_behavior="com.renyu.behaviordemo.behavior.BackBehavior"
android:background="@android:color/holo_red_dark">
</View>
事件流
算上刚才说的那种情况,这里一共有四种不同的事件流
- 布局事件
在CoordinatorLayout的onMeasure和onLayout方法中,会通过Behavior询问子视图是否需要进行相应操作,即执行Behavior中对应的方法,分别是onMeasureChild与onLayoutChild。这里onMeasureChild与onLayoutChild都会分别比Child的onMeasure与onLayout两方法优先执行
onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection)
- 触摸事件
触摸事件就是Behavior中的onInterceptTouchEvent与onTouchEvent。注意这里,如果Behavior对触摸事件进行了拦截,那么后续事件将不会再分发到Child View自身的触摸事件中了。而且事件由CoordinatorLayout分发下来,所以这里的touch事件都是未知View的,所以需要额外判断当前的点击事件是不是由我们的控件触发的 - 变化事件
这里需要穿插一个判断依赖对象的过程。之前我们已经提及过在自定义Behavior时要分2种情况去考虑
(1)某个view监听另一个view的状态变化,例如大小、位置、显示状态等
(2)某个view监听滑动嵌套里的滑动状态
第二种情况我们就不需要特别的去进行判断了。重点来说说第一种。
从之前的源码阅读中我们知道,CoordinatorLayout会将其子View遍历一遍,在遍历的过程中去不断的通知所有的Behavior,这样就会导致Behavior收到不一定是我们关心的滑动事件,所以我们可以根据情况使用类型或者ID去判断依赖属性,过滤掉不是我们关心的滑动事件
//判断child的布局是否依赖dependency
@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
这是通过类型去判断是否达成依赖。其中child为当前添加Behavior属性的视图,dependency为参考物的View。CoordinatorLayout收到某个子View变化或者嵌套滑动事件之后就会将事件下发到每一个Behavior,Behavior自行做出处理。
说完了用类型去判断之后,我们同样可以通过ID去进行判断。使用ID判断就需要我们自己去通过自定义属性将ID传到Behavior对象里面。由于写法与自定义View使用TypedValues一致,所以这里就不加多说了
这就是之前所说的view的状态发生了变化。我们的demo就是AppBarLayout的位置发生了移动,进而触发了这个事件,然后我们的child就随着AppBarLayout的移动而发生移动。当然你直接在xml中使用app:layout_anchor写死对应目标也是可以的
//返回true表示child的状态发生改变,反之就返回false
@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (dependency instanceof AppBarLayout) {
int height= (int) dependency.getY();
child.setTranslationY(-height);
}
return true;
}
- 嵌套滑动事件
我之前用了很大的篇幅对其进行了源码分析,所以这里也不再准备具体说概念了。只稍微提及一下重要的方法
/**
* 需要判断滑动的方向是否是我们需要的。
* nestedScrollAxes == ViewCompat.SCROLL_AXIS_HORIZONTAL 表示是水平方向的滑动
* nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL 表示是竖直方向的滑动
* 返回 true 表示继续接收后续的滑动事件,返回 false 表示不再接收后续滑动事件
* @param coordinatorLayout
* @param child
* @param directTargetChild
* @param target
* @param nestedScrollAxes
* @return
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
/**
* 滑动中调用
* 1. 正在上滑:dyConsumed > 0 && dyUnconsumed == 0
* 2. 已经到顶部了还在上滑:dyConsumed == 0 && dyUnconsumed > 0
* 3. 正在下滑:dyConsumed < 0 && dyUnconsumed == 0
* 4. 已经打底部了还在下滑:dyConsumed == 0 && dyUnconsumed < 0
* @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 velocityX
* @param velocityY
* @param consumed
* @return
*/
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
来看看FloatingActionBarBehavior是如何控制FloatingActionButton的
public class FloatingActionBarBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
int viewY=0;
ObjectAnimator animator;
public FloatingActionBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
if (viewY==0 && child.getVisibility()==View.VISIBLE) {
viewY= (int) (coordinatorLayout.getMeasuredHeight()-child.getY());
}
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyConsumed>0) {
hide(child);
}
else if (dyConsumed<0) {
show(child);
}
}
private void show(FloatingActionButton child) {
if (animator!=null) {
animator.cancel();
animator=null;
}
animator=ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, 0).setDuration(500);
animator.start();
}
private void hide(FloatingActionButton child) {
if (animator!=null) {
animator.cancel();
animator=null;
}
animator=ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, viewY).setDuration(500);
animator.start();
}
}
这里就是在垂直方向滚动时,根据滑动方向,区显示与隐藏FloatingActionButton。
我们特别要注意onNestedScroll与onNestedPreScroll两个方法的回调,大家学习的时候可以通过NestedScrollView或者RecyclerView的源码去分别处理。
这里简单说一下结论:以垂直方向为例,一般情况下onNestedScroll的达成条件是经过onNestedPreScroll处理之后,本次滚动中y方向的滚动总距离减去父布局要消费的滚动距离的值要比TouchSlop要大,才能将事件继续执行到onNestedScroll处,也就是交由target去自行处理。而这里stedScrollView或者RecyclerView自行处理的体现就是自己内部的item产生滚动。这里类似onIntercepTouchEvent与onTouchEvent的这种关系
// 这里是NestedScrollView中的Action_Move条件下的代码
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
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) {
// 省去无关代码
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {}
}
继续看
// 这里是RecyclerView中的Action_Move条件下的代码
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// dispatchNestedScroll事件在scrollByInternal内部
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
参考链接
CoordinatorLayout自定义Bahavior特效及其源码分析
CoordinatorLayout高级用法-自定义Behavior
AppBarLayout 有毒,我有一粒解药,要不?(修改)
CoordinatorLayout与Behavior的一己之见
Android 优化交互 —— CoordinatorLayout 与 Behavior
Android Design Support Library--FloatingActionButton及其Behavior的使用
网友评论