前言: 并不是热泪盈眶才叫青春,也不是莽撞热血才叫年轻。不忘初心,便始终都是年轻。多少人把放纵当热血,并把早熟和自律当做陈腐来嬉笑。岁月还未过多流逝之前,他们的身体和精神就已经被掏空,提早告别了青春。
上一篇文章分析了RecyclerView的滑动原理,依然是由onTouchEvent()
触控事件响应的,最终通过遍历所有子View,每个子View调用了底层View的offsetTopAndBottom()
或者offsetLeftAndRight()
方法来实现滑动的。不同的是RecyclerView采用嵌套滑动机制,会把滑动事件通知给支持嵌套滑动的父View先做决定。那么什么是嵌套滑动呢?RecyclerView是怎么处理嵌套滑动的呢?
我们来看看一个RecyclerView的滑动效果图:
这就是嵌套滑动的效果,当一个View产生嵌套滑动事件时,会先通知他的父View,询问父View是否处理这个事件,如果处理那么子View不处理,如果不处理那么子View处理,实际上父View只处理部分滑动距离的情况。可以看到由下往上滑动RecyclerView的时候,先处理头部的滑动事件,然后才处理RecyclerView自身的滑动事件,由上往下滑时,也是先询问父View是否处理滑动事件,如果不处理则交给RecyclerView自身的处理滑动事件。
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="#FF5722"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/flower"
android:scaleType="centerCrop"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:layout_collapseMode="pin"
app:title="二级标题" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这种高大上的视觉交互效果叫做“协调者布局”,实现这种效果的核心类就是一个CoordinatorLayout,它遵循Material Design风格,结合AppbarLayout,CollapsingToolbarLayout等可以产生各种炫酷的折叠悬浮效果。
照着上面的xml布局,效果是完成了,但是你是否有很多问号,CoordinatorLayout是啥,干什么的?AppBarLayout有什么用?CollapsingToolbarLayout又是啥?上面的布局控件除了ImageView,Toolbar,RecyclerView这几个控件外,其他的我基本不认识。
这是官网CoordinatorLayout的解析截图,它是一个超级的FrameLayout,注意它继承自ViewGroup,并没有继承FrameLayout,然后实现了NestedScrollingParent接口。
CoordinatorLayout可以作为一个容器与一个或多个子View进行特定的交互。通过指定Behaviors为子视图,可以在单个父视图中提供许多不同的交互,这些视图可以彼此交互。
Behavior是CoordinatorLayout的子视图的交互行为插件,一个行为实现了一个或多个用户可以在子视图上进行的交互。这些交互可能包括拖动,滑动,甩动或任何其他手势。
插件也就代表如果一个子View需要某种交互,它就需要加载对应的Behavior,否则它就是不具备交互能力的。而Behavior是一个抽象类,它的实现类都是为了让用户作用在一个View上面进行拖拽、滑动、快速滑动等手势操作。如果你需要其他的交互动作,则需要自定义Behavior。
但是,我们有了解到Behavior的真正含义吗?它到底是具体干什么的?
前面内容有讲过,CoordinatorLayout可以定义与它子View的交互或某些子View之间的交互。先来看看Behavior的源码:
public static abstract class Behavior<V extends View> {
public Behavior() {}
public Behavior(Context context, AttributeSet attrs) {}
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
}
我们自定义Behavior的一般目的只有两个,一是根据某些依赖的View的位置改变进行相应的操作;二是响应CoordinatorLayout中某些组件的滑动事件。先来看第一种情况:
如果一个View需要依赖另一个View,可能需要操作下面的API:
//确定提供的子视图是否有另一个特定的同级视图作为布局依赖项
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
//子View依赖的View的改变做出响应,当依赖视图在标准布局流之外的大小或者位置发生变化,此方法被调用。
//Behavior可以使用此方法更新子视图,如果子视图大小或者位置发生变化则返回true。
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { return false; }
//响应子View在依赖的视图中移除
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
通过layoutDependsOn()
确定一个View是否对另一个View进行依赖,注意:child是测试的子View,dependency该依赖的子View,如果child的布局依赖于dependency的布局则返回true,即依赖成立;反之不成立。当然你可以复写该方法对dependency进行类型判断然后再决定是否依赖,只有在layoutDependsOn()
返回true的时候后面的onDependentViewChanged()
和onDependentViewRemoved()
才会执行。
注释上面有说,无论子View 的顺序如何,总是在依赖的子View被布局后再布局这个子View,当依赖视图的布局发生改变时回调Behavior的onDependentViewChanged()
方法,可以适当更新响应的子视图。如果Behavior改变了child的位置和尺寸时,则返回true,默认返回false。
onDependentViewRemoved()
响应child从属性视图中被删除,从parent中移除dependency后调用此方法,
Behavior也可以使用此方法适当更新子视图来作为响应。
但是这样说有点抽象,到底是谁依赖谁?何为依赖?
下面举个例子加深理解:首先定义一个可以响应屏幕拖拽的View,DependencyView效果如下:
它的代码很简单,继承自TextView,在触摸事件onTouchEvent()
根据触摸点移动对自身位置进行位移。
public class DependencyView extends AppCompatTextView {
private int mTouchSlop;
private float mLastY;
private float mLastX;
public DependencyView(Context context) {
this(context, null);
}
public DependencyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DependencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClickable(true);
//在用户发生滚动之前,以像素为单位的触摸距离可能会发生飘移
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下
mLastX = event.getX();
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE://移动
float moveX = event.getX() - mLastX;
float moveY = event.getY() - mLastY;
if (Math.abs(moveX) > mTouchSlop || Math.abs(moveY) > mTouchSlop) {
ViewCompat.offsetLeftAndRight(this, (int) moveX);
ViewCompat.offsetTopAndBottom(this, (int) moveY);
mLastX = event.getX();
mLastY = event.getY();
}
break;
case MotionEvent.ACTION_UP://抬起
mLastX = event.getX();
mLastY = event.getY();
break;
}
return true;
}
}
然后实现一个Behavior,让它支配一个View,去紧紧依赖所支配的View。这里我们让依赖的View显示在被依赖的View的下面,不论被依赖的View位置如何变化,依赖的View都跟着变化:
public class MyBehavior extends CoordinatorLayout.Behavior<View> {
private static final String TAG = "MyBehavior";
public MyBehavior() {
}
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//确定提供的子视图是否有另一个特定的同级视图作为布局依赖项
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return dependency instanceof DependencyView;
}
//子View依赖的View的改变做出响应,当依赖视图在标准布局流之外的大小或者位置发生变化,此方法被调用。
//Behavior可以使用此方法更新子视图,如果子视图大小或者位置发生变化则返回true。
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
int bottom = dependency.getBottom();
child.setY(bottom );
child.setX(dependency.getLeft());
Log.d(TAG, "onDependentViewChanged: " + dependency);
return true;
}
}
在layoutDependsOn()
中通过判断dependency是否为DependencyView类型决定是否对其进行依赖,然后再onDependentViewChanged()
获取dependency的位置参数来设置child的位置参数,从而实现了child跟随dependency的位置改变而发生位置改变。
下面来验证MyBehavior,我们在布局文件中对ImageView设置了MyBehavior,然后观察它的现象:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_head"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<com.antiphon.recyclerviewdemo.weight.DependencyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:padding="4dp"
android:text="DependencyView"
android:textColor="#fff"
android:textSize="18sp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
注意:布局最外层需要是CoordinatorLayout作为父布局,效果如下:
可以看到,我们在拖动DependencyView的时候,ImageView也跟随着DependencyView移动。当然这种依赖并非只有一对一的关系,也可能是一对多或者多对多。
我们再修改一下MyBehavior中的代码,如果child是一个TextView则显示在 dependency的上方,否则显示在下方:
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
//child坐标
float childX = child.getX();
float childY = child.getY();
//dependency顶部底部坐标
int dependencyTop = dependency.getTop();
int dependencyBottom = dependency.getBottom();
childX = dependency.getX();
if (child instanceof TextView) {//如果是TextView则显示在dependency上面,否则显示在下面
childY = dependencyTop + child.getHeight();
} else {
childY = dependencyBottom;
}
child.setX(childX);
child.setY(childY);
return true;
我们在xml布局文件再添加一个TextView,设置MyBehavior:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_head"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="helloWord"
android:textColor="#000"
android:textSize="14sp"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
效果如下:
到这里我们知道,在Behavior中针对被依赖对象尺寸及位置变化时,依赖方该如何处理的流程,接下来就是处理滑动相关操作了。
我们一般接触到的滑动控件一般是 ScrollView和 RecyclerView,而CoordinatorLayout本身能滑动吗?如果能是怎么滑动的,到底是谁响应谁的滑动。
Behavior的相关滑动代码如下:
Behavior.java
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
为了观察Behavior行为,我在相关滑动方法添加了log:
Behavior.java
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onStartNestedScroll:axes == " + axes);
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onNestedScrollAccepted:axes == " + axes + " | type == " + type);
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dxConsumed == " + dxConsumed + " | dyConsumed == "
+ dyConsumed + " | dxUnconsumed == " + dxUnconsumed + " | dyUnconsumed == " + dyUnconsumed);
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int type) {
Log.e(TAG, "onNestedScrollAccepted:type == " + type);
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == "
+ velocityY + " | consumed == " + consumed);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
然后我在模拟器上面用鼠标滑动CoordinatorLayout制造一些滑动事件,观看MyBehavior相关的滑动函数API是否能触发,然后观察log:
可以看到,并没有触发任何的函数。那么先来了解嵌套滑动事件的API:
Behavior.java
/**
* 如果CoordinatorLayout后代的View尝试发起嵌套滑动时调用
* 任何与CoordinatorLayout的任何直接子元素相关联的Behavior都可以响应这个事件并返回true,
* 以指示CoordinatorLayout应该作为这个滑动嵌套滑动的父View,只有返回true才能接收后续的嵌套滑动事件。
*/
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
注释中说明,当一个CoordinatorLayout的后代想要触发嵌套滑动事件时,这个方法被调用,只有onStartNestedScroll()
返回true,后续的嵌套滑动事件才会响应。后续响应的函数指的是这几个函数:
Behavior.java
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {}
那么,我们先从onStartNestedScroll()
开始分析,查找在哪里调用到这个方法:
可以看到MyBehavior的onStartNestedScroll()
被调用了,原来它是在CoordinatorLayout中被调用:
CoordinatorLayout.java
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
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();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
这是CoordinatorLayout中的一个方法,先获取CoordinatorLayout下的子View,再获取子View中的Behavior,然后调用Behavior的onStartNestedScroll()
方法。
继续深入,那么谁调用了CoordinatorLayout的onStartNestedScroll()
呢?
继续追踪下去:
可以看到有多个地方调用它,其实归根到底最终都是View或者ViewParentCompat,这里以View为例:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {//是否启用嵌套滑动
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
当一个View触发onStartNestedScroll()
的时候,如果符合嵌套滑动,则获取Parent通过while循环调用Parent的onStartNestedScroll()
方法。因为CoordinatorLayout就是一个ViewGroup,所以它就是一个ViewParent对象,如果一个CoordinatorLayout的后代View触发了onStartNestedScroll()
方法,如果符合条件,那么CoordinatorLayout 的onStartNestedScroll()
方法就会被调用,再进一步调用Behavior的onStartNestedScroll()
方法。
当isNestedScrollingEnabled() = true时,它的ViewParent的onStartNestedScroll()
才能被触发,它是被判断自身是否能嵌套滑动,如果为true则在使用时作为嵌套滑动的子视图,可以通过setNestedScrollingEnabled(boolean enabled)
来设置View是否拥有嵌套滑动的能力。
如果一个View符合嵌套滑动的事件,也就是通过setNestedScrollingEnabled(true)
,然后调用它的onStartNestedScroll()
方法,理论上是可以产生嵌套滑动事件的。我们来尝试一下,在布局里面添加一个普通的TextView,然后设置点击事件。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mTv_nested_scroll.setNestedScrollingEnabled(true);
mTv_nested_scroll.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
}
之前在MyBehavior中对应的嵌套滑动方法打印了log,所以如果CoordinatorLayout中发生嵌套滑动的事件,log就有输出:
可以看到在一个View符合嵌套滑动的事件,则调用它的onStartNestedScroll()
方法。不过在安卓版本21(LOLLIPOP,5.0)及以上时才能调用View的嵌套滑动相关的API,那么在低于5.0版本呢?其实系统做了兼容:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
ViewCompat.setNestedScrollingEnabled(mTv_nested_scroll, true);
ViewCompat.startNestedScroll(mTv_nested_scroll, ViewCompat.SCROLL_AXIS_HORIZONTAL);
}
我们知道5.0以上版本View已经自带嵌套滑动功能和相关属性,可以根据ViewCompat
这个类完成低版本的兼容操作,继续跟踪ViewCompat.startNestedScroll()
:
public static void setNestedScrollingEnabled(@NonNull View view, boolean enabled) {
if (Build.VERSION.SDK_INT >= 21) {
view.setNestedScrollingEnabled(enabled);
} else {
if (view instanceof NestedScrollingChild) {
((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
}
}
}
public static boolean startNestedScroll(@NonNull View view, @ScrollAxis int axes) {
if (Build.VERSION.SDK_INT >= 21) {
return view.startNestedScroll(axes);
}
if (view instanceof NestedScrollingChild) {
return ((NestedScrollingChild) view).startNestedScroll(axes);
}
return false;
}
可以看到ViewCompat
的源码里面也做了版本兼容,在API>=21则会直接调用view.startNestedScroll()
相关嵌套滑动方法,否则会判断view是否为NestedScrollingChild
的实例,如果是则调用NestedScrollingChild
的startNestedScroll()
方法,NestedScrollingChild
是一个接口。
所以如果在5.0以上版本我们可以view.startNestedScroll()
,如果在5.0以下版本,如果一个View想发起嵌套滑动事件,你得保证这个View实现NestedScrollingChild
接口。
我们来看看NestedScrollingChild
:
public interface NestedScrollingChild {
/**
* 启用或者禁用此View的嵌套滑动。
* 注意:如果此属性设置为true,则允许该View使用当前层结构中兼容的父View启动嵌套滑动操作。
* 如果这个View没有任务嵌套滑动,则这个方法没有任何作用。
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 如果此View启用了嵌套滑动,则返回true。
* 如果启动了嵌套滑动,并且这个View类实现支持嵌套滑动,那么这个View将在适用时充当嵌套滑动子View,
* 将有关正在进行的滚动操作的数据转发到兼容且协作嵌套滑动的父View。
*/
boolean isNestedScrollingEnabled();
/**
* 沿着给定的方向(垂直或水平方向)开始一个可嵌套滚动的滑动操作。
* 返回true表示能处理父View传递上去的嵌套滑动事件,实际这个方法里面调用了NestedScrollingParent的onStartNestedScroll()
*@param axes axes表示方向
*/
boolean startNestedScroll(int axes);
/**
* 停止正在进行的嵌套滑动,嵌套滑动结束
*/
void stopNestedScroll();
/**
* 如果该View有一个嵌套的滑动父View,则返回true
*/
boolean hasNestedScrollingParent();
/**
* 调度一个正在执行的滑动步骤
* 在产生嵌套滑动的View已经滑动完成之后调用,该方法的作用是将剩余没有消耗的距离继续分发到View里面去
* @param dxConsumed 表示该View在x轴上消耗的距离
* @param dyConsumed 表示该View在y轴上消耗的距离
* @param dxUnconsumed 表示该View在x轴上未消耗的距离
* @param dyUnconsumed 表示该View在y轴未消耗的距离
* @param offsetInWindow 表示该该View在屏幕上滑动的距离,包括x轴上的距离和y轴上的距离
* 返回true表示父线程消耗了部分或者全部滑动的量,false表示不消耗
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow);
/**
* 在View消耗滑动的任何部分之前调度嵌套滑动的一个步骤。为嵌套滑动中的父View提供一个机会,在子View使用滑动操作之前,使用部分或者全部滑动操作。
* 在dispatchNestedScroll之前调用,也就是距离产生了,但是改View还没滑动。将滑动的距离报给父View,看父View是否优先消耗这部分距离
* @param dx x轴上产生的距离
* @param dy y轴上产生的距离
* @param consumed index为0的值表示父View在x轴消耗的的距离,index为1的值表示父View在y轴上消耗的距离
* @param offsetInWindow 该View在屏幕滑动的距离
* 返回true表示父线程消耗了部分或者全部滑动的距离,false表示父View不消耗
*/
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 如果父View不对惯性滑动做任何处理,那么子View会调用这个方法,作用是报告父View,子View此时正在fling
* @param velocityX x轴上的速度
* @param velocityY y轴的速度
* @param consumed true表示子View对这个fling事件有所行动,false表示没有行动
* 返回true表示嵌套滑动的父View消耗了或者以其他方式对惯性滑动做出反应,false则没有
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 在视图处理之前,将一个抛投运动发送给嵌套滑动的父View
* 在子View对fling有所行动之前,回调用这个方法,用于询问父View是否对fling有所行动
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
这个接口应该由View的子类实现并且希望支持将嵌套滑动操作分派到协作的父View(ViewGroup)。实现这个接口的类应该创建NestedScrollingChildHelper
并将任何View方法委托给它。调用嵌套滑动功能的View应该始终从ViewCompat,ViewGroupCompat,ViewParentCompat兼容性垫板静态方法执行,确保Android 5.0及以上版本的嵌套滑动视图的相护操作性。
通过AndroidStudio快捷键Ctrl+H,可以看到NestedScrollingChild
目前的实现者:
NestedScrollingChild
的实现者有两个SwipleRefreshLayout
和NestedScrollingChild2
,而NestedScrollingChild2
也有两个实现者RecyclerView
和NestedScrollingChild3
,那么RecyclerView也是NestedScrollingChild
间接实现者。而NestedScrollingChild3
的实现者是RecyclerView和NestedScrollView。
那么来到这里,NestedScrollingChild
可以说有三个实现类,RecyclerView,NestedScrollView,SwipleRefreshLayout,上面三个控件我们都认识,都是自带滑动的控件。
我们在上面验证了child之间的依赖互动关系,那么Behavior是如果响应滑动事件的?我们需要找到挑起嵌套滑动的View,我们往CoordinatorLayout布局中添加RecyclerView,滑动RecyclerView的内容,能产生嵌套滑动事件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:src="@mipmap/ic_gesture_down"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="50dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Behavior需要针对自身的业务逻辑进行处理,当我们滑动RecyclerView内容的时候,MyBehavior规定关联的ImageView进行相应的位移,主要是在垂直方向上。在MyBehavior的onStartNestedScroll()
做一些特别的处理:
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onStartNestedScroll:axes == " + axes);
return child instanceof ImageView && axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
只要child是ImageView并且滑动方向是垂直方向,返回true响应后续的嵌套滑动事件,针对滑动事件返回的位移对child进行操作:
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
ViewCompat.offsetTopAndBottom(child, dy);//让child进行垂直方向移动
}
复写onNestedPreScroll()
方法,通过读取dy值,让child进行垂直方向移动。dx是滑动水平方向的位移,dy是滑动垂直方向的位移,它是在滑动事件滑动onNestedScroll()
之前调用,然后把消耗的距离传递给consumed数组中。而onNestedScroll()
是滑动事件时调用,它的参数包括位移信息,以及已经在onNestedPreScroll()
消耗过的位移数。实现onNestedPreScroll()
方法就可以了。
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return false;
// return dependency instanceof DependencyView;
}
将MyBehavior之前做的一些处理,将它与DependencyView接触依赖。效果如下:
这里还有两个和惯性滑动相关的API:
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
Fling惯性滑动,RecyclerView和NestedScrollView快速滑动的时候,手指停下来的时候,滑动操作并没有停止,还会滑动一段距离。同样我们在Fling动作即将发生的时候,通过onNestedPreFling()
如果返回true则会拦截这次Fling动作,表明响应中的child自己处理这里fling事件,那么RecyclerView反而操作不了这个动作,因为child消耗了这个fling事件。
我们验证一下,将MyBehavior响应fling事件的时候,如果滑动向下,则ImageView放大,滑动向上,则ImageView缩小。
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
if (velocityY > 0) {//向下惯性滑动
child.animate().scaleX(2f).scaleY(2f).setDuration(2000).start();
} else {//向上惯性滑动
child.animate().scaleX(1f).scaleY(1f).setDuration(2000).start();
}
return false;
// return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
1.确定CoordinatorLayout中View与View之间的关系,通过layoutDependsOn()
方法返回true则表示依赖,否则不依赖;
2.当一个被依赖项dependency尺寸或者位置发生变化时,依赖方会通过Behavior获取到,然后在onDependentViewChanged()
中处理,如果方法中child的尺寸或者位置发生了变化,则需要返回true;
3.当Behavior中的View准备响应嵌套滑动时,它不需要通过layoutDependsOn()
来进行依赖绑定,只需要在onStartNestedScroll()
通过返回值告诉ViewParent,是否开启嵌套滑动功能,返回true后续的嵌套滑动事件才能响应。
4.嵌套滑动包括普通滑动(scroll)和惯性滑动(fling)两种。
前面我有尝试找出谁能产生嵌套滑动事件,结果发现他需要是NestedScrollChild对象,但是NestedScrollChild在调用startNestedScroll()
方法时,它需要借助它父View的力量,只有父View的startNestedScroll()
返回true的时候,它的后续事件才能延续下去。
View.java
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
可以看到ViewParent充当了非常重要的角色,回调了父View的onStartNestedScroll()
方法:
public interface ViewParent {
public void requestLayout();
public ViewParent getParent();
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
}
ViewParent是一个接口,常见的实现类是ViewGroup,它提供了嵌套滑动的相关API,是在Android5.0才加进去的,如果要兼容的话需要分析ViewParentCompat这个类,它为View以及它的父类提供了执行嵌套滑动的初始配置的机会,如果有这个方法的实现则应该调用父类来实现这个方法:
ViewParentCompat.java
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
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) {//5.0及以上
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} else if (parent instanceof NestedScrollingParent) {//5.0以下
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
可以看到在安卓5.0版本以下,如果ViewParent需要响应嵌套滑动事件,则需要保证自己是一个NestedScrollingParent对象:
public interface NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, int axes);
void onNestedScrollAccepted(View child, View target, int axes);
void onStopNestedScroll(View target);
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}
NestedScrollingParent的实现类有以下几个:
因为NestedScrollingParent2也是NestedScrollingParent的一个实现类,CoordinatorLayout间接实现了NestedScrollingParent,所以有四个实现了类:CoordinatorLayout、NestedScrollView、SwipeRefreshLayout、ActionBarOverlayLayout。所以CoordinatorLayout之所以能处理嵌套滑动事件,是因为它本身就是一个NestedScrollingParent。
嵌套滑动的流程:
总的来说一个嵌套滑动事件的起始,它是由一个NestedScrollingChild发起,通过向上遍历parent,借助parent的嵌套滑动相关方法来完成交互。注意安卓5.0以下版本,parent必须NestedScrollingParent接口。
如果要支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。而RecyclerView实现了NestedScrollingChild接口,CoordinatorLayout实现了NestedScrollingParent接口。
温馨提示:本文源码基于androidx.recyclerview:recyclerview:1.2.0-alpha01
嵌套滑动机制主要用到的接口和类:NestedScrollingChild,NestedScrollingParent,NestedScrollingParentHelper,NestedScrollingChildHelper。这里做个简单总结:
类名 | 含义 |
---|---|
NestedScrollingChild | 如果一个View想要产生滑动事件,这个View必须实现NestedScrollingChild接口, 从Android5.0开始View实现了这个接口,不需要手动实现了 |
NestedScrollingParent | 这个接口通常被ViewGroup来实现,表示能接收从子View发送过来的嵌套滑动事件 |
NestedScrollingChildHelper | 这个类通常在实现NestedScrollingChild接口的View里面辅助使用,它通常用来负责子View产生的嵌套滑动事件报告给父View。也就是说,一个子View想要把产生的嵌套滑动事件交给父View,那么NestedScrollingChildHelper会帮我们来处理。 |
NestedScrollingParentHelper | 与上述类似,都是传递事件的辅助类。通常用在实现了NestedScrollingParent接口的父View,如果父View不想处理这个事件,就通过NestedScrollingParentHelper传递 |
结合RecyclerView的源码来分析嵌套事件传递原理,先来看看RecyclerView的ACTION_DOWN事件的处理:
RecyclerView.java
case MotionEvent.ACTION_DOWN: {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//开始嵌套滑动
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
初始化nestedScrollAxis(线性方向)变量后,就会调用startNestedScroll()
告诉父View滑动事件已经开始,你是否需要有所行动,可以看到嵌套滑动事件是从下往上传递的。它是怎么将一个事件传递到父View的呢?
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
事件是通过NestedScrollingChildHelper帮助类传递的,通过NestedScrollingChildHelper实例调用startNestedScroll()
:
NestedScrollingChildHelper.java
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {//嵌套滑动进行中,直接返回true
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {//父View是否支持嵌套滑动
ViewParent p = mView.getParent();
View child = mView;
//while循环获取上级的view,看是否支持嵌套滑动,如果支持则返回true,结束循环;不支持则继续访问上层View。
while (p != null) {
//这里调用父View的方法,如果父View运行嵌套滑动则返回true,之后会消耗掉部分子View传上来的滑动距离。
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;
}
这里首先经过hasNestedScrollingParent()
来判断父View是否正在进行嵌套滑动,父View必须实现NestedScrollingParent接口,onStartNestedScroll()
方法返回true,如果父View不能够处理嵌套滑动事件时,就会while()
递归往上找。NestedScrollingChildHelper
有依靠ViewParentCompat类帮助传递事件,实际上ViewParentCompat也帮我们调用父View的onStartNestedScroll()
方法,这样做的目的是为了兼容不同的版本。前面说过,在Android5.0开始View实现了NestScrollingChild接口,但是5.0以下版本需要自己手动实现。
事件是如何传递到父View的呢?ViewParentCompat作为系统兼容扮演着重要的角色,它是如何保证系统兼容的呢?
ViewParentCompat.java
public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
@NonNull int[] consumed) {
//如果我们调用的是小于NestedScrollingParent3的函数,那么将未使用的距离添加到已使用的参数中,这样调用NestedScrollingChild3实现就会被告知整个滚动距离已被使用(用于向后比较)。
if (parent instanceof NestedScrollingParent3) {
((NestedScrollingParent3) parent).onNestedScroll(target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type, consumed);
} else {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2) parent).onNestedScroll(target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
//如果类型是默认的(触摸),尝试NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed);
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
}
}
}
}
其中NestedScrollingParent3
继承自NestedScrollingParent2
,NestedScrollingParent2
继承自NestedScrollingParent
,如果parent实现了NestedScrollingParent3
的接口则调用NestedScrollingParent3
中的onNestedScroll()
;如果parent实现了NestedScrollingParent2
的接口则调用NestedScrollingParent2
中的onNestedScroll()
;如果parent实现了NestedScrollingParent的接口则调用NestedScrollingParent
中的onNestedScroll()
;如果版本在Android5.0以上则直接调用parent的onNestedScroll()
。
该方法是对正在进行的嵌套滑动做出响应,将滚动距离的已使用部分和未使用部分报告给ViewParent。所以一个父View想要接收子View传递过来的事件,就的实现NestedScrollingParent
等相关接口。
在ACTION_DOWN
中涉及的嵌套滑动流程就是NestedScrollingChildHelper初始化,寻找支持嵌套滑动的父级View,接下来在ACTION_MOVE
中将滑动的距离首先交给这个找到的父View,父View会将消耗的滑动距离写入到传入的参数中,之后RecycleView会知道滑动的距离是否被父View消耗掉,来决定RecycleView的滑动是否更新。
RecyclerView.java
case MotionEvent.ACTION_MOVE:{
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
))
} break;
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
主要是通过ScrollingChildHelper类调用dispatchNestedPreScroll()
方法:
ScrollingChildHelper.java
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
//之前嵌套滑动的父View不匹配就直接返回
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
//offsetInWindow为非空就将view的坐标写入数组中
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
//回调父View相关方法,将消耗的数据写入consumed数组中
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//父View消耗了dy或者dx就返回true
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
ACTION_UP
根据手指抬起的X,Y抬起的速度来判断是否需要执行惯性滑动fling()
:
RecyclerView.java
case MotionEvent.ACTION_UP: {
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();
} break;
主要方法是在fling()
中:
public boolean fling(int velocityX, int velocityY) {
······
//如果父View未消耗本次的fling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
//嵌套的惯性滑动
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
//如果还可以滚动,就使用recyclerview自己的机制来实现一段惯性滑动
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
//根据计算速度实现惯性滑动
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
这里如果父View的惯性滑动尚未分发的话就dispatchNestedFling()
就将嵌套的fling操作分发给父View。
NestedScrollingChildHelper.java
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled()) {
//根据类型获取parent
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
return ViewParentCompat.onNestedFling(parent, mView, velocityX,
velocityY, consumed);
}
}
return false;
}
最后调用ViewParentCompat的onNestedFling()
方法:
ViewParentCompat.java
public static boolean onNestedFling(ViewParent parent, View target, float velocityX,
float velocityY, boolean consumed) {
if (Build.VERSION.SDK_INT >= 21) {
return parent.onNestedFling(target, velocityX, velocityY, consumed);
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY,
consumed);
}
return false;
}
如果在Android5.0以上则直接调用父view的onNestedFling()
方法,如果在在Android5.0以下则调用父View实现的NestedScrollingParent接口中的onNestedFling()
方法。(有关RecyclerView的惯性滑动相关原理可以参考理解RecyclerView(六))
画图总结RecyclerViewonTouchEvent()
的嵌套滑动流程:
简单总结RecyclerView嵌套滑动:
1.嵌套滑动事件由子View传递给父View,从下到上分发;
2.如果一个view想要传递嵌套滑动事件,实现NestedScrollingChild接口,并且支持嵌套滑动;如果父View想要支持嵌套滑动事件,必须实现NestedScrollingParent接口。
1.CoordinatorLayout是一个普通的ViewGroup,也是一个超级FrameLayout,能通过Behavior与child交互;
2.在Behavior中的layoutDepentOn()
返回true或者通过xml指定目标控件来确定两个View的依赖关系,当依赖方的尺寸和位置发生变化时,Behavior中onDependentViewChanged()
就会被调用,如果改变了主动依赖的View的尺寸和位置,方法返回true;
3.Behavior是一种插件机制,可以决定CoordinatorLayout对应childview的测量尺寸,布局位置,触摸响应;如果没有Behavior的存在CoordinatorLayout与普通的FrameLayout没有区别;
4.嵌套滑动分为nested scroll和 fling 两种,childView是否接受响应由onStartNestedScroll()
返回值决定,一般在startNestedScroll()
处理相应的nested scroll事件,在onNestedFling()
处理fling事件;
5.RecyclerView能够产生嵌套滑动事件是因为实现了NestedScrollingChild接口,CoordinatorLayout能够响应嵌套滑动事件是因为实现了NestedScrollingParent接口;由childView发起,通过由下向上循环遍历parent,借助parent的嵌套滑动相关方法来完成交互。
到这里一个嵌套滑动的起始才彻底水落石出,RecyclerView实现NestedScrollingChild接口,发起嵌套滑动时(Android5.0以下需要setNestedScrollEnable(true)),由下向上循环遍历自己的parent,parent实现NestedScrollingParent接口,parent调用onStartNestedScroll()
,进而调用CoordinatorLayout 的onStartNestedScroll()
方法,CoordinatorLayout里面其实调用了Behavior中的onStartNestedScroll()
,告知ViewParent是否对嵌套滑动事件感兴趣,如果返回true,CoordinatorLayout 中的嵌套滑动事件才会被响应。(Android5.0以下parent必须实现NestedScrollingParent接口 )
至此,本文结束!
源码地址:https://github.com/FollowExcellence/RecyclerViewDemo
相关文章:
理解RecyclerView(五)
● RecyclerView的绘制流程
理解RecyclerView(六)
● RecyclerView的滑动原理
理解RecyclerView(七)
● RecyclerView的嵌套滑动机制
理解RecyclerView(八)
● RecyclerView的回收复用缓存机制详解
理解RecyclerView(九)
● RecyclerView的自定义LayoutManager