Google
推出Support Design Library
已经两年了,没错,两年了!虽然推出了这么久,也只是使用,并没有深入研究过,所以想要深入了解一下,于是有了此文。
备注:本文源码基于25.3.0
监听View的变化
在onAttachedToWindow()
方法中,使用ViewTreeObserver
注册一个回调监听View变化。
@Override
public void onAttachedToWindow() {
...
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
//视图将要绘制时,会回调此接口
}
...
}
查看下OnPreDrawListener
的具体实现,发现其调用了onChildViewsChanged()
方法,并传递了一个type
为EVENT_PRE_DRAW
的参数,来进行标记。让我们来看看onChidViewsChanged()
方法具体做了些什么?
子View能够相互依赖工作的根源——onChildViewsChanged()
// type:根据type来判断是什么时期调用的此方法,具体有3个type
// EVENT_PRE_DRAW 将要绘制时, EVENT_NESTED_SCROLL 滚动, EVENT_VIEW_REMOVED 移除
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
//调整child的位置到其所依赖的View(layout_anchor所设置的)的相关位置
offsetChildToAnchor(child, layoutDirection);
}
}
// Get the current draw rect of the view
getChildRect(child, true, drawRect);
// Accumulate inset sizes
// 根据不同的Gravity,记录view进入CoordinatorLayout的尺寸
// lp.insetEdge保存的是子View以什么方式进入CoordinatorLayout
if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
final int absInsetEdge = GravityCompat.getAbsoluteGravity(
lp.insetEdge, layoutDirection);
switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
inset.top = Math.max(inset.top, drawRect.bottom);
break;
case Gravity.BOTTOM:
inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
break;
}
switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
inset.left = Math.max(inset.left, drawRect.right);
break;
case Gravity.RIGHT:
inset.right = Math.max(inset.right, getWidth() - drawRect.left);
break;
}
}
// lp.dodgeInsetEdges 保存的是子View(A)需要避免的‘位置’
// 其他子View(B)以相同的方式进入,会影响子View(A)的显示,子View(A)需要改变自身,避免被覆盖
// Dodge inset edges if necessary
if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
offsetChildByInset(child, inset, layoutDirection); //子View(A)改变自身位置
}
if (type != EVENT_VIEW_REMOVED) {
// Did it change? if not continue
getLastChildRect(child, lastDrawRect);
if (lastDrawRect.equals(drawRect)) {
continue;
}
recordLastChildRect(child, drawRect);
}
// 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();
final Behavior b = checkLp.getBehavior();
//layoutDependsOn方法用于确定两个View是否有依赖关系
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 回调Behavior的b.onDependentViewChanged,处理是否跟随依赖View,而改变自身状态(具体的改变状态的方式,在此方法中处理)
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
通过对onChildViewsChanged()
方法的解读,发现其使用了三种方式来处理View之间的关联。
- 通过判断
layout_ahchor
所设置的锚视图,使用offsetChildToAnchor()
方法来改变View的位置。 - 通过
LayoutParams
中存储的insetEdge
和dodgeInsetEdges
来进行判断(详细信息请查看注释),最后调用offsetChildByInset()
方法来改变View的位置。 - 通过
Behavior
的layoutDependsOn()
方法,如果Behavior
重写了layoutDependsOn()
方法,在其中做了View的依赖判断,最终会回调Behavior
的onDependentViewChanged()
方法,具体要怎么处理,就是onDependentViewChanged()
实现的。
注意:mDependencySortedChildren
是根据View的依赖关系排序存储的,具体排序涉及到的方法有prepareChildren()
和CoordinatorLayout.LayoutParams.dependsOn()
,在这里就不做阐述了,有兴趣的可以自行研读。
我们再来看看offsetChidToAnchor()
和offsetChildByInset()
方法。
设置layout_anchor
锚视图后,View的位置是如何改变的——offsetChildToAnchor
void offsetChildToAnchor(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mAnchorView != null) {
final Rect anchorRect = acquireTempRect();
final Rect childRect = acquireTempRect();
final Rect desiredChildRect = acquireTempRect();
// 获取Anchor View的Rect
getDescendantRect(lp.mAnchorView, anchorRect);
// 获取child View的Rect
getChildRect(child, false, childRect);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 获取到想要的Rect,保存在desiredChildRect中
getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
desiredChildRect, lp, childWidth, childHeight);
// 通过对desiredChildRect和childRect比对,看是否位置发生了变化
boolean changed = desiredChildRect.left != childRect.left ||
desiredChildRect.top != childRect.top;
// 加入margin和padding,计算最终的Rect,保存在desiredChildRect中
constrainChildRect(lp, desiredChildRect, childWidth, childHeight);
final int dx = desiredChildRect.left - childRect.left;
final int dy = desiredChildRect.top - childRect.top;
//改变View的位置
if (dx != 0) {
ViewCompat.offsetLeftAndRight(child, dx);
}
if (dy != 0) {
ViewCompat.offsetTopAndBottom(child, dy);
}
// 如果位置有变化,且View有Behavior,则通知其Behavior的onDependentViewChanged方法
if (changed) {
// If we have needed to move, make sure to notify the child's Behavior
final Behavior b = lp.getBehavior();
if (b != null) {
b.onDependentViewChanged(this, child, lp.mAnchorView);
}
}
releaseTempRect(anchorRect);
releaseTempRect(childRect);
releaseTempRect(desiredChildRect);
}
}
分析完offsetChildToAnchor()
,我们知道了,当设置了layout_anchor
,如果View的位置改变,也会回调Behavior
的onDependentViewChanged()
方法。
那offsetChildByInset()
又做了些什么呢?
子View之间是如何避免被“遮挡”——offsetChidByInset
private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) {
...
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
final Rect dodgeRect = acquireTempRect();
final Rect bounds = acquireTempRect();
bounds.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
// getInsetDodgeRect方法默认返回false,需要自己实现最终的Rect并设置给dodgeRect,并返回true
if (behavior != null && behavior.getInsetDodgeRect(this, child, dodgeRect)) {
// Make sure that the rect is within the view's bounds
if (!bounds.contains(dodgeRect)) {
throw new IllegalArgumentException("Rect should be within the child's bounds."
+ " Rect:" + dodgeRect.toShortString()
+ " | Bounds:" + bounds.toShortString());
}
} else {
dodgeRect.set(bounds);
}
// We can release the bounds rect now
releaseTempRect(bounds);
if (dodgeRect.isEmpty()) {
// Rect is empty so there is nothing to dodge against, skip...
releaseTempRect(dodgeRect);
return;
}
// 获取需要躲避的方向
final int absDodgeInsetEdges = GravityCompat.getAbsoluteGravity(lp.dodgeInsetEdges,
layoutDirection);
// 根据absDodgeInsetEdges,改变View相应的位置
boolean offsetY = false;
if ((absDodgeInsetEdges & Gravity.TOP) == Gravity.TOP) {
int distance = dodgeRect.top - lp.topMargin - lp.mInsetOffsetY;
if (distance < inset.top) {
setInsetOffsetY(child, inset.top - distance);
offsetY = true;
}
}
...// 省略Gravity.BOTTOM判断,和Gravity.TOP类似
if (!offsetY) {
setInsetOffsetY(child, 0);
}
boolean offsetX = false;
if ((absDodgeInsetEdges & Gravity.LEFT) == Gravity.LEFT) {
int distance = dodgeRect.left - lp.leftMargin - lp.mInsetOffsetX;
if (distance < inset.left) {
setInsetOffsetX(child, inset.left - distance);
offsetX = true;
}
}
...// 省略Gravity.RIGHT判断,和Gravity.LEFT类似
if (!offsetX) {
setInsetOffsetX(child, 0);
}
releaseTempRect(dodgeRect);
}
阅读完offsetChildByInset()
方法,发现如果我们的View需要避免某个方向的其他View进入,我们需要实现Behavior
的getInsetDodgeRect()
方法,还要设置LayoutParams.dodgeInsetEdges
,dodgeInsetEdges
的设置可以重写Behavior
的onAttachedToLayoutParams(CoordinatorLayout.LayoutParams params)
方法。
CoodinatorLayout中的滚动机制——NestedScrolling
NestedScrolling
机制,是从Android 5.0
开始引入,提供了一套父View和子View滑动交互的机制。包含两个接口和两个帮助类:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
父View必须实现NestedScrollingParent
接口,而其必须要有一个子View实现NestedScrollingChild
接口,只有这样才能达到预想的滑动交互效果。实现NestedScrollingChild
接口很简单,只需要在其实现的方法中调用NestedScrollingChildHelper
中对应的方法即可。并在相应的Touch事件中调用startNestedScroll()
方法以及stopNestedScroll()
方法,剩下的通知父View等事情,NestedScrollingChildHelper
都帮我们处理好了。
对与NestedScrolling
机制,这里只做简要的说明,具体的分析留待以后的文章。如果有想了解的,可先自行搜索相关文章。
回到正题CoordinatorLayout
,没错,CoordinatorLayout
充当的就是父View的角色,其实现了NestedScrollingParent
接口,具体的实现如下所示:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
//存储是否要处理滚动
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
...
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
}
}
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onStopNestedScroll(this, view, target);
}
lp.resetNestedScroll();
lp.resetChangedAfterNestedScroll();
}
...
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {// 调用了前面讲的重要方法
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
// consumed代表自身去执行相应方向的距离滑动
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {//注意调用了onChildViewsChanged
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
consumed);
}
}
if (handled) {// 注意调用了onChildViewsChanged
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
return handled;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
...// 省略获取view及其LayoutParams
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
}
}
return handled;
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
代码虽然有点长,但是没有什么难点,相应的方法都被委派给了Behavior
的对应方法处理。如果我们的子View想要响应滚动效果,只需要重写Behavior
的相关方法。
开发最关心的——Behavior
通过上面的分析,大家应该都发现了,几乎所有的东西都和Behavior
有关。我们想要自己的控件在CoordinatorLayout
中有炫酷的效果,那么我们只需要自定义自己的Behavior
,实现相关方法即可。
如何给View设置Behavior
- 在xml中通过
layout_behavior
绑定,例如app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
- 在自定义的控件中,使用
DefaultBehavior
注解绑定,例如AppBarLayout
中的@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
- 在代码中通过
LayoutParams
设置
需要注意的:
Behavior
需要设置给CoordinatorLayout
的直接子View,因为Behavior
的解析是在CoordinatorLayout.LayoutParams
的构造方法中进行的,只有直接子View才具有CoordinatorLayout.LayoutParams
。- 自定义
Behavior
必须具有Behavior(Context context, AttributeSet attrs)
构造方法,因为Behavior
的实例化是靠类的反射完成的,具体可看如下源码:
//指定Behavior的参数类型
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
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) {
// 利用反射获取Behavior
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中常用的方法
layoutDependsOn
通过之前的分析,layoutDependsOn
是用来确定依赖关系的,如果想要控件依赖某个控件,重写这个方法是必须的。例如给RecycleView
设置的ScrollingViewBehavior
,关联AppBarLayout
,代码如下:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
onDependentViewChanged
根据之前的分析,此方法会在依赖View发生改变的时候回调,我们可以在此方法中做相应的处理,达到想要的效果。
/* <p>If the Behavior changes the child view's size or position, it should return true.
* The default implementation returns false.</p>
*
* @param parent the parent view of the given child
* @param child the child view to manipulate
* @param dependency the dependent view that changed
* @return true if the Behavior changed the child view's size or position, false otherwise
*/
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
注意官方的注释,如果改变了child的位置、大小,需要返回true
。
onStartNestedScroll
如果想要实现滚动效果,在你想要滚动的条件下,此方法需要返回true
。其返回值会通过lp.acceptNestedScroll(accepted)
存储在LayoutParams
中。其他滚动相关回调都会基于此返回值,true
的时候才会被回调。可以查看前面的CoordinatorLayout
滚动机制部分,都用用下面的代码进行判断:
if (!lp.isNestedScrollAccepted()) {
continue;
}
onNestedPreScroll
Child滑动前,都会通知Parent,Parent会回调此方法,可以在此方法中做滑动拦截。该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2。
onNestedScroll
Child滑动以后,会通知Parent,回调onNestedScroll()。可以在此方法中做进一步的滚动处理,因为其可以获取到被消耗的和未被消耗的滚动距离。
结语
到这里CoordinatorLayout
的源码解析也算是完结了,虽然只是分析了部分源码,但是也大致清楚了其工作原理。如果有分析得不对的地方还望指正。最后推荐几篇不错的文章: