Android提供了一个官方的嵌套滑动机制,让子View实现NestedScrollingChild
或者NestedScrollingChild2
接口,父布局实现NestedScrollingParent
或NestedScrollingParent2
接口,Android官方还提供了NestedScrollingChildHelper
和NestedScrollingChildHelper
两个帮助类,让开发者更容易实现嵌套滑动的逻辑.
子View的接口有NestedScrollingChild
和NestedScrollingChild2
,NestedScrollingChild2
继承于NestedScrollingChild
,然后以多态的形式给大部分的方法都加了一个int类型的NestedScrollType
,这个int值是用来区分是用户触摸滑动还是其他滑动的,其共有两个类型
/**
* Indicates that the input type for the gesture is from a user touching the screen.
*/
public static final int TYPE_TOUCH = 0;
和
/**
* Indicates that the input type for the gesture is caused by something which is not a user
* touching a screen. This is usually from a fling which is settling.
*/
public static final int TYPE_NON_TOUCH = 1;
根据英文释义,TYPE_TOUCH
为用户触摸操作的类型,TYPE_NON_TOUCH
为非用户触摸操作类型,而且主要用于代码中的惯性操作,比如View滑动时的惯性滑动.
原始接口NestedScrollingChild
默认类型为TYPE_TOUCH
,如果需要实现子View和父View的惯性嵌套滑动则需要实现NestedScrollingChild2
接口
父View接口NestedScrollingParent
及NestedScrollingParent2
和子View一样在大部分方法中添加了NestedScrollType
,在此不做赘述.
在此只介绍原始接口的方法,对于扩展的第二接口由于只在原基础上加了一个类型,不多做介绍
public interface NestedScrollingChild {
/**
* 启用或者禁止嵌套滑动
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 用于判断嵌套滑动是否被启用
*/
boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动,参数为滑动方向,参数有如下几个
*
* 没有滑动方向
* public static final int SCROLL_AXIS_NONE = 0;
*
* 横向滑动
* public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0;
*
* 纵向滑动
* public static final int SCROLL_AXIS_VERTICAL = 1 << 1;
*
* 其返回值代表父View是否接受嵌套滑动,如果不接受返回false,后续的嵌套滑动都将失效
*/
boolean startNestedScroll(@ScrollAxis int axes);
/**
* 是否有实现了NestedScrollingParent的父View
* 如果父View没有实现接口,此方法返回false,且所有嵌套滑动无效
*/
boolean hasNestedScrollingParent();
/**
* 分发嵌套滑动事件,在子View滑动处理完之后调用
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* 分发预嵌套滑动事件,在子View滑动处理之前调用
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
* 分发嵌套滑动的惯性滑动处理,返回值表示是否处理
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 分发嵌套滑动的惯性滑动预处理,返回值表示是否处理,在子View处理之前调用
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
此接口都是NestedScrollingChild
的接口回调,在子View接口方法被调用时便会调用父View的NestedScrollingParent
的方法,它们有着一一对应的关系,具体如下
NestedScrollingChild |
NestedScrollingParent |
备注 |
---|---|---|
startNestedScroll |
onStartNestedScroll |
前者的调用会触发后者的调用,然后后者的返回值将决定后续的嵌套滑动事件是否能传递给父View,如果返回false,父View将不处理嵌套滑动事件,一般前者的返回值即后者的返回值 |
onNestedScrollAccepted |
如果onStartNestedScroll 返回true,则回调此方法 |
|
stopNestedScroll |
onStopNestedScroll |
|
dispatchNestedScroll |
onNestedScroll |
|
dispatchNestedPreScroll |
onNestedPreScroll |
|
dispatchNestedFling |
onNestedFling |
|
dispatchNestedPreFling |
onNestedPreFling |
|
getNestedScrollAxes |
获得滑动方向,没有回调,为主动调用的方法 |
子View的接口通常都是借助NestedScrollingChildHelper
通过委派模式实现的,没有直接写在某个嵌套滑动子View里,提升了代码复用性,还是很高明的做法.
具体如下
在类中声明NestedScrollingChildHelper
对象
private final NestedScrollingChildHelper mChildHelper;
然后在子View构造函数中实例化
mChildHelper = new NestedScrollingChildHelper(this);
接下来实现NestedScrollingChild
接口
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
在实现了子View的接口后,其实嵌套滑动并没有效果,因为根本没有哪里调用实现接口的方法.
然后一般,接口方法的调用其实是子View自己来调用的,可以说大部分NestedScrollingChild
接口的方法是自用的.
嵌套滑动的实现是通过子View再将触摸事件回传给父View的,所以大部分的嵌套滑动逻辑都会放在子ViewonInterceptTouchEvent
或者onTouchEvent
中.
其大致有如下流程
在构造函数中启用嵌套滑动
setNestedScrollingEnabled(true);
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
startNestedScroll(SCROLL_AXIS_VERTICAL);
// handle touch down event here
break;
case MotionEvent.ACTION_MOVE:
if (dispatchNestedPreScroll(dx, dy, comsumed, offsetInWindow)) {
}
// handle touch move event here
if (dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)) {
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (!dispatchNestedPreFling(velocityX, velocityY)) {
dispatchNestedFling(velocityX, velocityY, canScroll);
}
stopNestedScroll();
break;
default:
// do nothing
break;
}
return true;
}
在这里重点讲解下dispatchNestedPreScroll
和dispatchNestedScroll
方法
dispatchNestedPreScroll
方法参数这两个参数为上一次触摸点和当前触摸点的x轴和y轴坐标差值.在SDK中的NestedScrollView
和RecyclerView
的源码中这个值是使用的上一次触摸点的坐标减去当前触摸点的坐标,这和通常逻辑上的dx,dy正好相反,猜测是代指View应当滑动的距离.毕竟手指向上滑动,View的scroll是向下的.在写代码时,应当尽可能遵循其规则,使用上一次的坐标值减去当前值
- consumed
这是一个长度为2的int类型数组,用于储存父View的消耗的长度.然后在子View中处理滑动时需要减去父View的长度消耗,这样才能和真实的滑动的距离相平衡.
- offsetInWindow
这也是一个长度为2的int类型的数组,用于储存子View在父View中的偏移值,这个值一般会等于(comsumed中的值 * -1),但是也有绝对值不相等的时候,就是嵌套滑动不止两层,父View的parent也处理了部分的嵌套滑动.这时comsumed和offsetInWindow值是不相等的.
dispatchNestedScroll
方法参数@param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
@param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
对于这两个参数,源码的解释是这样的.在这个滑动阶段中子View的距离消耗.
查阅NestedScrollParent
的onNestedScroll
各View的方法实现,未发现这两个参数的实际使用.一般这两个参数使用也较少.
这两个参数是当子View滑动完后剩余应当滑动的距离.这个一般用在子View已经滑动到顶部或者底部时,将滑动事件分发给父View处理.所以这两个是关键父View需要处理的数据.
这个和dispatchNestedPreScroll
一样是一个长度为2的int类型的数组,用于储存子View在父View中的偏移值
dispatchNestedPreScroll
将整个的滑动距离dx
,dy
传递给父View,然后父View选择性处理一部分距离,将处理了的距离储存在consumed
数组中,其中consumed[0]为x轴处理距离,consumed[1]为y轴处理距离.dispatchNestedScroll
将剩余的滑动通过参数dxUnconsumed
,dyUnconsumed
交给父View处理.一般来说dispatchNestedPreScroll
和dispatchNestedScroll
只有一个会得到实际上的使用.在类中声明NestedScrollingParentHelper
对象
private final NestedScrollingParentHelper mParentHelper;
然后在子View构造函数中实例化
mParentHelper = new NestedScrollingParentHelper(this);
接下来实现NestedScrollingParent
接口
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return mParentHelper.getNestedScrollAxes();
}
原理解释完了,现在来实践一波
MainActivity.java
package com.yxf.nestedscrolldemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.list_view);
String[] data = new String[]{
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
};
ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, data);
listView.setAdapter(arrayAdapter);
}
}
NestedScrollParentLinearLayout.java
package com.yxf.nestedscrolldemo;
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
public class NestedScrollParentLinearLayout extends LinearLayout implements NestedScrollingParent {
private static final String TAG = NestedScrollParentLinearLayout.class.getSimpleName();
private final NestedScrollingParentHelper mParentHelper;
public NestedScrollParentLinearLayout(Context context) {
this(context, null);
}
public NestedScrollParentLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollParentLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
mParentHelper = new NestedScrollingParentHelper(this);
}
// NestedScrollingParent
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
int scrollY = getScrollY();
int step;
if (scrollY + dyUnconsumed < 0) {
step = -scrollY;
} else {
int height = getChildAt(getChildCount() - 1).getBottom();
Rect rect = new Rect();
getLocalVisibleRect(rect);
int visibleHeight = rect.bottom;
if (visibleHeight < height && dyUnconsumed > 0) {
step = Math.min(dyUnconsumed, height - visibleHeight);
} else if (rect.top > 0 && dyUnconsumed < 0) {
step = Math.max(dyUnconsumed, -rect.top);
} else {
step = 0;
}
}
scrollBy(0, step);
Log.d(TAG, "onNestedScroll: Y scrollBy : " + step);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int targetTop = target.getTop();
int targetBottom = target.getBottom();
int scrollY = getScrollY();
int currentTargetBottom = targetBottom - scrollY;
if (scrollY < targetTop && dy > 0 && scrollY >= 0) {
consumed[0] = 0;
consumed[1] = Math.min(dy, targetTop - scrollY);
Log.d(TAG, "onNestedPreScroll: Y scrollBy : " + consumed[1]);
} else if (currentTargetBottom < getBottom() && dy < 0) {
consumed[0] = 0;
consumed[1] = Math.max(dy, currentTargetBottom - getBottom());
} else {
consumed[0] = 0;
consumed[1] = 0;
}
scrollBy(0, consumed[1]);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
return true;
}
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return mParentHelper.getNestedScrollAxes();
}
}
NestedScrollChildListView.java
package com.yxf.nestedscrolldemo;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ListView;
import static android.support.v4.widget.ViewDragHelper.INVALID_POINTER;
public class NestedScrollChildListView extends ListView implements NestedScrollingChild {
private static final String TAG = NestedScrollChildListView.class.getSimpleName();
private final NestedScrollingChildHelper mChildHelper;
private final int[] mScrollConsumed = new int[2];
private final int[] mScrollOffset = new int[2];
private int mActivePointerId = INVALID_POINTER;
private int mNestedYOffset;
private int mLastScrollerY;
private int mLastMotionY;
private int lastY = -1;
private int oldTop = 0;
public NestedScrollChildListView(Context context) {
this(context, null);
}
public NestedScrollChildListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollChildListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
MotionEvent vtev = MotionEvent.obtain(event);
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mLastMotionY = (int) event.getY();
mActivePointerId = event.getPointerId(0);
oldTop = getTop();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
stopNestedScroll();
mActivePointerId = INVALID_POINTER;
break;
case MotionEvent.ACTION_MOVE:
int currentTop = getTop();
mNestedYOffset = currentTop - oldTop;
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) event.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
Log.d(TAG, "onTouchEvent: deltaY : " + deltaY + " , mScrollConsumedY : " + mScrollConsumed[1] + " , mScrollOffset : " + mScrollOffset[1]);
vtev.offsetLocation(0, mScrollConsumed[1]);
deltaY -= mScrollConsumed[1];
}
mLastMotionY = y - mScrollOffset[1];
Rect rect = new Rect();
if (getLocalVisibleRect(rect)) {
Log.d(TAG, "onTouchEvent: rect : " + rect);
} else {
Log.d(TAG, "onTouchEvent: visible rect got failed");
}
int consumeY = deltaY;
if (getFirstVisiblePosition() == 0) {
int top = getChildAt(0).getTop();
if (rect.top + deltaY < top) {
consumeY = top - rect.top;
}
} else if (getLastVisiblePosition() == getCount() - 1) {
int bottom = getChildAt(getChildCount() - 1).getBottom();
if (rect.bottom + deltaY > bottom) {
consumeY = bottom - rect.bottom;
}
}
if (Math.abs(consumeY) < Math.abs(deltaY)) {
deltaY -= consumeY;
Log.d(TAG, "onTouchEvent: consumeY :" + consumeY + " , deltaY : " + deltaY);
vtev.offsetLocation(0, consumeY);
if (dispatchNestedScroll(0, consumeY, 0, deltaY, mScrollOffset)) {
}
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = event.getActionIndex();
mLastMotionY = (int) event.getY(index);
mActivePointerId = event.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
mLastMotionY = (int) event.getY(event.findPointerIndex(mActivePointerId));
break;
default:
break;
}
return super.onTouchEvent(vtev);
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
activity_main.xml
<com.yxf.nestedscrolldemo.NestedScrollParentLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/colorAccent" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/colorPrimaryDark" />
<com.yxf.nestedscrolldemo.NestedScrollChildListView
android:layout_width="match_parent"
android:layout_height="600dp"
android:id="@+id/list_view"
>
com.yxf.nestedscrolldemo.NestedScrollChildListView>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorAccent" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorPrimaryDark" />
com.yxf.nestedscrolldemo.NestedScrollParentLinearLayout>
嵌套事件分发都写在了NestedScrollChildListView
中的onTouchEvent
中,
应当注意其中的对于消耗值的处理,在传给父类的onTouchEvent
方法之前必须将嵌套滑动的消耗距离减掉,不然滑动会卡顿或者距离不合理.
为了获得列表的最大显示效果,当列表的View不能完全占据屏幕时,需要先分发NestedPreScroll事件,在onNestedPreScroll
中处理让父View让出屏幕空间.
其效果如下
然后在列表已经滑动到顶部或者底部时,应当将列表推出屏幕让其他的View显示出来,这部分逻辑放在了onNestedScroll
中处理.
其效果如下
NestedScrollDemo