Android NestedScrolling嵌套滑动机制

Android NestedScrolling嵌套滑动机制

Android在发布5.0之后加入了嵌套滑动机制NestedScrolling,为嵌套滑动提供了更方便的处理方案。在此对嵌套滑动机制进行详细的分析。

嵌套滑动的常见用法比如在滑动列表的时候隐藏相关的TopBar和BottomBar,增加列表的信息展示范围,让用户聚焦于App想展示的内容上等。官方出的Design包里也有很多支持该机制的炫酷控件,比如CoordinatorLayout,AppBarLayout等,在用户体验上有很大的进步。

说道嵌套滑动,离不开一下几个内容:

NestedScrollingChild

NestedScrollingParent

NestedScrollingChildHelper

NestedScrollingParentHelper

简单看看这几个类是如何使用的,在系统为我们提供的控件中,NestedScrollView是实现了这个机制的控件,以它的实现为例,首先看作为嵌套滑动的子View:

// 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的NestedScrollView的实现

// 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);

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);

}

@Override

public void onStopNestedScroll(View target) {

mParentHelper.onStopNestedScroll(target);

stopNestedScroll();

}

@Override

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,

int dyUnconsumed) {

final int oldScrollY = getScrollY();

scrollBy(0, dyUnconsumed);

final int myConsumed = getScrollY() - oldScrollY;

final int myUnconsumed = dyUnconsumed - myConsumed;

dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);

}

@Override

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

dispatchNestedPreScroll(dx, dy, consumed, null);

}

@Override

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {

if (!consumed) {

flingWithNestedDispatch((int) velocityY);

return true;

}

return false;

}

@Override

public boolean onNestedPreFling(View target, float velocityX, float velocityY) {

return dispatchNestedPreFling(velocityX, velocityY);

}

@Override

public int getNestedScrollAxes() {

return mParentHelper.getNestedScrollAxes();

}

从上面的实现可以看出,基本上都是通过mParentHelper和mChildHelper来完成滑动的,没接触过这方面的同学看着肯定觉得很难理解,的确有些跳跃性,在说清楚这个问题之前必须先把这几个类之间的交互逻辑理清楚才能不至于不知所云。

先来梳理一下子View和父View的接中都有哪些方法。这种套路一般都是子View发起的然后父View进行回调从而完成配合。

子View父View

startNestedScrollonStartNestedScroll、onNestedScrollAccepted

dispatchNestedPreScrollonNestedPreScroll

dispatchNestedScrollonNestedScroll

stopNestedScrollonStopNestedScroll

为了避免重复造轮子,有个同学已经写了一套很炫酷的开源控件( 地址:https://github.com/race604/FlyRefresh),借用他的实现结合NestedScrollView来用,来讲解这套机制。这里的子View指的是实现了NestedScrollingChild的View,例如我们的NestedScrollView,父View指的是实现了NestedScrollingParent的View,比如这位同学开源控件里写的PullHeaderLayout。

首先在子View滑动还未开始之前将调用startNestedScroll,对应NestedScrollView中的ACTION_DOWN:

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_DOWN: {

......

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//在接到点击事件之初调用

break;

}

}

那么调用 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)寓意何在?跟进去看到其实是调用mChildHelper.startNestedScroll(axes)的实现

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开始滑动前通知父View,回调到父View的onStartNestedScroll(),

//父View需要滑动则返回true:

if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {

mNestedScrollingParent = p;

//---------> 如果父View决定要和子View一块滑动,调用父ViewonNestedScrollAccepted()

ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);

return true;

}

if (p instanceof View) {

child = (View) p;

}

p = p.getParent();

}

}

return false;

}

大家仔细看我在代码里加的注释,需要关心的就是父View在此时需要决定是否跟随子View滑动,看看父View的实现:

@Override

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;

}

ViewCompat.SCROLL_AXIS_VERTICAL的值是2(10),所以当nestedScrollAxes 也为2的时候,返回true,回到上面可以看到只要是竖直方向的 滑动,父View就会和子View进行嵌套滑动。而在父View的

onNestedScrollAccepted中,则把滑动的方向给保存下来了。这样父View和子View的第一次合作关系就结束了,再看看接下来是如何配合的。

当子View在滑动的Move事件中,又开始了嵌套滑动

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_MOVE:

final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);

int deltaY = mLastMotionY - y;

if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {

deltaY -= mScrollConsumed[1];

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

}

在子View决定滑动的时候,再次在进行自己的滑动前调用dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)

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;

}

//--------->重点在这,首先把consume封装好,consumed[0]表示X方向父View消耗的距离,

// consumed[1]表示Y方向上父View消耗的距离,在父View处理前当然都是0

consumed[0] = 0;

consumed[1] = 0;

//然后调用父View的onNestedPreScroll并把当前的dx,dy以及消耗距离的consumed传递过去

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;

}

看看父View是怎么处理的,也是实现了这套机制的,看看他是怎么处理的:

@Override

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

if (dy > 0 && mHeaderController.canScrollUp()) {

final int delta = moveBy(dy);

consumed[0] = 0;

consumed[1] = delta;

}

}

通过moveby计算父View滑动的距离,并将父ViewY方向消耗的距离记录下来

继续来看子View,在通知了父View并且父View消耗了滑动距离之后,剩下的就是自己进行滑动了

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_MOVE:

final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);

int deltaY = mLastMotionY - y;

if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {

deltaY -= mScrollConsumed[1];

//重点在这:-------->父View滑动之后调整自己的Offset为父View滑动的距离

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

.........

if(mIsBeingDragged){

mLastMotionY = y - mScrollOffset[1];

final int oldY = getScrollY();

final int range = getScrollRange();

final int overscrollMode = ViewCompat.getOverScrollMode(this);

boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||

(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&

range > 0);

// Calling overScrollByCompat will call onOverScrolled, which

// calls onScrollChanged if applicable.

//重点在这:-------->父View消耗了部分滑动距离后,子View自己开始滑动,通过overScrollByCompat

//把滑动距离的参数传给mScroller进行弹性滑动

if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,

0, true) && !hasNestedScrollingParent()) {

// Break our velocity if we hit a scroll barrier.

mVelocityTracker.clear();

}

}

......

//重点在这:-------->自己滑动完之后再调用dispatchNestedScroll通知父View滑动结束

if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {

mLastMotionY -= mScrollOffset[1];

vtev.offsetLocation(0, mScrollOffset[1]);

mNestedYOffset += mScrollOffset[1];

}

break;

}

接下来又是父View的回调了,来看看父View的处理:

@Override

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,

int dyUnconsumed) {

final int myConsumed = moveBy(dyUnconsumed);

final int myUnconsumed = dyUnconsumed - myConsumed;

}

父View在这里将最后子View滑动完后剩余的距离进行收尾处理,自我调整后第二轮的嵌套滑动也结束了。

那么再看看最后一轮滑动:

@Override

public boolean onTouchEvent(MotionEvent ev) {

case MotionEvent.ACTION_UP:

/* Release the drag */

mIsBeingDragged = false;

mActivePointerId = INVALID_POINTER;

recycleVelocityTracker();

if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {

ViewCompat.postInvalidateOnAnimation(this);

}

stopNestedScroll();

break;

}

在触控事件的最后一个阶段,也就是ACTION_UP时,调用stopNestedScroll(),这时会通知父View的onStopNestedScroll()来对整个系列的滑动来收尾

public void stopNestedScroll() {

if (mNestedScrollingParent != null) {

ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);

mNestedScrollingParent = null;

}

}

父类最后在自己的onStopNestedScroll()实现相关的收尾处理,比如重置滑动状态标记,完成动画操作,通知滑动结束等。这样,整个滑动嵌套流程就完成了。

最后来总结一下整个流程,分为三个步骤:

步骤一:子View的ACTION_DOWN调用startNestedScroll—->父View的onStartNestedScroll判断是否要一起滑动,父ViewonNestedScrollAccepted同意协同滑动

步骤二:子View的ACTION_MOVE调用dispatchNestedPreScroll—->父View的onNestedPreScroll在子View滑动之前先进行滑动并消耗需要的距离—->父View完成该次滑动之后返回消耗的距离,子View在剩下的距离中再完成自己需要的滑动

步骤三:子View滑动完成之后调用dispatchNestedScroll—->父View的onNestedScroll处理父View和子View之前滑动剩余的距离

步骤四:子View的ACTION_UP调用stopNestedScroll—->父View的onStopNestedScroll完成滑动收尾工作

这样,子View和父View的一系列嵌套滑动就完成了,可以看出来整个嵌套滑动还是靠子View来推动父View进行滑动的,这也解决了在传统的滑动事件中一旦事件被子View处理了就很难再分享给父View共同处理的问题,这也是嵌套滑动的一个特点。

来源:https://dreamerhome.github.io/2016/11/03/nestedscrolling/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容