Material Design——Coordinator Layout

楔子

最近正在模仿制作知乎日报,知乎日报的详情页有这样的效果。经过查询之后发现原来可以使用Coordinator Layout完成该效果,所以就好好学了一下这个View。

CoordinatorLayout简介

CoordinatorLayout的作用

首先我们要知道,CoordinatorLayout是继承于FrameLayout。然后才是其功能,作用是放入CoordinatorLayout中的View都能够发生相对滑动。也就是说CoordinatorLayout中的子View都可以监听其他子View的滑动。那么如何监听其他子View的行为并采取相应措施的呢?这就需要用到CoordinatorLayout的辅助类Behavior

CoordinatorLayout的简单使用

主要参数介绍

1、首先需要引入Meterial Design的外部依赖:

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

2、 CoordinatorLayout继承了FrameLayout,所以我们使用

    android:layout_gravity="left|top|right|bottom"

控制View在Layout中的位置。

3、 对子View设置Behavior类监听

    app:layout_behavior="类的绝对地址名"

这个就相当于setListener()方法,绑定View的行为。当被观察者发生改变,就调用Behavior的方法。让当前View对该事件做出相应的处理。

Behavior类的使用

Behavior的方法可以分为两组

第一组:某个view监听另一个view的状态变化,例如大小、位置、显示状态等

方法解析
    //作用:判断当前View监听CoordinatorLayout中的哪一个View

    /**
    *  CoordinatorLayout parent:这个不用说就是当前的Cooridnator
    *  View child:这个表示我们设置这个Behavior的View,
    *  View dependency:我们关心的那个View。或者说是被监听的View
    *  (这个View从哪里来的呢? CoordinatorLayout的子类都是dependency)
    *  返回值:是否view是否监听当前的这个dependency。
    */
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    //作用:当被监听对象的大小、位置、显示状态等发生改变的时候,就会回调该方法。

    /**
    *参数与layoutDependsOn()的参数一致就不解释了。
    */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }
实际操作

任务:实现一个联动的TextView

效果图

原理

  1. 首先我们需要制作一个可以跟着鼠标移动的TextView,叫做MoveTextView
  2. 右边的TextView通过监听左边TextView的滑动,创建自己的Behavior
  3. 通过使用CooridnatorLayout设置xml布局

展示代码

//第一步:设置可随手指移动的TextView
public class MoveTextView extends TextView {

    private int lastY = 0;

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

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

    public MoveTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取左上角的Y值
        int currentY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //获取距离差
                int dy = currentY - lastY;
                //获取移动后的Y值
                int tvY = (int) this.getY() + dy;
                //设置tv的位置
                this.setY(tvY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        lastY = currentY;
        return true;
    }
}
//第二步:将tvRight的逻辑写在Behavior中
public class MoveBehavior extends    CoordinatorLayout.Behavior<View> {

    //注意:必须重写构造方法
    public MoveBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

        return dependency instanceof MoveTextView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //设置值
        child.setY(dependency.getY());
        return true;
    }
}
    <!--第三步:设置xml布局-->
    <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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.newbiechen.usecoordinator.MainActivity">

    <com.newbiechen.usecoordinator.widget.MoveTextView
        android:id="@+id/main_tv_left"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:gravity="center"
        android:layout_gravity="left|top"
        android:background="@color/colorPrimary"
        android:text="Hello World!"
        android:textColor="@android:color/white"/>

    <!--在这里设置app:layout_behavior="xxx"-->

    <TextView
        android:id="@+id/main_tv_right"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:gravity="center"
        android:layout_gravity="right|top"
        android:text="Hello EveryOne"
        android:background="@color/colorAccent"
        android:textColor="@android:color/white"                                
        app:layout_behavior="com.newbiechen.usecoordinator.behavior.MoveBehavior"/>
</android.support.design.widget.CoordinatorLayout>

补充

  1. 在第二步中我们自定了一个Behavior,发现这个类居然有一个泛型参数,在一般情况下我们可以不需要管他,直接设置为View就可以了。

  2. 同样是在第二步自定义View中,有一个地方我们需要十分注意,那就是必须要重写Behavior的构造方法!!Behavior的构造方法!!Behavior的构造方法!!因为,CoordinatorLayout是通过反射调用Behavior的构造方法来创建Behavior的。所以必须要重写Behavior的构造方法,否则会报ClassNotFoundException。

第二组:某个view监听另一个View的滑动状态

方法解析
    //作用:当View开始滑动的时候回调。

    /**
    *  主要参数说明:
    *  View target:依赖的View
    *  int nestedScrollAxes:表示滑动的方法,(横向或者纵向)
    *  返回值:判断判断当前View是否接收这种滑动。
    */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    //作用:当正在滑动的时候回调

    /**
    * 主要参数说明:
    * int dx:表示与上次相比x的偏移量
    * int dy:表示与上次相比y的偏移量
    */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    //作用:当ScrollView脱离点击,自动滑动时候的回调

    /**
    * 主要参数说明:
    * float velocityX:表示滑动时候横向上的速度
    * float velocityY:表示滑动时候纵向上的速度
    */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }
实际操作

任务:实现联动的ScrollView

效果图

展示代码

public class ScrollBehavior extends CoordinatorLayout.Behavior<View>{

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

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        //表示:判断当前滑动是否为竖直方向上的滑动,如果是则接受该滑动
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        //获取被监听的ScrollView的内部Y的偏移量
        int scrollY = target.getScrollY();
        //将被监听View的Y的偏移量赋值给当前View
        child.setScrollY(scrollY);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
        //表示child自动滑动的速度与被监听的View的速度相同
        ((NestedScrollView) child).fling((int) velocityY);
        return true;
    }
}
<!--xml布局-->
    <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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.newbiechen.usecoordinator.MainActivity">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="150dp"
        android:layout_height="match_parent"
        android:layout_gravity="left|top">
        <!--滑动的内容,太多了,不展示-->
    </android.support.v4.widget.NestedScrollView>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="150dp"
        android:layout_height="match_parent"
        android:layout_gravity="right|top"
        app:layout_behavior="com.newbiechen.usecoordinator.behavior.ScrollBehavior">
        <!--滑动的内容,太多了,不展示-->
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

补充

发现在xml布局中,我并没有使用ScrollView,而是使用了NestedView。原因是ScrollView在滑动过程中,无法触发Behavior的onStartNestedScroll()等一系列滑动回调。为什么会这样呢?

原因是Coordinator是通过NestedScrollingChild接口,来回调Behavior的滑动方法的。在api23中,只有RecyclerView和NestedView继承了NestedScrollingChild接口,所以当滑动的时候,触发NestedScollingChild从而调用Behavior的滑动方法。所以如果想进行滑动监听ListView和ScrollView是无法使用的。如果真的想监听其他View,那么只能继承NestedScrollingChild并根据源码重写了。

Coordinator的优点

从上述的例子我们知道了Coordinator是如何使用的,那么Google为什么需要创建一个这样的Layout,在没有Coordinator之前,我们是通过什么方式建立相对滑动的呢?Coordinator相对于之前的方式带给我们的优点是什么呢?

不利用Coordinator实现第一幅Gif中,两个TextView的联动

展示代码:

/*  继承TextView,自定义MoveTextView。
 1.  通过OnTouchEvent()方法,监听点击事件。自定义Listener设置回调
 */
public class MoveTextView extends TextView {
    private OnMoveListener mMoveListener;
    private int lastY = 0;

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

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

    public MoveTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取左上角的Y值
        int currentY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //获取距离差
                int dy = currentY - lastY;
                //获取移动后的Y值
                int tvY = (int) this.getY() + dy;
                //设置tv的位置
                this.setY(tvY);
                if (mMoveListener != null){
                    mMoveListener.onMove(tvY);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        lastY = currentY;
        return true;
    }

    //设置TextView移动的监听器
    public interface OnMoveListener{
        void onMove(int y);
    }

    //添加监听器的方法
    public void setOnMoveListener(OnMoveListener listener){
        mMoveListener = listener;
    }
}
public class MainActivity extends AppCompatActivity {
    private MoveTextView mTvLeft;
    private TextView mTvRight;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvLeft = (MoveTextView) findViewById(R.id.main_tv_left);
        mTvRight = (TextView) findViewById(R.id.main_tv_right);
        initClick();
    }

    private void initClick(){
        //设置监听的回调
        mTvLeft.setOnMoveListener(new MoveTextView.OnMoveListener() {
            @Override
            public void onMove(int y) {
            //在Activity中,通过设置回调,实现右侧的TextView移动
                mTvRight.setY(y);
            }
        });
    }
}

根据当前代码与使用CoordinatorLayout的代码进行对比。我们可以很容易的得出下列结论:
1. 没有CoordinatorLayout的话,需要重写View并自定义一个监听器,通过监听器建立相对滑动。如果使用CoordinatorLayout就不需要自定义监听器。(方便)
2. CoordinatorLayout的行为是写在Behavior类中的,未使用Coordinator的行为是写在Activity中的,这样无疑增加了Activity类的复杂度,并且如果不同的View有相同的行为的话,这种做法的耦合度太高,不利于代码的复用。(对行为进行解耦 )
3. Behavior能够监听多种View的行为,并作出相应的行为。(监听多个View)

Coordinator常用实现

模仿知乎的Toolbar(楔子中的Gif图)

首先需要知道AppBarLayout

作用:是Toolbar的扩展,能够装填Toolbar以外的View合并成一个ActionBar。并且内部的View的滑动由AppBarLayout控制。并且设置沉浸式导航栏的背景为透明,Toolbar的标题从大到小的动画。

主要属性

  1. AppBarLayout继承了LinearLayout,默认是竖直排布

  2. app:layout_scrollFlags=”” 用来控制内部View的滑动。

可用参数

  • scroll: 所有想滚动出屏幕的view都需要设置这个flag- 没有设置这个flag的view将被固定在屏幕顶部。

  • enterAlways: 这个flag让任意向下的滚动都会导致该view变为可见,启用快速“返回模式”。

  • enterAlwaysCollapsed: 当你的视图已经设置minHeight属性又使用此标志时,你的视图只能已最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。

  • exitUntilCollapsed: 滚动退出屏幕,最后折叠在顶端。

其次需要知道CollaspingToolbarLayout

作用:折叠内部的View,必须在AppbarLayout中使用

主要参数

  1. app:layout_collapseMode=”pin | parallen”

    pin:固定模式,在折叠的时候最后固定在顶端
    
    parallen:视差模式,在折叠的时候会有个视差折叠的效果
    
  2. app:layout_collapseParallenMultiplier=”0~1” 滑动的时候视觉差的程度,参数由0~1

最后代码

    <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/content_app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/theme_brief_height"
        android:fitsSystemWindows="true"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/content_collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimaryDark"
            app:expandedTitleMarginStart="5dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/content_iv_title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7" />

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?android:attr/actionBarSize"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <WebView
            android:id="@+id/content_webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </WebView>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

补充

我们在xml中发现,有这么一行app:layout_behavior=”@string/appbar_scrolling_view_behavior”
这样使用是因为,android为我们提供了默认的behavior对一些特殊的View。当前的behavior就是针对AppbarLayout的。

FloatingActionButton的使用

作用:顾名思义,是一个浮动的Button,当和SnackBar一起使用的时候,会产生根据SnackBar出现与消失的移动效果。
(本篇主要是讲FloatingActionButton与CoordinatorLayout的关系,并不讲解FloatingActionButton的具体使用)

效果图

展示代码

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right|bottom"
        android:layout_marginRight="20dp"
        android:layout_marginBottom="20dp"
        android:src="@mipmap/ic_launcher"/>
</android.support.design.widget.CoordinatorLayout>
public void onCreate(Bundle saveBundle){
    setContent(R.layout.activity_main);
    //获取FloatButton,设置监听器
    findViewById(R.id.fab)
                .setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        //设置SnackBar
                        Snackbar.make(v,"Hello EveryOne",Snackbar.LENGTH_LONG)
                                .setAction("cancel", new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        //关闭SnackBar时候的监听
                                    }
                                }).show();
                    }
                });
}

为什么FloatingActionButton能够实现上移下沉的效果,原来是FloatButton默认使用了FloatingActionButton.Behavior。
根据源码:

        @Override
public boolean layoutDependsOn(CoordinatorLayout parent,
                FloatingActionButton child, View dependency) {
            // We're dependent on all SnackbarLayouts (if enabled)
       return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
}

我们可以得出,FloatActionButton依赖于SnackBar的行为。所以当SnackBar改变的时候,会调用Behavior的回调。

在知乎的首页就有利用FloatActionButton的实例
(黄色按钮的显示与隐藏)

效果图

我们只需要自定义一下Behavior就能达到这样的显示和隐藏效果了,由于效果比较简单,代码就不贴了。原理就是,当View向下滑动的时候,显示Button,怎么显示呢通过利用ObjectAnimation,或者在xml中定义d的Animation都是可以的。

结尾

参考文章Behavior的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值