本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布[ 2022-4-21]
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) {
if(ints instanceof Object){System.out.println("int[] extents Object");}
ints[0] = mRandom.nextInt(10);
ints[1] = mRandom.nextInt(10);
}
}
来看一眼运行的效果图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koZyw3oe-1649590679179)(/Users/shizhenjiang/Desktop/博客/nestedScrollView/ints.gif)]
结果很明显,我就不在多啰嗦了…
所以这里到底有什么用? 给我看这个有什么用?
在来细品一下NestedScrollView#onTouchEvent#ACTION_MOVE的源码:
public boolean onTouch(){
case ACTION_MOVE:
....
// 分发事件给 parentView,如果 parentView 消费则返回 true
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
....
}
break:
}
走进NestedScrollingChildHelper.dispatchNestedPreScroll源码细细品味一般
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
//如果开启了滑动就执行
if (isNestedScrollingEnabled()) {
...
if (dx != 0 || dy != 0) {
....
// 如果 consumed == null 就创建一个空数组返回
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
// 如果 parentView 没有消费 一点距离,则返回 false
// 反之消费了则返回 true
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
private int[] getTempNestedScrollConsumed() {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
return mTempNestedScrollConsumed;
}
通过这段源码得知,consumed 非常关键,是证明 parentView 是否消费,dispatchNestedPreScroll() == true 的条件
来两张图看看它到底是否和源码表达的一样
parentView消费了事件 | parentView 没有消费事件 |
---|---|
tips:这里代码不重要,代码底部会给出,重要的是思路!! , 有了思路,这些代码迟早闭着眼写出来
4. dispatchNestedPreScroll() 和 dispatchNestedScroll() 的区别
dispatchNestedPreScroll()
只有在 parentView 消费了事件的时候,并且有嵌套的 parentView,才返回 true,证明 parentView 消费了事件dispatchNestedScroll()
则不同,只要有嵌套的parentView
就会执行 (parentView extents NestedScrollingParent) , 无论 parentView 是否消费事件- 参数也很大不同,
dispatchNestedPreScroll()
是用来处理 x / y 滑动距离的,dispatchNestedScroll()
则是用来处理已经消费和未消费的滑动距离的 childView.dispatchNestedPreScroll()
会调用到ParentView.onNestedPreScroll()
方法childView.dispatchNestedScroll
会调用到ParentView.onNestedScroll()
方法
5.ParentView.onStartNestedScroll() child 和 taget 的区别
2张图搞清楚:
图 1 | 图 2 |
---|---|
可以很清晰的看出:
- child 代表嵌套的第一给 view
- taget 则代表嵌套滑动的 childView
源码位置:
# NestedScrollingChildHelper.java
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
// 如果有嵌套滚动的 view 就返回 true
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
// 此时 child == 嵌套滚动的 View,
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
// 找到嵌套滚动的 View 就立即返回
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
实战
先来看一眼效果:
这个效果非常典型,可以很好地练习 NestScrollChildView 和 NestScollParentView
通过前面的详细介绍,大家应该对 NestScrollView 有一定的了解了,
那么就直接来看代码了:
tips:为了整洁度,我把没必要的 log 和注释都删了,如果你需要细品,点击下载完成代码
ChildNestedScrollView.kt
# ChildNestedScrollView.kt
class ChildNestedScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr), NestedScrollingChild3 {
private val childHelper by lazy {
NestedScrollingChildHelper(this).apply { isNestedScrollingEnabled = true }
}
// 滚动消耗
private val mScrollConsumed = IntArray(2)
// 偏移量
private val mScrollOffset = IntArray(2)
private var lastTouchY = 0
override fun onTouchEvent(event: MotionEvent): Boolean {
val touchX = event.x.toInt()
val touchY = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchY = touchY
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_MOVE -> {
var tempY = lastTouchY - touchY
// 分发事件给parent 询问parent是否执行
// true 表示父view消费了事件
if (dispatchNestedPreScroll(
0,
tempY,
mScrollConsumed,
mScrollOffset,
ViewCompat.TYPE_TOUCH
)
) { // 父亲消费
tempY -= mScrollConsumed[1]
if (tempY == 0) return true
} else {
// 自己消费
scrollBy(0, tempY)
}
lastTouchY = touchY
// true 支持嵌套滚动
if( dispatchNestedScroll(0,
tempY,
0,
scrollY - measuredHeight,
mScrollOffset,
ViewCompat.TYPE_TOUCH)){
Log.i("szj分发事件","dispatchNestedScroll\t lastTouchY:${lastTouchY}")
}
}
// 抬起/取消
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
return true
}
override fun startNestedScroll(axes: Int, type: Int): Boolean = let {
Log.i(TAG, "child startNestedScroll axes:$axes type:$type ")
childHelper.startNestedScroll(axes)
}
override fun stopNestedScroll(type: Int) {
Log.i(TAG, "child stopNestedScroll $type")
childHelper.stopNestedScroll(type)
}
// NestedScrollingChild2
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int
): Boolean = let {
childHelper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type
)
}
override fun dispatchNestedPreScroll(
dx: Int,
dy: Int,
consumed: IntArray?,
offsetInWindow: IntArray?,
type: Int
): Boolean = let {
childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
}
/*
* 作者:android 超级兵
* 创建时间: 4/9/22 3:47 PM
* TODO 最终xml会调用到这里..添加
*/
override fun addView(child: View, params: ViewGroup.LayoutParams?) {
super.addView(child, params)
}
@SuppressLint("LongLogTag")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var tempHeightMeasureSpec = heightMeasureSpec
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
// 遍历所有的view 用来测量高度
children.forEach {
tempHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(tempHeightMeasureSpec),
MeasureSpec.UNSPECIFIED
)
// 测量子view
measureChild(it, widthMeasureSpec, tempHeightMeasureSpec)
}
setMeasuredDimension(widthSize, children.first().measuredHeight)
}
override fun scrollTo(x: Int, y: Int) {
var tempY = y
if (tempY < 0) tempY = 0
super.scrollTo(x, tempY)
}
}
ParentNestedScrollView.kt
# ParentNestedScrollView.kt
class ParentNestedScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr), NestedScrollingParent3 {
private val parentHelper by lazy {
NestedScrollingParentHelper(this)
}
// 第一个View
private val firstView by lazy {
children.first()
}
private var mChildHeight = 0
@SuppressLint("LongLogTag")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var tempHeightMeasureSpec = heightMeasureSpec
mChildHeight = 0
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(tempHeightMeasureSpec)
children.forEach {
tempHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.UNSPECIFIED)
// 测量子view
measureChild(it, widthMeasureSpec, tempHeightMeasureSpec)
mChildHeight += it.measuredHeight
}
setMeasuredDimension(widthSize, heightSize)
}
/*
* 作者:android 超级兵
* 创建时间: 4/7/22 4:51 PM
* TODO 子view调用 startNestedScroll()时候执行
*/
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean = true
/*
* 作者:android 超级兵
* 创建时间: 4/7/22 4:52 PM
* TODO 如果onStartNestedScroll()返回true的话,就会紧接着调用该方法
* 常用来做一些初始化工作
*/
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
parentHelper.onNestedScrollAccepted(child, target, axes, type)
}
/*
* 作者:android 超级兵
* 创建时间: 4/7/22 4:55 PM
* TODO 当子view调用 stopNestedScroll() 时候调用
*/
override fun onStopNestedScroll(target: View, type: Int) {
parentHelper.onStopNestedScroll(target, type)
}
/*
* 作者:android 超级兵
* 创建时间: 4/7/22 4:45 PM
* TODO 当子view调用 dispatchNestedPreScroll() 时候调用
* tips:在childNestedScrollView.onTouchEvent#ACTION_MOVE:中
*/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int,
consumed: IntArray, type: Int) {
// (dy > 0 && scrollY < firstView.height) 如果 向上滑动 并且 当前滑动的距离 < 第一个View的高 说明还有滑动空间
// (dy < 0 && scrollY > 0) 如果当前向下滑动 并且还有滑动空间
if ((dy > 0 && scrollY < firstView.height) || (dy < 0 && scrollY > 0)) {
// 父容器消费了多少通知子view
consumed[1] = dy // 关键代码!!parentView正在消费事件,并且通知 childView
scrollBy(0, dy)
}
}
override fun scrollTo(x: Int, y: Int) {
var tempY = y
if (tempY < 0) tempY = 0
super.scrollTo(x, tempY)
}
}
下期分享: CoordinatorLayout源码分析与实战!
分析源码和写博客都很费精力,请给存粹的分享者一波关注,感谢!
猜你喜欢:
- android material design 风格组件(MaterialButton,MaterialButtonToggleGroup,Chip,ChipGroup)大汇总(一).
- android MD风格组件(TextInputLayout AutoCompleteTextView MaterialButton SwitchMaterial MaterialRadio)(二)
- android MD风格组件(BottomNavigationView,配合lottie使用) (三)
参考链接:
原创不易,您的点赞就是对我最大的支持!