CoordinatorLayout 是一个 “加强版” FrameLayout
CoordinatorLayout利用CoordinatorLayout.Behavior
类实现嵌套滚动以及childView之间的交互行为。
Behavior必须作用于CoordinatorLayout直接子View才会生效
Behavior的实例化
我们在view中可以通过app:layout_behavior
然后指定一个字符串来表示使用哪个behavior,显然CoordinatorLayout是采用反射的方式生成实例的。
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
boolean mBehaviorResolved = false;
...
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();
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
}
// 通过类名,生成Behavior实例
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
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);
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);
}
}
值得注意的是:
生成实例时,调用的是Behavior两个参数的构造方法:
c.newInstance(context, attrs);
这也是为什么自定义Behavior时,一定要重写两个参数的构造方法的原因
Behavior处理嵌套滚动
- 我们可以在自定义的
Behavior.onStartNestedScroll
方法中根据实际情况决定是否对滚动事件感兴趣。 - 我们可以在RecyclerView滚动之前,重写
Behavior.onNestedPreScroll
方法中处理CoordinatorLayout的子View的滚动事件,然后根据实际情况填写消费的滚动距离。 - 我们可以在RecyclerView滚动时或滚动后,重写
Behavior.onNestedScroll
方法处理CoordinatorLayout的子View的滚动事件,去消耗RecyclerView的滚动量 - 我们可以在自定义的
Behavior.onStopNestedScroll
方法中检测到滚动事件的结束。
详细可看第一篇:NestedScrolling嵌套滚动机制
Behavior处理CoordinatorLayout子View之间的状态变化
通过Behavior监听View之间的状态变化,例如:位置、大小、背景色等,要实现这种状态监听,需要重写Behavior的两个方法:
-
layoutDependsOn
: 决定需要监听的View对象 -
onDependentViewChanged
: 当监听对象的状态发生变化时,会回调该方法,我们可以根据监听对象的状态,变换自己View的状态。
经过代码摸索和测试,发现这两个方法基本都是在CoordinatorLayout.dispatchOnDependentViewChanged
方法中被调用的,而该方法则会在CoordinatorLayout每次绘制之前被调用,核心代码如下所示:
//View绘制前的全局监听器
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
//触发关联View的回调
dispatchOnDependentViewChanged(false);
return true;
}
}
//当CoordinatorLayout绑定到窗口时,就会被注册
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
//注册每次绘制前的回调
vto.addOnPreDrawListener(mOnPreDrawListener);
}
//...
mIsAttachedToWindow = true;
}
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
//布局方向
final int layoutDirection = ViewCompat.getLayoutDirection(this);
//mDependencySortedChildren是根据View之间的依赖关系,重新排序后的子View列表,假如A依赖于B,那么B就在A的前面,可参见prepareChildren方法
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams)child.getLayoutParams();
//向前寻找当前View依赖的View,这里的依赖关系是通过app:layout_anchor属性指定的
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
//lp.mAnchorDirectChild表示当前View所依赖View的父View,并且为CoordinatorLayout直接子View,因为checkChild是直接子View,只有这样他们才可以在一起比较是否相等
if (lp.mAnchorDirectChild == checkChild) {
//找到了当前View依赖的View,offsetChildToAnchor方法会根据当前View的位置和所依赖View的位置,计算出当前View期望的新位置,然后对当前View进行位移,同时调用onDependentViewChanged方法。感兴趣可直接查阅该方法源码,此处不再赘述
offsetChildToAnchor(child, layoutDirection);
}
}
// 这里oldRect表示当前View上一次的位置,newRect表示当前View现在的位置,若两次位置相同,说明没有发生偏移,则所有依赖于该View的子View都将收不到onDependentViewChanged回调(感觉这里应该是为了效率考虑吧)
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
//重新记录当前View的位置
recordLastChildRect(child, newRect);
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//找出View的Behavior
final Behavior b = checkLp.getBehavior();
//通过layoutDependsOn判断checkChild是否依赖于当前View
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//...省略代码
//若依赖于当前View,那么调用onDependentViewChanged方法
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
//...省略代码
}
}
}
从上面代码可以看出:
- 形成依赖关系的方法有两种
- 通过app:layout_anchor属性指定参照的View;
- 通过layoutDependsOn方法判断
- 若是通过第二种方式形成的依赖关系,那么只有当被依赖View的Rect区域发生变化时,所有依赖于该View的其他View才会收到onDependentViewChanged回调。
- 若在Behavior.onDependentViewChanged方法中根据所依赖View的状态修改了当前View的位置,那么也应该重写Behavior的onLayoutChild,这样才能保持一致。