您当前的位置:首页 > Bug笔记

嵌套滑动NestedScrolling机制完全解析

时间:2022-09-06 19:17:53 阅读数:11,603人阅读
版权声明:转载请注明出处,谢谢!
—— 你以为的极限弄不好是别人的起点。

概述

Android在support.v4包中为大家提供了两个非常神奇的类:

  • NestedScrollingParent
  • NestedScrollingChild

如果你从未听说过这两个类,没关系,听我慢慢介绍,你就明白这两个类可以用来干嘛了。相信大家都见识过或者使用过CoordinatorLayout,通过这个类可以非常便利的帮助我们完成一些炫丽的效果,例如下面这样的:

这样的效果就非常适合使用NestedScrolling机制去完成,并且CoordinatorLayout背后其实也是利用着这套机制,So,我相信你已经明白这套机制可以用来干嘛了。

但是,我相信你还有个问题

  • 这个机制相比传统的自定义ViewGroup事件分发处理有什么优越的地方吗?

我们简单分析下:

假设我们按照传统的事件分发去理解,首先我们滑动的是下面的内容区域,而移动却是外部的ViewGroup在移动,所以按照传统的方式,肯定是外部的Parent拦截了内部的Child的事件;但是,上述效果图,当Parent滑动到一定程度时,Child又开始滑动了,中间整个过程是没有间断的。从正常的事件分发(不手动调用分发事件,不手动去发出事件)角度去做是不可能的,因为当Parent拦截之后,是没有办法再把事件交给Child的,事件分发,对于拦截,相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给Parent(拦截者)来处理。

但是NestedScrolling机制来处理这个事情就很好办,所以对这个机制进行深入学习,一来有助于我们编写嵌套滑动时一些特殊的效果;二来是我为了对CoordinatorLayout做分析的铺垫~~~

预期效果

这里我们用NestedScrolling来实现。上述效果图,分为3部分:顶部布局;中间的ViewPager指示器;以及底部的RecyclerView;RecyclerView其实就是NestedSrollingChild的实现类,所以本例主要的角色是去实现NestedScrollingParent。

1)布局文件

		<com.nestedscrolling.view.StickyNavLayout xmlns:tools="http://schemas.android.com/tools"
			xmlns:android="http://schemas.android.com/apk/res/android"
			android:layout_width="match_parent"
			android:layout_height="match_parent"
			android:orientation="vertical" >
			<RelativeLayout
				android:id="@id/id_stickynavlayout_topview"
				android:layout_width="match_parent"
				android:layout_height="wrap_content"
				android:background="#4400ff00" >
				<TextView
					android:layout_width="match_parent"
					android:layout_height="256dp"
					android:gravity="center"
					android:text="软件介绍"
					android:textSize="30sp"
					android:textStyle="bold" />
			</RelativeLayout>

			<com.zhy.view.SimpleViewPagerIndicator
				android:id="@id/id_stickynavlayout_indicator"
				android:layout_width="match_parent"
				android:layout_height="50dp"
				android:background="#ffffffff" >
			</com.zhy.view.SimpleViewPagerIndicator>

			<android.support.v4.view.ViewPager
				android:id="@id/id_stickynavlayout_viewpager"
				android:layout_width="match_parent"
				android:layout_height="match_parent"
				 >
			</android.support.v4.view.ViewPager>

		</com.nestedscrolling.view.StickyNavLayout>
	  

StickyNavLayout是直接继承自LinearLayout的,并且设置的是orientation="vertical",所以直观的就是控件按顺序纵向排列。

2) 实现NestedScrollingParent

NestedScrollingParent是一个接口,实现它需要实现如下方法:

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

		public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

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

		public int getNestedScrollAxes();
	  

在写具体的实现前,先对需要用到的上述方法做一下简单的介绍:

  • onStartNestedScroll该方法,一定要按照自己的需求返回true,该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断。
  • onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
  • onNestedFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件。

下面看一下我们具体的实现:

		public class StickyNavLayout extends LinearLayout implements NestedScrollingParent
		{
			@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)
			{
				boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
				boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);

				if (hiddenTop || showTop)
				{
					scrollBy(0, dy);
					consumed[1] = dy;
				}
			}
			@Override
			public boolean onNestedPreFling(View target, float velocityX, float velocityY)
			{
				
				fling((int) velocityY);
				return true;
			}
		}
	  
  • onStartNestedScroll中,我们判断了如果是纵向返回true,这个一般是需要内部的View去传入的,你要是不确定,或者担心内部View编写的不规范,你可以直接return true;
  • onNestedPreScroll中,我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部View已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是我们的StickNavLayout滑动。
  • onStartNestedScroll中,我们判断了如果是纵向返回true,这个一般是需要内部的View去传入的,你要是不确定,或者担心内部View编写的不规范,你可以直接return true;
  • 此外,这里还处理了fling,通过onNestedPreFling方法,这个可以根据自己需求定了,当顶部控件显示时,fling可以让顶部控件隐藏或者显示。

以上代码就能实现下面的效果:

对于fling方法,我们利用了OverScroll的fling的方法,对于边界检测,是重写了scrollTo方法:

		public void fling(int velocityY)
		{
			mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
			invalidate();
		}

		@Override
		public void scrollTo(int x, int y)
		{
			if (y < 0)
			{
				y = 0;
			}
			if (y > mTopViewHeight)
			{
				y = mTopViewHeight;
			}
			if (y != getScrollY())
			{
				super.scrollTo(x, y);
			}
		}
	  

到这里呢,可以看到NestedScrolling机制说白了非常简单:

就是NestedScrollingParent内部的View,在滑动到时候,会首先将dx、dy传入给NestedScrollingParent,NestedScrollingParent可以决定是否对其进行消耗,一般会根据需求消耗部分或者全部(不过这里并没有实际的约束,你可以随便写消耗多少,可能会对内部View造成一定的影响)。

用白话和原本的事件分发机制作对比就是这样的(针对正常流程下一次手势):

  • 事件分发是这样的:子View首先得到事件处理权,处理过程中,父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。
  • NestedScrolling机制是这样的:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。

原理

原理其实就是看内部View什么时候回调NestedScrollingParent各种方法的,直接定位到内部View的onTouchEvent:

		@Override
		public boolean onTouchEvent(MotionEvent e) {
			switch (action) {
				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);
				} break;
				case MotionEvent.ACTION_MOVE: {
					if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
						dx -= mScrollConsumed[0];
						dy -= mScrollConsumed[1];
						vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
					}
				} break;
				case MotionEvent.ACTION_UP: {
					fling((int) xvel, (int) yvel);
					resetTouch();
				} break;

				case MotionEvent.ACTION_CANCEL: {
					cancelTouch();
				} break;
			}
			return true;
		}
	  

可以看到:ACTION_DOWN调用了startNestedScroll;ACTION_MOVE中调用了dispatchNestedPreScroll;ACTION_UP可能会触发fling以调用resetTouch。而startNestedScroll内部实际上:

#NestedScrollingChildHelper
		public boolean startNestedScroll(int axes) {
			if (hasNestedScrollingParent()) {
				// Already in progress
				return true;
			}
			if (isNestedScrollingEnabled()) {
				ViewParent p = mView.getParent();
				View child = mView;
				while (p != null) {
					if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
						mNestedScrollingParent = p;
						ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
						return true;
					}
					if (p instanceof View) {
						child = (View) p;
					}
					p = p.getParent();
				}
			}
			return false;
		}
	  

去寻找NestedScrollingParent,然后回调onStartNestedScroll和onNestedScrollAccepted。

dispatchNestedPreScroll中会回调onNestedPreScroll方法,内部的scrollByInternal中还会回调onNestedScroll方法。

fling中会回调onNestedPreFling和onNestedFling方法。

resetTouch中则会回调onStopNestedScroll。

代码其实没什么贴的,大家直接找到onTouchEvent一眼就能看到,调用的方法名都是dispatchNestedXXX方法,实际内部都是通过NestedScrollingChildHelper实现的。

所以如果你需要实现和NestedScrollingParent协作的内部View,记得实现NestedScrollingChild,然后内部借助NestedScrollingChildHelper这个辅助类,核心的方法都封装好了,你只需要在恰当的实际去传入参数调用方法即可。

ok,这样的一个机制一定要去试试,很多滑动相关的效果都可以借此实现;此外,后面可能会对CoordinatorLayout写一些博文~

源码地址

------转载请注明出处,感谢您对原创作者的支持------

有偿提供技术支持、Bug修复、项目外包、毕业设计、大小作业

Android学习小站

Q Q:1095817610

微信:jx-helu

邮箱:1095817610@qq.com

添加请备注"Android学习小站"