CoordinatorLayout——自定义Behavior

在之前的文章里,我们在源码的基础上学习了CoordinatorLayout的大致执行流程,那么今天我们将来到另一个重要的关键点–自定义Behavior。
本次代码的地址在github上,欢迎大家star、follow

这里再一次啰嗦下什么是Behavior

  • Behavior是Android Support Design库中的布局概念。只有CoordinatorLayout的直接子View使用Behavior才有效果。
  • 你可以为任何View添加一个Behavior。
  • 在新的嵌套滑动机制中,引入了NestedScrollingChildNestedScrollingParent两个接口,用于协调父子控件滑动状态。CoordinatorLayout实现了NestedScrollingParent接口,实现NestedScrollingChild这个接口的子控件在滑动时会调用NestedScrollingParent接口的相关方法,将事件发给父控件CoordinatorLayout,由CoordinatorLayout决定是否消费当前事件。与此同时,在CoordinatorLayout实现的NestedScrollingParent相关方法中,会分别调用Behavior内部的不同方法。所以说Behavior是一系列回调,让你有机会以非侵入的方式动态的为View添加依赖布局以及处理父布局(CoordinatorLayout)的滑动手势

盗2张图来体会下

《CoordinatorLayout——自定义Behavior》 Behavior的功能
《CoordinatorLayout——自定义Behavior》 Behavior在整套体系中的位置

本篇将使用N个Demo来具体说明下如何完成自定义功能

  • BackBehavior 快速返回效果的Behavior,根据AppBarLayout的滚动来控制自定义View的滚动

    《CoordinatorLayout——自定义Behavior》 BackBehavior

  • FloatingActionBarBehavior 控制FloatingActionButton滚动的Behavior,根据NestedScrollView的滚动方向来决定是否显示FloatingActionButton

    《CoordinatorLayout——自定义Behavior》 嵌套滑动

  • 另外一种嵌套滑动展示

    《CoordinatorLayout——自定义Behavior》 嵌套滑动

  • 通过自定义的NestedScrollingParent与NestedScrollingChild实现嵌套滚动

    《CoordinatorLayout——自定义Behavior》 嵌套滑动

  • BottomSheetBehavior

    《CoordinatorLayout——自定义Behavior》 b.gif

由于篇幅有限,这里仅简单的举例子稍微说明下,如果有疑问,请直接在简书中评论区或者在github仓库Issues中给我留言。再啰嗦一句,本次代码的地址在github上,欢迎大家star、follow

通用流程

  1. 重写构造方法
  2. 绑定到需要处理的View上
  3. 事件流

重写构造方法

这个在之前的文章中已经说够了,自定义的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>

事件流

算上刚才说的那种情况,这里一共有四种不同的事件流

  1. 布局事件
    在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)
  1. 触摸事件
    触摸事件就是Behavior中的onInterceptTouchEvent与onTouchEvent。注意这里,如果Behavior对触摸事件进行了拦截,那么后续事件将不会再分发到Child View自身的触摸事件中了。而且事件由CoordinatorLayout分发下来,所以这里的touch事件都是未知View的,所以需要额外判断当前的点击事件是不是由我们的控件触发的
  2. 变化事件
    这里需要穿插一个判断依赖对象的过程。之前我们已经提及过在自定义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;
}
  1. 嵌套滑动事件
    我之前用了很大的篇幅对其进行了源码分析,所以这里也不再准备具体说概念了。只稍微提及一下重要的方法
/**
 * 需要判断滑动的方向是否是我们需要的。
 * 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。
我们特别要注意onNestedScrollonNestedPreScroll两个方法的回调,大家学习的时候可以通过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的使用

    原文作者:皮球二二
    原文地址: https://www.jianshu.com/p/5091b928bec2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞