目录
0.引言
1.利用分发机制实现嵌套滑动的不足
2.NestedScrolling设计的独特之处
3.NestedScrolling 原理分析
3.1相关接口和类
3.2NestedScrollingChild接口
3.3NestedScrollingParent接口4.流程梳理
5分发机制和Nested机制代码实现对比
5.1场景分析
5.2布局代码
5.3分发机制实现
5.4Nested机制实现
5.5小结
0.引言
在android系统版本5.0(API 21)之前,是没有官方控件支持嵌套滑动的。要实现类似ScrollView嵌套ListView或ScrollView嵌套ScrollView这些功能,往往要在自定义控件上作成百上千行的复杂处理,且耦合度高、性能底下、代码实现难度大。
Android 在发布 Lollipop(5.0)版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性。
1.利用分发机制实现嵌套滑动的不足
在Lollipop(5.0)版本之前,想要实现嵌套滑动,必须在分发机制中处理。我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
网上流传
这种分发机制有一个漏洞:
如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。
//事件分发伪代码逻辑,源自《Android开发艺术探索》章节:3.4.1
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}```
![Android事件分发流程图.png](http://upload-images.jianshu.io/upload_images/4969082-d964da0734867008.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#2 NestedScrolling设计的独特之处
为了解决不能够一次按下拖动到底的痛点,NestedScrolling机制能够让父view和子view在滚动时进行配合,其基本流程如下:
>当子view开始滚动之前,可以通知父view,让其先于自己进行滚动;
>子view自己进行滚动
>子view滚动之后,还可以通知父view继续滚动
![coordinator_sample.gif](http://upload-images.jianshu.io/upload_images/4969082-876d97104b6a392c.gif?imageMogr2/auto-orient/strip)
要实现这样的交互,父View需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。
在这套交互机制中,child是动作的发起者,parent只是接受回调并作出响应。
另外:父view和子view并不需要是直接的父子关系,即如果“parent1包含parent2,parent2包含child”,则parent1和child仍能通过nestedScrolling机制进行交互。
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;
}
static class ViewParentCompatStubImpl implements ViewParentCompatImpl {
@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes) {
if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
return false;
}
public class NestedScrollView implements NestedScrollingParent{
...
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
...
}
#3.NestedScrolling 原理分析
##3.1相关接口和类
![图片2.png](http://upload-images.jianshu.io/upload_images/4969082-333a0414def6cd98.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>主要接口:NestedScrollingChild、NestedScrollingParent
>帮助类:NestedScrollingChildHelper、NestedScrollingParentHelper
使用NestedScrolling机制,父View需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。
而NestedScrollingChildHelper和NestedScrollingParentHelper是两个帮助类,当我们在实现NestedScrollingChild和NestedScrollingParent接口时,使用这两个帮助类可以简化我们的工作。
以上接口和类都在support-v4包中提供。另外,一些较新的系统view都已经实现了NestedScrollingChild或NestedScrollingParent接口,也就是说他们直接支持NestedScrolling,例如:
>NestedScrollView 已实现 NestedScrollingParent和NestedScrollingChild
>RecyclerView 已实现 NestedScrollingChild
>CoordinatorLayout 已实现 NestedScrollingParent
>...
##3.2NestedScrollingChild接口
###3.2.1接口概述
public interface NestedScrollingChild {
//开始、停止嵌套滚动
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
//触摸滚动相关
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//惯性滚动相关
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
}
####3.2.1.1
`public boolean startNestedScroll(int axes);`
- 开启嵌套滚动流程(实际上是进行了一些嵌套滚动前准备工作)。
- 当找到了能够配合当前子view进行嵌套滚动的父view时,返回值为true(Returns:true if a cooperative parent was found and nested scrolling has been enabled for the current gesture)。
####3.2.1.2
`public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);`
- 在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
- 此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
- 第一第二个参数为输入参数,即常规的函数参数,调用函数的时候我们需要为其传递确切的值。而第三第四个参数为输出参数,调用函数时我们只需要传递容器(在这里就是两个数组),在调用结束后,我们就可以从容器中获取函数输出的值。
- 如果parent消费了一部分或全部距离,则此方法返回true。
####3.2.1.3
`public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);`
- 在子view自己进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
- 前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
- 返回值:(翻译出来可能有歧义,直接放原文)true if the event was dispatched, false if it could not be dispatched.
####3.2.1.4
最后,stopNestedScroll()方法与startNestedScroll(int axes)对应,用于结束嵌套滚动流程;而惯性滚动相关的两个方法与触摸滚动相关的两个方法类似,这里不再赘述。
###3.2.2接口实现
上面只是讲了接口中的主要方法和调用时机,那么这些方法具体该如何实现呢?这时候就要用到上面提到的帮助类了。具体操作很简单:首先实例化一个帮助类对象,然后在要实现的接口方法中调用帮助类对象中的同名方法即可——帮助类对象已经帮我们完成了一切。
public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
NestedScrollingChild, ScrollingView
{
//........................
@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);
}
//........................
}
##3.3NestedScrollingParent接口
###3.3.1 接口概述
public interface NestedScrollingParent {
//当开启、停止嵌套滚动时被调用
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
//当触摸嵌套滚动时被调用
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//当惯性嵌套滚动时被调用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
}
从命名可以看出,这几个都是回调方法。当调用NestedScrollingChild中的方法时,NestedScrollingParent中与之相对应的方法就会被回调。方法之间的具体对应关系如下:
| 子(发起者) | 父(被同步调用) |
| ------------- |:-------------:|
| startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
| dispatchNestedPreScroll | onNestedPreScroll|
| dispatchNestedScroll | onNestedScroll |
| dispatchNestedPreFling| onNestedPreFling|
| dispatchNestedFling| onNestedFling|
| stopNestedScroll| onStopNestedScroll |
####3.3.1.1
`public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);`
| 参数 | 说明 |
| ------------- |:-------------|
| target:| 发起嵌套滚动的子View,此子view必须实现NestedScrollingChild接口。上面提到过,此子view并不需要是当前view的直接子view |
| child:| 当前view的包含target的直接子view|
| nestedScrollAxes:| 嵌套滚动的方向,可能是SCROLL_AXIS_HORIZONTAL 或 SCROLL_AXIS_VERTICAL 或 二者都有 |
####3.3.1.2
`onNestedPreScroll()、onNestedPreScroll()、onNestedPreFling()、onNestedFling()`
这几个方法分别对应NestedScrollingChild中的dispatchNestedPreScroll()、dispatchNestedScroll()、dispatchNestedPreFling()和dispatchNestedFling()。
它们的参数也是基本对应的,以onNestedPreScroll()为例,参数dx、dy、consumed实际就是dispatchNestedPreScroll()中的dx、int dy、consumed。
###3.3.2
onNestedScrollAccepted、onStopNestedScroll的实现同样是调用帮助类中的同名方法即可:
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
...
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
...
}
#4 流程梳理
经过以上的介绍,我们可以大致将嵌套滚动的流程概括如下(以触摸滚动为例,惯性滚动(fling)的流程与此类似):
>1.调用child的startNestedScroll()来发起嵌套滚动流程(实质是寻找能够配合child进行嵌套滚动的parent)。parent的onStartNestedScroll()会被回调,如果此方法返回true,则onNestedScrollAccepted()也会被回调。
>2.child每次滚动前,可以先询问parent是否要滚动,即调用dispatchNestedPreScroll(),这会回调到parent的onNestedPreScroll(),parent可以在这个回调中先于child滚动。
>3.dispatchNestedPreScroll()之后,child可以进行自己的滚动操作。
child滚动以后,可以调用dispatchNestedScroll(),会回调到parent的onNestedScroll(),在这里parent可以进行后于child的滚动。
>4.滚动结束,调用stopNestedScroll()。
调用时序图:
![调用时序图.png](http://upload-images.jianshu.io/upload_images/4969082-b0160bcea8f480b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#5分发机制和Nested机制代码实现对比
先看效果:
![嵌套实现效果.gif](http://upload-images.jianshu.io/upload_images/4969082-e2d80a727661d43a.gif?imageMogr2/auto-orient/strip)
##5.1场景分析
当头部可见时:父控件滑动
当头部不可见时:子控件滑动
头部从可见到不可见时:控制权从父控件→子控件
头部从可见到不可见时:控制权从子控件→父控件
##5.2布局代码:
![布局层次图.png](http://upload-images.jianshu.io/upload_images/4969082-1a0cabd951dd09d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
StickyNavLayout是公用父类,负责处理子View的初始化、页面测量和滚动实现:
public class StickyNavLayout extends LinearLayout
{
protected static final String TAG = "StickyNavLayout";
protected View mTop;
protected View mNav;
protected ViewPager mViewPager;
protected int mTopViewHeight;
protected OverScroller mScroller;
public StickyNavLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
mScroller = new OverScroller(context);
}
@Override
protected void onFinishInflate()
{
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
mNav = findViewById(R.id.id_stickynavlayout_indicator);
View view = findViewById(R.id.id_stickynavlayout_viewpager);
if (!(view instanceof ViewPager))
{
throw new RuntimeException(
"id_stickynavlayout_viewpager show used by ViewPager !");
}
mViewPager = (ViewPager) view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
//不限制顶部的高度
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
getChildAt(0).measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
setMeasuredDimension(getMeasuredWidth(), mTop.getMeasuredHeight() + mNav.getMeasuredHeight() + mViewPager.getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTop.getMeasuredHeight();
}
@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);
}
}
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
public void fling(int velocityY) {
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
}
##5.3分发机制实现
public class DispatchStickyNavLayout extends StickyNavLayout {
protected static final String TAG = "DispatchStickyNavLayout";
private ViewGroup mInnerScrollView;
private boolean isTopHidden = false;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private float mLastY;
private boolean mDragging;
private boolean isInControl = false;
public DispatchStickyNavLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context)
.getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context)
.getScaledMinimumFlingVelocity();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"dispatchTouchEvent:ACTION_DOWN");
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"dispatchTouchEvent:ACTION_MOVE");
float dy = y - mLastY;
getCurrentScrollView();
if (mInnerScrollView instanceof ScrollView||mInnerScrollView instanceof NestedScrollView) {
//嵌套子类滑动到顶&头部视图隐藏&向上滑动
if (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0
&& !isInControl) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
} else if (mInnerScrollView instanceof ListView) {
ListView lv = (ListView) mInnerScrollView;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
if (!isInControl && c != null && c.getTop() == 0 && isTopHidden
&& dy > 0) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
} else if (mInnerScrollView instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) mInnerScrollView;
if (!isInControl && android.support.v4.view.ViewCompat.canScrollVertically(rv, -1) && isTopHidden
&& dy > 0) {
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
*
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onInterceptTouchEvent:ACTION_UP");
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"onInterceptTouchEvent:ACTION_MOVE");
float dy = y - mLastY;
getCurrentScrollView();
if (Math.abs(dy) > mTouchSlop) {
mDragging = true;
if (mInnerScrollView instanceof ScrollView||mInnerScrollView instanceof NestedScrollView) {
// 如果topView没有隐藏
// 或嵌套子视图滑动到顶 && topView隐藏 && 上滑,则拦截
if (!isTopHidden
|| (mInnerScrollView.getScrollY() == 0
&& isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
} else if (mInnerScrollView instanceof ListView) {
ListView lv = (ListView) mInnerScrollView;
View c = lv.getChildAt(lv.getFirstVisiblePosition());
// 如果topView没有隐藏
// 或sc的listView在顶部 && topView隐藏 && 上滑,则拦截
if (!isTopHidden || //
(c != null //
&& c.getTop() == 0//
&& isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
} else if (mInnerScrollView instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) mInnerScrollView;
if (!isTopHidden || (!android.support.v4.view.ViewCompat.canScrollVertically(rv, -1) && isTopHidden && dy > 0)) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mLastY = y;
return true;
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG,"onInterceptTouchEvent:ACTION_CANCEL");
case MotionEvent.ACTION_UP:
Log.e(TAG,"onInterceptTouchEvent:ACTION_UP");
mDragging = false;
recycleVelocityTracker();
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(event);
int action = event.getAction();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouchEvent:ACTION_DOWN");
if (!mScroller.isFinished())
mScroller.abortAnimation();
mLastY = y;
return true;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"onTouchEvent:ACTION_MOVE");
float dy = y - mLastY;
Log.e("TAG", "dy = " + dy + " , y = " + y + " , mLastY = " + mLastY);
if (!mDragging && Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (mDragging) {
scrollBy(0, (int) -dy);
// 如果topView隐藏,且向下滑动时,则改变当前事件为ACTION_DOWN
if (getScrollY() == mTopViewHeight && dy < 0) {
event.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(event);
isInControl = false;
}
}
mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG,"onTouchEvent:ACTION_CANCEL");
mDragging = false;
recycleVelocityTracker();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"onTouchEvent:ACTION_UP");
mDragging = false;
//计算一秒内的移动的像素,且不会超过
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) mVelocityTracker.getYVelocity();
if (Math.abs(velocityY) > mMinimumVelocity) {
fling(-velocityY);
}
recycleVelocityTracker();
break;
}
return super.onTouchEvent(event);
}
private void getCurrentScrollView() {
int currentItem = mViewPager.getCurrentItem();
PagerAdapter a = mViewPager.getAdapter();
if (a instanceof FragmentPagerAdapter) {
FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
Fragment item = (Fragment) fadapter.instantiateItem(mViewPager,
currentItem);
mInnerScrollView = (ViewGroup) (item.getView()
.findViewById(R.id.id_stickynavlayout_innerscrollview));
} else if (a instanceof FragmentStatePagerAdapter) {
FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
Fragment item = (Fragment) fsAdapter.instantiateItem(mViewPager,
currentItem);
mInnerScrollView = (ViewGroup) (item.getView()
.findViewById(R.id.id_stickynavlayout_innerscrollview));
}
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
isTopHidden = getScrollY() == mTopViewHeight;
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
}
###5.3.1 onTouchEvent
被拦截后的事件会在这个函数处理
1.在ACTION_DOWN的时候记录下y的值
2.在ACTION_MOVE时,计算出与上次的偏差,调用scrollBy滑动,如果滑动到topView隐藏了,那么分发一个ACTION_DOWN事件,然后事件最终会分发到内部ScrollView处理(关键1:此处是把事件分派给子)
3.ACTION_CANCEL时,回收VelocityTracker(速度追踪器),停止动画
4.ACTION_UP时,计算速度,判断是否有惯性滑动,然后调用fling方法
public void fling(int velocityY) {
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
###5.3.2 onInterceptTouchEvent
onInterceptTouchEvent默认是不拦截的,父类方法返回false
1.在ACTION_DOWN的时候记录下y的值
2.ACTION_MOVE时,计算出与上次的偏差,情况1:如果topView没隐藏,则拦截事件,return true;情况2:如果topView隐藏且向上滑动且子控件滑动滑动到顶,拦截事件
###5.3.3 dispatchTouchEvent
如果事件能够传递给当前View,那么此方法一定会被调用
1.在ACTION_DOWN的时候记录下y的值
2.ACTION_MOVE时,向上滑动到顶部即将可见时,先发起一个ACTION_CANCEL表示结束当前分发响应流程。再分发一个ACTION_DOWN事件,重走流程。此事件最终会到onTouch事件中(关键2:此处是把事件重新交回父类)
###5.3.4 分发机制小结
整个流程能够一个手势按下后无缝连接运转,关键在于以下几点
1.在dispatchTouchEvent的ACTION_MOVE函数时,在符合条件下再次分发一个ACTION_CANCEL和ACTION_DOWN事件
2.在onInterceptTouchEvent的ACTION_MOVE函数时,如果topView没隐藏,则主动拦截下事件在父类中处理,否则交给子类
3.onTouchEvent的ACTION_MOVE函数时,滑动到topView隐藏后,分发一个ACTION_DOWN事件,把事件交给子类处理
##5.4Nested机制实现
public class NestedStickyNavLayout extends StickyNavLayout implements NestedScrollingParent {
private NestedScrollingParentHelper mParentHelper;
private static final String TAG = "NestedStickyNavLayout";
public NestedStickyNavLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onStartNestedScroll");
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
getScrollingParentHelper().onNestedScrollAccepted(child, target, nestedScrollAxes);
Log.e(TAG, "onNestedScrollAccepted");
}
@Override
public void onStopNestedScroll(View target) {
getScrollingParentHelper().onStopNestedScroll(target);
Log.e(TAG, "onStopNestedScroll");
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.e(TAG, "onNestedScroll");
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
Log.e(TAG, "onNestedPreScroll");
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 onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.e(TAG, "onNestedFling");
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
Log.e(TAG, "onNestedPreFling");
//down - //up+
if (getScrollY() >= mTopViewHeight) return false;
fling((int) velocityY);
return true;
}
@Override
public int getNestedScrollAxes() {
Log.e(TAG, "getNestedScrollAxes");
return getScrollingParentHelper().getNestedScrollAxes();
}
private NestedScrollingParentHelper getScrollingParentHelper() {
if (mParentHelper == null) {
mParentHelper = new NestedScrollingParentHelper(this);
}
return mParentHelper;
}
}
用Nested机制实现,只需实现NestedScrollingParent 接口的八个方法即可。虽然看起来方法多,但实现起来代码量却少很多。
在`onNestedScrollAccepted(),onStopNestedScroll(),getNestedScrollAxes()`函数内,只需调用NestedScrollingParentHelper 的同名同参函数即可。
`onStartNestedScroll()`函数的一般写法就是
`return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;`
,表示其接受垂直方向的滚动。
剩下的就是四个方法
`onNestedPreScroll(),onNestedScroll(),onNestedPreFling(),onNestedFling()`
onNestedPreScroll:头部视图尚未完全消失,视图向上滑动隐藏头部或向下滑动引出头部时,父类完全消耗滑动距离,并把消耗量记录在consumed中以供回调
onNestedScroll:因为onNestedPreScroll已完全消耗,所以这里不再作处理
onNestedPreFling:如果scrollY大于头部高度,返回false,表示不消耗。否则,则相应惯性滑动,调用fling()方法
onNestedFling:不二次响应惯性滑动
##5.5小结
分发机制要实现内容:
>1.分发函数dispatchTouchEvent
2.拦截函数onInterceptTouchEvent
3.触摸事件函数onTouchEvent
4.速度追踪器VelocityTracker:
5.找到并直接持有嵌套子View(强耦合)
Nested机制要实现内容:
>实现NestedScrollingParent 接口(主要onNestedPreScroll(),onNestedScroll(),onNestedPreFling(),onNestedFling()方法)
[comment]:***
#6总结
兼容性比较:
因为分发机制可以持有嵌套子View,所以可以兼容ListView、ScrollView、NestedScrollView、RecyclerView等。
Nested机制只能支持NestedScrollView、RecyclerView这类实现了NestedScrollingChild接口的滑动控件
性能:
减少了层级依赖,提高视图绘制效率,减轻了CPU计算能耗
开发效率:
比传统分发机制实现简单,条理会更加清晰。只需在相应函数中写入业务要求的处理代码即可
在此机制上,google还发布了一个强大的控件CoordinatorLayout,一个FrameLayout。该布局的强大在于,能够协调子元素之间的依赖关系。