android MD进阶[四] NestedScrollView 从源码到实战..
Posted android超级兵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android MD进阶[四] NestedScrollView 从源码到实战..相关的知识,希望对你有一定的参考价值。
android MD进阶[四] NestedScrollView 从源码到实战..
前言:相信大家在开发过程中经常会遇到嵌套滚动的场景,最常见的莫过于 nestedScrollView,前段时间一直在搞别的,把 md 系列都断更了,从现在开始慢慢的都补起来!
NestedScrollView比较特殊 ,要想看懂他的源码,必须得了解2个东西,NestedScrollingChild
和NestedScrollingParent
,首先就从这两个接口
的参数聊起~
NestedScrollingChild
public interface NestedScrollingChild
/**
开启/关闭滚动视图
*/
void setNestedScrollingEnabled(boolean enabled);
/**
是否开启滚动时图
*/
boolean isNestedScrollingEnabled();
/**
开启滚动时候时候调用,用来通知parentView开始滚动,常在TouchEvent.ACTION_DOWN事件中调用
tips:代理给 NestedScrollingChildHelper.startNestedScroll()方法即可
@param axes: 滚动方向
SCROLL_AXIS_HORIZONTAL 水平
SCROLL_AXIS_VERTICAL 垂直
SCROLL_AXIS_NONE 没有方向
*/
boolean startNestedScroll(@ScrollAxis int axes);
/**
停止滚动时候调用,用来通知parentView停止滚动,常在TouchEvent.ACTION_UP / ACTION_CANCLE 中调用
tips: 代理给 NestedScrollingChildHelper.stopNestedScroll()即可
*/
void stopNestedScroll();
/**
判断当前view是否有嵌套滑动的parentView正在接受事件
tips:代理给 NestedScrollingChildHelper.hasNestedScrollingParent()即可
return true:有嵌套滑动的parentView
*/
boolean hasNestedScrollingParent();
/**
当前view消费滚动距离后调用该方法,吧剩下的滚动距离传递给parentView,
如果当前没有发生嵌套滚动,或者不支持嵌套滚动,那么该方法就没啥用.. 常在TouchEvent.ACTION_MOVE中调用
tips:代理给NestedScrollingChildHelper.dispatchNestedScroll()即可
@param dxConsumed: 已经消费的水平(x)方向距离
@param dyConsumed: 已经消费的垂直方(y)向距离
@param dxUnconsumed: 未消费过的水平(x)方向距离
@param dyUnconsumed: 未消费过的垂直(y)方向距离
@param offsetInWindow: 滑动之前和滑动之后的偏移量
if(offsetInWindow != null)
x = offsetInWindow[0]
y = offsetInWindow[1]
return true: 有嵌套滚动(parentView extents NestedScrollingParent)
*/
boolean dispatchNestedScroll(int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
@Nullable int[] offsetInWindow);
/**
将事件分发给 parentView,如果 parentView 消费则返回true
常在TouchEvent.ACTION_MOVE中调用
tips:代理给 NestedScrollingChildhelper.dispatchNestedPreScroll()即可
@param dx:水平(x)滚动的距离(以像素为单位)
@param dy:垂直(y)滚动的距离(以像素为单位)
@param consumed: 主要用来父容器消费封装,并且通知子容器 x = consumed[0]; y = consumed[1];
@param offsetInWindow:滑动之前和滑动之后的偏移量
return true: 表示父容器消费了事件
*/
boolean dispatchNestedPreScroll(int dx,
int dy,
@Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
用来处理惯性滑动
tips:代理给 NestedScrollingChildhelper.dispatchNestedFling()即可
@param velocityX: 用来处理x轴惯性滑动
@param velocityY: 用来处理y轴惯性滑动
@param consumed: 当前view是否消费了事件
return true: 有嵌套滚动(parentView extents NestedScrollingParent)
*/
boolean dispatchNestedFling(float velocityX,
float velocityY,
boolean consumed);
/**
分发fling事件给parentView
tips:代理给 NestedScrollingChildhelper.dispatchNestedPreFling()即可
@param velocityX: 用来处理x轴惯性滑动
@param velocityY: 用来处理y轴惯性滑动
return true: 父容器消费了事件
*/
boolean dispatchNestedPreFling(float velocityX,
float velocityY);
NestedScrollingChild 和 NestedScrollingChild2的区别:
可以看出,NestedScrollingChild2只是比NestedScrollingChild多了一个参数NestedScrollType
:
@IntDef(TYPE_TOUCH, TYPE_NON_TOUCH)
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @interface NestedScrollType
- NestedScrollType.TYPE_TOUCH 表示正常的滑动
- NestedScrollType.TYPE_NON_TOUCH 表示在滑动过程中迅速点击屏幕,终止滑动
NestedScrollingChild3 和 NestedScrollingChild2 的区别:
可以看出,也是多了一个参数,其实很简单,就是google工程师在编写NestedScrollView的时候,没有考虑清楚,所以就这样加上了… 可以理解
NestedScrollingParent
public interface NestedScrollingParent
/**
当NestedScrollingChildHelper.startNestedScroll()时候执行,用来接受ChildView#onTouchEvent#DOWN事件
@param child: 如果只有嵌套一层 那么 child = target
<ParentNestedScrollView>
<A_ViewGroup>
<B_ViewGroup>
<ChildNestedScrollView/>
</B_ViewGroup>
</A_ViewGroup>
</ParentNestedScrollView>
如果格式为这样,child = A_ViewGroup
@param target: 本次嵌套滚动的view (ChildNestedScrollView)
@param axes: 滚动方向
SCROLL_AXIS_HORIZONTAL 水平
SCROLL_AXIS_VERTICAL 垂直
return true: 表示接收嵌套事件
*/
boolean onStartNestedScroll(@NonNull View child,
@NonNull View target,
@ScrollAxis int axes);
/**
当 onStartNestedScroll() 返回true时候执行,常用来做一些初始化工作
tips: 代理给NestedScrollingParent.onNestedScrollAccepted()方法即可
参数和onStartNestedScroll()相同
*/
void onNestedScrollAccepted(@NonNull View child,
@NonNull View target,
@ScrollAxis int axes);
/**
当NestedScrollingChildHelper.stopNestedScroll()时候执行
tips:代理给NestedScrollingParent.onStopNestedScroll()即可
@param target:childNestedScrollView
*/
void onStopNestedScroll(@NonNull View target);
/**
当NestedScrollingChildHelper.dispatchNestedScroll()时候调用
@param target:childNestedScrollView
@param dxConsumed: 已经消费的x距离
@param dyConsumed: 已经消费的y距离
@param dxUnconsumed: 未消费的x距离
@param dyUnconsumed: 未消费的y距离
*/
void onNestedScroll(@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed);
/**
当NestedScrollingChildHelper.dispatchNestedPreScroll()时候调用
@param target:childNestedScrollView
@param dx: x位置
@param dy: y位置
@param consumed: 表示parentView需要消费的距离 x = consumed[0]; y = consumed[1];
tips: 只有consumed 改变值才说明parentView消费了事件
那么 NestedScrollingChild.dispatchNestedPreScroll() 才会返回true
*/
void onNestedPreScroll(@NonNull View target,
int dx,
int dy,
@NonNull int[] consumed);
/**
fling事件
@param target:childNestedScrollView
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
@param consumed: 是否消费
return true:有嵌套滚动事件
*/
boolean onNestedFling(@NonNull View target,
float velocityX,
float velocityY,
boolean consumed);
/**
fling事件parentView消费
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
*/
boolean onNestedPreFling(@NonNull View target,
float velocityX,
float velocityY);
/**
获取滚动的方向
ViewCompat#SCROLL_AXIS_HORIZONTAL
ViewCompat#SCROLL_AXIS_VERTICAL
ViewCompat#SCROLL_AXIS_NONE
*/
int getNestedScrollAxes();
tips: NestedScrollingParent2 和 NestedScrollingParent3 改动和 NestedScrollingChlid2/NestedScrollingChlid3 一样,就不重复解释啦.
走到这里,前胃菜就结束啦,接下来先来分析一波 NestedScrollView 源码!
NestedScrollView源码分析
我通过分析 NestedScrollView 能够知道那些内容:
1.为什么NestedScrollView只能添加 1个 ChildView
先来捋一遍 setContentView流程:
流程图非常清晰,最终会调用到 ViewGroup.addView(View,LauoutParams)上,先来测试一下这个 addView 是什么
从图这里得知,在super.addView()中累加 ChildCount 的值,但是说了这么多,和 NestedScrollView
有什么关系呢?
回到 NestedScrollView
的源码中…
可以从 NestedScrollView#addView(View child, ViewGroup.LayoutParams params)
中看出,在添加第二个 View 的时候,直接就报错了,报错信息为:
ScrollView can host only one direct child
2.NestedScrollView的事件分发流程
众所周知,事件分发主要分为:
- onInterceptTouchEvent
- onTouchEvent
- ACTION_DOWM
- ACTION_MOVE
- ACTION_UP / ACTION_CANCEL
本篇主要讲解事件传递流程,onInterceptTouchEvent
就不提了,就从 onTouchEvent
来开始聊
onTouchEvent#ACTION_DOWN事件:
# NestedScrollView.java
public boolean onTouchEvent(MotionEvent ev)
switch(ev.getActionMasked())
case MotionEvent.ACTION_DOWN:
.... 省略....
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
public boolean startNestedScroll(int axes, int type)
return mChildHelper.startNestedScroll(axes, type);
# NestedScrollingChildHelper.java
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type)
// 是否有嵌套滚动的 parentView
if (hasNestedScrollingParent(type))
// Already in progress
return true;
// 是否开启了嵌套滚动机制
if (isNestedScrollingEnabled())
while (p != null)
// 调用parentView 的 onStartNestedScroll() 方法
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type))
// 如果返回 true 则再次调用parentView 的onNestedScrollAccepted()方法
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
... 省略...
// 如果有嵌套滚动的 parentView 就直接调用他的 onStartNestedScroll()方法
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type)
if (parent instanceof NestedScrollingParent2)
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
else if (type == ViewCompat.TYPE_TOUCH)
... 省略....
return false;
// 如果 onStartNestedScroll() 返回 true 那么就立即执行 该方法
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes, int type)
if (parent instanceof NestedScrollingParent2)
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedScrollAccepted(child, target,
nestedScrollAxes, type);
else if (type == ViewCompat.TYPE_TOUCH)
... 省略....
再来看一眼流程图:
至此,DOWN 第一步的事件就传递完成了,第一步聊的详细一些,那么就再来捋一遍流程
在 TouchEvent.DOWN 事件中通过NestedScrollingChildHelper
调用 NestedScrollingChild#startNestedScroll()
方法,那么NestedScrollingChildHelper
就会通过么ViewParentCompat
调用到 NestedScrollingParent#onStartNestedScroll()
上,parentView
用来判断是否需要嵌套滚动,如果需要的话,返回 true,则立即调用到NestedScrollingParent#onNestedScrollAccepted
上 完成最初的事件传递
onTouchEvent#ACTION_MOVE事件:
ACTION_MOVE事件和 ACTION_DOWN 事件原理相同
# NestedScrollView.java
public boolean onTouchEvent(MotionEvent ev)
switch(ev.getActionMasked())
case MotionEvent.ACTION_MOVE:
.... 省略....
// 如果父 view 消费了事件,则返回 true
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH))
.... 省略....
// 将当前消费的和未消费的距离再次传递给 parentView
dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH, mScrollConsumed);
//代理给 NestedScrollingChildHelper 的同名方法即可
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type)
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
//代理给 NestedScrollingChildHelper的同名方法即可
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed)
mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, consumed);
# NestedScrollingChildHelper.java
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type)
// 是否支持嵌套滚动
if (isNestedScrollingEnabled())
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
# ViewParentCompat.java
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type)
if (parent instanceof NestedScrollingParent2)
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
else if (type == ViewCompat.TYPE_TOUCH)
...省略...
通过当前方法,即可吧 chlidView 的 move 事件传递给 parentView来消费
来看看流程图:
ACTION_UP / ACTION_CANCEL 原理和 ACTION_DOWN / ACTION_MOVE 一样,都是通过 ViewParentCompat调用到 parentView
public boolean onTouchEvent(MotionEvent ev)
switch(..)
case MotionEvent.ACTION_UP:
// 通过 VelocityTracker 与 OverScroller 来实现 fling 事件传递
final VelocityTracker velocityTracker = mVelocityTracker;
if (!edgeEffectFling(initialVelocity)
&& !dispatchNestedPreFling(0, -initialVelocity) // 分发事件给parentView,询问 parentView 是否消费
)
dispatchNestedFling(0, -initialVelocity, true); // 分发事件给 parentView 表示有嵌套滚动事件
fling(-initialVelocity); // 如果 parentView 没有消费 fling 事件.则自身消费掉
// 传递结束事件(stopNestedScroll)给 parentView
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
...省略...
// 传递结束事件(stopNestedScroll)给 parentView
endDrag();
break;
private void endDrag()
... 省略 ...
stopNestedScroll(ViewCompat.TYPE_TOUCH);
public void stopNestedScroll(int type)
mChildHelper.stopNestedScroll(type);
继续往下执行NestedScrollingChildHelper.stopNestedScroll()方法
# NestedScrollingChildHelper.java
public void stopNestedScroll(@NestedScrollType int type)
...
ViewParentCompat.onStopNestedScroll(parent, mView, type);
# ViewParentCompat.java
public static void onStopNestedScroll(ViewParent parent, View target, int type)
if (parent instanceof NestedScrollingParent2)
((NestedScrollingParent2) parent).onStopNestedScroll(target, type);
...
最终就会调用到 parentView 的 onStopNestedScroll() 方法上.
看一眼流程图:
tips: 这里 fling 是借助的 OverScroller() 就不展开说了,有兴趣的同学可以自主了解一下.
3.站在设计者的角度思考,为什么要这样设计
就以 ACTION_MOVE
中 childView
通过dispatchNestedPreScroll()
分发事件给parentView
的onNestedPreScroll()
来举例
首先看看这两个方法
# NestedScrollingChild.java
/**
@param dx:水平(x)滚动的距离(以像素为单位)
@param dy:垂直(y)滚动的距离(以像素为单位)
@param consumed: 主要用来父容器消费封装,并且通知子容器 x = consumed[0]; y = consumed[1];
@param offsetInWindow:滑动之前和滑动之后的偏移量
return true: 表示父容器消费了事件
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
# NestedScrollingParent.java
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
问题 :这里为什么要通过数组传递?
在 java
中,没有指针的概念,所以就没办法像 C 一样来操作内存
那么就导致传递一个基本基本数据类型传递给方法,那么到了方法中,就会生成一个新的基本数据类型
来看一段代码就明白了:
public static class Test
int[] mTestInts = new int[2];
ArrayList<Integer> mIntList = new ArrayList<>(2);
int mInt = 23;
Random mRandom = new Random();
public void test()
loadInts(mTestInts);
loadIntArray(mIntList);
loadInt(mInt);
System.out.println("int[] first:"+mTestInts[0]+"\\tsecond:"+mTestInts[1]);
System.out.println("list first:"+mIntList.get(0)+"\\tsecond:"+mIntList.get(1));
System.out.println("mInt:"+mInt);
public void loadInt(int tempInt)
tempInt += 52;
public void loadIntArray(ArrayList<Integer> list)
list.add(mRandom.nextInt(10));
list.add(mRandom.nextInt(10));
public void loadInts(int[] ints)
ifAndroid进阶 四 一个APP引发的思索之ArrayList的add总是添加相同的值
Android NestedScrolling解决滑动冲突问题 - 相关接口
Android NestedScrolling解决滑动冲突问题 - 相关接口
我的渲染技术进阶之旅如何在Windows系统编译Filament的android版本程序?