徹底搞懂 CoordinatorLayout
本系列文章會從官方文檔出發,從基本使用姿勢到工作原理,試圖把CoordinatorLayout、AppBarLayout等一系列Material Desgin風格控件徹底講明白。本篇文章主要介紹CoordinatorLayout的基本概念,是后續篇章的基礎。
從是什么開始
首先我們先來看看CoordinatorLayout究竟是個什么東東,它究竟是用來做什么的。官方文檔對CoordinatorLayout是這樣描述的:
CoordinatorLayout是一個“加強版”FrameLayout,它主要有兩個用途:
-
用作應用的頂層布局管理器,也就是作為用戶界面中所有UI控件的容器
-
用作相互之間具有特定交互行為的UI控件的容器
通過為CoordinatorLayout的子View指定Behavior,就可以實現它們之間的交互行為。 Behavior可以用來實現一系列的交互行為和布局變化,比如說側滑菜單、可滑動刪除的UI元素,以及跟隨著其他UI控件移動的按鈕等。
上面的描述可能有些抽象,現在我們只需要知道CoordinatorLayout是一個布局管理器,主要用來實現它的子View間的交互行為。那么什么是交互行為呢?我們來看一個簡單的例子:
在上圖中,我們拖動按鈕,可以看到,一個TextView會跟著按鈕一起移動。這就是交互行為的一個簡單地例子。也就是說首先需要Button的位置發生變化,然后TextView對Button的位置變化做出響應,這個例子中,TextView做出響應的方式就是跟隨著Button一起移動。那么這個交互行為時如何實現的呢?我們接著往下看。
如何實現交互行為
上面我們看到了一個簡單的交互行為的例子,下面我們通過分析這個例子來介紹一下實現交互行為的一般性步驟。
上面的例子實現的是Button和TextView的交互行為:Button發生變化時,TextView要對這個變化做出響應。那么首先我們要讓TextView知道Button產生了變化,還要指明TextView對Button的變化做出什么反應。
這里實際上是一個觀察者模式的運用:TextView是觀察者,Button是被觀察者。TextView需要向系統注冊一個回調,告知系統在Button發生變化時通知它,這樣當Button發生變化時,TextView會得到關于這個變化的通知,而后就可以對這個變化做出響應。如此一來我們便實現了TextView和Button的交互行為。下面我們來介紹如何通過給TextView設置一個Behavior來實現上面的交互行為。
XML布局文件
首先我們先來看一下用戶界面的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
app:layout_behavior=".FollowBehavior"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="觀察者" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="被觀察者" />
</android.support.design.widget.CoordinatorLayout>
我們為TextView指定了一個layout_behavior屬性,這樣就給它設置了一個Behavior。我們可以看到layout_behavior屬性的值為".FollowBehavior",指的是當前Module中一個名為FollowBehavior的類,它實際上是CoordinatorLayout.Behavior類的子類,我們來看一下FollowBehavior類的實現。
自定義交互行為類
FollowBehavior類的代碼如下:
public class FollowBehavior extends CoordinatorLayout.Behavior<TextView> {
public FollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
return dependency instanceof Button;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child,
View dependency) {
child.setX(dependency.getX() + 150);
child.setY(dependency.getY() + 150);
return true;
}
}
在上面的代碼中我們重寫了父類的兩個方法:layoutDependsOn()方法和onDependentViewChanged()方法。在介紹這兩個方法的作用前,我們先來介紹一下dependent view。在一個交互行為中,dependent view的變化決定了另一個相關View的行為。在這個例子中,Button就是dependent view,因為TextView跟隨著它。實際上dependent view就相當于我們前面介紹的被觀察者。知道了這個概念,讓我們看看重寫的兩個方法的作用:
-
layoutDependsOn():這個方法在對界面進行布局時至少會調用一次,用來確定本次交互行為中的dependent view,在上面的代碼中,當dependency是Button類的實例時返回true,就可以讓系統知道布局文件中的Button就是本次交互行為中的dependent view。
-
onDependentViewChanged():當dependent view發生變化時,這個方法會被調用,參數中的child相當于本次交互行為中的觀察者,觀察者可以在這個方法中對被觀察者的變化做出響應,從而完成一次交互行為。
現在我們已經定義好了一個交互行為,但是Button還不會跟著我們的手指移動,接下來我們讓它動起來。
讓Button動起來
我們只需讓Button在屏幕上的位置隨我們手指移動,從而讓TextView跟著它移動,相關代碼如下:
public class CoorDemoActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.follow);
findViewById(R.id.btn).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
v.setX(event.getRawX()-v.getWidth()/2);
v.setY(event.getRawY()-v.getHeight()/2);
}
return true;
}
});
}
}
這樣一來,我們就完成了為TextView和Button設置跟隨移動這個交互行為。很簡單有木有,其實為CoordinatorLayout的子View設置交互行為只需三步:
-
自定義一個繼承自Behavior類的交互行為類;
-
把觀察者的layout_behavior屬性設置為自定義行為類的類名;
-
重寫Behavior類的相關方法來實現我們想要的交互行為。
值得注意的是,有些時候,并不需要我們自己來定義一個Behavior類,因為系統為我們預定義了不少Behavior類。在接下來的篇章中,我們會做出進一步的介紹。
更進一步
現在我們已經知道了怎么通過給CoordinatorLayout的子View設置Behavior來實現交互行為。現在,讓我們更進一步地挖掘下CoordinatorLayout,深入了解一下隱藏在表象背后的神秘細節。
實際上,CoordinatorLayout本身并沒有做過多工作,實現交互行為的主要幕后推手是CoordinatorLayout的內部類——Behavior。通過為CoordinatorLayout的 直接子View 綁定一個Behavior,這個Behavior就會攔截發生在這個View上的Touch事件、嵌套滾動等。不僅如此,Behavior還能攔截對與它綁定的View的測量及布局。關于嵌套滾動,我們會在后續文章中進行詳細介紹。下面我們來深入了解一下Behavior是如何做到這一切的。
深入理解Behavior
攔截Touch事件
當我們為一個CoordinatorLayout的直接子View設置了Behavior時,這個Behavior就能攔截發生在這個View上的Touch事件,那么它是如何做到的呢?實際上,CoordinatorLayout重寫了onInterceptTouchEvent()方法,并在其中給Behavior開了個后門,讓它能夠先于View本身處理Touch事件。具體來說,CoordinatorLayout的onInterceptTouchEvent()方法中會遍歷所有直接子View,對于綁定了Behavior的直接子View調用Behavior的onInterceptTouchEvent()方法,若這個方法返回true,那么后續本該由相應子View處理的Touch事件都會交由Behavior處理,而View本身表示懵逼,完全不知道發生了什么。
攔截測量及布局
了解了Behavior是怎養攔截Touch事件的,想必大家已經猜出來了它攔截測量及布局事件的方式——CoordinatorLayout重寫了測量及布局相關的方法并為Behavior開了個后門。沒錯,真相就是如此。
CoordinatorLayout在onMeasure()方法中,會遍歷所有直接子View,若該子View綁定了一個Behavior,就會調用相應Behavior的onMeasureChild()方法,若此方法返回true,那么CoordinatorLayout對該子View的測量就不會進行。這樣一來,Behavior就成功接管了對View的測量。
同樣,CoordinatorLayout在onLayout()方法中也做了與onMeasure()方法中相似的事,讓Behavior能夠接管對相關子View的布局。
View的依賴關系的確定
現在,我們在探究一下交互行為中的兩個View之間的依賴關系是怎么確定的。我們稱child為交互行為中根據另一個View的變化做出響應的那個個體,而dependent view為child所依賴的View。實際上,確立child和dependent view的依賴關系有兩種方式:
-
顯式依賴:為child綁定一個Behavior,并在Behavior類的layoutDependsOn()方法中做手腳。即當傳入的dependency為dependent view時返回true,這樣就建立了child和dependent view之間的依賴關系。
-
隱式依賴:通過我們最開始提到的錨(anchor)來確立。具體做法可以這樣:在XML布局文件中,把child的layout_anchor屬性設為dependent view的id,然后child的layout_anchorGravity屬性用來描述為它想對dependent view的變化做出什么樣的響應。關于這個我們會在后續篇章給出具體示例。
無論是隱式依賴還是顯式依賴,在dependent view發生變化時,相應Behavior類的onDependentViewChanged()方法都會被調用,在這個方法中,我們可以讓child做出改變以響應dependent view的變化。
來自:http://mp.weixin.qq.com/s?__biz=MzIzMjE1Njg4Mw==&mid=2650117781&idx=1&sn=187bcdbbbfa0610e131c03a0e8a0fbf5&chksm=f0980d29c7ef843f8e5614cb2907277913022f0376363310fe206b1bf413ffec025f593866af#rd