天天看點

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的源碼入手,看下整個調用流程是怎麼樣的。

繼續閱讀