Behavior
要研究的几个问题
一、Behavior是什么?为什么要用Behavior?
二、怎么使用Behavior?
三、从源码角度看为什么要这么使用Behavior?
一、Behavior是什么?为什么要用Behavior?
CoordinatorLayout是android support design推出的新布局,主要用于作为视图根布局以及协调子控件的行为,而Behavior就是用于直接子控件来协调自身CoordinatorLayout以及和其他子控件的关系,使用Behavior的控件必须是直接从属于CoordinatorLayout。
在传统的事件分发流程中,在子控件处理事件过程中,父控件是可以进行拦截的,但一旦父控件进行拦截,那么这次事件只能由父控件处理,而不能再由子控件处理了。
在android5.0之后新的嵌套滑动机制中,引入了:NestScrollChild和NestedScrollingParent两个接口,用于协调子父控件滑动状态,而CoordinatorLayout实现了NestedScrollingParent接口,在实现了NestScrollChild这个接口的子控件在滑动时会调用NestedScrollingParent接口的相关方法,将事件发给父控件,由父控件决定是否消费当前事件,在CoordinatorLayout实现的NestedScrollingParent相关方法中会调用Behavior内部的方法。
我们实现Behavior的方法,就可以嵌入整个CoordinatorLayout所构造的嵌套滑动机制中,可以获取到两个方面的内容:
1、某个view监听另一个view的状态变化,例如大小、位置、显示状态等
需要重写layoutDependsOn和onDependentViewChanged方法
2、某个view监听CoordinatorLayout内NestedScrollingChild的接口实现类的滑动状态
重写onStartNestedScroll和onNestedPreScroll方法。注意:是监听实现了NestedScrollingChild的接口实现类的滑动状态,这就可以解释为什么不能用ScrollView而用NestScrollView来滑动了。
二、怎么使用Behavior?
我们先看下Behavior最常见的几个方法,Behavior还有其他比如onMeasureChild、onLayoutChild等一些方法,列举的这几个方法平时还是比较常见的,知道常见方法的使用后,在研究下其他方法,思路还是相通的。
public static abstract class Behavior {
//指定Behavior关注的滑动方向
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
//用来监听滑动状态,对象消费滚动距离前回调
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// TODO
}
//确定子视图与同级视图的依赖
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View
child, View dependency) {
return Build.VERSION.SDK_INT >= 11 && dependency instanceof Snackbar.SnackbarLayout;
}
//依赖布局变化时调用
//If the Behavior changes the child view's size or position,
//it should return true. The default implementation returns false
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
@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);
}
//所有Behavior能在子View之前收到CoordinatorLayout的所有触摸事件
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent,View child, MotionEvent ev) {
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
return super.onTouchEvent(parent, child, ev);
}
}
1、某个view监听另一个view的状态变化
这样的效果最常见的如知乎导航栏那样:
底部跟随顶部导航栏显示隐藏
前面已经说了,如果要监听另一个view的状态变化,需要重写layoutDependsOn和onDependentViewChanged方法,看下具体实现:
layout:
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/behavior_demo_coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|snap"
android:background="?attr/colorPrimary" />
android:layout_width="match_parent"
android:layout_height="match_parent"
>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="400dp"
android:text="哈哈哈"
android:gravity="center"/>
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:background="@color/colorPrimary"
android:gravity="center"
app:aucher_id="@id/appbar"
app:layout_behavior="com.mrzk.newstudy.behavior.MyCustomBehavior">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="底部导航栏"/>
attrs:
MyCustomBehavior.java:
public class MyCustomBehavior extends CoordinatorLayout.Behavior{
private int id;
public MyCustomBehavior(Context context, AttributeSet attrs) {
super(context,attrs);
TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.MyCustomStyle);
id = typedArray.getResourceId(R.styleable.MyCustomStyle_anchor_id, -1);
typedArray.recycle();
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// return dependency instanceof AppBarLayout;
return dependency.getId() == id;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
child.setTranslationY(-dependency.getTop());
return true;
}
}
重点关注几点:
首先,我们必须重写两个参数的构造方法,因为通过反射实例化的时候就是用的这个构造方法,在这个构造方法中我们也可以获取一些东西,比如我们的依赖控件ID。
之后layoutDependsOn方法我们来决定要依赖哪个view,如果我们知道要依赖的控件,可以直接写:
return dependency instanceof AppBarLayout
而如果我们不知道,也可以由外部传入,在构造方法中获取资源ID来进行判断,这样具有更高的灵活性:
return dependency.getId() == id
我们看下在CoordinatorLayout中两个方法的调用过程:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged(false);
return true;
}
}
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
...
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//如果Behavior不为null,layoutDependsOn方法返回true
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//调用onDependentViewChanged方法
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
}
从调用上来看,在CoordinatorLayout内部的任何子view均可产生依赖关系。
2、某个view监听CoordinatorLayout内NestedScrollingChild的接口实现类的滑动状态
如前所说,重写onStartNestedScroll和onNestedPreScroll方法。它可以监听实现了NestedScrollingChild的接口实现类的滑动状态。
如果用WebView来滚动的,结果预期要隐藏和显示的appbar没有反应,在外层加上NestScrollView就解决了问题,这是因为WebView没有实现NestedScrollingChild接口造成的,因为滑动控件的滑动状态是通过NestedScrollingChild接口方法处理中来调用NestedScrollingParent接口方法来实现。
实现上面的效果我们还可以用重写onStartNestedScroll和onNestedPreScroll来实现。来看看吧:
public class MyCustomBehavior extends CoordinatorLayout.Behavior{
private boolean isAnimate;
public MyCustomBehavior(Context context, AttributeSet attrs) {
super(context,attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL)!=-1;//判断是否为垂直滚动
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
//super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
if (dy>0 &&!isAnimate && child.getTranslationY()
child.setTranslationY(child.getTranslationY() + dy);
}else if (dy<0 &&!isAnimate && child.getTranslationY()>0){
child.setVisibility(View.VISIBLE);
if (child.getTranslationY()+dy<0){
child.setTranslationY(0);
}else {
child.setTranslationY(child.getTranslationY()+dy);
}
}
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
//super.onStopNestedScroll(coordinatorLayout, child, target);
if (child.getTranslationY()
changeState(child,0);
}else{
changeState(child,child.getHeight());
}
}
private void changeState(final View view, final int scrollY) {
ViewPropertyAnimator animator = view.animate().translationY(scrollY).setInterpolator(new FastOutSlowInInterpolator()).setDuration(200*scrollY/view.getHeight());
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
isAnimate=true;
}
@Override
public void onAnimationEnd(Animator animator) {
if (view.getTranslationY() == view.getHeight()){
view.setVisibility(View.GONE);
}
isAnimate=false;
}
@Override
public void onAnimationCancel(Animator animator) {
view.setTranslationY(scrollY);
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
}
用这个来实现的话,需要注意的是滚动控件必须实现NestedScrollingChild接口,而没有实现该接口且不调用dispatchNestedScroll相关接口的滚动控件如ScrollView、WebView、ListView是没有作用的。
三、从源码角度看为什么要这么使用Behavior
我们从Behavior获取实例化开始看,看CoordinatorLayout.LayoutParams源码:
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_LayoutParams);
this.gravity = a.getInteger(
R.styleable.CoordinatorLayout_LayoutParams_android_layout_gravity,
Gravity.NO_GRAVITY);
mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_LayoutParams_layout_anchor,
View.NO_ID);
this.anchorGravity = a.getInteger(
R.styleable.CoordinatorLayout_LayoutParams_layout_anchorGravity,
Gravity.NO_GRAVITY);
this.keyline = a.getInteger(R.styleable.CoordinatorLayout_LayoutParams_layout_keyline,
-1);
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
//在这里解析获取Behavior
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
a.recycle();
}
接着来看看具体是怎么获取到Behavior的:
static final Class>[] CONSTRUCTOR_PARAMS = new Class>[] {
Context.class,
AttributeSet.class
};
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor c = constructors.get(fullName);
if (c == null) {
//这里通过反射获取到Behavior
final Class clazz = (Class) Class.forName(fullName, true,
context.getClassLoader());
//获取两个参数的构造方法
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
//在这里实例化
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
这里就解释了为什么我们每次继承都要写两个参数的构造方法了,如果没有,则会报Caused by: java.lang.NoSuchMethodException: [class android.content.Context, interface android.util.AttributeSet]错误。
然后我们看看主要关注的onStartNestedScroll和onNestedPreScroll的调用时机,当实现了NestScrollChild接口的子控件滑动时,会回调CoordinatorLayout中的onStartNestedScroll方法:
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();
//获取Behavior
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//true if the Behavior wishes to accept this nested scroll
//调用viewBehavior.onStartNestedScroll方法,如果返回true表示希望接受滚动事件
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
当实现了NestScrollChild接口的子控件滚动时,在消费滚动距离之前把总的滑动距离传给父布局,即CoordinatorLayout。然后回调onNestedPreScroll方法:
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//遍历所有子控件 如果不希望接受处理事件 跳出本次循环
if (!lp.isNestedScrollAccepted()) {
continue;
}
//获得child view的Behavior
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
//调用viewBehavior.onNestedPreScroll方法
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
//dy大于0是向上滚动 小于0是向下滚动
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
//consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
dispatchOnDependentViewChanged(true);
}
}
然后我们来研究layoutDependsOn和onDependentViewChanged的调用时机,看CoordinatorLayout的dispatchOnDependentViewChanged方法:
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//behavior不为null同时layoutDependsOn返回了true
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//this:CoordinatorLayout
//checkChild:behavior所属的view
//child:依赖的view
//true if the Behavior changed the child view's size or position, false otherwise
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
if (fromNestedScroll) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}
这段代码在onNestedScroll、onNestedPreScroll、onNestedFling和OnPreDrawListener.onPreDraw方法中都有调用,判断依赖控件大小或者位置变化时及时通知behavior,子控件作出相应调整。
这里把我们主要关心的控件的调用时机大体走读了一遍,对于为什么在behavior中调用相关方法可以依赖和监听其他控件的滑动事件应该有了一定认识,如果关注CoordinatorLayout的实现细节,务必要搞明白NestScrollChild和NestedScrollingParent机制的调用关系,建议查看NestScrollView源码,这里给出NestScrollChild和NestedScrollingParent的一些主要方法说明,对其具体了解还可以看Android 嵌套滑动机制(NestedScrolling)这篇文章。
NestScrollChild
public void setNestedScrollingEnabled(boolean enabled)
enabled:true表示view使用嵌套滚动,false表示禁用
public boolean startNestedScroll(int axes)
axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和 ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动)
return:true表示本次滚动支持嵌套滚动,false不支持
startNestedScroll表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动
public void stopNestedScroll()
在事件结束比如ACTION_UP或者ACTION_CANCLE中调用stopNestedScroll,告诉父布局滚动结束
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
dxConsumed: 表示view消费了x方向的距离长度
dyConsumed: 表示view消费了y方向的距离长度
dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费>dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费
offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
在view消费滚动距离之后,把剩下的滑动距离再次传给父布局
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
dx: 表示view本次x方向的滚动的总距离长度
dy: 表示view本次y方向的滚动的总距离长度
consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向
参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
view消费滚动距离之前把总的滑动距离传给父布局
** public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)**
velocityX:X方向滚动的距离
velocityY:Y方向滚动的距离
consumed:父布局是否消费
public boolean dispatchNestedPreFling(float velocityX, float velocityY)
velocityX:X方向滚动的距离
velocityY:Y方向滚动的距离
NestedScrollingParent
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
child:ViewParent包含触发嵌套滚动的view的对象
target:触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的
nestedScrollAxes:就是嵌套滚动的滚动方向了.
当子view的调用NestedScrollingChild的方法startNestedScroll时,会调用该方法
该方法决定了当前控件是否能接收到其内部View(并非是直接子View)滑动时的参数
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
如果onStartNestedScroll方法返回true,之后就会调用该方法.它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化(React to the successful claiming of a nested scroll operation)
public void onStopNestedScroll(View target)
当子view调用stopNestedScroll时会调用该方法,停止滚动
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
target:同上
dxConsumed:表示target已经消费的x方向的距离
dyConsumed:表示target已经消费的x方向的距离
dxUnconsumed:表示x方向剩下的滑动距离
dyUnconsumed:表示y方向剩下的滑动距离
当子view调用dispatchNestedScroll方法时,会调用该方法
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
target:同上
dx:表示target本次滚动产生的x方向的滚动总距离
dy:表示target本次滚动产生的y方向的滚动总距离
consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
当子view调用dispatchNestedPreScroll方法是,会调用该方法
调用时机:
子view
父view
startNestedScroll
onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll
onNestedPreScroll
dispatchNestedScroll
onNestedScroll
stopNestedScroll
onStopNestedScroll