1. 程式人生 > >CoordinatorLayout的使用(二)——自定義Behavior

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)在程式碼裡面,通過LayoutParamssetBehavior(@Nullable Behavior behavior)方式繫結。

知道流程後,我們就開始擼程式碼吧。

1、產生依賴關係的使用

先放一個我們需要實現效果圖

可以看到這裡的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的原始碼入手,看下整個呼叫流程是怎麼樣的。