Android Design Support Library系列之七:CoordinatorLayout

CoordinatorLayout------协调者布局.
首先来看一下CoordinatorLayout的继承结构:


CoordinatorLayout其本质是一个超级 FrameLayout,其功能主要有2个:
1)作为顶层布局
2)协调其直接子View进行交互

官方文档
使用前添加依赖:

compile 'com.android.support:design:25.3.1'

一、Behavior

CoordinatorLayout的神奇之处就在于Behavior对象,CoordinatorLayout通过Behavior对象来处理其子View的下列事件:
1)布局事件
2)触摸事件
3)View状态变化事件
4)嵌套滑动事件
Behavior就是CoordinatorLayout处理事件的媒介,在Behavior中定义了 CoordinatorLayout 中直接子 View 的行为规范,决定了当收到不同事件时,应该做怎样的处理。

Behavior继承结构

Behavior是一个定义在CoordinatorLayout中的抽象内部类.

public static abstract class Behavior<V extends View> {

     public Behavior() {
     }

     public Behavior(Context context, AttributeSet attrs) {
     }
     ......
}
下面我们通过自定义Behavior来简单了解:

3)View状态变化事件
某个View监听另一个View的状态变化,例如大小、位置、显示状态等
此时,我们需要关心Behavior的下面两个方法:

layoutDependsOn()
onDependentViewChanged()

4)嵌套滑动事件
某个View监听另一个View(实现NestedScrollingChild接口)的滑动状态
此时,我们需要关心Behavior中与NestedScrolling相关的方法.

二、自定义Behavior之一

某个View监听另一个View的状态变化

我们首先来看一下效果图:
1)水平拖动dependency时,child朝着与dependency相反方向移动
2)竖直拖动dependency时,child在相同方向上同步移动


这里我们要理解两个概念:childdependency.
1) child是CoordinatorLayout的直接子View,也就是要执行动作的View
2) dependency是指child依赖的View,也就是child要监听的View

在上面的效果图中:child的动作依赖于dependency,当dependency这个View发生了变化,那么child这个View就发生相应变化。
child具体变化的动作就定义在Behavior中:
我们定义一个类,继承CoordinatorLayout.Behavior<T>,其中,泛型参数T是我们要执行动作的View类,也就是Child,然后实现Behavior的两个方法:

layoutDependsOn()
onDependentViewChanged()
public class DependencyBehavior extends CoordinatorLayout.Behavior<Button> {

    private int width;

    /**
     * 这个构造方法必须重载,因为在CoordinatorLayout里利用反射去获取Behavior的时候就是拿的这个构造
     */
    public DependencyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        width  = context.getResources().getDisplayMetrics().widthPixels;
    }

    /**
     *  确定依赖关系
     * @param child     要执行动作的View
     * @param dependency    child要依赖的View,也就是child要监听的View
     * @return  根据逻辑确定依赖关系
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
        return dependency instanceof com.my.DragTextView;
    }

    /**
     * dependency状态(大小、位置、显示与否等)发生变化时该方法执行
     *        在这里我们定义child要执行的具体动作
     * @return  child是否要执行相应动作
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {

        int top = dependency.getTop();
        int left = dependency.getLeft();

        int x = width - left - child.getWidth();
        int y = top;

        setPosition(child, x, y);
        return true;
    }

    private void setPosition(View child, int x, int y) {
        CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) child.getLayoutParams();
        layoutParams.leftMargin = x;
        layoutParams.topMargin = y;
        child.setLayoutParams(layoutParams);
    }
}

这里的DragTextView是一个自定义的TextView

/**
 * 随着手指移动的TextView
 */
public class DragTextView extends TextView {
    private int lastX;
    private int lastY;

    public DragTextView(Context context) {
        super(context);
    }

    public DragTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) getLayoutParams();
                int left = layoutParams.leftMargin + x - lastX;
                int top = layoutParams.topMargin + y - lastY;

                layoutParams.leftMargin = left;
                layoutParams.topMargin = top;
                setLayoutParams(layoutParams);
                requestLayout();
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
        }
        lastX = x;
        lastY = y;
        return true;
    }
}

Behavior的使用方式:

  1. 在布局文件中通过app:layout_behavior=" "引用
  2. 使用注解添加,系统的控件一般使用这种方式,例AppBarLayout:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
  ......
  public static class Behavior extends HeaderBehavior<AppBarLayout> {
    ......
  }
  ......
}
  1. 在代码中添加
DependencyBehavior mBehavior = new DependencyBehavior(this,null);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) childView.getLayoutParams();
params.setBehavior(mBehavior);

这里我们使用第一种方式:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.my.DragTextView
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="dependency"
        android:textColor="@android:color/white" />

    <Button
        app:layout_behavior="com.my.DependencyBehavior"
        
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="child"
        android:textAllCaps="false"
        android:textColor="@android:color/white"/>
         
</android.support.design.widget.CoordinatorLayout>

这里需要注意的是:
1)一个child可以同时依赖多个dependency
2)dependency也有可能依赖一个或者多个另外的dependency
3)如果你添加了一个依赖,不管child的顺序如何,你的child将总是在所依赖的View放置之后才会被放置
ok,简单的自定义Behavior已经完成了,你对于CoordinatorLayout使用Behavior协调子View之间的交互是否有所了解了?

三、自定义Behavior之二

某个View监听另一个View(实现NestedScrollingChild接口)的滑动状态

我们首先来看一下效果图:


1)向上滑动时,右下角FloatingActionButton隐藏
2)向下滑动时,右下角FloatingActionButton显示
3)点击FloatingActionButton时,弹出Snackbar,同时FloatingActionButton自动上移.

其中效果(3)是FloatingActionButton中自带的Behavior的效果,相信看了上面你也大概了解这个Behavior中FloatingActionButton一定是依赖Snackbar的了吧。

所以这里我们是直接继承FloatingActionButton.Behavior:

public class ScrollBehavior extends FloatingActionButton.Behavior {

    public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
    }

    /**
     * 嵌套滑动事件开始
     *
     * @return  根据返回值确定我们关心哪个方向的滑动(x轴/y轴),这里我们关心的是y轴方向
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }


    /**
     *  嵌套滑动正在进行中
     *  参数有点多,由于这里我们只关心y轴方向的滑动,所以简单测试了dyConsumed、dyUnconsumed
     *      dyConsumed > 0 && dyUnconsumed == 0 上滑中
     *      dyConsumed == 0 && dyUnconsumed > 0 到边界了还在上滑
     *
     *      dyConsumed < 0 && dyUnconsumed == 0 下滑中
     *      dyConsumed == 0 && dyUnconsumed < 0 到边界了还在下滑
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (((dyConsumed > 0 && dyUnconsumed == 0) 
                || (dyConsumed == 0 && dyUnconsumed > 0))
                && child.getVisibility() != View.INVISIBLE) {// 上滑隐藏
            child.setVisibility(View.INVISIBLE);
        } else if (((dyConsumed < 0 && dyUnconsumed == 0) 
                || (dyConsumed == 0 && dyUnconsumed < 0)) 
                && child.getVisibility() != View.VISIBLE ) {//下滑显示
            child.setVisibility(View.VISIBLE);
        }
    }
}

这里我们采用一种高大上的方式来使用Behavior ,也是我们使用系统定义好Behavior 的常用方式:
res/values/strings.xml中:

<resources>
    <string name="app_name">CoordinatorLayout</string>
    
    <string name="my_scroll_behavior">com.my.ScrollBehavior</string>
    <!--看这里-->

    <string name="text">\n
                    从前现在过去了再不来\n\n
                    红红落叶长埋尘土内\n\n
                    开始终结总是没变改\n\n
                    天边的你飘泊白云外\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n\n
                    情人别后永远再不来(消散的情缘)\n\n
                    无言独坐放眼尘世外(愿来日再续)\n\n
                    鲜花虽会凋谢(只愿)\n\n
                    但会再开(为你)\n\n
                    一生所爱隐约(守候)\n\n
                    在白云外(期待)\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n
                    ----------------\n
                    从前现在过去了再不来\n\n
                    红红落叶长埋尘土内\n\n
                    开始终结总是没变改\n\n
                    天边的你飘泊白云外\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n\n
                    情人别后永远再不来(消散的情缘)\n\n
                    无言独坐放眼尘世外(愿来日再续)\n\n
                    鲜花虽会凋谢(只愿)\n\n
                    但会再开(为你)\n\n
                    一生所爱隐约(守候)\n\n
                    在白云外(期待)\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n\n
                    苦海翻起爱恨\n\n
                    在世间难逃避命运\n\n
                    相亲竟不可接近\n\n
                    或我应该相信是缘份\n
    </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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="start|top">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/text" />

    </android.support.v4.widget.NestedScrollView>


    <android.support.design.widget.FloatingActionButton
        app:layout_behavior="@string/my_scroll_behavior"
        
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="16dp"
        android:src="@mipmap/add"
        app:backgroundTint="@color/colorPrimary"
        app:borderWidth="0dp"
        app:elevation="8dp"
        app:fabSize="normal"
        app:pressedTranslationZ="16dp" />


</android.support.design.widget.CoordinatorLayout>

Activiy中:

public class MainActivity extends AppCompatActivity {

    private FloatingActionButton mFAB;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroll);

        mFAB = (FloatingActionButton) findViewById(R.id.fab);
        mFAB.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(mFAB,"Snackbar",Snackbar.LENGTH_SHORT).show();
            }
        });
    }
}

这里需要注意的是:
1)你不需要在嵌套滑动的Behavior中定义依赖,CoordinatorLayout的每个child都有机会接收到嵌套滑动事件,这里继承的FloatingActionButton.Behavior中存在依赖是因为要和Snackbar实现联动.
2)虽然我叫它嵌套滑动,但其实它包含滚动(scrolling)和滑动(flinging)两种
3)监听的滑动View必须实现NestedScrollingChild的接口,这是因为CoordinatorLayout中一个View想向外界传递滑动事件,即通知 NestedScrollingParent(CoordinatorLayout实现了此接口),就必须实现此接口.而 Child 与 Parent 的具体交互逻辑, NestedScrollingChildHelper 辅助类基本已经帮我们封装好了,所以我们只需要调用对应的方法即可。
这就可以解释为什么不能用ScrollView、ListView而用NestScrollView来滑动了,当然,如果你要自己自定义一个View实现NestedScrollingChild接口也是可以的,不过那样太麻烦了。像NestScrollView、RecyclerView、SwipeRefreshLayout中都实现了NestedScrollingChild接口.

如果你想了解关于嵌套滑动机制更多的详情,你可以去看一看下面几个类:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

本文参考:
CoordinatorLayout的使用如此简单
Intercepting everything with CoordinatorLayout Behaviors

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容