转载请注意:
http://blog.youkuaiyun.com/wjzj000/article/details/53894449
本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)
写在前面
本篇博客记录我个人对NestedScrolling机制学习的过程以及记录。
关于scrollTo,scrollBy以及Scroller相关的内容,请参考我的上一篇博客:
http://blog.youkuaiyun.com/wjzj000/article/details/53874285
我相信大家在看到这篇博客的时候,应该已经看了不少的博客。但是我猜大家依然没有搞懂,不然不会费尽心思的继续找NestedScrolling相关的博客内容。(PS:看官如果懂这个过程,就没必要看了继续往下看了。因为这里我只是记录的自己学习的过程,没有啥高深的用法。)
让我们先看一个效果:
接下来的内容将依托于这个效果,来梳理NestedScrolling机制。
什么是NestedScrolling?
让我们先翻译一下这个词:嵌套滑动。我们知道我们的正常操作都是一个事件,有一套自己的事件分发机制。一个View只要选择处理这个事件,那么正常情况下谁也拿不走这个事件了,就是一条路走到黑。
也正是因为此,并不利于我们做一些效果。比如我们想俩个控件共享这此的事件。那么我们就可以想到NestedScrolling机制。而这个NestedScrolling机制可以干什么呢?
简单来说,当我们俩个控件时嵌套关系正准备处理一个滚动事件时:那么子View在想要滑动的时候会想问问它的父View:爹,你滚吗?此时,父View会根据自己是否需要滚动而对子View说:儿砸,我滚。那么子View得到这个消息,就会在自己的滑动事件中源源不断的把自己的滚动的数据回调给父View,供其使用。所以父View在此时的滚动,其实是子View提供的!
何以见得?
首先,各位看官应该都或多或少的见过下面这四个妖艳贱货:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
直到昨天我才恍然大悟明白了它们之间的关系。上文中我们提到父View的滚动其实是子View进行提供的。为什么?因为源码就是如此。
走进源码
在开始源码之前,我们先树立一个概念。这套机制的流程是这样的:由父View实现NestedScrollingParent根据需要重写一些方法。然后子View实现NestedScrollingChild在onTouchEvent中对对应的方法进行回调。而xxxHelper的作用是:封装了一些方法,方便我们去处理一系列情况。
那么接下来让我们看一步步开始看源码:
首先是这个效果的布局文件:
<com.example.mbenben.studydemo.view.nestedscroll.MyNestedScrollLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/one"
android:text="我是来卖萌的"
android:textColor="@color/white"
android:textSize="16sp"
android:background="@color/black"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="60dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rlv_main"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.example.mbenben.studydemo.view.nestedscroll.MyNestedScrollLayout>
- 效果很简单,只有一个自定义的父View。没错就是它实现了NestedScrollingParent。
public class MyNestedScrollLayout extends LinearLayout implements NestedScrollingParent{
//暂时省略内部代码
}
- 到这里肯定有看官有疑惑,那实现NestedScrollingChild是谁?还能有谁?RecyclerView呗!
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
//省略内部代码
}
- 所以现在啥都有了,实现NestedScrollingChild的RecyclerView以及实现NestedScrollingParent的自定义View。
- 那么接下来就是让我们看一下,内部原理。刚才提到嵌套滑动是由子View进行控制的。所以让我们看看这个例子中作为子View的RecyclerView。直接把目光定位到它的onTouchEvent中
@Override
public boolean onTouchEvent(MotionEvent e) {
//省略部分代码
switch (action) {
case MotionEvent.ACTION_DOWN: {
//省略部分代码
//nestedScrollAxis 就是对应是水平滚动还是垂直滚动
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//此方法用于最开始去询问父View是否需要获取这个滚动事件。PS:关于它的作用请往下看即可。
startNestedScroll(nestedScrollAxis);
} break;
//省略部分代码,直接看ACTION_MOVE情况
case MotionEvent.ACTION_MOVE: {
//省略部分代码
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//dispatchNestedPreScroll()此方法就是NestedScrollingChild中需要实现的方法。这里我们的RecyclerView将自己的滑动参数,传递进了这个方法。
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
//省略部分代码
}
}
}
- 让我们重点看这俩个方法:
- 一个是DOWN中的startNestedScroll
- 一个是MOVE中的dispatchNestedPreScroll
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
- 在这里,我们可以看到它是直接通过Helper类完成这个事件的分发。
- 接下来让我们直接看一看Helper的内部实现。
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
//最终会通过父View的onStartNestedScroll()的返回值来决定回调什么方法。
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
//如果父View返回true,那么将调用这个方法。
/**
* 这里是官方关于这个方法的解释:
* 此方法将在{@link #onStartNestedScroll(View,View,int)
* onStartNestedScroll}返回true后调用。
* 这个方法的实现应该总是调用他们的超类的这个方法的实现(如果存在)。
*/
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
- 简单可以这么理解,在我的子View中的onTouchEvent中DOWN的时候去询问父View是否需要这个事件,如果父View需要,那么进行一系列的赋值,然后在MOVE的时候调用dispatchNestedPreScroll()不断的将滑动的值传递给父View,知道父View不想处理了为止。
//请注意,内部注释的那一行。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//OK,在这里就是对实现NestedScrollingParent的父View进行回调。
//接下来我们简单看一下ViewParentCompat,其实就是封装了不同安卓版本实现。
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
static final ViewParentCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
IMPL = new ViewParentCompatLollipopImpl();
} else if (version >= 19) {
IMPL = new ViewParentCompatKitKatImpl();
} else if (version >= 14) {
IMPL = new ViewParentCompatICSImpl();
} else {
IMPL = new ViewParentCompatStubImpl();
}
}
到这,整套NestedScrolling机制的流程就梳理完了。不知道各位看官看懂了没有。
那么接下来就是我们该怎么去使用?
上文代码中我们也可以看到,onTouchEvent事件中最终回调了onNestedPreScroll()方法,因此在父View对事件进行消费的核心也就是它!
- 让我们走进自定义的这个父View中去瞅一瞅:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
boolean hiddenTop = dy > 0 && getScrollY() < topHeight;
boolean showTop = dy < 0 && getScrollY() >= 0 && !ViewCompat.canScrollVertically(target, -1);
if (hiddenTop||showTop) {
consumed[1] = dy;
scrollBy(0, dy);
}
}
//其他方法怎么处理?我们可以直接交给Helper去处理。
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
nestedScrollingParentHelper.onNestedScrollAccepted(child,target,nestedScrollAxes);
}
- PS:注意重写scrollTo方法:
- 作用避免滚动时越界。
- scrollBy本质是调用scrollTo,详见http://blog.youkuaiyun.com/wjzj000/article/details/53874285
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > topHeight) {
y = topHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
- 这里我们就可以呼应前边所说的内容,onStartNestedScroll()返回true,告诉子View我希望处理并消耗一定的事件,然后onNestedScrollAccepted()方法被回调。紧接着子View将所获得的事件坐标通过回调onNestedPreScroll()传递进来。
- 最终父View根据自己的需要对事件进行消耗。
- consumed[1]=dy;就是在告诉子View你传递的事件坐标我已经消耗了。1代表消耗y;0代表消耗x。
- 这里我们进行了判断,在满足条件的时候进行对事件消费,否则并不做处理。
最后梳理一遍:
梳理一下过程:
如果我们想进行嵌套滑动,那么我们的外层View就要实现NestedScrollingParent,内层View实现NestedScrollingChild,并且在onTouchEvent中在特定的条件下对startNestedScroll(),dispatchNestedScroll()等方法进行合适的回调。而外层View则需要重写onStartNestedScroll()返回true告诉内层View,我要进行消费事件,并重写onNestedPreScroll()通过自己的需求进行相应的消费。
当然其他的方法如果没有特殊需求,直接交付给Helper去处理也不失为一种好方法。
PS:相关源码基本都存放于我的这个开源项目之中:
https://github.com/zhiaixinyang/PersonalCollect
尾声
OK,到此关于NestedScrolling机制的简单流程就梳理完毕,希望各位看官能够浪费这么多时间有所收获。
最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp