观菊展

时间:2018-04-03 11:37:47来源:杰瑞文章网点击:作文字数:800字
  好久没有写博客了,感觉自己的手变得生疏了,今天来记录一下自己对Android里面的嵌套滚动的理解。   本文参考资料:   1.NestedScrollingParent, NestedScrollingChild 详解   2.针对 CoordinatorLayout 及 Behavior 的一次细节较真 1.什么是嵌套滑动?   在这里,楼主先贴出一个Demo图片,来直观的展示一下,什么是嵌套滑动。   我们发现,当我们向下滑动时,首先是外部的布局向下滑动,然后才是RecyclerView滑动,向上滑动也是如此。这就是嵌套滑动的效果。   我们认真的想一想,如果使用传统的事件分发机制来实现这个功能,应该怎么实现?是使用传统的事件分发机制来实现,还是不是很难的。但是又没有更加优秀的方法来实现这种效果呢?当然有咯,不然今天说什么。这个答案就是嵌套滑动机制。   可能有些老哥没有听过嵌套滑动机制,其实不是很难,楼主觉得比传统的事件分发机制简单的多。其中我们需要注意一点就是,传统的事件分发是从上向下分发,而嵌套滑动事件是从下到上,也就是说,当一个View会产生了一个嵌套滑动的事件,首先会报告给他的父View,询问他的父View是否处理这个事件,如果处理的话,那么子View就不处理(实际上存在父View只处理处理部分滑动距离的情况)。这里解释的比较简单,待会会详细的解释这些细节。   嵌套滑动机制,主要的用到的接口和类有:NestedScrollingChild,NestedScrollingParent,NestedScrollingParentHelper,NestedScrollingChildHelper。   这里先对这4个类做一个统一的解释: 类名 解释 NestedScrollingChild 如果一个View想要能够产生嵌套滑动事件,这个View必须实现NestedScrollChild接口,从Android 5.0开始,View实现了这个接口,不需要我们手动实现 NestedScrollingParent 这个接口通常用来被ViewGroup来实现,表示能够接收从子View发送过来的嵌套滑动事件 NestedScrollingChildHelper 这个类通常在实现NestedScrollChild接口的View里面使用,他通常用来负责将子View产生的嵌套滑动事件报告给父View。也就是说,如果一个子View想要将产生的嵌套滑动事件交给父View,这个过程不需要我们来实现,而是交给NestedScrollingChildHelper来帮助我们处理 NestedScrollingParentHelper 这个类跟NestedScrollingChildHelper差不多,也是帮助来传递事件的,不过这个类通常用在实现NestedScrollingParent接口的View。如果一个父View不想处理一个事件,通过NestedScrollingParentHelper类帮助我们传递就行了   本文不对嵌套滑动的基本使用进行展开,只对其基本原理进行解释。 2. 子View事件的产生和传递   如果想要了解嵌套滑动机制的原理,必须得知道,一个嵌套事件是怎么产生的,是怎么传递到父View里面的。这些都必须知道NestedScrollingChild的工作原理。 (1).NestedScrollingChild的接口   在了解NestedScrollingChild的工作原理,我们先来看看NestedScrollChild接口里面的方法,然后在结合RecyclerView的源码来分析时事件是怎么传递到父View里面的。 public interface NestedScrollingChild { /** * 设置当前View是否能够产生嵌套滑动的事件 * @param enabled true表示能够产生嵌套滑动的事件,反之则不能 */ void setNestedScrollingEnabled(boolean enabled); /** * 判断当前View是否能够产生嵌套滑动的事件 * @return */ boolean isNestedScrollingEnabled(); /** * 当嵌套事件开始产生时会调用这个方法,这个方法通常是在ACTION_DOWN里面被调用 * @param axes axes表示方向,如果(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 表示当前滑动方向是垂直方向 * ,水平方向也是如此 * @return 返回true表示有父View能够处理传递传递上去的嵌套滑动事件,实际上这个这个方法里面调用NestedScrollingParent的onStartNestedScroll * 方法来判断是否有父View能够处理,这个在后面源码分析时,我们具体讲解 */ boolean startNestedScroll(@ViewCompat.ScrollAxis int axes); /** * 这个方法表示本次嵌套滑动的行为结束了,通常在ACTION_UP或者ACTION_CANCEL里面调用 */ void stopNestedScroll(); /** * 判断是否能够处理嵌套滑动的父View * @return 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轴上的距离 * @return true表示父View消耗这部分的未消耗的距离,反之表示父View不消耗 */ boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow); /** * 这个方法在方法调用之前调用,也就是调用这个方法时,滑动距离产生了,但是该View还未滑动。 * 这个方法的作用是将滑动的距离报给父View,看看父View是否优先消耗这个这部分距离 * @param dx x轴上产生的距离 * @param dy y轴上产生的距离 * @param consumed index为0的值表示父View在x轴消耗的的距离,index为1的值表示父View在y轴上消耗的距离 * @param offsetInWindow 该View在屏幕滑动的距离 * @return true表示父View有消耗距离,false表示父View不消耗 */ boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow); /** * 如果父View不对fling事件做任何处理,那么子View会调用这个方法,这个方法的作用是报告父View,子View此时在fling * 然而具体是否在fling,还要consumed为true还是false,在这方法里面会调用NestedScrollingParent的onNestedFling * @param velocityX x轴上的速度 * @param velocityY y轴的速度 * @param consumed true表示子View对这个fling事件有所行动,false表示没有行动 * @return */ boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); /** * 在子View对fling有所行动之前,会调用这个方法。这个方法的作用是,用来询问父View是否对fling事件有所行动 * @param velocityX * @param velocityY * @return */ boolean dispatchNestedPreFling(float velocityX, float velocityY); }   我相信,可能很多老哥看了每个方法的注释还是一头雾水。哎,能力所致!!现在我在对整个做一个小小的总结。   整个事件传递过程中,首先能保证传统的事件能够到达该View,当一个事件序列开始时,首先会调用startNestedScroll方法来告诉父View,马上就要开始一个滑动事件了,请问爸爸需要处理,如果处理的话,会返回true,不处理返回fasle。跟传统的事件传递一样,如果不处理的话,那么该事件序列的其他事件都不会传递到父View里面。   然后就是调用dispatchNestedPreScroll方法,这个方法调用时,子View还未真正滑动,所以这个方法的作用是子View告诉它的爸爸,此时滑动的距离已经产生,爸爸你看看能消耗多少,然后父View会根据情况消耗自己所需的距离,如果此时距离还未消耗完,剩下的距离子View来消耗,子View滑动完毕之后,会调用dispatchNestedScroll方法来告诉父View,爸爸,我已经滑动完毕,你看看你有什么要求没?这个过程里面可能有子View未消耗完的距离。   其次就是fling事件产生,过程跟上面也是一样,也是先调用dispatchNestedPreFling方法来询问父View是否有所行动,然后调用dispatchNestedFling告诉父View,子View已经fling完毕。   最后就是调用stopNestedScroll表示本次事件序列结束。   整个过程中,我们会发现子View开始一个动作时,会询问父View是否有所表示,结束一个动作时,也会告诉父View,自己的动作结束了,父View是否有所指示。 (2).RcyclerView的嵌套滑动机机制   简单的了解NestedScrollingView的工作流程,我们结合RecyclerView的源码分析一下事件传递的原理。由于本文只分析嵌套滑动的原理,所以RecyclerView其他的知识这个不讲解,实际上我也不懂!   我感觉我们以前真的是小看了RecyclerView,没想到他在背后帮我们做了这么事情,以后有机会一定好好看看RecyclerView的代码。现在来看看RecyclerView在嵌套滑动的实现。   先来看看RecyclerView对ACTION_DOWN事件的处理: case MotionEvent.ACTION_DOWN: { mScrollPointerId = e.getPointerId(0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); 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;   在ACTION_DOWN里面,首先是对nestedScrollAxis变量进行处理话。在前面提及到过,nestedScrollAxis表示滑动的方向,如果nestedScrollAxis & ViewCompat. SCROLL_AXIS_VERTICAL != 0,表示在垂直方向有滑动。初始化nestedScrollAxis变量之后,就会调用startNestedScroll方法来告诉父View滑动事件已经开始,你是否需要有所行动。这里就可以体现嵌套滑动的事件是从下到上传递的。   我们再来看看RecyclerView是怎么将一个事件传递到父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是怎么帮我们传递的 if (hasNestedScrollingParent(type)) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { 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的父View不能够处理,那就会递归上去找,直到找到一个为止。   同时,我们发现,NestedScrollingChildHelper有依靠ViewParentCompat类来帮助我们传递事件,实际上ViewParentCompat里面也是帮我们调用父View的onStartNestedScroll方法,这里做的目的是为了兼容不同版本的系统。在前面已经说过,从Android 5.0开始,View实现了NestScrollingChild接口,而5.0以下,需要我们自己来实现了。这里不对ViewParentCompat怎么进行系统兼容的实现进行讨论,待会再来讨论。   在这里,对startNestedScroll方法的工作流程做一个简单的梳理。首先RecyclerView的ACTION_DOWN事件来到,RecyclerView的会调用startNestedScroll方法,在startNestedScroll方法里面,把具体的执行代理给NestedScrollingChildHelper的startNestedScroll方法,在NestedScrollingChildHelper的startNestedScroll方法里面,会不断的往上找能够处理该事件的父View,找到的话会调用父View的onStartNestedScroll方法。   在整个事件传递过程中,我们还需要注意的一点就是:isNestedScrollingEnabled()方法,只要保证isNestedScrollingEnabled方法返回为true才能保证事件能够顺利往上的传递。这个方法的返回值取决于我们是否设置了setNestedScrollingEnabled方法。   当一个ACTION_DOWN结束之后,通常来说,接下来就是ACTION_MOVE,会涉及到View的滑动的情况。让我们来看看滑动事件是怎么传递过来的,实现先贴出代码: case MotionEvent.ACTION_MOVE: { ······ if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ······ } break;   这里减省了很多无关的代码,只看dispatchNestedPreScroll方法。需要注意的是,此时RecyclerView还未滑动,因为RecyclerView真正滑动操作是在scrollByInternal方法里面进行的,所以dispatchNestedPreScroll只是用来表示此时滑动距离已经产生,询问父View是否要消耗距离。其中mScrollConsumed变量里面存储的就是父View消耗的距离。   我们来看看子View是怎么将产生的滑动距离传递到父View里面的,这个还是结合NestedScrollingChildHelper来看,因为子View的dispatchNestedPreScroll方法最终会调用到NestedScrollingChildHelper的dispatchNestedPreScroll方法里面来。 public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) { if (isNestedScrollingEnabled()) { final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; } 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; ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type); 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; }   整个事件传递能够顺利进行的前提还是isNestedScrollingEnabled返回为true。整个方法的执行比较简单,在这里面会调用会调用父View的onNestedPreScroll方法来询问父View是否消耗距离,其中父View消耗的距离保存在consumed数组,然后根据父View消耗的距离来计算,此时子View还有多少能够消耗,具体计算就是差值计算,比较简单。最后这个方法的返回值true表示父View消耗了距离,包括全部消耗和部分消耗两种情况。   整个dispatchNestedPreScroll方法过程还是比较简单的。我们再来看看当RecyclerView消耗了父View未消耗的那部分距离之后,会发生什么。   当RecyclerView滑动完毕之后,会调用dispatchNestedScroll方法来通知父View,自己已经滑动完毕了。具体来看看代码: boolean scrollByInternal(int x, int y, MotionEvent ev) { ······ if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, TYPE_TOUCH)) { // Update the last touch co-ords, taking any scroll offset into account mLastTouchX -= mScrollOffset[0]; mLastTouchY -= mScrollOffset[1]; if (ev != null) { ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); } mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ······ }   整个过程还是简单,事件传递通过NestedScrollingChildHelper来进行的,这里就不在进行分析了。   剩下的fling事件,stop事件,这些都与上面类似,这里就不在多说了。 (3). ViewParentCompat   在分析事件是如何传递到父View的时候,我们发现ViewParentCompat在这个过程中扮演着重要的角色,前面只是说了使用ViewParentCompat是为了系统的兼容。让我们来看看ViewParentCompat是如何来保证系统的兼容性的。这里就拿ViewParentCompat的startNestedScroll方法来进行分析,其他方法也是如此。 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 return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); } return false; }   我们看到的首先判断当前View是否实现了NestedScrollingParent2接口,如果实现的话了,直接回调到NestedScrollingParent2的onStartNestedScroll方法。之前我们说过NestedScrollingParent接口,而这个NestedScrollingParent2是什么东西?我们来看看NestedScrollingParent2的声明: public interface NestedScrollingParent2 extends NestedScrollingParent { ······ }   我们发现NestedScrollingParent2接口继承了NestedScrollingParent接口,相比于NestedScrollingParent接口,NestedScrollingParent2重载了NestedScrollingParent接口的几个方法,其他的就没有什么区别了。   我们还是来看看这部分的含义吧: else if (type == ViewCompat.TYPE_TOUCH) { // Else if the type is the default (touch), try the NestedScrollingParent API return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); } ······ static final ViewParentCompatBaseImpl IMPL; static { if (Build.VERSION.SDK_INT >= 21) { IMPL = new ViewParentCompatApi21Impl(); } else if (Build.VERSION.SDK_INT >= 19) { IMPL = new ViewParentCompatApi19Impl(); } else { IMPL = new ViewParentCompatBaseImpl(); } }   其中ViewParentCompatApi21Impl和ViewParentCompatApi19Impl都继承于ViewParentCompatBaseImpl,所以我们来看看ViewParentCompatBaseImpl的onStartNestedScroll方法。 public boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes) { if (parent instanceof NestedScrollingParent) { return ((NestedScrollingParent) parent).onStartNestedScroll(child, target, nestedScrollAxes); } return false; }   是不是瞬间来了一句卧了个槽?这么简单?就判断了一下是否实现了NestedScrollingParent接口。从这里得知,如果想要一个父View能够接受到子View传递过来的事件,实现NestedScrollingParent接口是必要的!   最后,我们发现其实ViewParentCompat根本不是很神秘,其实就是在里面创建不同的对象来支持不同版本的系统。 3. 父View事件的接收和消耗   讲解了子View产生和传递事件之后,可能对这个嵌套滑动还是一脸懵逼。不要着急,当我们将整个机制梳理通,就柳暗花明了。   在系统中,没有特定ViewGroup用来接收和消耗子View传递的事件。因此,只能自己动手了。 public class NestedScrollLinearLayout extends LinearLayout implements NestedScrollingParent { private static final int OFFSET = 200; private NestedScrollingParentHelper mNestedScrollingParentHelper; public NestedScrollLinearLayout(Context context) { super(context); } public NestedScrollLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public NestedScrollLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { //向下 if (dy < 0) { if (getTranslationY() >= 0) { consumed[0] = 0; consumed[1] = (int) Math.max(getTranslationY() - OFFSET, dy); setTranslationY(getTranslationY() - consumed[1]); } } else { if (getTranslationY() <= OFFSET) { consumed[0] = 0; consumed[1] = (int) Math.min(dy, getTranslationY()); setTranslationY(getTranslationY() - consumed[1]); } } } @Override public void onNestedScrollAccepted(View child, View target, int axes) { getNestedScrollingParentHelper().onNestedScrollAccepted(child, target, axes); } @Override public void onStopNestedScroll(View child) { getNestedScrollingParentHelper().onStopNestedScroll(child); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } @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; } private NestedScrollingParentHelper getNestedScrollingParentHelper() { if (mNestedScrollingParentHelper == null) { mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); } return mNestedScrollingParentHelper; } }   如上的代码就是实现了上面Demo图片中的效果。在整个实现过程中,我们发现,我们只对onStartNestedScroll方法和onNestedPreScroll方法做了我们自己的实现,其他的要么空着,要么就是通过NestedScrollingParentHelper来帮助我们来实现。整个过程比较清晰和明了。   不过,这其中,我们需要注意的是,每个方法的含义和调用的时机。onStartNestedScroll方法对应子View的startNestedScroll方法,当子View调用startNestedScroll方法会回调父View的onStartNestedScroll方法。其他方法也是类似的,不过需要注意的是,通常子View的方法都是以dispatch开头的,父View的方法都是以on开头的。   对于NestedScrollingParnet这一块,感觉没有需要注意的,因为这部分需要咱们自己实现,而实现这部分的功能,需要了解子View的是怎么将事件传递到父View。 5. 总结   最后来对Android里面的嵌套滑动做一个简单的总结。   1.跟传统的事件分发不同,嵌套滑动是由子View传递给父View,是从下到上的,传统事件的分发是从上到下的。   2.如果一个View想要传递嵌套滑动的事件,有两个前提:实现NestedScrollingChild接口;setNestedScrollingEnabled方法设置为true。如果一个ViewGroup想要接收和消耗子View传递过来的事件,必须实现NestedScrollingParent接口。
作文投稿

观菊展一文由杰瑞文章网免费提供,本站为公益性作文网站,此作文为网上收集或网友提供,版权归原作者所有,如果侵犯了您的权益,请及时与我们联系,我们会立即删除!

杰瑞文章网友情提示:请不要直接抄作文用来交作业。你可以学习、借鉴、期待你写出更好的作文。

说说你对这篇作文的看法吧