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

摘要

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

CoordinatorLayout

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

Google官方地址

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

稍微解释一下这个动画,蓝色的矩形是我们一个普通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的两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 判断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的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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实例来绑定这些。方法很简单,直接在布局文件指定就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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一样。

1
2
3
4
5
6
7
8
9
10
11
<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文件里面就可以啦~~~,很方便~

1
2
3
4
5
6
7
8
<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。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?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进行放大和缩小,我们看看效果吧~

实现一个特殊效果

布局的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?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对象去监听滑动过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
});