CoordinatorLayout顾名思义是一个协调布局,其作用是监听其子View对象的变化,如位移,滚动事件,并将事件传递到另外的对象上
根据源码分析可知CoordinatorLayout主要通过两种方式监听事件的产生
- ViewTreeObserver.addOnPreDrawListener
- 实现NestedScrollingParent接口
前者监听视图树绘制事件,而后者可监听子View滚动事件
监听到事件后如何将事件传递到对应的子View响应方法上呢?CoordinatorLayout是通过Behavior实现的,Behavior是CoordinatorLayout的一个内部抽象类,该类主要完成两件事
- 指出当前子View依赖于另外某一个子View的事件发生
- 实现事件响应
通过Behavior实现类,CoordinatorLayout可以在Layout.xml里设定子View间的连动关系,而修改界面代码或使用自定义View来完成。事件发生的目标对象并不知道有别的对象监听自己的变化,响应对象也不知道自己响应了别人的事件,其中都是CoordinatorLayout与Behavior在监听与响应。
根据上面的分析我们来实现自己的CoordinatorLayout类,思路如下
- 创建ViewGroup实现类,重写其构造方法,onMeasure, onLayout,实现其ViewGroup.LayoutParams实现类并重写generateLayoutParams方法
- 自定义子View使用的XML配置项behavior_class,类型为string,并在LayoutParams构造方法里读取该值并构造对应的Behavior实现类
- 定义抽象Behavior类,添加抽象方法layoutDependsOn,用于返回依赖的对象
- 通过为ViewTree注册监听来监听界面View的变化
- 通过实现NestedScrollingParent接口并实现onStartNestedScroll与onNestedScroll方法来监听NestedScrollView子view的滚动事件
- 在viewtree及scrollview监听事件方法里循环子View,当发现存在behavior_class配置项且依赖对象发生变化时,调用子类上beavior实现的onDepdentViewChanged或onNestedScroll方法
- 自定义ViewGroup实现类
public class MyCoordinatorLayout extends ViewGroup{
public
MyCoordinatorLayout(Context context) {
super(context);
}
public
MyCoordinatorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void
onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize, heightSize;
//Get the width based on the measure specs
widthSize = getDefaultSize(0, widthMeasureSpec);
//Get the height based on measure specs
heightSize = getDefaultSize(0, heightMeasureSpec);
int majorDimension = Math.min(widthSize, heightSize);
//Measure all child views
measureChildren(widthMeasureSpec, heightMeasureSpec);
//MUST call this to save our own dimensions
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void
onLayout(boolean changed, int l, int t, int r, int b) {
for (int i=0; i < getChildCount(); i++) {
View child = getChildAt(i);
View lastChild = getChildAt(i-1);
if(lastChild==null) {
child.layout(l,
t,
l+child.getMeasuredWidth(),
t+child.getMeasuredHeight());
} else {
child.layout(lastChild.getLeft(),
lastChild.getBottom(),
lastChild.getLeft()+child.getMeasuredWidth(),
lastChild.getBottom()+child.getMeasuredHeight());
}
}
}
@Override
protected LayoutParams
generateLayoutParams(ViewGroup.LayoutParams p) {
return new MyLayoutParams(p);
}
@Override
protected LayoutParams
generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public
LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
class MyLayoutParams extends ViewGroup.LayoutParams{
MyBehavior behavior;
public MyLayoutParams(ViewGroup.LayoutParams p){
super(p);
}
public MyLayoutParams(int width, int height){
super(width,height);
}
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
}
}
|
- 添加自定义属性并在LayoutParams实现类里添加读取并实例化的代码
<declare-styleable name="MyCoordinatorLayout_LayoutParams">
<attr name="behavior_class" format="string" />
</declare-styleable>
|
并修改LayoutParams实现类为如下
class MyLayoutParams extends ViewGroup.LayoutParams{
MyBehavior behavior;
public MyLayoutParams(ViewGroup.LayoutParams p){
super(p);
}
public MyLayoutParams(int width, int height){
super(width,height);
}
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.getTheme ().obtainStyledAttributes(attrs,R.styleable.MyCoordinatorLayout_LayoutParams,0,0);
try {
String fullName = a.getString(R.styleable.MyCoordinatorLayout_LayoutParams_behavior_class);
if(fullName != null) {
Class clazz = Class.forName(fullName, true, c.getClassLoader());
Constructor constructor = clazz.getConstructor(new Class[]{Context.class, AttributeSet.class});
constructor.setAccessible(true);
behavior = (MyBehavior)constructor.newInstance(c, attrs);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} finally {
a.recycle();
}
}
}
|
- 我们需要新增MyBehavior类,该类主要用于定义依赖对象及响应逻辑
public static abstract class MyBehavior{
public MyBehavior(){};
public MyBehavior(Context c, AttributeSet as){};
public abstract int layoutDependsOn();
public void onDependentViewChanged(View child, View dependency){};
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { }
}
|
- 注册ViewTree事件并添加监听逻辑
public MyCoordinatorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
dispatchOnDependentViewChanged();
return true;
}
});
}
|
当监听到ViewTree发生变化时轮循子View,当子View有定义behavior时调用其behavior实例上的onDependentViewChanged方法,当然最好是先判断变化的是否其依赖对象
private void dispatchOnDependentViewChanged() {
int childCount = getChildCount();
for(int i = 0; i < childCount; i++) {
View child = getChildAt(i);
MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
if(lp.behavior!=null) {
lp.behavior.onDependentViewChanged(child,findViewById(lp.behavior.layoutDependsOn()));
}
}
}
|
实现NestedScrollingParent并实现onStartNestedScroll与onNestedScroll方法
也是直接将事件传递到behavior实现类上对应方法,可以看出CoordinatorLayout实现做为一个事件监听者,但实际的逻辑由其子View上Behavior对应的方法执行
public class MyCoordinatorLayout extends ViewGroup implements NestedScrollingParent {
。。。。。。。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return true;//表示接收监听的事件
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
int childCount = getChildCount();
for(int i = 0; i < childCount; i++) {
View child = getChildAt(i);
MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
if(lp.behavior!=null) {
lp.behavior.
onNestedScroll(target, dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed);
}
}
}
}
|
- 在界面上使用我们自定义的CoordinatorLayout
例子1:onDependentViewChanged
添加了两个子View, second添加了behavior_class,实现类为MyBehavior2
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.MyCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<View
android:id="@+id/first"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@android:color/holo_blue_light"/>
<View
android:id="@+id/second"
android:layout_width="match_parent"
android:layout_height="128dp"
custom:behavior_class="com.example.myapplication.MyBehavior2"
android:background="@android:color/holo_green_light"/>
</com.example.myapplication.MyCoordinatorLayout>
|
创建MyBehavior2类
public class MyBehavior2 extends MyCoordinatorLayout.MyBehavior{
//当依赖对象发生变化时让当前对象(即second对象一直保持位于依赖对象下方)
public void onDependentViewChanged(View child, View dependency){
child.setY(dependency.getY()+dependency.getHeight());
};
public MyBehavior2(Context c, AttributeSet as) {
}
//依赖的对象为first对象
public int layoutDependsOn() {
return R.id.first;
}
}
|
让依赖对象发生变化,每500ms发生50的Y轴位移
final View iv = findViewById(R.id.first);
Runnable r = new Runnable(){
@Override
public void run() {
int i = 0;
while(i < 10) {
iv.setY(iv.getY()+50);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
};
new Thread(r).start();
|
例子2:NestedScrollView
添加界面元素
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.MyCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<View
android:id="@+id/first"
android:layout_width="match_parent"
android:layout_height="128dp"
custom:behavior_class="com.example.myapplication.MyBehavior3"
android:background="@android:color/holo_blue_light"/>
<android.support.v4.widget.NestedScrollView
android:id="@+id/second"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Display3"
android:text="A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ"
android:background="@android:color/holo_red_light"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</com.example.myapplication.MyCoordinatorLayout>
|
实现MyBehavior3类
package com.example.myapplication;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by ling on 2016/6/29.
*/
public class MyBehavior3 extends MyCoordinatorLayout.MyBehavior {
int offsetTotal = 0;
boolean scrolling = false;
public MyBehavior3(Context c, AttributeSet as) {
super(c, as);
}
@Override
public int layoutDependsOn() {
return R.id.second;
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
offset(target, dyConsumed);
}
public void offset(View child,int dy){
int old = offsetTotal;
int top = offsetTotal - dy;
top = Math.max(top, -child.getHeight());
top = Math.min(top, 0);
offsetTotal = top;
if (old == offsetTotal){
scrolling = false;
return;
}
int delta = offsetTotal-old;
child.offsetTopAndBottom(delta);
scrolling = true;
}
}
|
界面上拉动NestedScrollView会看到first对象的位置发生了变化