彻底搞懂 CoordinatorLayout

lkim6487 7年前
   <p>本系列文章会从官方文档出发,从基本使用姿势到工作原理,试图把CoordinatorLayout、AppBarLayout等一系列Material Desgin风格控件彻底讲明白。本篇文章主要介绍CoordinatorLayout的基本概念,是后续篇章的基础。</p>    <h2><strong>从是什么开始</strong></h2>    <p>首先我们先来看看CoordinatorLayout究竟是个什么东东,它究竟是用来做什么的。官方文档对CoordinatorLayout是这样描述的:</p>    <p>CoordinatorLayout是一个“加强版”FrameLayout,它主要有两个用途:</p>    <ol>     <li> <p>用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器</p> </li>     <li> <p>用作相互之间具有特定交互行为的UI控件的容器</p> </li>    </ol>    <p>通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。 Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。</p>    <p>上面的描述可能有些抽象,现在我们只需要知道CoordinatorLayout是一个布局管理器,主要用来实现它的子View间的交互行为。那么什么是交互行为呢?我们来看一个简单的例子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/af497c5dddc19a05bd13a724f28fb406.gif"></p>    <p>在上图中,我们拖动按钮,可以看到,一个TextView会跟着按钮一起移动。这就是交互行为的一个简单地例子。也就是说首先需要Button的位置发生变化,然后TextView对Button的位置变化做出响应,这个例子中,TextView做出响应的方式就是跟随着Button一起移动。那么这个交互行为时如何实现的呢?我们接着往下看。</p>    <h2><strong>如何实现交互行为</strong></h2>    <p>上面我们看到了一个简单的交互行为的例子,下面我们通过分析这个例子来介绍一下实现交互行为的一般性步骤。</p>    <p>上面的例子实现的是Button和TextView的交互行为:Button发生变化时,TextView要对这个变化做出响应。那么首先我们要让TextView知道Button产生了变化,还要指明TextView对Button的变化做出什么反应。</p>    <p>这里实际上是一个观察者模式的运用:TextView是观察者,Button是被观察者。TextView需要向系统注册一个回调,告知系统在Button发生变化时通知它,这样当Button发生变化时,TextView会得到关于这个变化的通知,而后就可以对这个变化做出响应。如此一来我们便实现了TextView和Button的交互行为。下面我们来介绍如何通过给TextView设置一个Behavior来实现上面的交互行为。</p>    <h3><strong>XML布局文件</strong></h3>    <p>首先我们先来看一下用户界面的布局文件:</p>    <pre>  <code class="language-java"><?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></code></pre>    <p>我们为TextView指定了一个layout_behavior属性,这样就给它设置了一个Behavior。我们可以看到layout_behavior属性的值为".FollowBehavior",指的是当前Module中一个名为FollowBehavior的类,它实际上是CoordinatorLayout.Behavior类的子类,我们来看一下FollowBehavior类的实现。</p>    <h3><strong>自定义交互行为类</strong></h3>    <p>FollowBehavior类的代码如下:</p>    <pre>  <code class="language-java">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;    }  }</code></pre>    <p>在上面的代码中我们重写了父类的两个方法:layoutDependsOn()方法和onDependentViewChanged()方法。在介绍这两个方法的作用前,我们先来介绍一下dependent view。在一个交互行为中,dependent view的变化决定了另一个相关View的行为。在这个例子中,Button就是dependent view,因为TextView跟随着它。实际上dependent view就相当于我们前面介绍的被观察者。知道了这个概念,让我们看看重写的两个方法的作用:</p>    <ul>     <li> <p>layoutDependsOn():这个方法在对界面进行布局时至少会调用一次,用来确定本次交互行为中的dependent view,在上面的代码中,当dependency是Button类的实例时返回true,就可以让系统知道布局文件中的Button就是本次交互行为中的dependent view。</p> </li>     <li> <p>onDependentViewChanged():当dependent view发生变化时,这个方法会被调用,参数中的child相当于本次交互行为中的观察者,观察者可以在这个方法中对被观察者的变化做出响应,从而完成一次交互行为。</p> </li>    </ul>    <p>现在我们已经定义好了一个交互行为,但是Button还不会跟着我们的手指移动,接下来我们让它动起来。</p>    <h3><strong>让Button动起来</strong></h3>    <p>我们只需让Button在屏幕上的位置随我们手指移动,从而让TextView跟着它移动,相关代码如下:</p>    <pre>  <code class="language-java">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;        }      });    }  }</code></pre>    <p>这样一来,我们就完成了为TextView和Button设置跟随移动这个交互行为。很简单有木有,其实为CoordinatorLayout的子View设置交互行为只需三步:</p>    <ul>     <li> <p>自定义一个继承自Behavior类的交互行为类;</p> </li>     <li> <p>把观察者的layout_behavior属性设置为自定义行为类的类名;</p> </li>     <li> <p>重写Behavior类的相关方法来实现我们想要的交互行为。</p> </li>    </ul>    <p>值得注意的是,有些时候,并不需要我们自己来定义一个Behavior类,因为系统为我们预定义了不少Behavior类。在接下来的篇章中,我们会做出进一步的介绍。</p>    <h2><strong>更进一步</strong></h2>    <p>现在我们已经知道了怎么通过给CoordinatorLayout的子View设置Behavior来实现交互行为。现在,让我们更进一步地挖掘下CoordinatorLayout,深入了解一下隐藏在表象背后的神秘细节。</p>    <p>实际上,CoordinatorLayout本身并没有做过多工作,实现交互行为的主要幕后推手是CoordinatorLayout的内部类——Behavior。通过为CoordinatorLayout的 <strong>直接子View</strong> 绑定一个Behavior,这个Behavior就会拦截发生在这个View上的Touch事件、嵌套滚动等。不仅如此,Behavior还能拦截对与它绑定的View的测量及布局。关于嵌套滚动,我们会在后续文章中进行详细介绍。下面我们来深入了解一下Behavior是如何做到这一切的。</p>    <h3><strong>深入理解Behavior</strong></h3>    <p>拦截Touch事件</p>    <p>当我们为一个CoordinatorLayout的直接子View设置了Behavior时,这个Behavior就能拦截发生在这个View上的Touch事件,那么它是如何做到的呢?实际上,CoordinatorLayout重写了onInterceptTouchEvent()方法,并在其中给Behavior开了个后门,让它能够先于View本身处理Touch事件。具体来说,CoordinatorLayout的onInterceptTouchEvent()方法中会遍历所有直接子View,对于绑定了Behavior的直接子View调用Behavior的onInterceptTouchEvent()方法,若这个方法返回true,那么后续本该由相应子View处理的Touch事件都会交由Behavior处理,而View本身表示懵逼,完全不知道发生了什么。</p>    <p>拦截测量及布局</p>    <p>了解了Behavior是怎养拦截Touch事件的,想必大家已经猜出来了它拦截测量及布局事件的方式——CoordinatorLayout重写了测量及布局相关的方法并为Behavior开了个后门。没错,真相就是如此。</p>    <p>CoordinatorLayout在onMeasure()方法中,会遍历所有直接子View,若该子View绑定了一个Behavior,就会调用相应Behavior的onMeasureChild()方法,若此方法返回true,那么CoordinatorLayout对该子View的测量就不会进行。这样一来,Behavior就成功接管了对View的测量。</p>    <p>同样,CoordinatorLayout在onLayout()方法中也做了与onMeasure()方法中相似的事,让Behavior能够接管对相关子View的布局。</p>    <p>View的依赖关系的确定</p>    <p>现在,我们在探究一下交互行为中的两个View之间的依赖关系是怎么确定的。我们称child为交互行为中根据另一个View的变化做出响应的那个个体,而dependent view为child所依赖的View。实际上,确立child和dependent view的依赖关系有两种方式:</p>    <ul>     <li> <p>显式依赖:为child绑定一个Behavior,并在Behavior类的layoutDependsOn()方法中做手脚。即当传入的dependency为dependent view时返回true,这样就建立了child和dependent view之间的依赖关系。</p> </li>     <li> <p>隐式依赖:通过我们最开始提到的锚(anchor)来确立。具体做法可以这样:在XML布局文件中,把child的layout_anchor属性设为dependent view的id,然后child的layout_anchorGravity属性用来描述为它想对dependent view的变化做出什么样的响应。关于这个我们会在后续篇章给出具体示例。</p> </li>    </ul>    <p>无论是隐式依赖还是显式依赖,在dependent view发生变化时,相应Behavior类的onDependentViewChanged()方法都会被调用,在这个方法中,我们可以让child做出改变以响应dependent view的变化。</p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzIzMjE1Njg4Mw==&mid=2650117781&idx=1&sn=187bcdbbbfa0610e131c03a0e8a0fbf5&chksm=f0980d29c7ef843f8e5614cb2907277913022f0376363310fe206b1bf413ffec025f593866af#rd</p>    <p> </p>