本文已在公众号鸿洋原创发布。未经许可,不得以任何形式转载!java
在前面《浅析NestedScrolling嵌套滑动机制之基础篇》里的常见效果提到Behavior也是走NestedScrolling机制来实现各类神奇的滑动效果,它伴随CoordinatorLayout在Revision 24.1.0的android.support.v4兼容包被引入,和CoordinatorLayout结合实现各个控件联动,能够拦截代理CoordinatorLayout的测量、布局、WindowInsets、触摸事件、嵌套滑动。node
Behavior是做用于 CoordinatorLayout的直接子View 的交互行为插件。一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其余一些手势。android
/** * 泛型<V>是Behavior关联的View */
public static abstract class Behavior<V extends View> {
/** * 默认构造方法,用于注解的方式建立或者在代码中建立 */
public Behavior() {}
/** * 用于xml解析layout_Behavior属性的构造方法,若是须要Behavior支持在xml中使用,则必须有此构造方法 */
public Behavior(Context context, AttributeSet attrs) {}
/** * 在LayoutParams实例化后调用,或者在调用了LayoutParams.setBehavior(behavior)时调用. */
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
/** * 同上面onAttachedToLayoutParams相反 * 当LayoutParams移除Behavior时调用,例如调用了LayoutParams.setBehavior(null). * View被从View Tree中移除时不会调用此方法. */
public void onDetachedFromLayoutParams() {}
/** * 在CoordinatorLayout分发给子View前拦截Touch事件 */
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
/** * 在CoordinatorLayout分发给子View前消费Touch事件 */
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
/** * 阻断此Behavior所关联View下层的View的交互 */
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
return getScrimOpacity(parent, child) > 0.f;
}
/** * 当blocksInteractionBelow返回为true时,CoordinatorLayout将会在View的上层绘制 * 一个屏蔽的getScrimColor()颜色来显示没法进行交互的区域 */
@ColorInt
public int getScrimColor(CoordinatorLayout parent, V child) {
return Color.BLACK;
}
/** * getScrimColor()绘制颜色的透明度 */
@FloatRange(from = 0, to = 1)
public float getScrimOpacity(CoordinatorLayout parent, V child){
return 0.f;
}
/** * 关联的View和感兴趣的View进行依赖 */
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
/** * 依赖View的位置、大小改变时回调 */
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
/** * 依赖View从布局移除时回调 */
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
/** * 代理CoordinatorLayout子View的测量,注意这个子View是关联了当前Behavior, * 返回true表示使用Behavior的*onMeasureChild()来测量参数里child的这个子View, * 返回false则使用*CoordinatorLayout的默认测量子View的方法。 */
public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
return false;
}
/** * 代理CoordinatorLayout子View的布局 * 返回true表示使用Behavior的onLayoutChild()来布局子View * 返回false则使用CoordinatorLayout的默认测量子View的方法。 */
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}
/** *代理消费CoordinatorLayout的WindowInsets */
@NonNull
public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) {
return insets;
}
//如下是NestedScrolling相关方法//
@Deprecated
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes) {
return false;
}
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
target, axes);
}
return false;
}
@Deprecated
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes) {
}
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
target, axes);
}
}
@Deprecated
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target) {
}
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onStopNestedScroll(coordinatorLayout, child, target);
}
}
@Deprecated
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
}
}
@Deprecated
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
}
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
return false;
}
//省略部分很是用方法
...
}
复制代码
<!-- 布局文件 -->
<android.support.design.widget.CoordinatorLayout>
<android.support.v4.widget.NestedScrollView app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
<!-- values.xml -->
<string name="appbar_scrolling_view_behavior" translatable="false">
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
</string>
复制代码
在布局文件对CoordinatorLayout的直接子View添加app:layout_behavio属性,属性是Behavior类全限包名,你能够把值放在values文件里,也能够直接写在布局文件里。在CoordinatorLayout的parseBehavior()调用Behavior两个参数的构造方法建立。app
AppBarLayout.ScrollingViewBehavior behavior = new AppBarLayout.ScrollingViewBehavior();
CoordinatorLayout.LayoutParams params =(CoordinatorLayout.LayoutParams) view.getLayoutParams();
params.setBehavior(behavior);
复制代码
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}
复制代码
注意若是同时使用注解和xml布局文件为同一个view设置Behavior,生效的是注解方式的Behavior,若在自定义Behavior使用此方式须要一个无参的构造函数,由于CoordinatorLayout在getResolvedLayoutParams()解析时调用反射Behavior的无参构造函数建立,而这种注解方式在support27.1.0版本打上了@Deprecated过期标签。less
View实现CoordinatorLayout.AttachedBehavior接口并复写getBehavior()返回Behavior。在CoordinatorLayout在getResolvedLayoutParams()解析时调用getBehavior()获取Behavior,而后调用CoordinatorLayout.LayoutParams.setBehavior()传入。ide
public class MyLayout extends LinearLayout implements CoordinatorLayout.AttachedBehavior{
@NonNull
@Override
Behavior getBehavior(){
return new AppBarLayout.ScrollingViewBehavior()
};
}
复制代码
Behavior的onMeasureChild()能够代理CoordinatorLayout子View的测量,注意这个子View是关联了当前Behavior,它的返回值为Boolean类型,返回true表示使用Behavior的onMeasureChild()来测量参数里child的这个子View,返回false则使用CoordinatorLayout的默认测量子View的方法。函数
//CoordinatorLayout.Behavior
public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
return false;
}
复制代码
在CoordinatorLayout的onMeasure()里能够看出Behavior中的代理子View的测量:布局
//CoordinatorLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
...
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
//Behavior判空检测是否能够代理measure
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
...
}
}
复制代码
和上面相似,Behavior的onLayoutChild()能够代理CoordinatorLayout子View的布局,它的返回值为Boolean类型,返回true表示使用Behavior的onLayoutChild()来布局子View,返回false则使用CoordinatorLayout的默认测量子View的方法。post
//CoordinatorLayout.Behavior
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}
复制代码
在CoordinatorLayout的onLayout()里能够看出Behavior中的代理子View的布局:动画
//CoordinatorLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
...
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
//Behavior判空检测是否能够代理layout
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
复制代码
Behavior的onApplyWindowInsets()能够代理消费CoordinatorLayout的WindowInsets。
//CoordinatorLayout.Behavior
public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) {
return insets;
}
复制代码
在CoordinatorLayout的onLayout()里能够看出Behavior中的消费CoordinatorLayout的WindowInsets: setFitsSystemWindows()->setupForInsets()->setWindowInsets()->dispatchApplyWindowInsetsToBehaviors()
//CoordinatorLayout
private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
...
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
if (ViewCompat.getFitsSystemWindows(child)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
// If the view has a behavior, let it try first
insets = b.onApplyWindowInsets(this, child, insets);
if (insets.isConsumed()) {
// If it consumed the insets, break
break;
}
}
}
}
return insets;
}
复制代码
Behavior的onInterceptTouchEvent()、onTouchEvent()能够在CoordinatorLayout分发给子View前被拦截消费,若Behavior拦截了来自CoordinatorLayout的Touch事件,CoordinatorLayout的各个子View天然就接受不到Touch事件,Behavior的blocksInteractionBelow()表示是否阻断此Behavior所关联View下层的View的交互,则这个方法能影响Touch事件的拦截,若blocksInteractionBelow()为true时,getScrimOpacity()返回值大于0,CoordinatorLayout将会在View的上层绘制一个屏蔽的getScrimColor()颜色来显示没法进行交互的区域:
//CoordinatorLayout.Behavior
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
return getScrimOpacity(parent, child) > 0.f;
}
public float getScrimOpacity(CoordinatorLayout parent, V child) {
return 0.f;
}
public int getScrimColor(CoordinatorLayout parent, V child) {
return Color.BLACK;
}
复制代码
接下来看看CoordinatorLayout的onInterceptTouchEvent()、onTouchEvent()如何被Behavior代理:
//CoordinatorLayout
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
...
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
...
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
//Behavior不为空,事件分发给Behavior
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// Keep the super implementation correct(走CoordinatorLayout默认方法)
if (mBehaviorTouchView == null) {
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
...
return handled;
}
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
//记录是否Behavior的blocksInteractionBelow()返回true,根据这个标
//识来给剩余遍历的Behavior分发个CANCEL的MotionEvent
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
//根据View的层级由高到低排序,储放在临时的容器
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
//(先遍历最外层View的Behavior的Touch事件代理)
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
//若Touch事件已经被前面遍历的Behavior拦截或者newBlock为true表示前面遍历的Behavior已阻断交互、且action不是DOWN时
//那么后面剩余遍历的Behavior分发个CANCEL的MotionEvent
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
//没有拦截Touch事件,Behavior不为空,事件分发给Behavior
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
//若是Behavior拦截了Touch事件,标记其关联的View
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
//这里要考虑onInterceptTouchEvent()进入performIntercept()Behavior阻断过,
//再到onTouchEvent()进入performIntercept()就没必要再遍历
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
//CoordinatorLayout.LayoutParams
/** * Behavior是否以前已经阻断过此Behavior所关联View下层的View的交互 */
boolean didBlockInteraction() {
if (mBehavior == null) {
mDidBlockInteraction = false;
}
return mDidBlockInteraction;
}
/** * Behavior已经阻断过此Behavior所关联View下层的View的交互返回true, * 不然返回调用Behavior的blocksInteractionBelow并记录已阻断过 */
boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
if (mDidBlockInteraction) {
return true;
}
return mDidBlockInteraction |= mBehavior != null
? mBehavior.blocksInteractionBelow(parent, child)
: false;
}
复制代码
CoordinatorLayout的onInterceptTouchEvent()执行拦截主要逻辑在performIntercept()里:
CoordinatorLayout的onTouchEvent()逻辑以下:
这里小结一下:若是重写Behavior的onInterceptTouchEvent()、onTouchEvent()应当很是注意其逻辑在 CoordinatorLayout中onInterceptTouchEvent()、onTouchEvent()的合理性,由于在Behavior代理触摸事件的处理显得有点复杂并且繁琐,并且会有大量的非正常的cancel事件出现。
CoordinatorLayout实现了NestedScrollingParent2接口并也覆写兼容NestedScrollingParent,但它自己并无处理嵌套滑动而是所有给Behavior代理,Behavior代理嵌套滑动是经过NestedScrollingParent二、NestedScrollingParent对应的方法多了两个参数:一个是CoordinatorLayout,一个是Behavior关联的View。由于涉及到方法比较多,这里不宜展开,关于嵌套滑动能够参考我以前写的的《浅析NestedScrolling嵌套滑动机制之基础篇》
//CoordinatorLayout.Behavior
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
@Deprecated
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
// Do nothing
}
复制代码
接下来看看CoordinatorLayout的嵌套滑动让Behavior代理,这里分析只两个方法,其余的方法十分相似:
//CoordinatorLayout
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
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) {
//Behavior代理onStartNestedScroll
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
//在Behavior关联的View的LayoutParams记录是否接受嵌套滑动
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
//CoordinatorLayout.LayoutParams
void setNestedScrollAccepted(int type, boolean accept) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
mDidAcceptNestedScrollTouch = accept;
break;
case ViewCompat.TYPE_NON_TOUCH:
mDidAcceptNestedScrollNonTouch = accept;
break;
}
}
复制代码
在CoordinatorLayout的onStartNestedScroll()里遍历子View,获取子View的Behavior并调用onStartNestedScroll()并在LayoutParams记录是否接受嵌套滑动。
//CoordinatorLayout
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
...
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
...
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//判断Behavior是否接受嵌套滑动
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
...
////Behavior代理onNestedPreScroll
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
...
}
}
...
}
复制代码
在CoordinatorLayout的onNestedPreScroll()里遍历子View,获取子View的LayoutParams判断Behavior是否接受嵌套滑动,若接受则获取子View的Behavior并调用onNestedPreScroll()。
Behavior很强大,可是通常而言子View的测量、布局这部分逻辑能够放在自定义View内部处理,而CoordinatorLayout的分发WindowInsets、Touch事件给子View都有固定的顺序,若是你在Behavior处理时应该注意其逻辑在CoordinatorLayout的合理性,不必为了使用Behavior而是用它,嵌套滑动在实现神奇滑动的效果倒是十分有用,也能够解耦自定义NestedScrollParent的逻辑。
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout android:id="@+id/app_bar"/>
<android.support.design.widget.FloatingActionButton app:layout_anchor="@id/app_bar" app:layout_anchorGravity="bottom|end" />
</android.support.design.widget.CoordinatorLayout>
复制代码
还有一种就是在布局文件添加layout_anchor设置锚点来创建依赖关系,不过这种依赖关系 只能监听依赖View的位置、大小改变时回调onDependentViewChanged()。
//CoordinatorLayout.Behavior
/** * 返回值表示child是否依赖dependency */
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
/** * 返回值表示Behavior是否改变child的大小或者位置 */
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
复制代码
CoordinatorLayout对View的依赖关系经过support包的DirectedAcyclicGraph有向无环图进行拓扑排序。
在图论中,若是一个有向图从任意顶点出发没法通过若干条边回到该点,则这个图是一个有向无环图(DAG,directed acyclic graph)--维基百科
在CoordinatorLayout的onMeasure()里的prepareChildren()就是对View依赖关系进行排序:
private final List<View> mDependencySortedChildren = new ArrayList<>();
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
//找到View的Anchor锚点
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
//将view当节点添加进有向无环图
mChildDag.addNode(view);
// Now iterate again over the other children, adding any dependencies to the graph
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {//判断view与other是否存在的依赖关系
if (!mChildDag.contains(other)) {
//(若是other没在图里则添加才能确保view与other在图创建依赖)
// Make sure that the other node is added
mChildDag.addNode(other);
}
//(将view与other在图添加边创建依赖)
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
//(将图节点以深度优先排序的list存放在list容器里)
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
//(反转list让没有依赖关系的view排在list的前面)
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
复制代码
Behavior的onDependentViewChanged()和onDependentViewRemoved()被触发在CoordinatorLayout的onChildViewsChanged(),这方法type参数有三个值:EVENT_PRE_DRAW(依赖view绘制以前事件类型)、EVENT_NESTED_SCROLL(依赖view嵌套滑动事件类型)、EVENT_VIEW_REMOVED(依赖view从布局移除事件类型)。
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
...
final int childCount = mDependencySortedChildren.size();
...
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
//检测view的anchor锚点位置是否发生变化来调整依赖view的位置
offsetChildToAnchor(child, layoutDirection);
}
}
...
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();
//判断checkChild是否依赖child
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
...
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
//(分发依赖view从布局移除事件给Behavior)
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
//(分发依赖view绘制以前事件或嵌套滑动事件给Behavior)
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
...
}
void offsetChildToAnchor(View child, int layoutDirection) {
...
//注意:这里view和anchor锚点位置都调整了,将这变化通知给Behavior
// If we have needed to move, make sure to notify the child's Behavior
final Behavior b = lp.getBehavior();
if (b != null) {
b.onDependentViewChanged(this, child, lp.mAnchorView);
}
...
}
复制代码
在CoordinatorLayout的onNestedFling()、onNestedPreScroll()、onNestedPreScroll()里若是NestedScrollingChild处理了嵌套滑动都会经过onChildViewsChanged(EVENT_NESTED_SCROLL)将依赖view嵌套滑动事件分发给Behavior,下面以onNestedScroll代码为例。
//CoordiantorLayout.java
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
...
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
复制代码
在CoordinatorLayout的构造方法里经过setOnHierarchyChangeListener()注册OnHierarchyChangeListener监听添加或移除View的层级变化,而CoordinatorLayout.OnHierarchyChangeListener在View被移除回调中调用onChildViewsChanged(EVENT_VIEW_REMOVED)将依赖view从布局移除事件类型分发给Behavior。
public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
...
super.setOnHierarchyChangeListener(new HierarchyChangeListener();
}
private class HierarchyChangeListener implements OnHierarchyChangeListener {
...
@Override
public void onChildViewRemoved(View parent, View child) {
//将依赖view从布局移除事件类型分发给Behavior
onChildViewsChanged(EVENT_VIEW_REMOVED);
...
}
}
复制代码
在CoordinatorLayout的onAttachedToWindow()中往ViewTreeObserver注册个CoordinatorLayout.OnPreDrawListener,它会在每次刷新肯定各View大小位置后并绘制以前回调,而在回调里调用onChildViewsChanged()将依赖view绘制以前事件类型分发给对应的Behavior。
//是否须要注册mOnPreDrawListener标识
private boolean mNeedsPreDrawListener;
//是否已经执行onAttachedToWindow()标识
private boolean mIsAttachedToWindow;
private OnPreDrawListener mOnPreDrawListener;
@Override
public void onAttachedToWindow() {
...
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
...
mIsAttachedToWindow = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
//分发依赖view绘制以前事件类型
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
复制代码
虽然onAttachedToWindow()会被调用在onDraw()以前,但也可能在onMeasure()以前调用,若是View之间不存在依赖关系则mOnPreDrawListener从ViewTree移除防止内存泄露,因此在onMeasure()的ensurePreDrawListener()里检测View之间是否存在依赖关系对mOnPreDrawListener进行注册或注销。
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
//遍历子View,看它们是否存在依赖关系
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
//存在依赖,注册mOnPreDrawListener
addPreDrawListener();
} else {
////不存在依赖,注销mOnPreDrawListener
removePreDrawListener();
}
}
}
void addPreDrawListener() {
//若是已经执行onAttachedToWindow()
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
//(由于onMeasure()与onAttachedToWindow()调用顺序不肯定,
//因此这里标识mNeedsPreDrawListener变量来处理注册mOnPreDrawListener)
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
void removePreDrawListener() {
if (mIsAttachedToWindow) {
if (mOnPreDrawListener != null) {
final ViewTreeObserver vto = getViewTreeObserver();
vto.removeOnPreDrawListener(mOnPreDrawListener);
}
}
mNeedsPreDrawListener = false;
}
}
复制代码
1.在自定义Behavior以前您能够参考系统自带的Behavior可否知足需求,如FloatActionButton内部的Behavior能保证Snackbar弹出的时候不被FAB遮挡等:
2.是否有必要为子View的测量、布局、分发WindowInsets和Touch事件而使用CoordinatorLayout+Behavior,这部分逻辑是否能够放在自定义View内部处理。
3.Behavior的View依赖关系与NestedScrolling结合实现滑动更为方便。
CoordinatorLayout和Behavior结合很强大,但本文偏向概念性内容,不免有些枯燥,下篇文章实践自定义Behavior,因为本人水平有限仅给各位提供参考,但愿可以抛砖引玉,若是有什么能够讨论的问题能够在评论区留言或联系本人。