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在相同方向上同步移动
这里我们要理解两个概念:child、dependency.
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的使用方式:
- 在布局文件中通过app:layout_behavior=" "引用
- 使用注解添加,系统的控件一般使用这种方式,例AppBarLayout:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
......
public static class Behavior extends HeaderBehavior<AppBarLayout> {
......
}
......
}
- 在代码中添加
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