美文网首页support designUI优秀案例
CoordinatorLayout——自定义Behavior

CoordinatorLayout——自定义Behavior

作者: 皮球二二 | 来源:发表于2016-11-11 17:03 被阅读2264次

    在之前的文章里,我们在源码的基础上学习了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张图来体会下

    Behavior的功能 Behavior在整套体系中的位置

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

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


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


      嵌套滑动
    • 另外一种嵌套滑动展示


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


      嵌套滑动
    • BottomSheetBehavior


      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的使用

    相关文章

      网友评论

      本文标题:CoordinatorLayout——自定义Behavior

      本文链接:https://www.haomeiwen.com/subject/gfuvettx.html