深入剖析 Android NestedScrollView 使用原理
一、引言
在 Android 开发中,滚动视图是非常常见的 UI 组件,用于展示超出屏幕范围的内容。NestedScrollView
作为 Android 系统提供的一个重要滚动视图组件,它支持嵌套滚动机制,使得开发者可以更灵活地实现复杂的滚动效果。本文将从源码级别深入分析 NestedScrollView
的使用原理,帮助开发者更好地理解和运用这个组件。
二、NestedScrollView 概述
2.1 基本介绍
NestedScrollView
是 Android 提供的一个垂直滚动的视图容器,它继承自 FrameLayout
,并实现了 NestedScrollingParent
和 NestedScrollingChild
接口,从而支持嵌套滚动功能。嵌套滚动允许父视图和子视图之间协同处理滚动事件,实现更流畅的滚动效果。
2.2 应用场景
NestedScrollView
常用于以下场景:
- 复杂布局滚动 :当布局中包含多个滚动视图或需要实现多层嵌套滚动时,
NestedScrollView
可以帮助协调各个滚动视图之间的滚动行为。 - 沉浸式体验 :在实现沉浸式界面时,
NestedScrollView
可以与其他组件(如AppBarLayout
)配合使用,实现滚动时的标题栏渐变、隐藏等效果。
三、NestedScrollView 源码结构分析
3.1 类定义
java
// NestedScrollView 继承自 FrameLayout,这意味着它可以作为一个容器来包含其他视图
public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,
NestedScrollingChild2, ScrollingView {
// 构造函数,用于在代码中创建 NestedScrollView 实例
public NestedScrollView(Context context) {
this(context, null);
}
// 构造函数,用于在 XML 布局中使用 NestedScrollView 时调用
public NestedScrollView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.scrollViewStyle);
}
// 构造函数,允许指定自定义样式
public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
// 构造函数,支持自定义样式和资源 ID
public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// 初始化滚动相关的属性和对象
initScrollView();
// 解析 XML 布局中的属性
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.NestedScrollView, defStyleAttr, defStyleRes);
// 获取滚动条相关属性
setVerticalScrollBarEnabled(a.getBoolean(
R.styleable.NestedScrollView_verticalScrollBarEnabled, true));
// 获取滚动条风格属性
setScrollBarStyle(a.getInt(R.styleable.NestedScrollView_scrollBarStyle,
View.SCROLLBARS_INSIDE_OVERLAY));
// 获取滚动条动画持续时间属性
mScrollAnimationDuration = a.getInt(
R.styleable.NestedScrollView_scrollAnimationDuration,
DEFAULT_SCROLL_ANIMATION_DURATION);
// 回收 TypedArray 对象,避免内存泄漏
a.recycle();
}
}
3.2 主要成员变量
java
// 用于处理滚动动画的 Scroller 对象
private Scroller mScroller;
// 用于跟踪触摸事件速度的 VelocityTracker 对象
private VelocityTracker mVelocityTracker;
// 用于记录上一次触摸事件的 Y 坐标
private float mLastMotionY;
// 用于判断是否正在被拖动的标志位
private boolean mIsBeingDragged;
// 最小触摸滑动阈值,用于判断是否开始拖动
private int mTouchSlop;
// 最大滚动速度
private int mMaximumVelocity;
// 最小滚动速度
private int mMinimumVelocity;
// 嵌套滚动相关的变量
private final int[] mScrollOffset = new int[2];
private final int[] mScrollConsumed = new int[2];
private int mNestedYOffset;
private NestedScrollingParentHelper mNestedScrollingParentHelper;
private NestedScrollingChildHelper mNestedScrollingChildHelper;
3.3 接口实现
NestedScrollView
实现了 NestedScrollingParent2
和 NestedScrollingChild2
接口,这两个接口定义了嵌套滚动的相关方法,使得 NestedScrollView
可以作为嵌套滚动的父视图和子视图参与滚动事件的处理。
java
// 实现 NestedScrollingParent2 接口,作为嵌套滚动的父视图
public interface NestedScrollingParent2 extends NestedScrollingParent {
// 开始嵌套滚动时调用,返回值表示是否接受嵌套滚动
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type);
// 嵌套滚动开始时调用,用于准备处理滚动事件
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type);
// 嵌套滚动结束时调用
void onStopNestedScroll(@NonNull View target, int type);
// 子视图滚动时调用,用于处理嵌套滚动
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type);
// 子视图预滚动时调用,用于提前处理滚动事件
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type);
}
// 实现 NestedScrollingChild2 接口,作为嵌套滚动的子视图
public interface NestedScrollingChild2 extends NestedScrollingChild {
// 开始嵌套滚动,参数表示滚动方向
boolean startNestedScroll(int axes, int type);
// 停止嵌套滚动
void stopNestedScroll(int type);
// 判断是否有嵌套滚动的父视图
boolean hasNestedScrollingParent(int type);
// 分发嵌套滚动事件
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type);
// 分发预嵌套滚动事件
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, int type);
}
四、NestedScrollView 触摸事件处理
4.1 事件拦截
NestedScrollView
通过 onInterceptTouchEvent
方法来判断是否拦截触摸事件,当满足一定条件时,会拦截事件并开始处理滚动。
java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 获取触摸事件的动作类型
final int action = ev.getActionMasked();
// 如果当前正在进行滚动动画,拦截事件
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true;
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
// 记录触摸事件的 Y 坐标
mLastMotionY = (int) ev.getY();
// 初始化速度跟踪器
ensureVelocityTracker();
// 开始嵌套滚动,指定垂直方向
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
// 标记未开始拖动
mIsBeingDragged = false;
break;
}
case MotionEvent.ACTION_MOVE: {
// 获取当前触摸事件的 Y 坐标
final int y = (int) ev.getY();
// 计算 Y 方向的偏移量
final int yDiff = (int) Math.abs(y - mLastMotionY);
// 如果偏移量超过最小触摸滑动阈值,开始拖动
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
// 记录当前触摸事件的 Y 坐标
mLastMotionY = y;
// 初始化速度跟踪器
ensureVelocityTracker();
// 开始嵌套滚动,指定垂直方向
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
return true;
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 停止嵌套滚动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
// 标记未开始拖动
mIsBeingDragged = false;
// 回收速度跟踪器
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
// 如果速度跟踪器不为空,添加当前触摸事件
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
}
return mIsBeingDragged;
}
4.2 事件处理
当 NestedScrollView
拦截触摸事件后,会通过 onTouchEvent
方法来处理滚动事件。
java
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 确保速度跟踪器已初始化
ensureVelocityTracker();
// 将当前触摸事件添加到速度跟踪器中
mVelocityTracker.addMovement(ev);
// 获取触摸事件的动作类型
final int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
// 如果当前正在进行滚动动画,停止动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// 记录触摸事件的 Y 坐标
mLastMotionY = (int) ev.getY();
// 开始嵌套滚动,指定垂直方向
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}
case MotionEvent.ACTION_MOVE: {
// 获取当前触摸事件的 Y 坐标
final int y = (int) ev.getY();
// 计算 Y 方向的偏移量
int deltaY = (int) (mLastMotionY - y);
// 记录上一次的 Y 坐标
mLastMotionY = y;
// 分发预嵌套滚动事件
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
// 减去父视图消费的滚动距离
deltaY -= mScrollConsumed[1];
// 更新嵌套滚动的 Y 偏移量
mNestedYOffset += mScrollOffset[1];
}
// 如果正在被拖动
if (mIsBeingDragged) {
// 处理滚动事件
if (deltaY > 0) {
// 向上滚动
int scrollY = getScrollY();
if (scrollY < getScrollRange()) {
// 计算滚动距离
int scrollBy = Math.min(deltaY, getScrollRange() - scrollY);
// 调用 scrollBy 方法进行滚动
scrollBy(0, scrollBy);
}
} else if (deltaY < 0) {
// 向下滚动
int scrollY = getScrollY();
if (scrollY > 0) {
// 计算滚动距离
int scrollBy = Math.min(-deltaY, scrollY);
// 调用 scrollBy 方法进行滚动
scrollBy(0, -scrollBy);
}
}
// 分发嵌套滚动事件
if (dispatchNestedScroll(0, 0, 0, deltaY, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
// 更新嵌套滚动的 Y 偏移量
mNestedYOffset += mScrollOffset[1];
}
}
break;
}
case MotionEvent.ACTION_UP:
// 计算当前的滚动速度
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) mVelocityTracker.getYVelocity();
// 如果速度大于最小滚动速度,进行惯性滚动
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
// 回弹动画
invalidate();
}
// 停止嵌套滚动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
// 标记未开始拖动
mIsBeingDragged = false;
// 回收速度跟踪器
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_CANCEL:
// 如果正在被拖动
if (mIsBeingDragged) {
// 回弹动画
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
invalidate();
}
}
// 停止嵌套滚动
stopNestedScroll(ViewCompat.TYPE_TOUCH);
// 标记未开始拖动
mIsBeingDragged = false;
// 回收速度跟踪器
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
五、NestedScrollView 嵌套滚动机制
5.1 嵌套滚动流程
嵌套滚动的流程主要包括以下几个步骤:
- 开始嵌套滚动 :子视图调用
startNestedScroll
方法通知父视图开始嵌套滚动,父视图通过onStartNestedScroll
方法决定是否接受嵌套滚动。 - 预滚动处理 :子视图在滚动前调用
dispatchNestedPreScroll
方法分发预滚动事件,父视图通过onNestedPreScroll
方法处理预滚动事件,并返回消费的滚动距离。 - 滚动处理 :子视图处理剩余的滚动距离,并调用
dispatchNestedScroll
方法分发滚动事件,父视图通过onNestedScroll
方法处理滚动事件。 - 停止嵌套滚动 :子视图调用
stopNestedScroll
方法通知父视图停止嵌套滚动,父视图通过onStopNestedScroll
方法进行清理工作。
5.2 源码分析
java
// 开始嵌套滚动
@Override
public boolean startNestedScroll(int axes, int type) {
// 调用 NestedScrollingChildHelper 的 startNestedScroll 方法
return mNestedScrollingChildHelper.startNestedScroll(axes, type);
}
// 停止嵌套滚动
@Override
public void stopNestedScroll(int type) {
// 调用 NestedScrollingChildHelper 的 stopNestedScroll 方法
mNestedScrollingChildHelper.stopNestedScroll(type);
}
// 判断是否有嵌套滚动的父视图
@Override
public boolean hasNestedScrollingParent(int type) {
// 调用 NestedScrollingChildHelper 的 hasNestedScrollingParent 方法
return mNestedScrollingChildHelper.hasNestedScrollingParent(type);
}
// 分发嵌套滚动事件
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
// 调用 NestedScrollingChildHelper 的 dispatchNestedScroll 方法
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
// 分发预嵌套滚动事件
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, int type) {
// 调用 NestedScrollingChildHelper 的 dispatchNestedPreScroll 方法
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed,
offsetInWindow, type);
}
// 作为父视图,处理开始嵌套滚动事件
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
// 判断是否为垂直滚动
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
// 作为父视图,处理嵌套滚动接受事件
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
// 调用 NestedScrollingParentHelper 的 onNestedScrollAccepted 方法
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
// 开始嵌套滚动,指定垂直方向
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
// 作为父视图,处理停止嵌套滚动事件
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
// 调用 NestedScrollingParentHelper 的 onStopNestedScroll 方法
mNestedScrollingParentHelper.onStopNestedScroll(target, type);
// 停止嵌套滚动
stopNestedScroll(type);
}
// 作为父视图,处理嵌套滚动事件
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
// 分发嵌套滚动事件
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type);
}
// 作为父视图,处理预嵌套滚动事件
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 如果向上滚动
if (dy > 0) {
int scrollY = getScrollY();
if (scrollY < getScrollRange()) {
// 计算消费的滚动距离
int scrollBy = Math.min(dy, getScrollRange() - scrollY);
// 调用 scrollBy 方法进行滚动
scrollBy(0, scrollBy);
// 记录消费的滚动距离
consumed[1] = scrollBy;
}
}
}
六、NestedScrollView 滚动动画处理
6.1 滚动动画原理
NestedScrollView
使用 Scroller
类来处理滚动动画,Scroller
类通过计算滚动的起始位置、结束位置和持续时间,模拟出平滑的滚动效果。
6.2 源码分析
java
// 初始化 Scroller 对象
private void initScrollView() {
// 创建 Scroller 对象
mScroller = new Scroller(getContext(), sInterpolator);
// 获取最小触摸滑动阈值
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
// 获取最大滚动速度
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
// 获取最小滚动速度
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
// 初始化嵌套滚动相关的帮助类
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
// 设置是否支持嵌套滚动
setNestedScrollingEnabled(true);
}
// 处理惯性滚动
private void flingWithNestedDispatch(int velocityY) {
// 判断是否可以进行滚动
final boolean canFling = (getScrollY() > 0 || velocityY > 0)
&& (getScrollY() < getScrollRange() || velocityY < 0);
// 分发预嵌套滚动事件
if (!dispatchNestedPreFling(0, velocityY)) {
// 分发嵌套滚动事件
dispatchNestedFling(0, velocityY, canFling);
// 调用 fling 方法进行惯性滚动
fling(velocityY);
}
}
// 进行惯性滚动
public void fling(int velocityY) {
// 如果有子视图
if (getChildCount() > 0) {
// 获取视图的高度
int height = getHeight() - getPaddingBottom() - getPaddingTop();
// 获取子视图的高度
int bottom = getChildAt(0).getHeight();
// 调用 Scroller 的 fling 方法开始滚动动画
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY,
0, 0, 0, Math.max(0, bottom - height));
// 如果需要回弹,触发重绘
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
Math.max(0, bottom - height))) {
invalidate();
}
}
}
// 计算滚动偏移量
@Override
public void computeScroll() {
// 如果 Scroller 还在滚动中
if (mScroller.computeScrollOffset()) {
// 获取当前的滚动位置
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
// 如果滚动位置发生变化
if (oldX != x || oldY != y) {
// 调用 scrollTo 方法更新滚动位置
scrollTo(x, y);
}
// 触发重绘,继续滚动动画
invalidate();
}
}
七、NestedScrollView 与其他组件的配合使用
7.1 与 RecyclerView 的配合
NestedScrollView
可以与 RecyclerView
配合使用,实现嵌套滚动效果。在布局中,将 RecyclerView
放置在 NestedScrollView
内部,通过设置 RecyclerView
的 nestedScrollingEnabled
属性为 false
,可以避免 RecyclerView
自己处理滚动事件,从而让 NestedScrollView
统一处理滚动。
xml
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
</androidx.core.widget.NestedScrollView>
7.2 与 AppBarLayout 的配合
NestedScrollView
可以与 AppBarLayout
配合使用,实现滚动时标题栏的渐变、隐藏等效果。在布局中,将 AppBarLayout
放置在 NestedScrollView
外部,通过设置 AppBarLayout
的 ScrollingViewBehavior
,可以实现滚动联动。
xml
<com.google.android.material.appbar.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- 内容视图 -->
</androidx.core.widget.NestedScrollView>
</com.google.android.material.appbar.CoordinatorLayout>
八、总结与展望
8.1 总结
通过对 NestedScrollView
的源码分析,我们深入了解了其使用原理。NestedScrollView
作为一个支持嵌套滚动的视图容器,通过实现 NestedScrollingParent
和 NestedScrollingChild
接口,与父视图和子视图之间协同处理滚动事件,实现了流畅的滚动效果。其触摸事件处理机制、嵌套滚动机制和滚动动画处理机制相互配合,为开发者提供了强大的滚动功能。
8.2 展望
随着 Android 系统的不断发展,NestedScrollView
可能会在以下方面得到进一步的优化和扩展:
- 性能优化:进一步优化滚动性能,减少滚动时的卡顿现象,提高用户体验。
- 功能扩展:增加更多的滚动效果和交互方式,如弹性滚动、阻尼滚动等。
- 兼容性提升:更好地兼容不同版本的 Android 系统和各种设备,确保在不同环境下都能正常使用。
总之,NestedScrollView
作为 Android 开发中重要的滚动视图组件,将在未来的开发中发挥更加重要的作用。开发者可以根据实际需求,灵活运用 NestedScrollView
的特性,实现各种复杂的滚动效果。
以上是一篇关于 Android NestedScrollView 使用原理的技术博客,通过对源码的详细分析,帮助开发者深入理解 NestedScrollView
的工作原理和使用方法。希望对你有所帮助!