Android(安卓)NestedScroll嵌套滑动机制解析
Android NestedScroll嵌套滑动机制解析
目录
- Android NestedScroll嵌套滑动机制解析
- 目录
- 最初的接口和接口2的区别
- 接口方法介绍
- NestedScrollingChild
- NestedScrollingParent
- 使用方法
- 子View接口的使用
- 子View接口常用实现
- 子View接口的自我调用
- dispatchNestedPreScroll方法参数
- dispatchNestedScroll方法参数
- 具体使用操作流程
- 父View接口的使用
- 父View接口常用实现
- 子View接口的使用
- 使用举例
- 主要代码
- 代码分析
- 注意
- 示例源码
Android提供了一个官方的嵌套滑动机制,让子View实现NestedScrollingChild
或者NestedScrollingChild2
接口,父布局实现NestedScrollingParent
或NestedScrollingParent2
接口,Android官方还提供了NestedScrollingChildHelper
和NestedScrollingChildHelper
两个帮助类,让开发者更容易实现嵌套滑动的逻辑.
最初的接口和接口2的区别
子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
,在此不做赘述.
接口方法介绍
在此只介绍原始接口的方法,对于扩展的第二接口由于只在原基础上加了一个类型,不多做介绍
NestedScrollingChild
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);}
NestedScrollingParent
此接口都是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接口的使用
子View接口常用实现
子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的接口后,其实嵌套滑动并没有效果,因为根本没有哪里调用实现接口的方法.
然后一般,接口方法的调用其实是子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
方法参数
- dx,dy
这两个参数为上一次触摸点和当前触摸点的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
方法参数
- dxConsumed,dyConsumed
@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的方法实现,未发现这两个参数的实际使用.一般这两个参数使用也较少.
- dxUnconsumed,dyUnconsumed
这两个参数是当子View滑动完后剩余应当滑动的距离.这个一般用在子View已经滑动到顶部或者底部时,将滑动事件分发给父View处理.所以这两个是关键父View需要处理的数据.
- offsetInWindow
这个和dispatchNestedPreScroll
一样是一个长度为2的int类型的数组,用于储存子View在父View中的偏移值
具体使用操作流程
- 在move事件处理时,先通过
dispatchNestedPreScroll
将整个的滑动距离dx
,dy
传递给父View,然后父View选择性处理一部分距离,将处理了的距离储存在consumed
数组中,其中consumed[0]为x轴处理距离,consumed[1]为y轴处理距离. - 然后子View根据自己的需要处理剩余的距离.
- 如果子View未能将剩余距离消耗掉,通过
dispatchNestedScroll
将剩余的滑动通过参数dxUnconsumed
,dyUnconsumed
交给父View处理.一般来说dispatchNestedPreScroll
和dispatchNestedScroll
只有一个会得到实际上的使用.
父View接口的使用
父View接口常用实现
在类中声明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
<?xml version="1.0" encoding="utf-8"?><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
中处理.
其效果如下
注意
- 当前使用的嵌套滑动和Materials Design中使Toolbar上滑的效果实现有相似的地方但是并不一样,在下一篇文章中,将介绍其中使用到的CoordinatorLayout和Behavior原理.
- 嵌套滑动是从Android 5.0开始引入的,在4.4及以前的系统上说不定会产生一定的bug,不过既然已经了解原理,其实也可以自己尝试自己造轮子实现.
示例源码
NestedScrollDemo
更多相关文章
- Android接口描述语言。
- Samung LCD接口原理
- Android(安卓)FrameWork 之Binder机制初识
- Android(安卓)滑动效果进阶篇(五)—— 3D旋转
- android监控应用(app)前后台切换(状态)
- Android中滑屏实现----手把手教你如何实现触摸滑屏以及Scroller
- android audio系统的概况
- Android一步一步带你实现RecyclerView的拖拽和侧滑删除功能
- android okhttp+解析json( okhttp 工具类)