重要的ui元件——Behavior

Bacer發表於2021-09-09

v7包下的元件類似CoordinatorLayout推出也有一段時間了,大家使用的時候應該會體會到其中很多的便利,今天這篇文章帶大家來了解一個比較重要的ui元件——Behavior。從字面意思上就可以看出它的作用,就是用來規定某些元件的行為的,那它到底是什麼,又該怎麼用呢?看完這篇文章希望大家會有自己的收穫~

前言

寫這篇文章的起因是因為我無意中在GitHub上發現了大神新建了一個Repo,內容是。有興趣的同學可以去看看,其實就是透過Behavior去構造一個類似於DrawerLayout的佈局。想了想已經挺長時間沒有搞ui方面的程式碼了,所以趁著這個機會複習了一下,順便寫一篇文章鞏固,也給想要了解這方面內容的同學一個平臺吧。

Behavior是什麼

在文章的開始,我們先要了解什麼是Behavior。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
/**
    * Interaction behavior plugin for child views of {@link CoordinatorLayout}.
    *
    * 

A Behavior implements one or more interactions that a user can take on a child view.     * These interactions may include drags, swipes, flings, or any other gestures.

    *     * @param  The View type that this Behavior operates on     */    public static abstract class  {        ...........            public boolean (CoordinatorLayout parent, V child, MotionEvent ev)       {            return false;        }            public boolean (CoordinatorLayout parent, V child, int layoutDirection) {            return false;        }            ...........    }

它是CoordinatorLayout的內部類,從它的註釋和其中的方法可以看出來,它其實就是給CoordinatorLayout的子View提供了一些互動的方法,用來規範它們的互動行為,比如上面出現的onTouchEvent可以用來規範子View的觸控事件,onLayoutChild可以用來規範子View的佈局。

說到這裡,大家可能會有一個問題,CoordinatorLayout又是個什麼東西?

1
  2
public class  extends  implements  {
   }

可以看出,它其實就是一個ViewGroup,實現了NestedScrollingParent用來執行巢狀滑動。至於巢狀滑動的機制大家可以看我部落格的第一篇文章,這不是我們這篇文章的重點。

既然CoordinatorLayout僅僅只是一個ViewGroup,它又為什麼能展示出它在xml佈局中展示的威力呢?其中的秘密就是在Behavior中。我們可以這麼說,CoordinatorLayout利用了Behavior作為一個代理,去控制管理其下的子View做到各種佈局和動畫效果。那為什麼要使用Behavior呢?我想原因大概就是解耦吧,如果把所有的邏輯都寫死在CoordinatorLayout中,一來不利於維護,二來我們就沒有做一些自定義的事情,會顯得非常的笨重。

為什麼要用Behavior

這裡我們舉一個非常簡單的例子。首先來看看我們的佈局檔案。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
   
   
       
   
           
       
   
       
   
       
   
   

非常簡單有木有,CoordinatorLayout作為根佈局,裡面一個AppBarLayout一個RecyclerView。讓我們看看介面是怎麼樣的。

圖片描述

可以看到顯示是正確的。但是如果我把xml裡RecyclerView的那行layout_behavior刪掉呢?就像這樣。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
   
   
       
   
           
       
   
       
   
       
   
   


圖片描述

最終介面的展示就像這樣,RecyclerView把AppBarLayout給覆蓋了。這裡其實很好理解,如剛才的程式碼所示,CoordinatorLayout其實只是一個ViewGroup,它不像LinearLayout那樣具有特定的佈局特點,甚至可以說它內部的邏輯和FrameLayout是沒什麼差別的,所以如果你不設定對應的Behavior的話,佈局就會有問題。從這裡也可以反映出Behavior的作用,就是規範子View的顯示和互動。

原理&系統是怎麼用Behavior的

說完了Behavior的作用,那該怎麼用它呢?這一小節讓我們來講講Behavior的原理以及系統是如何使用它的。

首先先看原理。我們知道Behavior是用來幫助CoordinatorLayout的,所以我們要從CoordinatorLayout中尋找答案。首先,我們可以看到CoordinatorLayout中有一個LayoutParams,它的子View的LayoutParams都是這個,其中它的建構函式如下。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
LayoutParams(Context context, AttributeSet attrs) {
       super(context, attrs);
   
       .........
       if (mBehaviorResolved) {
           mBehavior = parseBehavior(context, attrs, a.getString(
                   R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
       }
   
       a.recycle();
   }

可以看到它透過parseBehavior去得到了對應子View的Behavior。大家可以試試用RecyclerView的getLayoutParams方法去獲取LayoutParams並且呼叫getBehavior方法,可以得到的就是我們在xml檔案中設定的那個Behavior。

知道了如何將Behavior設定進去,那它是如何發揮作用的呢?讓我們來看看onLayout函式。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
@Override
   protected void (boolean changed, int l, int t, int r, int b) {
       final int layoutDirection = ViewCompat.getLayoutDirection(this);
       final int childCount = mDependencySortedChildren.size();
       for (int i = 0; i 

可以看到的是其中會先呼叫behavior.onLayoutChild(this, child, layoutDirection)。也就是說,Behavior的邏輯要優先於CoordinatorLayout自己的邏輯。其實不止是onLayout,我們還可以看看onTouchEvent這個函式。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
public boolean (MotionEvent ev) {
       boolean handled = false;
       boolean cancelSuper = false;
       MotionEvent cancelEvent = null;
   
       final int action = MotionEventCompat.getActionMasked(ev);
   
       if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
           // Safe since performIntercept guarantees that
           // mBehaviorTouchView != null if it returns true
           final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
           final Behavior b = lp.getBehavior();
           if (b != null) {
               handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
           }
       }
   
       .........
   
       return handled;
   }

可以看到也是呼叫了Behavior的onTouchEvent,我們可以下判斷說Behavior中的那些方法在CoordinatorLayout中都會在合適的時機去呼叫。這也證明了我們剛才的那句話:[Behavior就是CoordinatorLayout的代理,幫助它去管理子View]。

我們做一個總結,Behavior可以代理哪些行為呢?

1.Measure和Layout的佈局行為。

2.onTouchEvent和onInterceptTouchEvent的觸控行為。比如design包中的SwipeDismissBehavior就是透過這樣的方式完成的。

3.巢狀滑動行為(NestedScrollingParent和NestedScrollingChild中的邏輯)。

4.子View間的依賴行為。

對於第四點我們這裡可以細說一下,什麼叫子View的依賴行為呢?這裡我們舉個例子,我們都知道如果在CoordinatorLayout中使用了FAB並且點選展示SnackbarLayout的話,FAB會在Snackbar顯示的時候對應的上移,這是因為FAB依賴了SnackbarLayout。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
public static class  extends . {
   
       ........
   
       @Override
       public boolean (CoordinatorLayout parent,
               FloatingActionButton child, View dependency) {
           // We're dependent on all SnackbarLayouts (if enabled)
           return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
       }
   
      @Override
      public boolean (CoordinatorLayout parent, FloatingActionButton child,View dependency) {
          if (dependency instanceof Snackbar.SnackbarLayout) {
                   updateFabTranslationForSnackbar(parent, child, dependency);
          } else if (dependency instanceof AppBarLayout) {
                      // If we're depending on an AppBarLayout we will show/hide it automatically
                      // if the FAB is anchored to the AppBarLayout
                     updateFabVisibility(parent, (AppBarLayout) dependency, child);
          }
          return false;
       }
   
       ........
   }

這是FAB中的Behavior,可以看到它重寫了layoutDependsOn和onDependentViewChanged,裡面的邏輯很簡單的就可以看明白。這裡我們[將程式碼翻譯成語言]就是說FAB要依賴的元件是SnackbarLayout,所以在之後的操作裡當DependentView(SnackbarLayout)發生了改變,自己(FAB)也會相應的做出改變。

值得一提的是,onDependentViewChanged這個函式的呼叫時機並不是在onLayout之前,而是在onPreDraw中,具體程式碼如下:

1
  2
  3
  4
  5
  6
  7
class  implements . {
       @Override
       public boolean () {
           dispatchOnDependentViewChanged(false);
           return true;
       }
   }

如此簡單的處理View間的依賴,可見Behavior配合CoordinatorLayout是有多強大。下面我們可以再舉一個例子來講講Behavior的作用。還記得我們上面說的嗎?RecyclerView設定了一個Behavior它就可以和AppBarLayout很好的展示出來。這個Behavior的名字是:

1
  2
  3
app:layout_behavior="@string/appbar_scrolling_view_behavior"
   
   android.support.design.widget.AppBarLayout$ScrollingViewBehavior

可以看到它是AppBarLayout裡的一個內部類,讓我們看看它做了什麼。

1
  2
  3
  4
  5
  6
  7
  8
  9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
@Override
   public boolean (CoordinatorLayout parent, View child, View dependency) {
       // We depend on any AppBarLayouts
       return dependency instanceof AppBarLayout;
   }
   
   @Override
   public boolean (CoordinatorLayout parent, View child,
           View dependency) {
       offsetChildAsNeeded(parent, child, dependency);
       return false;
   }
   
   private void (CoordinatorLayout parent, View child, View dependency) {
       final CoordinatorLayout.Behavior behavior =
               ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
       if (behavior instanceof Behavior) {
           // Offset the child, pinning it to the bottom the header-dependency, maintaining
           // any vertical gap, and overlap
           final Behavior ablBehavior = (Behavior) behavior;
           final int offset = ablBehavior.getTopBottomOffsetForScrollingSibling();
           child.offsetTopAndBottom((dependency.getBottom() - child.getTop())
                   + ablBehavior.mOffsetDelta
                   + getVerticalLayoutGap()
                   - getOverlapPixelsForOffset(dependency));
       }
   }

我們知道,如果不設定這個Behavior的話,RecyclerView會覆蓋AppBarLayout。而上面這段程式碼裡的邏輯就可以很好的解釋這個原因了。值得一提的是,在offsetChildAsNeeded方法中有這麼一段:

1
  2
  3
  4
  5
final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
   if (behavior instanceof Behavior) {
       // Offset the child, pinning it to the bottom the header-dependency, maintaining
       // any vertical gap, and overlap
       final Behavior ablBehavior = (Behavior) behavior;

這裡dependency就是AppBarLayout,所以我們可以知道,AppBarLayout中有兩個Behavior,一個是我們前面提到的ScrollingViewBehavior,用來處理它和其他滑動View的關係,另外一個就是Behavior,用來處理自己的邏輯,比如Layout。透過這種巧妙的方式,我們就可以做到非常簡便的控制View本身和View之間的邏輯。

如何自定義Behavior

本來想寫個demo給大家看一看的,不過感覺還是不要重複造輪子了,還是沒用的輪子。推薦大家看SwipeDismissBehavior用法及實現原理這篇文章和一開始提到的Jake大神的新作。如果你把這兩個東西搞懂,那麼Behavior你可以說已經完全沒問題了~

後記

最近一段時間都在搞hotPatch和外掛化相關的東西,看了很多Framework層的原始碼,要做的東西也做的七七八八,希望快點解決最後的幾個bug並且之後能開源和大家見面吧~

原文連結:http://www.apkbus.com/blog-705730-61715.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2768/viewspace-2814684/,如需轉載,請註明出處,否則將追究法律責任。

相關文章