CoordinatorLayout简介
CoordinatorLayout是一个继承于ViewGroup的布局容器。CoordinatorLayout监听滑动子控件的滑动通过Behavior反馈到其他子控件并执行一些动画。简单来说,就是通过协调并调度里面的子控件或者布局来实现触摸(一般是指滑动)产生一些相关的动画效果。
其中,view的Behavior是通信的桥梁,我们可以通过设置view的Behavior来实现触摸的动画调度。
注意:滑动控件指的是:RecyclerView/NestedScrollView/ViewPager,意味着ListView、ScrollView不行。
示例
下面介绍一些常见的示例,进一步介绍CoordinatorLayout与其他控件的配合使用,从而做出更好的效果。
示例一、CoordinatorLayout与FloatingActionButton、Snackbar
FloatingActionButton随列表滚动的动画
在上一篇文章当中,我们使用CoordinatorLayout实现了FloatingActionButton随着界面的滑动而进行相应的显示与隐藏动画的效果,我们可以自己通过监听滑动事件实现,同理,我们也可以通过根布局使用CoordinatorLayout来实现,给FloatingActionButton自定义了一个Behavior(实质也是监听滑动事件)。
Snackbar弹出之后被FloatingActionButton遮挡的问题
在一个界面中,既有FloatingActionButton,又有Snackbar的时候,那么Snackbar的弹出可能就会挡住FloatingActionButton。例如:
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Snackbar snackbar = Snackbar.make(v, "是否打开GPS?", Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("好的", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}).show();
}
});
那么最好的解决办法就是使用CoordinatorLayout来作为根布局,从而解决这个问题。我们可以在Snackbar的showView方法中看到Behavior相关的操作:
final void showView() {
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
// If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
view.setVisibility(View.GONE);
dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
}
@Override
public void onDragStateChanged(int state) {
switch (state) {
case SwipeDismissBehavior.STATE_DRAGGING:
case SwipeDismissBehavior.STATE_SETTLING:
// If the view is being dragged or settling, cancel the timeout
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
});
clp.setBehavior(behavior);
// Also set the inset edge so that views can dodge the snackbar correctly
clp.insetEdge = Gravity.BOTTOM;
}
mTargetParent.addView(mView);
}
//...省略
}
在Snackbar的博客中,我们就已经知道了:Snackbar在选择锚点的时候,如果遇到了CoordinatorLayout,那么就会默认选择它作为最合适的父容器。
示例二、CoordinatorLayout与AppBarLayout联合使用
AppBarLayout是一个继承LinearLayout的布局容器,它与CoordinatorLayout联合使用就可以实现一些动态效果(例如标题栏滑出去)。
在使用AppBarLayout的时候,AppBarLayout里面的子控件需要设置一个scrollFlags属性:
app:layout_scrollFlags="scroll"
flag包括:
scroll: 里面所有的子控件想要当滑出屏幕的时候view都必须设置这个flag,没有设置flag的view将被固定在屏幕顶部。
enterAlways:一旦往下滑,就会马上出现
enterAlwaysCollapsed:当你的视图设置了minHeight属性的时候,那么视图只能以最小高度进入,
只有当滚动视图到达顶部时才扩大到完整高度。
exitUntilCollapsed:滚动退出屏幕,最后折叠在顶端。
snap:
实现标题栏滑出去效果
下面先来布局文件:
<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="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:paddingTop="?attr/actionBarSize">
<android.support.v7.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:divider="@drawable/abc_list_divider_mtrl_alpha"
app:dividerPadding="10dp"
app:showDividers="middle">
<!--这里放置大量控件使得NestedScrollView可以滑动-->
</android.support.v7.widget.LinearLayoutCompat>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="标题栏"
app:titleTextColor="#fff"/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
根布局使用了CoordinatorLayout,然后放置了一个用于产生滚动的NestedScrollView。这里需要说明的是,NestedScrollView相对于传统的ScrollView来说,NestedScrollView解决了一些事件冲突的问题(例如内嵌ListView)。
NestedScrollView需要添加layout_behavior,告知CoordinatorLayout我是滑动的:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
然后NestedScrollView需要添加下面两句属性,保证有上padding,同时滑动的时候可以滑到padding区域(不然的话,就会被Toolbar遮挡住了):
android:paddingTop="?attr/actionBarSize"
android:clipChildren="false"
android:clipToPadding="false"
然后我们需要一个标题栏,我们用AppBarLayout对Toolbar进行包裹:
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="标题栏"
app:titleTextColor="#fff"/>
</android.support.design.widget.AppBarLayout>
被包裹的Toolbar需要设置scrollFlags属性,证明Toolbar可以滑出去,并且回滑的时候可以马上滑回来:
app:layout_scrollFlags="scroll|enterAlways"
AppBarLayout还可以添加其他各种各样的控件,里面TabLayout等等,不需要滑出去的话就不添加scrollFlags,在下面一个部分会有所体现。
使用ViewPager+TabLayout+Fragment+AppBarLayout实现传统的APP架构
同样的,看布局文件:
<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.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="标题栏"
app:titleTextColor="#fff"
>
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="center"
app:tabIndicatorColor="#4ce91c"
app:tabIndicatorHeight="5dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="#4ce91c"
app:tabTextColor="#ccc"
/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
根布局还是用CoordinatorLayout。然后写一个全屏的ViewPager用来存放Fragment,由于CoordinatorLayout是支持ViewPager的,因此直接添加:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
同理我们还需要一个AppBarLayout,里面包裹标题栏Toolbar以及TabLayout,其中TabLayout没有设置scrollFlags因此不会滑出去。至于Java代码这里就不再赘述了。
示例三、CoordinatorLayout与CollapsingToolbarLayout结合使用
在示例二的基础之上,增加一个CollapsingToolbarLayout:
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:title="标题栏"
app:titleTextColor="#fff">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/test"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.5"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_collapseMode="parallax"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
属性分析:
- 给ImageView设置的layout_collapseMode是CollapsingToolbarLayout特有的属性。其中parallax是视差动画效果。layout_collapseParallaxMultiplier是设置视差动画的程度;none:没有任何效果;pin:固定模式,在折叠的时候最后固定在顶端。
- contentScrim是折叠之后的背景颜色。
- layout_scrollFlags属性需要给CollapsingToolbarLayout设置exitUntilCollapsed,使得页面滑动的时候Toolbar可以留在顶端。
自定义Behavior
自定义Behavior的情形:
- 某个View需要监听另外一个View的状态(位置、大小、显示状态)。
- 某个View需要监听CoordinateLayout里面的滑动状态。
情形一例子、两个控件同时动作
这时候,child需要监听dependency的状态,并且作出相应的动作。
- 需要重写layoutDependsOn判断监听谁,这里可以巧妙利用Tag。
- 需要重写onDependentViewChanged,child的动作根据dependency的状态进行相应。
代码如下:
public class MyBehavior1 extends CoordinatorLayout.Behavior<View> {
private static final String TAG = MyBehavior1.class.getSimpleName();
public MyBehavior1(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 用来决定需要监听哪些控件或者容器的状态(1.知道监听谁;2.什么状态改变)
* CoordinatorLayout parent ,父容器
* View child, 子控件---需要监听dependency这个view的视图们---观察者
* View dependency,你要监听的那个View
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child,
View dependency) {
//还可以根据ID或者TAG来判断需要监听哪一个子控件
return (dependency instanceof TextView && dependency.getTag().toString().equals("dependent"))
|| super.layoutDependsOn(parent, child, dependency);
}
/**
* 当被监听的view发生改变的时候回调
* 可以在此方法里面做一些响应的联动动画等效果。
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
//获取被监听的view的状态---垂直方向位置
int offset = dependency.getTop() - child.getTop();
//让被监听的child进行平移、旋转等操作
ViewCompat.offsetTopAndBottom(child, offset);
child.animate().rotation(child.getTop() * 20);
return true;
}
}
布局文件如下:
<android.support.design.widget.CoordinatorLayout>
<TextView
android:id="@+id/tv_dependent"
android:tag="dependent"
android:text="被观察--dependent"/>
<TextView
android:text="观察者" app:layout_behavior="com.nan.advancedui.coordinatorlayout.mybehavior.MyBehavior1"
/>
</android.support.design.widget.CoordinatorLayout>
我们点击tv_dependent,进行平移,那么观察者就会平移和旋转:
findViewById(R.id.tv_dependent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewCompat.offsetTopAndBottom(v, 9);
}
});
情形二例子、两个滑动控件同步
与FloatingActionButton的自定义Behavior一样,需要重写:
- onStartNestedScroll判断需要监听什么方向的滑动。
- onNestedPreScroll、onNestedFling等进行相应动作。
例子如下:
public class MyBehavior2 extends CoordinatorLayout.Behavior<View> {
private static final String TAG = MyBehavior2.class.getSimpleName();
public MyBehavior2(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)
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
View child, View target, int dx, int dy, int[] consumed) {
int scrollY = target.getScrollY();
child.setScrollY(scrollY);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout,
View child, View target, float velocityX, float velocityY,
boolean consumed) {
// 快速滑动的惯性移动(松开手指后还会有滑动效果)
((NestedScrollView) child).fling((int) velocityY);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
}
看看布局文件:
<android.support.design.widget.CoordinatorLayout>
<android.support.v4.widget.NestedScrollView>
...省略
</android.support.v4.widget.NestedScrollView>
<android.support.v4.widget.NestedScrollView
app:layout_behavior="com.nan.advancedui.coordinatorlayout.mybehavior.MyBehavior2">
...省略
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
最终结果就是NestedScrollView同步的滑动。
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。