完全搞懂CoordinatorLayout Behavior 你能做些什么
完全搞懂CoordinatorLayout Behavior 系列之API讲解
完全搞懂CoordinatorLayout Behavior之源码学习
完全搞懂CoordinatorLayout Behavior之实战一
前面我们已经简单介绍了CoordinatorLayout 的工作机制以及Behavior核心API,原则上是已经可以上手写demo。但是这一节,我想从源码的角度在讲一次CoordinatorLayout 与 Behavior的工作原理。
在看源码之前先要明白一个前提: 1、CoordinatorLayout 之所以能够协调子视图的相关动作,是因为它实现了NestedScrollingParent2接口;2、NestedScrollView之所以能够作为嵌套滑动的子视图,让其他View跟随它的滑动变化而变化,是因为它实现了NestedScrollingChild2接口。
一、布局加载过程
了解这个过程帮助我们知道 父View 是怎么得到 子布局的app:layout_behavior 属性然后并创建出behavior对象的。
系统得到布局layoutId 通过LayoutInflator类去加载布局View,布局加载过程中先创建出父View然后加载子布局。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
//merge布局
if (TAG_MERGE.equals(name)) {
} else {
//得到一个布局节点以后,先创建出布局View
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
//加载所有的子View
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
} finally {
}
return result;
}
}
由上面的注释可以看到,当在加载根布局CoordinatorLayout时然后会依次遍历加载所有的子布局xml文件,然后createViewFromTag创建出来,同时会调用父布局的viewGroup.generateLayoutParams(attrs);
得到一个LayoutParams。最后viewGroup.addView(view, params);
这个过程跟我们动态创建View过程是一样的,只不过系统加载xml布局使用的是反射创建,我们则使用的是new 创建。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//循环创建
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
} else {
//如果不满足上面的情况就会走这个逻辑,先得到子布局的name,反射得到View对象,然后把父布局的LayoutParams 创建出来的最后addView。
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
通过上面的代码逻辑我们证明了在子View创建以后,他会得到父View 然后调用一个generateLayoutParams(AttributeSet attrs)
方法,在add到父View中。
二、Behavior创建过程
为什么要看generateLayoutParams 方法,因为CoordinatorLayout的这个方法被重写了它创建了一个自己定义的静态内部类。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
//LayoutParams 的构造方法 在这个构造方法中 attrs 就是xml中 子View 也就是app:layout_behavior 属性所在位置
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
//解析得到Behavior对象
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
可以看到创建LayoutParams时 就会创建出Behavior,至于怎么创建 也是使用的反射创建出来的。
小结一下:在加载布局文件时系统会创建出CoordinatorLayout所有子View,同时将它自己定义的一个LayoutParams设置给子视图,这个LayoutParams就可能携带有一个Behavior实例对象。
三、测量 和 布局
CoordinatorLayout是一个ViewGroup对象,里面有很多子View, 他们都可以添加Behavior, 同时也可以相互观察。所以需要在测量的时候将他们的观察调用循序使用一个数据结构保存起来,便于后面的嵌套滑动方法的顺序调用。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
//后面省略。。
}
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
//双重for循环 排序所有的子View 理清所有子View之间的依赖关系
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
// Now iterate again over the other children, adding any dependencies to the graph
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
//判断是否有依赖关系
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
上面的代码利用一个双重for循环加上图的形式把所有的View之间的关系进行排序,因为依赖关系可能会比较复杂,使用一种特定的数据结构来表示。这里不做深究,只是做一个猜测。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//每一次得到behavior对象都是从我们第一步和第二步分析的LayoutParams中获取的
final Behavior behavior = lp.getBehavior();
// 调用我们的初始化摆放布局的方法
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
注意从此处开始我们的layoutDependsOn
和onLayoutChild
至少被调用一次。而onLayoutChild
方法就可以设置我们需要显示的子View的初始位置(至少我是这么做的)。如果我们 return true 就不会调用系统的onLayoutChild方法,而是有我们自己控制依赖View的位置。
那么onDependentViewChanged
什么时候调用呢?前面一节我们说到当被监听View的大小和位置发生改变时,这个方法就会被调用。 看测量方法调用的第二行执行了什么样的逻辑。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
//后面省略。。
}
添加和移除一个绘制监听
/**
* Add or remove the pre-draw listener as necessary.
*/
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
//排序以后,遍历所有子view 查看是否设置有依赖监听
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
// 如果有添加绘制监听
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
遍历了所有子View然后只有有一个满足监听关系就会返回true 然后会添加一个绘制View的监听。
/**
* Add the pre-draw listener if we're attached to a window and mark that we currently
* need it when attached.
*/
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
//View树观察者
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
//监听的实现是执行child的改变方法
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
从上面的代码可以看到它会创建一个View树观察者对象然后把OnPreDrawListener添加进去 当View树发生变化的时候可能就会触发onChildViewsChanged方法。
/**
* Register a callback to be invoked when the view tree is about to be drawn
*
* @param listener The callback to add
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*/
public void addOnPreDrawListener(OnPreDrawListener listener)
证实我的猜想invoked when the view tree is about to be drawn
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;
}
//获取当前view的Rect
// Get the current draw rect of the view
getChildRect(child, true, drawRect);
// Accumulate inset sizes
// Dodge inset edges if necessary
if (type != EVENT_VIEW_REMOVED) {
//获取到上一个View的位置Rect 和这一次的Rect比较 如果相等就不会继续向下执行,继续遍历下一个View看看他的位置是否发生改变。
// 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();
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;
}
//由于我们传递的是 EVENT_PRE_DRAW所以当代码走到这个位置时,必定会走onDependentViewChanged方法
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:
// Otherwise we dispatch onDependentViewChanged()
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);
}
从上面的代码分析可以看到 CoordinatorLayout
的测量方法中会添加一个监听绘制的方法, 这个绘制方法通过一个叫做ViewTreeObserver
对象实现,每次View树发生绘制的时候都会去检查 这个中的View是否有 有layoutDependsOn
依赖关系的View的位置发生了改变,如果改变就执行一次onDependentViewChanged
方法。
小结一下: 在测量和布局两个方法中CoordinatorLayout完成了三件事:1、理清所有监听和被监听View的排序顺序;2、设置绘制监听,当View树发生绘制的时候,遍历检查所有子View跟上一次位置是否发生改变,如果改变就调用onDependentViewChanged; 3、在onLayout 的时候,调用了behavior的onLayoutChild方法给子View进行初始化位置的摆放。
上面讲完了布局初始化后,到界面显示这个过程CoordinatorLayout
是怎么显示以及设置监听,调用behavior相关的API 。剩下的api都是nested相关的,也就是说跟嵌套滑动相关的功能,需要配合NestedScrollView
讲解。
四、分析NestedScrollView 事件处理方法,找到NestedScrollView 与 CoordinatorLayout 回调的逻辑
首先我们的滑动肯定是因为NestedScrollView
的滑动所以Behavior才产生调用,所以我们阅读NestedScrollView
滑动事件处理相关的方法就可以找到如何调用的。
复习一下事件传递相关知识,ViewGroup的事件先从dispatchTouchEvent方法然后到onInterceptTouchEvent 接着到onTouchEvent 。如果需要深入了解可以阅读这篇文章Android View事件分发机制 (一)
1、startNestedScroll 与 onNestedScrollAccepted
阅读发现NestedScrollView
并没有dispatchTouchEvent
方法 ,所以我们从onInterceptTouchEvent
开始,首先从Down事件开始。
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
//开始startNestedScroll 方法的调用
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
很明显我们看到调用了startNestedScroll 方法,但是并不是在这个方法内部完成的调用。需要使用一个叫做NestedScrollingChildHelper
代理完成。
@Override
public boolean startNestedScroll(int axes, int type) {
return mChildHelper.startNestedScroll(axes, type);
}
NestedScrollingChildHelper的startNestedScroll方法。
/**
* Start a new nested scroll for this view.
*
* <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
* method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same
* signature to implement the standard policy.</p>
*
* @param axes Supported nested scroll axes.
* See {@link androidx.core.view.NestedScrollingChild2#startNestedScroll(int,
* int)}.
* @return true if a cooperating parent view was found and nested scrolling started successfully
*/
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//很明显在这里调用了onStartNestedScroll 同时也调用了onNestedScrollAccepted
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
从一个API 找到了两个Behavior名称类似的方法,之前我们也有说到如果Behavior的onStartNestedScroll
返回false , 那么后面相关的滑动嵌套方法都得不到调用。看看ViewParentCompat
类完成了怎样的操作。
@SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
//这里的parent 就是CoordinatorLayout
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
可以看到无论是NestedScrollingParent 还是 NestedScrollingParent2 都会调用onStartNestedScroll方法。我们返回回去看看onNestedScrollAccepted是不是也是这样的。
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedScrollAccepted", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes);
}
}
}
几乎一模一样的逻辑, 都是通过ViewParentCompat
然后在调用parent
(CoordinatorLayout
)相同的名称的api方法。
接下来我们回到CoordinatorLayout的源码中 是做了些什么操作。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
//遍历所有子View
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
//拿到所有具有Behavior对象的子View 然后调用他们的onStartNestedScroll
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
//同时把accepted 设置到LayoutParams 身上
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
看到这个方法它坐了两件事 调用了viewBehavior.onStartNestedScroll 这个是我们最后真正的onStartNestedScroll方法。同时设置了一个属性给LayoutParams。
接下来看一下onNestedScrollAccepted 做了那些处理。它调用的前提是onStartNestedScroll 返回true。
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
//
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
mNestedScrollingTarget = target;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
//直接取出上一步操作的accepted 如果是false 直接返回后面不会调用,说明onStartNestedScroll 决定了所有嵌套方法是否执行。
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//最终调用了onNestedScrollAccepted
viewBehavior.onNestedScrollAccepted(this, view, child, target,
nestedScrollAxes, type);
}
}
}
如我们所料真的是调用了viewBehavior.onNestedScrollAccepted 方法,但是在这之前有两个操作 上一步的accepted 决定代码是否会执行后面的onNestedScrollAccepted。还有调用了一个mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type)
记录mNestedScrollAxes 值。
至此,一个事件的处理回调周期全部完成,我们可以完整的看到 CoordinatorLayout
、 NestedScrollView
是怎么样通过 一些中介回调,然后回调到Behavior身上的。后面的情况其实跟这个逻辑完全一样,大概就是NestedScrollView 滑动了多少距离然后通过NestedScrollingChildHelper
以及 ViewParentCompat
回调到 父类身上,前提是父类必须是一个 NestedScrollingParent2
或者 NestedScrollingParent
方法。
2、onNestedPreScroll 、 onNestedScroll
这两个都是滑动的方法,分别在OnTouchEvent的MOVE事件中被调用的。 有什么区别的呢?
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
//注释1 当得到滑动距离以后,首先不是执行的 NestedScrollView的滑动处理而是调用了onNestedPreScroll
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
//注释 2 计算 滑动 和未消耗距离
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
//执行NestedScrollView 的滑动处理
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 执行 onNestedScroll
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
ensureGlows();
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex)
/ getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
可以看到区别了吗,一个是在滑动处理之前调用 ,一个是在滑动处理之后调用。接下来分析一下这几个参数。
在注释1 处有这样的一段代码
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
//计算的滑动距离最后减除了一个mScrollConsumed[1]
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
在讲解Behavior API讲解是我们说到了一个操作方法,他可以去改变滑动距离,原文我放在下面。Behavior API详解
consumed:这是个重要的参数consumed,可以修改这个数组表示你消费了多少距离。假设用户滑动了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,这样coordinatorLayout就能知道只处理剩下的10px的滚动。
看到没有加入我们在自定义的Behavior的onNestedPreScroll
调用时更改consumed 的值,就可以操作NestedScrollView
的值。 比如明明它滑动了100px像素,但是这个时候我给consumed[1]
设置成 50px, 经过deltaY -= mScrollConsumed[1]
那么NestedScrollView最后就只能通过计算以后的deltaY去执行滑动了。
接着看看注释2 位置如何计算 onNestedScroll
相关参数的值
//先记录上一次滑动的距离
final int oldY = getScrollY();
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
|| (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollByCompat will call onOverScrolled, which
// calls onScrollChanged if applicable.
//执行滑动
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
//在用滑动后的距离减去 没有滑动之前的距离 得到的是 这一步过程 滑动的实际距离
final int scrolledDeltaY = getScrollY() - oldY;
//然后用预计计算的滑动距离 减去实际滑动的距离 得到的就是未被消耗的距离
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
上面的注释已经写的很清楚了。scrolledDeltaY
的计算是毋庸置疑的,但是unconsumedY
距离是为什么呢?难道他不应该是0 吗。 可能原因就是,滑动了100px但是NestedScrollView 实际只能滑动90px就已经到底了,剩下的距离消费不了所以才会出现未消费的距离。
分析了这么多可能会有人怀疑,到底是不是调用onNestedPreScroll
、 onNestedScroll
可能有人不会死心,接下来我一次打印他们调用的方法顺序,方便大家查看源码。
dispatchNestedPreScroll 最后会调用mChildHelper.dispatchNestedPreScroll方法
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
NestedScrollingChildHelper 的dispatchNestedPreScroll 最后会调用这个
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
// 。。。省略
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
// 。。。。。省略
return false;
}
跟之前的方法调用一样的逻辑,判断NestedScrollingParent
和NestedScrollingParent2
接口然后调用parent 方法也就是CoordinatorLayout
相关的方法。
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
parent.onNestedPreScroll(target, dx, dy, consumed);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedPreScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
//
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
最后parent 也就是CoordinatorLayout 它就能够获取到所有的子View 然后一次调用对应的Behavior身上的方法。