天天看点

CoordinatorLayout的使用(二)——自定义Behavior

 我们在上一篇文章CoordinatorLayout的使用(一)——简单使用中介绍了CoordinatorLayout的基本用法。为什么CoordinatorLayout能够这么方便的帮助我们非常简单的就实现炫酷的UI交互效果呢?这就不得不提到它的内部类Behavior了。其实CoordinatorLayout本身并没有做太多的事情,就是充当一个触摸事件桥梁的作用,所有的核心实现都是交给Behavior去做的。而我们之前文章使用的AppBarLayout和就是在内部默认使用了

AppBarLayout.Behavior

实现了交互逻辑。

既然Behavior这么重要,所以本篇,我们就介绍一下Behavior,简单实现两个自定义的Behavior。

一、类介绍

这里我们先看下Behavior这个类:

public static abstract class Behavior<V extends View> {
  ​
          public Behavior() {
          }
  ​
          public Behavior(Context context, AttributeSet attrs) {
          }
          
          // 将Behavior设置个LayoutParams的时候调用
          public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
          }
  ​
          // 从LayoutParams移除的时候对调
          public void onDetachedFromLayoutParams() {
          }
  ​
          // 这个是在有触摸事件产生的时候,由CoordinatorLayout分发过来。由我们自己决定是否拦截。
          // 类似ViewGroup的onInterceptTouchEvent()方法。
          /* @param parent 分发此次事件的CoordinatorLayout
           * @param child 和该Behavior关联的View
           * @param ev the 触摸事件
           * @return true:表示要拦截事件,就将后续事件分发给onTouchEvent方法进行处理,fasle表示不进行拦截,默认返回false
           */
          public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
              return false;
          }
  ​
          // 类似于View的onTouchEvent()方法,可以在里面具体处理触摸事件的逻辑。
           /* @param parent 分发此次事件的CoordinatorLayout
           * @param child 和该Behavior关联的View
           * @param ev the 触摸事件
           * @return true表示自己消费掉了事件,就不会往后传递事件了。fasle表示自己不消费事件,默认返回false
           */
          public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
              return false;
          }
  ​
          /**
           * 给和当前Behavior关联的View区域之外的蒙层,相当于是突出当前的View
           * 默认是Black
           */
          @ColorInt
          public int getScrimColor(CoordinatorLayout parent, V child) {
              return Color.BLACK;
          }
  ​
          /**
           * 用于指定上面设置蒙层颜色的透明度
           * 默认是0.0f
           */
          @FloatRange(from = 0, to = 1)
          public float getScrimOpacity(CoordinatorLayout parent, V child) {
              return 0.f;
          }
  ​
          /**
           * 是否阻止交互位于该Behavior绑定View下方View的交互
           * 默认是根据这个判断getScrimOpacity(parent, child) > 0.f
           */
          public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
              return getScrimOpacity(parent, child) > 0.f;
          }
  ​
          /**
           * 指定当前的View(child)是否要依赖另外一个View(dependency)的位置、大小等的变化而进行调整
           * @param parent 
           * @param child 当前和Behavior绑定的View
           * @param dependency 需要依赖关联的View
           * @return 如果需要关联,返回true,否则返回fasle
           */
          public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
              return false;
          }
  ​
          /**
           * 在layoutDependsOn()方法产生关联(返回true)后,dependency的大小、位置等属性有变化,就会回调该方法。我们可以在这里进行相应的处理。比如跟随dependency上移而上移。
           * @param parent 
           * @param child 
           * @param dependency 所依赖的View
           * @return 如果child做出了相应的改变,返回true,否则返回false
           */
          public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
              return false;
          }
  ​
          /**
           * 所依赖的View被移除了当前的视图数,会接收到该回调。
           * @param parent 
           * @param child 
           * @param dependency 所依赖的View
           */
          public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
          }
  ​
          /**
           * CoordinatorLayout在测量Child的时候,会调用该方法。你可以在该方法里面完成自己的测量逻辑
           * @param parent 
           * @param child 
           * @param parentWidthMeasureSpec 
           * @param widthUsed 已经被使用里的宽度
           * @param parentHeightMeasureSpec 
           * @param heightUsed 已经被使用了的高度
           * @return 如果自己完成了测量逻辑返回true,CoordinatorLayout就不会再自己对该child进行测量,否则返回false
           */
          public boolean onMeasureChild(CoordinatorLayout parent, V child,
                  int parentWidthMeasureSpec, int widthUsed,
                  int parentHeightMeasureSpec, int heightUsed) {
              return false;
          }
  ​
          /**
           * CoordinatorLayout在对子View进行layout的时候会回调该方法。
           * @param parent 
           * @param child 
           * @param layoutDirection 布局的方向ViewCompat#LAYOUT_DIRECTION_LTR或者ViewCompat#LAYOUT_DIRECTION_RTL
           * @return 如果你自己完成了布局,返回true,CoordinatorLayout不会再对该child进行布局,否则返回false
           */
          public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
              return false;
          }
  ​
          /**
           * 设置标志,和View.setTag作用一样,我们可以在里面保存一个我们需要的对象
           * @param child child view to set tag with
           * @param tag tag object to set
           */
          public static void setTag(View child, Object tag) {
              final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              lp.mBehaviorTag = tag;
          }
  ​
          /**
           * 获得我们设置的标志对象
           * @param child child view to get tag with
           * @return the previously stored tag object
           */
          public static Object getTag(View child) {
              final LayoutParams lp = (LayoutParams) child.getLayoutParams();
              return lp.mBehaviorTag;
          }
  ​
  ​
          /**
           * 如果CoordinatorLayout有可以可支持的嵌套滑动View(如NestedScrollView等),在NestedScrollView触发滑动后,但是还没有对手指滑动距离进行处理前,会先回调该方法。
           * @param coordinatorLayout 
           * @param child 
           * @param directTargetChild 包含NestedScrollView的CoordinatorLayout的直接子View
           * @param target 真正触发滑动的View
           * @param nestedScrollAxes 滑动方向ViewCompat#SCROLL_AXIS_HORIZONTAL或者ViewCompat#SCROLL_AXIS_VERTICAL}
           * @return 如果我们想要自己处理滑动,返回true,否则返回false。返回false后,后面有关嵌套滑动的几个方法就不会被调用了。
           */
          public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                  V child, View directTargetChild, View target, int nestedScrollAxes) {
              return false;
          }
  ​
          // onStartNestedScroll()放回true后,会紧接着被调用,我么可以做一些滑动的准备工作。
          // 参数同上
          public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
                  View directTargetChild, View target, int nestedScrollAxes) {
              // Do nothing
          }
  ​
          // 本次嵌套滑动停止的时候(是指用户停止滑动,不是指NestedScrollView停止滚动,因为有惯性的因素,后续还会继续滚动)
          public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
              // Do nothing
          }
      
      /**
           如果onStartNestedScroll()返回true,系统会将本次用户滑动的距离传过来,可以做优先处理。
           * @param coordinatorLayout 
           * @param child the 
           * @param target t
           * @param dx 水平方向滑动的距离
           * @param dy 垂直方向滑动的距离
           * @param consumed 传出参数,用于记录我们自己消费掉的参数consumed[0]记录我们水平方向消费的距离,consumed[1]记录垂直方向我们消费的距离
           * @see NestedScrollingParent#onNestedPreScroll(View, int, int, int[])
           */
          public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                  int dx, int dy, int[] consumed) {
          }
  ​
          /**
           * 在onNestedPreScroll()调用后,NestedScrollView会根据我们消费的距离,自己再做处理,然后再调用该方法,通知我们是否还有未消费完的距离。
           * @param coordinatorLayout 
           * @param child 
           * @param target 
           * @param dxConsumed 被NestedScrollView消费的水平距离
           * @param dyConsumed 被NestedScrollView消费的垂直距离
           * @param dxUnconsumed 未被NestedScrollView消费的水平距离
           * @param dyUnconsumed 未被NestedScrollView消费的垂直距离
           */
          public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                  int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
              // Do nothing
          }
  ​
          
  ​
          /**
           * 嵌套滑动中,惯性事件的处理
           * @param coordinatorLayout 
           * @param child 
           * @param target 
           * @param velocityX 水平方向的速度
           * @param velocityY 垂直方向的速度
           * @param consumed true NestedScrollView是否消费了惯性事件
           * @return 如果我们消费了事件,返回true
           *
           * @see NestedScrollingParent#onNestedFling(View, float, float, boolean)
           */
          public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                  float velocityX, float velocityY, boolean consumed) {
              return false;
          }
  ​
          /**
           * CoordinatorLayout的子View里面如果有支持嵌套滑动的,在嵌套滑动过程中的Fling开始的时候会首先回调该方法,在里面处理Fling事件。并通过返回值告诉CoordinatorLayout自己是否处理了
           * @param coordinatorLayout 
           * @param child 
           * @param target CoordinatorLayout的子View里面支持嵌套查询的那个View。也就是触发本次嵌套滑动的View
           * @param velocityX 水平方向的速度
           * @param velocityY 垂直方向的速度
           * @return 如果自己消费了Fling事件,返回true,否则返回fasle
           */
          public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                  float velocityX, float velocityY) {
              return false;
          }
  ​
          // 如果给CoordinatorLayout设置了fitSystemWindow=true,可以在这里自己处理WindowInsetsCompat
          @NonNull
          public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
                  V child, WindowInsetsCompat insets) {
              return insets;
          }
  ​
          // 在CoordinatorLayout的requestChildRectangleOnScreen()中被调用
          public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout,
                  V child, Rect rectangle, boolean immediate) {
              return false;
          }
  ​
          /**
           * 恢复之前保存的状态
           */
          public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
              // no-op
          }
  ​
          /**
           * 保存状态
           */
          public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
              return BaseSavedState.EMPTY_STATE;
          }
  ​
          // 处理遮挡覆盖的问题,rect是一个传出参数,需要我们把调整好的位置记录在里面
          /**
           * @param parent 
           * @param child  
           * @param rect   记录调整后的位置
           * @return true:说明我们进行位置调整,fasle:我们没有调整位置
           */
          public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
                  @NonNull Rect rect) {
              return false;
          }
      }
           

Behavior

里面的方法,在注释里面都写的比较清楚了。方法还不少,里面有些方法平时用的不多,后面就没有进行示例介绍,感兴趣可以自己研究研究。平时我们使用的时候,主要就是用在两个方面。

1、一个View跟随另外一个View的变化而变化

2、嵌套滑动的交互。

根据这两个用途的不同,我们所需要关注的方法也不同,下面我们就从这两个方面进行自定义Behavior的介绍。

二、自定义Behavior

这里在具体介绍示例之前,先大致说下自定义Behavior的流程,很简单就两步

首先、自定义类继承自Behavior,然后选择需要重写的方法进行重写实现。

然后,将Behavior绑定到的指定的View上。绑定也有两种方式,a)在xml布局文件中,通过

app:layout_behavior

属性,设置好我们自定义Behavior的类全名。b)在代码里面,通过

LayoutParams

setBehavior(@Nullable Behavior behavior)

方式绑定。

知道流程后,我们就开始撸代码吧。

1、产生依赖关系的使用

先放一个我们需要实现效果图

CoordinatorLayout的使用(二)——自定义Behavior

可以看到这里的HelloWorld的View随着我们向上滑动展示出来了,向下滑动隐藏了。

那我们就看具体的实现吧,在这种使用情景下,我们需要重点关注一下几个方法:

boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
  boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
  void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency)    
           

根据上面的自定义Behavior的步骤,先创建Behavior类。如下:

/**
   * @author Created by victor on 2018/12/11.
   * @since Version
   */
  public class DependencyBehavior extends CoordinatorLayout.Behavior<View> {
  ​
      private float deltaY;
  ​
      public DependencyBehavior() {
      }
  ​
      public DependencyBehavior(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
  ​
      // 这里的child就是我们上面中HelloWord所在的View咯
      @Override
      public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
          // 这里表示 我们需要依赖RecyclerView
          boolean isDependency = dependency instanceof RecyclerView;
          if (isDependency) {
              RecyclerView recyclerView = (RecyclerView) dependency;
          }
          return isDependency;
      }
  ​
      @Override
      public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
          // 获取到RecyclerView的Y坐标
          float dependencyY = dependency.getY();
          if (deltaY == 0) {
              // 第一次先获取到初始状态下RecyclerView的Y坐标和绑定的View的高度差值作为后面计算的基值
              deltaY = dependencyY - child.getHeight();
          }
  ​
          // 根据RecyclerView移动,计算当前的差值
          float dy = dependencyY - child.getHeight();
          dy = dy < 0 ? 0 : dy;
          // 求出当前需要移动的距离
          float y = -(dy / deltaY) * child.getHeight();
          float preTranslationY = child.getTranslationY();
          if (y != preTranslationY) {
              // 移动HelloWorld 并返回true
              child.setTranslationY(y);
              return true;
          }
          return false;
      }
  }
           

实现很简单,在这个

layoutDependsOn()

方法里面,我们告诉系统需要依赖RecyclerView。然后我们滑动RecyclerView的时候,就会回调到

onDependentViewChanged()

这个方法里面。然后根据当前滑动的距离,通过

setTranslationY()

来控制被绑定View的显示和隐藏就可以了。

这里有个注意点:

我们在自定义Behavior的时候,如果要在xml中使用的话,一定要有两个参数的构造方法,否则就会报如下错误

Caused by: java.lang.RuntimeException: Could not inflate Behavior subclass com.victor.coordinatorlayoutdemo.behavior.DependencyBehavior

        at android.support.design.widget.CoordinatorLayout.parseBehavior(CoordinatorLayout.java:615)

…… 

Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]

        at java.lang.Class.getConstructor0(Class.java:2204)

        at java.lang.Class.getConstructor(Class.java:1683)

这里

onDependentViewRemoved()

方法我么没有重写处理,这个例子中暂时没有看到有需要用到的地方。感兴趣的可以自己去试试。

接下来,我们的布局文件:

<?xml version="1.0" encoding="utf-8"?>
  <android.support.design.widget.CoordinatorLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
  ​
      <android.support.design.widget.AppBarLayout
          android:id="@+id/app_bar"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          app:elevation="0dp">
  ​
          <android.support.design.widget.CollapsingToolbarLayout
              android:id="@+id/toolbar_layout"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              app:contentScrim="#00ffffff"
              app:layout_scrollFlags="scroll|exitUntilCollapsed">
  ​
              <ImageView
                  android:layout_width="match_parent"
                  android:layout_height="200dp"
                  android:background="@mipmap/ctl_bg"
                  android:fitsSystemWindows="true"
                  android:scaleType="fitXY"
                  app:layout_collapseMode="parallax"
                  app:layout_collapseParallaxMultiplier="0.7"/>
  ​
          </android.support.design.widget.CollapsingToolbarLayout>
  ​
      </android.support.design.widget.AppBarLayout>
  ​
      <android.support.v7.widget.RecyclerView
          android:id="@+id/recycler_view"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
  ​
      <TextView
          android:id="@+id/tv_title"
          android:layout_width="match_parent"
          android:layout_height="50dp"
          android:background="#ff0000"
          android:gravity="center"
          android:text="Hello World"
          android:textColor="#ffffff"
          android:textSize="18sp"
          app:layout_behavior="@string/dependency_behavior"/>
      <!-- 这里设置给TextView上 -->
  ​
  </android.support.design.widget.CoordinatorLayout>
           

这里我们也参考Google官方的做法,将类全类名放到string.xml资源文件中

<resources>
      <string name="app_name">CoordinatorLayoutDemo</string>
      <string name="dependency_behavior">com.victor.coordinatorlayoutdemo.behavior.DependencyBehavior</string>
  </resources>
           

这样,一个自定义Behavior步骤就完成了,后面就是在代码里面添加模拟数据了,是不是很简单呢。

public class CustomerBehaviorActivity extends AppCompatActivity {
  ​
      List<String> mDatas = new ArrayList<>();
  ​
      @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_customer_befavior);
  ​
          RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
  ​
  ​
          for (int i = 0; i < 50; i++) {
              mDatas.add("Item  " + i);
          }
  ​
          recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
          recyclerView.setAdapter(new RecyclerView.Adapter<CustomerBehaviorActivity.MyViewHolder>() {
              @Override
              public CustomerBehaviorActivity.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                  TextView textView = new TextView(CustomerBehaviorActivity.this);
                  textView.setPadding(0,20, 0, 20);
                  return new MyViewHolder(textView);
              }
  ​
              @Override
              public void onBindViewHolder(MyViewHolder holder, int position) {
                  holder.mTextView.setText(mDatas.get(position));
              }
  ​
              @Override
              public int getItemCount() {
                  return mDatas.size();
              }
          });
  ​
      }
  ​
      class MyViewHolder extends RecyclerView.ViewHolder {
  ​
          public TextView mTextView;
  ​
          public MyViewHolder(View itemView) {
              super(itemView);
              mTextView = (TextView) itemView;
  ​
          }
      }
  }
           

跑起来之后,就能看到上面的效果了。接下来,我们接着说第二种使用情景,嵌套滑动。

2、嵌套滑动的使用

这种使用情景下,我们主要是关心下面的方法:

boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                  V child, View directTargetChild, View target, int nestedScrollAxes)
  void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
                  View directTargetChild, View target, int nestedScrollAxes)
  void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)
  void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                  int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
  void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                  int dx, int dy, int[] consumed)
  boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                  float velocityX, float velocityY, boolean consumed)
  boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                  float velocityX, float velocityY)
           

方法介绍还是看上面类介绍的方法注释吧。这里我们还是先看下要实现的效果吧

这里我们滑动RecyclerView列表的时候,顶部的HelloWorld也跟着上下滑动了。

还是先来自定义个Behavior吧

public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> {
  ​
      private int mOffsetTopAndBottom;
      private int mLayoutTop;
  ​
      public SampleHeaderBehavior() {
      }
  ​
      public SampleHeaderBehavior(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
  ​
      // 这个方法里,我们并没有自己布局,还是直接通过parent去布局,重写该方法只是为了获取初始top值
      @Override
      public boolean onLayoutChild(CoordinatorLayout parent, TextView child, int layoutDirection) {
          parent.onLayoutChild(child, layoutDirection);
          // 获取到child初始的top值
          mLayoutTop = child.getTop();
          return true;
      }
  ​
      @Override
      public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, TextView child, View directTargetChild, View target, int nestedScrollAxes) {
          // 这里我们只关系垂直方向的滚动
          return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
      }
  ​
      @Override
      public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, TextView child, View target, int dx, int dy, int[] consumed) {
          if (dy != 0 ) {
              // 如果本次滑动距离不为0,进行自己的滚动操作
              consumed[1] = scroll(child, dy);
          }
      }
  ​
      // 获取childView最大可滑动距离
      private int getChildScrollRang(View childView) {
          if (childView == null) {
              return 0;
          }
          return childView.getHeight();
      }
  ​
      // 滚动child
      private int scroll(View child, int dy) {
          int consumed = 0; // 记录我们消费的距离
          int offset = mOffsetTopAndBottom - dy; // 计算出本次需要滚动到的位置
          int minOffset = -getChildScrollRang(child);
          int maxOffset = 0;
          // 调整滚动距离,在0和最大可滑动距离的负数之间(因为是向上滑动,所以是负数哦)
          offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);
          // 通过offsetTopAndBottom()进行滚动
          ViewCompat.offsetTopAndBottom(child, offset - (child.getTop() - mLayoutTop));
          // 计算消费的距离
          consumed = mOffsetTopAndBottom - offset;
          // 将本次滚动到的位置记录下来
          mOffsetTopAndBottom = offset;
          return consumed;
      }
  }
           

这里只是做个展示举例,所以实现也很简单,代码里面注释也比较清楚了,就不再讲解了。

不过这里并没有处理Fling事件哦,如果我们快速滑动,产生Fling的时候,我们的HelloWord是不会滚动的。有兴趣的可以自己通过OverScroller去实现Fling的逻辑。

由于这里我们把滚动事件交给我们自己的Behavior消费处理了。那RecyclerView就没法消费滑动距离,也就不会产生滚动了,所以这里我们还需要多处理一步,手动移动RecyclerView,这里也就是我们上面第一种情景下的使用方式了,所以我们再新建一个自定义Behavior

public class ScrollerBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
  ​
      public ScrollerBehavior() {
      }
  ​
      public ScrollerBehavior(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
  ​
      @Override
      public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
          // 依赖TextView(也就是上面HellorWorld所在的View)
          return dependency instanceof TextView;
      }
  ​
      @Override
      public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
          // 如果我们所依赖的View有变化,也是通过offsetTopAndBottom移动我们的RecyclerView
          ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));
          return false;
      }
  }
           

类写完了,使用起来吧,还是将上面两个Behavior的全类名定义到string.xml中

<resources>
      <string name="app_name">CoordinatorLayoutDemo</string>
      <string name="dependency_behavior">com.victor.coordinatorlayoutdemo.behavior.DependencyBehavior</string>
      <string name="behavior_sample_header">com.victor.coordinatorlayoutdemo.behavior.SampleHeaderBehavior</string>
      <string name="behavior_recyclerview">com.victor.coordinatorlayoutdemo.behavior.ScrollerBehavior</string>
  </resources>
           

然后再布局文件中使用:

<?xml version="1.0" encoding="utf-8"?>
  <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                   xmlns:app="http://schemas.android.com/apk/res-auto"
                                                   xmlns:tools="http://schemas.android.com/tools"
                                                   android:layout_width="match_parent"
                                                   android:layout_height="match_parent">
  ​
      <TextView
          android:id="@+id/header"
          android:layout_width="match_parent"
          android:layout_height="200dp"
          android:background="#ff0000"
          android:gravity="center"
          android:text="Hello World"
          android:textColor="#ffffff"
          android:textSize="18sp"
          app:layout_behavior="@string/behavior_sample_header" />
  ​
      <android.support.v7.widget.RecyclerView
          android:id="@+id/rv_nested_scrolling"
          android:layout_width="match_parent"
          app:layout_behavior="@string/behavior_recyclerview"
          android:layout_height="wrap_content" />
  ​
  </android.support.design.widget.CoordinatorLayout>
           

最后再代码里面模拟一组数据给RecyclerView

public class CustomerBehaviorNestedScrollActivity extends AppCompatActivity {
  ​
      List<String> mDatas = new ArrayList<>();
  ​
      @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_nested_scrolling);
  ​
          RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_nested_scrolling);
  ​
  ​
          for (int i = 0; i < 50; i++) {
              mDatas.add("Item  " + i);
          }
  ​
          recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
          recyclerView.setAdapter(new RecyclerView.Adapter<MyViewHolder>() {
              @Override
              public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                  TextView textView = new TextView(CustomerBehaviorNestedScrollActivity.this);
                  textView.setPadding(0, 20, 0, 20);
                  return new MyViewHolder(textView);
              }
  ​
              @Override
              public void onBindViewHolder(MyViewHolder holder, int position) {
                  holder.mTextView.setText(mDatas.get(position));
              }
  ​
              @Override
              public int getItemCount() {
                  return mDatas.size();
              }
          });
  ​
      }
  ​
  ​
      class MyViewHolder extends RecyclerView.ViewHolder {
  ​
          public TextView mTextView;
  ​
          public MyViewHolder(View itemView) {
              super(itemView);
              mTextView = (TextView) itemView;
  ​
  ​
          }
      }
  }
           

这样就完成了我们第二种情景的自定义Behavior。相对于第一种使用方式,此种使用稍微复杂一定。

通过上面的两个例子,我们发现,起始自定义

Behavior

并不复杂,复杂的是要理解其中的调用逻辑。如:每个方法是怎么和CoordinatorLayout配合的,是什么时候被调用的等等。只要我们搞明白调用逻辑后,我们就能根据实际情况,选择我们需要实现的方法,做出相应的逻辑处理,到时我们自己也能实现类似AppBarLayout这种复杂的炫酷的交互逻辑了。所以我们下一篇文章就需要从CoordinatorLayout的源码入手,看下整个调用流程是怎么样的。

继续阅读