CoordinatorLayout
CoordinateLayout已经出来有一段时间了,但是一直没有使用,最近项目开发中遇到一个效果,使用CoordinatorLayout来实现的,瞬间感觉到了他的强大之处,所以趁着空闲时间,自己深入了解总结了一下,不到之处,请多多包涵:
官方API:
https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html
CoordinatorLayout:协调布局,是一个更加强大的FrameLayout 继承于ViewGroup,主要被设计来应用于两种场景:
1.作为一个顶层布局
2.作为与一个或多个子视图进行特定交互的容器
注:CoordinatorLayout不一定要作为最外层的根节点!
通过设置CoordinatorLayout子视图的Behavior来为不同的子视图之间提供交互。
同时CoordinatorLayout的子视图可以设置anchor(锚点:即为该子视图指定一个参照物)
该父容器视图必须是CoordinatorLayout或其子类锁包含的子视图。使用id进行绑定,类似于RelativeLayout设置相对位置时。
子View和锚点View必须在同一CoordinateLayout视图内。
官方API
1.使用CoordinatorLayout作为一个普通的容器(类似于FrameLayout)
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.lbjfan.coordinatorlayoutview.MainActivity">
<TextView
android:id="@+id/lbj_text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="Hello World1!" />
<TextView
android:id="@+id/lbj_text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="Hello2!" />
</android.support.design.widget.CoordinatorLayout>
运行效果:
2.使用anchor来设置子视图相对位置
layout_anchorGravity:描述子视图相对于锚点View的位置
布局文件:
<?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.example.lbjfan.coordinatorlayoutview.MainActivity">
<TextView
android:id="@+id/lbj_text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="Hello World1!" />
<TextView
android:id="@+id/lbj_text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="Hello2!"
app:layout_anchor="@+id/lbj_text1"
app:layout_anchorGravity="bottom" />
</android.support.design.widget.CoordinatorLayout>
运行效果:
疑惑:为什么没有直接在lbj_text1的下方,还有一半高度覆盖了!(有知道的朋友解释一下),
当然,我们也可以通过设置margin来控制两个View之间的距离
layout_anchorGravity的其他取值:right,left,center,clip_vertical等。
将layout_anchorGravity属性设置为center,效果如下:
3.自定义Behavior来进行CoordinatorLayout子视图之间的交互
Behavior是CoordinatorLayout的一个静态内部抽象类(静态内部类不持有外部类的引用)
public static abstract class Behavior<V extends View> {
}
需要注意的是 layout_behavior 是CoordinatorLayout的layoutParams属性, 因此只有它的直接子View才能设置。
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
}
CoordinatorLayout中Behavior的初始化
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_LayoutParams);
...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}
a.recycle();
}
在LayoutParams的构造方法中,首先是去检查了是不是有layout_behavior,然后调用了parseBehavior方法,返回了Behavior的实例
parseBehavior函数
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
// 所关联behavior的完整路径
final String fullName;
// 如果是".MyBehavior",则在前面加上程序的包名
if (name.startsWith(".")) {
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// 指定了全名
// Fully qualified package name.
fullName = name;
} else {
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
// 这里利用反射去实例化了指定的Behavior
// 并且值得注意到是,这里指定了构造的参数类型
// 也就是说我们在自定义Behavior的时候,必须要有这种类型的构造方法
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
CONSTRUCTOR_PARAMS:
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
因此自定义Behavior时必须重载带有参数的构造函数,因为在CoordinatorLayout里利用反射去获取这个Behavior的时候就是使用这个构造函数。
几个重要的方法
onDependentViewChanged
* <p>This method is called whenever a dependent view changes in size or position outside
* of the standard layout flow. A Behavior may use this method to appropriately update
* the child view in response.</p>
* <p>If the Behavior changes the child view's size or position, it should return true.
* The default implementation returns false.</p>
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
意思是说:当一个标准布局中所关联View的大小或位置发生改变时,Behavior可以用这个方法去更新响应这个变化的View的状态。如果Behavior改变了这个响应View的大小或位置,应返回true,默认实现返回false。
layoutDependsOn
* Determine whether the supplied child view has another specific sibling view as a layout dependency.
This method will be called at least once in response to a layout request
**/
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
判断是否向设置Behavior的View提供了一个依赖View,至少会调用一次,加载布局的时候。(dependencyView大小或位置发生变化的时候,也会调用)
onStartNestedScroll
/**
* <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
* to this event and return true to indicate that the CoordinatorLayout should act as
* a nested scrolling parent for this scroll. Only Behaviors that return true from
* this method will receive subsequent nested scroll events.</p>
* 个人理解:CoordinatorLayout中设置Behavior的子View响应这个事件并且返回true标志着CoordinatorLayout作为
* 嵌套滚动的Parent
*
* @param coordinatorLayout:设置Behavior的View对应的CoordinatorLayout
* @param child:设置Behavior的View
* @param directTargetChild:
* @param target:
* @param nestedScrollAxes:滚动方向:SCROLL_AXIS_HORIZONTAL和SCROLL_AXIS_VERTICAL
* @return:默认返回false
*/
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
Log.i("ScrollBehavior:", "onStartNestedScroll" + nestedScrollAxes);
//竖直方向就消耗滚动事件
if (nestedScrollAxes == SCROLL_AXIS_VERTICAL) {
return true;
} else {
return false;
}
}
onNestedScroll
/**
* 滑动时调用
*
* @param coordinatorLayout
* @param child:设置Behavior的子View
* @param target:触发嵌套滚动的子View
* @param dxConsumed:横向滑动距离
* @param dyConsumed:纵向滑动距离
*/
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.i("ScrollBehavior:", "onNestedScroll:" + dyConsumed);
}
onNestedPreFling
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
Log.i("ScrollBehavior:", "onNestedPreFling");
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
onNestedFling
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
Log.i("ScrollBehavior:", "onNestedFling");
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
简单自定义一个竖直方向位移的Behavior
让一个View移动时,它下方的View始终在它下方
运行效果:
布局文件:
<?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.example.lbjfan.coordinatorlayoutview.MainActivity">
<TextView
android:id="@+id/lbj_text1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="Dependency View" />
<TextView
android:id="@+id/lbj_text2"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="ChildView"
android:textSize="15sp"
app:layout_behavior="com.example.lbjfan.coordinatorlayoutview.TranslationBehavior" />
</android.support.design.widget.CoordinatorLayout>
TranslationBehavior
/**
* 移动的Behavior
* Created by fanxudong on 2017/10/16.
*/
public class TranslationBehavior extends CoordinatorLayout.Behavior<TextView> {
public TranslationBehavior() {
}
public TranslationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
Log.i("Behavior:", "layoutDependsOn");
return dependency.getId() == R.id.lbj_text1;
}
/**
* @param parent:最外层的CoordinatorLayout或者其子类视图
* @param child:设置Behavior的View
* @param dependency:依赖的View
* @return
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
Log.i("Behavior:", "onDependentViewChanged=====" + (dependency.getId() == R.id.lbj_text1));
if (dependency.getId() == R.id.lbj_text1) {
// CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
// params.topMargin = dependency.getHeight() + dependency.getTop();
// child.setLayoutParams(params);
child.setTranslationY(dependency.getTranslationY());
}
return true;
}
实现类似于AppBarLayout的Behavior
我们知道CoordinatorLayout实现了NestedScrollingParent接口, 而要监听其子View的滑动事件,那么该子View就必须实现NestedScrollingChild接口,这样CoordinatorLayout才能收到这个子类的滑动事件,V4包里面的嵌套滑动实现了NestedScrollingChild接口的View包括:
- HorizontalGridView
- NestedScrollView
- RecyclerView
- SwipeRefreshLayout
- VerticalGridView
因此,ListView和ScrollView是不支持的。
运行效果:
布局文件:
<?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.v7.widget.RecyclerView
android:id="@+id/lbj_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="Child View"
android:textColor="@android:color/white"
android:textSize="15sp"
app:layout_behavior=".ScrollBehavior" />
</android.support.design.widget.CoordinatorLayout>
ScrollBehavior
/**
* 滑动的Behavior
* Created by fanxudong on 2017/10/17.
*/
public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {
public ScrollBehavior() {
}
public ScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
* to this event and return true to indicate that the CoordinatorLayout should act as
* a nested scrolling parent for this scroll. Only Behaviors that return true from
* this method will receive subsequent nested scroll events.</p>
* 译文:CoordinatorLayout中设置Behavior的子View响应这个事件并且返回true标志着CoordinatorLayout作为
* 嵌套滚动的Parent
*
* @param coordinatorLayout:设置Behavior的View对应的CoordinatorLayout
* @param child:设置Behavior的View
* @param directTargetChild:
* @param target:
* @param nestedScrollAxes:滚动方向:SCROLL_AXIS_HORIZONTAL和SCROLL_AXIS_VERTICAL
* @return:默认返回false
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
Log.i("ScrollBehavior:", "onStartNestedScroll" + nestedScrollAxes);
//竖直方向就消耗滚动事件
if (nestedScrollAxes == SCROLL_AXIS_VERTICAL) {
return true;
} else {
return false;
}
}
/**
* 当一个嵌套的滑动子View触发Fling时调用
*/
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
Log.i("ScrollBehavior:", "onNestedPreFling");
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
/**
* 当一个嵌套的滑动子View Fling时调用
*/
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
Log.i("ScrollBehavior:", "onNestedFling");
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
/**
* 滑动时调用
*
* @param coordinatorLayout
* @param child:设置Behavior的子View
* @param target:触发嵌套滚动的子View
* @param dxConsumed:横向滑动距离
* @param dyConsumed:纵向滑动距离
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.i("ScrollBehavior:", "onNestedScroll:" + dyConsumed);
if (dyConsumed > 0) {
// 手势从下向上滑动(列表往下滚动), 隐藏
if (dyConsumed >= child.getHeight() / 5 && child.getAlpha() != 0f) {
hideChild(child);
}
} else if (dyConsumed < 0) {
// 手势从上向下滑动(列表往上滚动), 显示
if (child.getAlpha() != 1.0f) {
showChild(child);
}
}
}
/**
* 隐藏Child
*/
private void hideChild(final View child) {
ObjectAnimator.ofFloat(child, "alpha", child.getAlpha(), 0f).setDuration(500).start();
ObjectAnimator.ofFloat(child, "translationY", child.getTranslationY(), -child.getHeight()).setDuration(500).start();
}
/**
* 显示Child
*/
private void showChild(final View child) {
ObjectAnimator.ofFloat(child, "alpha", child.getAlpha(), 1f).setDuration(500).start();
ObjectAnimator.ofFloat(child, "translationY", child.getTranslationY(), 0).setDuration(500).start();
}
/**
* 滑动结束后调用
*/
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
Log.i("ScrollBehavior:", "onStopNestedScroll");
super.onStopNestedScroll(coordinatorLayout, child, target);
}
}