使用CoordinatorLayouts实现的首页炫酷效果

摘要

介绍CoordinatorLayout和AppBarLayout的各自用法以及嵌套使用原理并实现如下炫酷效果

CoordinatorLayout

CoordinatorLayout

CoordinatorLayout

CoordinatorLayout是在 Google IO/15 大会发布的,遵循Material 风格,包含在 support Library中,结合AppbarLayout, CollapsingToolbarLayout等 可 产生各种炫酷的效果

Google官方地址

在学习CoordinatorLayout之前,很有必要了解CoordinatorLayout能帮我们做什么,从名字上可以看出,就是帮我们协调子View的。怎么个协调法呢?就是它根据我们的定制,帮助我们协调各个子View的布局。我们先看一组动画图~

CoordinatorLayout

稍微解释一下这个动画,蓝色的矩形是我们一个普通View,黄色的Hello是一个Button。我们水平拖动蓝色矩形时,黄色Button查着与蓝色矩形相反方向移动;竖直移动蓝色矩形时,黄色也跟着竖直。简而言之:它们在竖直方向同步移动,在水平方向相反。

这个效果如果让你不用CoordinatorLayout去实现,应该没有任何问题,但是代码的耦合度应该非常大,你的代码必须要持有2个View的引用,然后在onTouchEvent里面做各种判断。如果我们想要实现的功能是,有更多的View要根据蓝色的View的移动相应作出响应,那么那就得在蓝色View的onTounchEvent里面针对其他的View处理各种逻辑。这耦合度未免太伤感了~

而CoordinatorLayout既然号称能帮我们协调子View的布局,我们接下来看看CoordinatorLayout如何实现~

CoordinatorLayout使用

CoordinatorLayout的使用核心是Behavior,Behavior就是执行你定制的动作。在讲Behavior之前必须先理解两个概念:Child和Dependency,什么意思呢?Child当然是子View的意思了,是谁的子View呢,当然是CoordinatorLayout的子View;其实Child是指要执行动作的CoordinatorLayout的子View。而Dependency是指Child依赖的View。比如上面的gif图中,蓝色的View就是Dependency,黄色的View就是Child,因为黄色的View的动作是依赖于蓝色的View。简而言之,就是如过Dependency这个View发生了变化,那么Child这个View就要相应发生变化。发生变化是具体发生什么变化呢?这里就要引入Behavior,Child发生变化的具体执行的代码都是放在Behavior这个类里面。

怎么使用Behavior呢,首先,我们定义一个类,继承CoordinatorLayout.Behavior,其中,泛型参数T是我们要执行动作的View类,也就是Child。然后就是去实现Behavior的两个方法:

/**
* 判断child的布局是否依赖dependency
*/
   @Override
 public boolean layoutDependsOn(CoordinatorLayout parent, T child, View dependency) {
    boolean rs;
    //根据逻辑判断rs的取值
    //返回false表示child不依赖dependency,ture表示依赖
    return rs;  
}

/**
* 当dependency发生改变时(位置、宽高等),执行这个函数
* 返回true表示child的位置或者是宽高要发生改变,否则就返回false
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, T child, View dependency) {
     //child要执行的具体动作
        return true;
}

有了上面的概念后,我们看看具体怎么去实现吧~

为了响应跟随手指移动的操作,我们定义一个非常简单的View,这个View只响应跟随手指移动,将这个View作为Dependency。由于过于简单,这个View源码不粘贴,我们只需知道这个View的类名叫:TempView。

我们看看Behavior的使用:

package com.hc.studyCoordinatorLayout;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;

/**
 * Package com.hc.studyCoordinatorLayout
 * Created by HuaChao on 2016/6/1.
 */
public class MyBehavior extends CoordinatorLayout.Behavior<Button> {
    private int width;

    public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        DisplayMetrics display = context.getResources().getDisplayMetrics();
        width = display.widthPixels;
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
        //如果dependency是TempView的实例,说明它就是我们所需要的Dependency
        return dependency instanceof TempView;
    }

    //每次dependency位置发生变化,都会执行onDependentViewChanged方法
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button btn, View dependency) {

        //根据dependency的位置,设置Button的位置

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

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

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

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


}

OK,现在我们为Button类指定了Dependency,并且定义好了跟随Dependency一直变化的动作(Behavior),接下来我们就要指定好为哪个具体的Button实例来绑定这些。方法很简单,直接在布局文件指定就好:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hc.studyCoordinatorLayout.MainActivity">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#FFCC00"
        android:text="Hello"
        app:layout_behavior="com.hc.studyCoordinatorLayout.MyBehavior" />

    <com.hc.studyCoordinatorLayout.TempView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#3366CC"  />
</android.support.design.widget.CoordinatorLayout>

是不是很简单呢?我们只需关注Behavior的编写就好了,把Child和Dependency之间的关系完全解耦了~

AppBarLayout

AppBarLayout继承自LinearLayout,布局方向为垂直方向。所以你可以把它当成垂直布局的LinearLayout来使用。AppBarLayout是在LinearLayou上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作。

内部的子View通过在布局中加app:layout_scrollFlags设置执行的动作,那么app:layout_scrollFlags可以设置哪些动作呢?分别如下: scroll

设为scroll的View会跟随滚动事件一起发生移动。当指定的ScrollView发生滚动时,该View也跟随一起滚动,就好像这个View也是属于这个ScrollView一样。

CoordinatorLayout

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll" />
</android.support.design.widget.AppBarLayout>

enterAlways

设为enterAlways的View,当ScrollView往下滚动时,该View会直接往下滚动。而不用考虑ScrollView是否在滚动。

exitUntilCollapsed

值设为exitUntilCollapsed的View,当这个View要往上逐渐“消逝”时,会一直往上滑动,直到剩下的的高度达到它的最小高度后,再响应ScrollView的内部滑动事件。

enterAlwaysCollapsed

是enterAlways的附加选项,一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是enterAlways效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到ScrollView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束。

将AppBarLayout与ScrollView通过CoordinatorLayout关联起来

前面说了一直反复说“当ScrollView发生滚动时”,那么怎么将AppBarLayout与ScrollView关联起来呢?我们注意到,AppBarLayout与ScrollView之间动作“相互依赖”,这不就是我们上面介绍CoordinatorLayout使用的内容吗?把ScrollView和AppBarLayout作为CoordinateLayout的子View,然后编写一个Behavior,在这个Behavior里面判断当前的操作是应该让ScrollView时刻保持在AppBarLayout之下(即只要改变AppBarLayout的位置就可以一起滑动),还是应该让ScrollView内部滚动而不让AppBarLayout位置发生变化等等这些需求,都是可以在Behavior里面处理的。你可以去针对你的ScrollView编写Behavior。然而,我们看到我们的AppBarLayout事先的功能比较复杂,如果我们自己去定义这样的效果,代码非常复杂,还要考虑很多方面,好在Android帮我们写好啦,我们直接用就是了,这个ScrollView就是NestedScrollView,请注意,它并没有继承ScrollView,它继承的是FrameLayout,但是它实现的效果把它可以看成是ScrollView。

把NestedScrollView放入到我们的layout文件里面就可以啦~~~,很方便~

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <!--将你的内容放在这里-->

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

有没有注意到有个属性:app:layout_behavior=“@string/appbar_scrolling_view_behavior”,它就是指定Behavior的,appbar_scrolling_view_behavior对应的类的名称是:android.support.design.widget.AppBarLayout$ScrollingViewBehavior感兴趣的可以去分析源码。

好了,我们现在会用AppBarLayout啦~是不是发现用起来so easy!接下来我们把剩下CollapsingToolbarLayout的给”消化”掉!

CollapsingToolbarLayout

CollapsingToolbarLayout是用来对Toolbar进行再次包装的ViewGroup,主要是用于实现折叠(其实就是看起来像伸缩~)的App Bar效果。它需要放在AppBarLayout布局里面,并且作为AppBarLayout的直接子View。CollapsingToolbarLayout主要包括几个功能(参照了官方网站上内容,略加自己的理解进行解释):

  1. 折叠Title(Collapsing title):当布局内容全部显示出来时,title是最大的,但是随着View逐步移出屏幕顶部,title变得越来越小。你可以通过调用setTitle函数来设置title。

  2. 内容纱布(Content scrim):根据滚动的位置是否到达一个阀值,来决定是否对View“盖上纱布”。可以通过setContentScrim(Drawable)来设置纱布的图片.

  3. 状态栏纱布(Status bar scrim):根据滚动位置是否到达一个阀值决定是否对状态栏“盖上纱布”,你可以通过setStatusBarScrim(Drawable)来设置纱布图片,但是只能在LOLLIPOP设备上面有作用。

  4. 视差滚动子View(Parallax scrolling children):子View可以选择在当前的布局当时是否以“视差”的方式来跟随滚动。(PS:其实就是让这个View的滚动的速度比其他正常滚动的View速度稍微慢一点)。将布局参数app:layout_collapseMode设为parallax

  5. 将子View位置固定(Pinned position children):子View可以选择是否在全局空间上固定位置,这对于Toolbar来说非常有用,因为当布局在移动时,可以将Toolbar固定位置而不受移动的影响。 将app:layout_collapseMode设为pin。

了解这些概念后,我们来看看布局吧~

<?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.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/main.backdrop"
                android:layout_width="wrap_content"
                android:layout_height="300dp"
                android:scaleType="centerCrop"
                android:src="@drawable/material_img"
                app:layout_collapseMode="parallax" />

            <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="wrap_content"
        android:paddingTop="50dp"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/my_txt"
            android:textSize="20sp" />

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

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

上面的都看得懂吧,每个陌生的属性都是讲过的哦,忘记了的话回头看,稍微解释一下,图片被设置为有视差的滑动,Toolbar设置为固定不动,另外,CollapsingToolbarLayout会对title进行放大和缩小,我们看看效果吧~

CollapsingToolbarLayout

实现一个特殊效果

CollapsingToolbarLayout

布局的核心代码

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 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="wrap_content"
    android:background="@color/colorPrimaryDark"
    android:orientation="vertical">

    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dip">

            <LinearLayout
                android:id="@+id/main_search_area"
                android:layout_width="300dip"
                android:layout_height="30dip"
                android:layout_centerInParent="true"
                android:orientation="horizontal"
                android:background="@drawable/main_home_search_bg"
                android:gravity="center">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="@dimen/text_size"
                    android:textColor="@color/app_second_textcolor"
                    android:text="搜索"
                    android:drawableLeft="@mipmap/icon_main_home_search"
                    android:drawablePadding="2dip"/>
            </LinearLayout>
            
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="50dip"
                android:paddingLeft="16dip"
                android:paddingRight="8dip"
                android:gravity="center_vertical"
                android:id="@+id/toobar_view"
                android:background="@mipmap/toolbar_bg_nomal"
                android:alpha="0"
                android:orientation="horizontal">

                <ImageView
                    android:id="@+id/main_top_small_menu1"
                    android:layout_width="40dip"
                    android:layout_height="40dip"
                    android:src="@mipmap/button_main_home_menu1"
                    android:scaleType="centerInside"/>

                <ImageView
                    android:id="@+id/main_top_small_menu2"
                    android:layout_marginLeft="10dip"
                    android:layout_width="40dip"
                    android:layout_height="40dip"
                    android:src="@mipmap/button_main_home_menu2"
                    android:scaleType="centerInside"/>

                <ImageView
                    android:id="@+id/main_top_small_menu3"
                    android:layout_marginLeft="10dip"
                    android:layout_width="40dip"
                    android:layout_height="40dip"
                    android:src="@mipmap/button_main_home_menu3"
                    android:scaleType="centerInside"/>

                <LinearLayout
                    android:id="@+id/main_search_area2"
                    android:layout_width="match_parent"
                    android:layout_marginLeft="10dip"
                    android:layout_marginRight="8dip"
                    android:layout_height="30dip"
                    android:layout_centerInParent="true"
                    android:orientation="horizontal"
                    android:background="@drawable/main_home_search_bg"
                    android:gravity="center">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textSize="@dimen/text_size"
                        android:textColor="@color/app_second_textcolor"
                        android:text="搜索"
                        android:drawableLeft="@mipmap/icon_main_home_search"
                        android:drawablePadding="2dip"/>
                </LinearLayout>
            </LinearLayout>
        </RelativeLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/main_home_refreshview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <android.support.design.widget.CoordinatorLayout
            android:id="@+id/app_coordinatorlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <android.support.design.widget.AppBarLayout
                android:id="@+id/app_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_scrollFlags="exitUntilCollapsed"
                android:theme="@style/AppTheme.AppBarOverlay">

                <android.support.design.widget.CollapsingToolbarLayout
                    android:id="@+id/toolbar_layout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:contentScrim="?attr/colorPrimary"
                    app:layout_scrollFlags="scroll|exitUntilCollapsed">

                    <include layout="@layout/main_head_content_layout"
                        android:layout_width="match_parent"
                        android:layout_height="248dip"/>
                </android.support.design.widget.CollapsingToolbarLayout>
            </android.support.design.widget.AppBarLayout>

            <RelativeLayout
                android:id="@+id/main_home_videolist"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

        </android.support.design.widget.CoordinatorLayout>
    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

在Activity中获取AppBarLayout对象去监听滑动过程

 mAppBarLayout = getView(R.id.app_bar);
        mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                if (verticalOffset >= 0) {
                    mPtrFrameLayout.setEnabled(true);
                } else {
                    mPtrFrameLayout.setEnabled(false);
                }
                int total = appBarLayout.getTotalScrollRange();
                float alpha = (float)(total + verticalOffset) / total;
                mToolbarAlpha = 1.0 - alpha;
                if(mToolbarAlpha < 0.3f){
                    mToobarView.setClickable(false);
                }else{
                    mToobarView.setClickable(true);
                }
                mToobarView.setAlpha((float)mToolbarAlpha);
                mSearchView.setAlpha(alpha);
            }
        });