Android嵌套滑动详解

Android嵌套滑动详解

一、引言

在 Android 开发的广阔天地中,用户界面的设计与交互体验的优化始终是开发者们关注的焦点。其中,嵌套滑动作为一种常见且重要的交互方式,在众多应用中发挥着关键作用,为用户带来了更加流畅、自然的操作体验。

以电商类应用为例,在商品详情页面,往往会采用嵌套滑动的设计。外层是一个包含商品图片、基本信息、价格等内容的滚动布局,而内层则可能是一个可滑动的评论列表或相关推荐商品的展示区域。当用户滑动屏幕时,外层布局和内层列表能够协同工作,根据用户的操作进行相应的滚动,使用户可以无缝地浏览商品的所有信息,无需频繁切换页面或进行复杂的操作。

再如社交类应用的个人资料页面,外层是用户的头像、简介等固定信息以及一些可展开的板块,内层则可能是动态列表、关注列表等。通过嵌套滑动,用户可以在一个页面内轻松查看所有内容,并且在滑动过程中,外层的固定信息和内层的动态内容能够协调显示,提升了用户获取信息的效率和体验。

对于开发者而言,深入理解嵌套滑动的原理,尤其是从源码级别进行剖析,具有至关重要的意义。只有掌握了底层原理,才能在面对复杂的布局和交互需求时,游刃有余地进行开发和优化。例如,在处理多层嵌套的滑动布局时,如果不了解嵌套滑动的机制,可能会出现滑动冲突、卡顿等问题,严重影响用户体验。而通过深入研究源码,开发者可以清晰地了解嵌套滑动的实现流程、事件传递机制以及各种优化策略,从而能够有针对性地解决这些问题,确保应用的稳定性和流畅性。

此外,随着 Android 系统的不断更新和发展,嵌套滑动的实现方式也在不断演进。从早期版本中开发者需要手动处理复杂的事件分发逻辑,到后来官方提供了更为完善的嵌套滑动机制,每一次的变革都带来了开发效率和用户体验的提升。因此,持续关注嵌套滑动原理的研究,有助于开发者紧跟技术潮流,充分利用新特性,为用户打造出更加优秀的应用。

本文将深入到 Android View 的源码层面,全面而细致地分析嵌套滑动的原理。通过对相关类和接口的深入解读,以及对滑动过程中关键方法的详细剖析,旨在帮助开发者从根本上理解嵌套滑动的工作机制,掌握其核心要点,为在实际开发中灵活运用嵌套滑动技术提供坚实的理论基础和实践指导。

二、嵌套滑动基础概念

2.1 什么是嵌套滑动

嵌套滑动是指在一个视图层次结构中,当子视图进行滑动操作时,父视图能够与之协同工作,共同完成滑动交互的过程。这种滑动机制打破了传统的单一视图滑动模式,使得多个视图之间能够实现更加复杂和流畅的交互效果。

在实际应用中,嵌套滑动有着广泛的应用场景。以常见的滑动菜单为例,外层是一个包含多个功能模块入口的主菜单布局,内层则是每个功能模块对应的详细内容列表。当用户在主菜单区域滑动时,主菜单可以根据用户的操作进行相应的滚动,同时,当用户深入到某个功能模块的详细内容列表时,列表也能够独立滑动,并且在滑动过程中,主菜单和详细内容列表之间能够实现无缝切换和协同工作,为用户提供了便捷的操作体验。

再如吸顶效果的实现,通常外层是一个包含多个部分的滚动布局,内层是一个需要吸顶的视图,如导航栏或标题栏。当用户向上滑动页面时,外层布局正常滚动,当内层吸顶视图到达顶部时,它会固定在屏幕顶部,不再随外层布局滚动,而当用户向下滑动页面时,吸顶视图又会随着外层布局的滚动而恢复到原来的位置。这种吸顶效果不仅方便了用户随时访问重要信息,还提升了页面的整体美观度和用户体验。

嵌套滑动在提升用户体验方面发挥着重要作用。它使得用户能够在一个页面内更加自然、流畅地进行操作,减少了页面切换带来的打断感和等待时间。通过合理的嵌套滑动设计,用户可以轻松地在不同层次的内容之间进行切换和浏览,提高了信息获取的效率。此外,嵌套滑动还能够为应用增添更多的交互性和趣味性,使应用在众多竞品中脱颖而出,吸引用户的关注和使用。

2.2 嵌套滑动相关接口和类

在 Android 中,实现嵌套滑动主要涉及到以下几个关键的接口和类:NestedScrollingChild、NestedScrollingChildHelper、NestedScrollingParent 和 NestedScrollingParentHelper。这些接口和类相互协作,共同完成了嵌套滑动的复杂逻辑。下面我们将详细介绍它们的功能和使用方法。

2.2.1 NestedScrollingChild

NestedScrollingChild是一个接口,定义了子视图与父视图进行嵌套滑动交互的方法。当一个视图需要作为嵌套滑动的子视图时,它必须实现这个接口。以下是NestedScrollingChild接口中主要方法的详细介绍:

  • void setNestedScrollingEnabled(boolean enabled):设置是否允许嵌套滑动。当参数enabled为true时,开启嵌套滑动功能;为false时,关闭嵌套滑动功能。例如:
java 复制代码
// 假设mChildView是一个实现了NestedScrollingChild接口的子视图
mChildView.setNestedScrollingEnabled(true); 
  • boolean isNestedScrollingEnabled():用于判断当前视图是否允许嵌套滑动。返回true表示允许,返回false表示不允许。例如:
java 复制代码
if (mChildView.isNestedScrollingEnabled()) {
    // 进行相关操作,比如开始处理滑动事件
}
  • boolean startNestedScroll(int axes):开始嵌套滑动流程。该方法会向上查找父视图,判断是否存在愿意响应嵌套滑动的父视图。参数axes表示支持嵌套滚动的轴,可取值为ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向)、ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向)或两者的组合。如果找到愿意响应的父视图,则返回true,否则返回false。通常在触摸事件的ACTION_DOWN阶段调用该方法,示例代码如下:
java 复制代码
@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN:
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            if (startNestedScroll(nestedScrollAxis)) {
                // 找到了愿意响应的父视图,进行相关初始化操作
            }
            break;
        // 其他事件处理代码
    }
    return true;
}
  • void stopNestedScroll():停止嵌套滑动流程。通常在触摸事件的ACTION_UP或ACTION_CANCEL阶段调用,通知父视图嵌套滑动结束。例如:
java 复制代码
@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (e.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            stopNestedScroll();
            // 其他清理操作
            break;
        // 其他事件处理代码
    }
    return true;
}
  • boolean hasNestedScrollingParent():判断是否存在嵌套滑动对应的父视图。返回true表示存在,返回false表示不存在。例如:
java 复制代码
if (hasNestedScrollingParent()) {
    // 存在父视图,进行相关操作,比如通知父视图滑动情况
}
  • boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow):在子视图滑动之后调用该函数向父视图汇报滑动情况。参数dxConsumed和dyConsumed分别表示子视图在水平和垂直方向上已经滑动的距离;dxUnconsumed和dyUnconsumed分别表示子视图在水平和垂直方向上未滑动的距离;offsetInWindow是一个可选的长度为 2 的数组,如果父视图滑动导致子视图的窗口发生变化(子视图的位置发生变化),该参数返回x(offsetInWindow[0])和y(offsetInWindow[1])方向的变化。如果父视图对滑动做出了相应处理,则返回true,否则返回false。示例代码如下:
java 复制代码
// 假设dx和dy是子视图计算出的滑动距离
int[] consumed = new int[2];
int[] offsetInWindow = new int[2];
// 子视图先进行自己的滑动操作
scrollBy(dx, dy);
// 计算已滑动和未滑动的距离
int dxConsumed = dx;
int dyConsumed = dy;
int dxUnconsumed = 0;
int dyUnconsumed = 0;
if (dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)) {
    // 父视图对滑动做出了处理,可能需要根据offsetInWindow调整子视图的位置
}
  • boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow):在子视图滑动之前,调用该方法告诉父视图滑动的距离,让父视图做相应的处理。参数dx和dy分别表示子视图在水平和垂直方向上需要滑动的距离;consumed是一个出参,如果不为null,则父视图会将自己滑动的情况记录在其中,consumed[0]表示父视图在水平方向上滑动的距离,consumed[1]表示父视图在垂直方向上滑动的距离;offsetInWindow的作用与dispatchNestedScroll方法中的相同。如果父视图滑动了,则返回true,否则返回false。示例代码如下:
java 复制代码
// 假设dx和dy是用户触摸产生的滑动距离
int[] consumed = new int[2];
int[] offsetInWindow = new int[2];
if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
    // 父视图滑动了,子视图需要根据父视图消费的距离调整自己的滑动距离
    dx -= consumed[0];
    dy -= consumed[1];
}
// 子视图进行自己的滑动操作
scrollBy(dx, dy);
  • boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed):在子视图执行fling(快速滑动)操作之后调用该函数向父视图汇报fling情况。参数velocityX和velocityY分别表示水平和垂直方向的速度;consumed表示子视图是否进行了fling操作。如果父视图对fling做出了相应处理,则返回true,否则返回false。例如:
java 复制代码
// 假设velocityX和velocityY是子视图计算出的fling速度
if (dispatchNestedFling(velocityX, velocityY, true)) {
    // 父视图对fling做出了处理
}
  • boolean dispatchNestedPreFling(float velocityX, float velocityY):在子视图执行fling操作之前,调用该方法告诉父视图fling的情况。参数velocityX和velocityY分别表示水平和垂直方向的速度。如果父视图对fling做出了相应处理,则返回true,否则返回false。例如:
java 复制代码
// 假设velocityX和velocityY是用户滑动产生的fling速度
if (dispatchNestedPreFling(velocityX, velocityY)) {
    // 父视图对fling做出了处理,子视图可能需要调整自己的fling行为
}
2.2.2 NestedScrollingChildHelper

NestedScrollingChildHelper是一个辅助类,用于协助实现NestedScrollingChild接口。它封装了与父视图交互的大部分逻辑,使得子视图在实现嵌套滑动时更加简单和便捷。在实现NestedScrollingChild接口的子视图中,通常会创建一个NestedScrollingChildHelper对象,并在相应的方法中调用它的方法。

下面是NestedScrollingChildHelper中一些关键方法的实现逻辑分析:

  • public void setNestedScrollingEnabled(boolean enabled):设置是否允许嵌套滑动。如果当前已经处于嵌套滑动状态(mIsNestedScrollingEnabled为true),则先调用ViewCompat.stopNestedScroll(mView)停止嵌套滑动,然后再更新mIsNestedScrollingEnabled的值。代码如下:
java 复制代码
public void setNestedScrollingEnabled(boolean enabled) {
    if (mIsNestedScrollingEnabled) {
        ViewCompat.stopNestedScroll(mView);
    }
    mIsNestedScrollingEnabled = enabled;
}
  • public boolean isNestedScrollingEnabled():返回当前是否允许嵌套滑动,直接返回mIsNestedScrollingEnabled的值。
java 复制代码
public boolean isNestedScrollingEnabled() {
    return mIsNestedScrollingEnabled;
}
  • public boolean startNestedScroll(int axes):开始嵌套滑动流程。首先检查是否已经存在嵌套滑动的父视图(hasNestedScrollingParent()),如果存在则直接返回true,表示已经在进行嵌套滑动。然后检查当前视图是否允许嵌套滑动(isNestedScrollingEnabled()),如果允许,则向上查找父视图。通过ViewParentCompat.onStartNestedScroll(p, child, mView, axes)方法通知父视图开始嵌套滑动,如果父视图接受嵌套滑动(返回true),则将该父视图记录为mNestedScrollingParent,并调用ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes)方法,最后返回true。如果没有找到接受嵌套滑动的父视图,则继续向上查找,直到找到或遍历完所有父视图,最终返回false。代码如下:
java 复制代码
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;
}
  • public void stopNestedScroll():停止嵌套滑动流程。将mNestedScrollingParent设置为null,表示不再与父视图进行嵌套滑动交互。代码如下:
java 复制代码
public void stopNestedScroll() {
    mNestedScrollingParent = null;
}
  • public boolean hasNestedScrollingParent():判断是否存在嵌套滑动的父视图,通过检查mNestedScrollingParent是否为null来确定。
java 复制代码
public boolean hasNestedScrollingParent() {
    return mNestedScrollingParent != null;
}
  • public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow):在子视图滑动之后,调用该方法向父视图汇报滑动情况。首先检查当前是否允许嵌套滑动并且存在嵌套滑动的父视图(isNestedScrollingEnabled() && mNestedScrollingParent != null),如果满足条件,则调用ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)方法通知父视图滑动情况。如果父视图对滑动做出了处理(返回true),并且offsetInWindow不为null,则计算子视图窗口位置的变化并更新offsetInWindow的值。最后返回父视图是否对滑动做出了处理的结果。代码如下:
java 复制代码
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (offsetInWindow != null) {
            mView.getLocationInWindow(offsetInWindow);
            // 计算窗口位置变化,这里省略具体计算逻辑
        }
        return true;
    }
    return false;
}
  • public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow):在子视图滑动之前,调用该方法告诉父视图滑动的距离,让父视图做相应的处理。首先检查当前是否允许嵌套滑动并且存在嵌套滑动的父视图(isNestedScrollingEnabled() && mNestedScrollingParent != null),如果满足条件,且dx或dy不为 0,则进行相关处理。如果consumed为null,则创建一个临时的mTempNestedScrollConsumed数组来记录父视图消费的距离。将consumed数组初始化为 0,然后调用ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed)方法通知父视图滑动情况。如果offsetInWindow不为null,则计算子视图窗口位置的变化并更新offsetInWindow的值。最后根据consumed数组中是否有值来判断父视图是否滑动了,返回相应的结果。代码如下:
java 复制代码
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }
            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            consumed[0] = 0;
            consumed[1] = 0;
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

通过使用NestedScrollingChildHelper辅助类,子视图只需在实现NestedScrollingChild接口的方法中简单调用它的对应方法,就可以完成与父视图的嵌套滑动交互逻辑,大大简化了开发过程。

2.2.3 NestedScrollingParent

NestedScrollingParent是一个接口,定义了父视图与子视图进行嵌套滑动交互的方法。当一个视图需要作为嵌套滑动的父视图时,它必须实现这个接口。该接口中的方法与NestedScrollingChild接口中的方法大致有一一对应的关系,父视图通过这些方法来响应子视图的嵌套滑动请求,并进行相应的处理。以下是NestedScrollingParent接口中主要方法的详细介绍:

  • boolean onStartNestedScroll(View child, View target, int nestedScrollAxes):当子

三、嵌套滑动事件分发流程

3.1 传统事件分发与嵌套滑动事件分发的区别

在深入探讨嵌套滑动事件分发流程之前,我们先来对比一下传统事件分发和嵌套滑动事件分发的差异。传统的 Android 事件分发机制是一个由父视图到子视图的单向传递过程,遵循 "自上而下,由外向内" 的原则。当一个触摸事件发生时,首先由 Activity 的dispatchTouchEvent方法接收事件,然后将事件传递给顶层的 ViewGroup。ViewGroup 会依次调用自身的onInterceptTouchEvent方法来判断是否拦截该事件。如果 ViewGroup 拦截了事件,那么事件将由该 ViewGroup 的onTouchEvent方法处理;如果不拦截,事件会继续向下传递给子 View,子 View 同样会通过dispatchTouchEvent方法来处理事件,并且可以通过onTouchEvent方法来消费事件。如果事件在传递过程中没有被任何 View 消费,那么它将向上冒泡,由父 View 的onTouchEvent方法来处理,直到 Activity。

例如,在一个简单的 LinearLayout 嵌套 Button 的布局中,当用户点击 Button 时,事件首先传递到 LinearLayout,LinearLayout 的onInterceptTouchEvent方法默认返回 false,不拦截事件,事件继续传递到 Button。Button 的dispatchTouchEvent方法接收到事件,然后调用onTouchEvent方法处理事件。如果 Button 处理了事件(onTouchEvent返回 true),事件就不会再向上冒泡;如果 Button 没有处理事件(onTouchEvent返回 false),事件会返回给 LinearLayout 的onTouchEvent方法处理。

而嵌套滑动事件分发机制则打破了这种传统的单向传递模式,它允许子视图和父视图之间进行双向通信和协同工作。在嵌套滑动中,子视图在处理滑动事件之前,会先向父视图询问是否需要参与滑动,父视图可以根据自身的状态和业务逻辑来决定是否消费部分或全部的滑动距离。在子视图滑动过程中,也会实时向父视图汇报滑动情况,父视图可以根据这些信息来进行相应的处理,比如调整自身的位置或状态。这种机制使得嵌套滑动能够实现更加复杂和流畅的交互效果,解决了传统事件分发在处理嵌套滑动时容易出现的滑动冲突等问题。

以 RecyclerView 嵌套在 NestedScrollView 中为例,当用户滑动 RecyclerView 时,RecyclerView 会先调用dispatchNestedPreScroll方法通知 NestedScrollView 即将进行滑动,并将滑动的距离等信息传递给 NestedScrollView。NestedScrollView 可以根据自身的状态(比如是否已经滑动到边界)来决定是否消费部分滑动距离,如果 NestedScrollView 消费了部分距离,它会将消费的距离记录在consumed数组中返回给 RecyclerView,RecyclerView 再根据剩余的距离进行自身的滑动处理。在 RecyclerView 滑动结束后,还会调用dispatchNestedScroll方法将剩余的未消费距离传递给 NestedScrollView,NestedScrollView 可以根据这些信息进行进一步的处理,比如继续滑动或调整布局。

3.2 嵌套滑动事件分发的详细流程

下面我们以 RecyclerView 嵌套在 NestedScrollView 中为例,详细分析嵌套滑动事件分发的整个流程。

3.2.1 ACTION_DOWN 阶段

当用户手指触摸屏幕时,首先会产生一个 ACTION_DOWN 事件。这个事件会从 Activity 的dispatchTouchEvent方法开始传递,经过一系列的 ViewGroup,最终到达最内层的 RecyclerView。RecyclerView 在接收到 ACTION_DOWN 事件后,会进行以下操作:

  1. 检查嵌套滑动是否启用:RecyclerView 会调用isNestedScrollingEnabled方法检查自身是否启用了嵌套滑动功能。如果未启用,后续的嵌套滑动相关逻辑将不会执行,事件将按照传统的事件分发机制进行处理。在 RecyclerView 的源码中,isNestedScrollingEnabled方法通常返回一个布尔值,这个值可以通过setNestedScrollingEnabled方法进行设置。例如:
java 复制代码
// RecyclerView类中的部分代码示意
private boolean mNestedScrollingEnabled = true; // 假设默认启用嵌套滑动
public boolean isNestedScrollingEnabled() {
    return mNestedScrollingEnabled;
}
public void setNestedScrollingEnabled(boolean enabled) {
    mNestedScrollingEnabled = enabled;
}
  1. 开始嵌套滑动流程:如果嵌套滑动已启用,RecyclerView 会调用startNestedScroll方法,尝试寻找愿意响应嵌套滑动的父视图,即 NestedScrollView。startNestedScroll方法会向上遍历父视图层级,通过ViewParentCompat.onStartNestedScroll方法通知每个父视图开始嵌套滑动。如果某个父视图返回 true,表示它愿意响应嵌套滑动,那么这个父视图就会被记录为当前的嵌套滑动父视图,并且会调用ViewParentCompat.onNestedScrollAccepted方法进行相关的初始化操作。RecyclerView 中startNestedScroll方法的简化实现如下:
java 复制代码
// RecyclerView类中的startNestedScroll方法简化示意
public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // 已经在进行嵌套滑动,直接返回true
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, this, axes)) {
                // 找到愿意响应的父视图
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, this, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

在这个过程中,axes参数表示支持嵌套滚动的轴,它可以是ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向)、ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向)或两者的组合。对于 RecyclerView 嵌套在 NestedScrollView 中的场景,通常是垂直方向的滑动,所以axes会设置为ViewCompat.SCROLL_AXIS_VERTICAL。

3.2.2 ACTION_MOVE 阶段

当用户手指在屏幕上移动时,会产生一系列的 ACTION_MOVE 事件。RecyclerView 在接收到 ACTION_MOVE 事件后,会按照以下步骤进行处理:

  1. 通知父视图预滑动:RecyclerView 会调用dispatchNestedPreScroll方法,将滑动的距离(dx 和 dy)传递给父视图 NestedScrollView,询问父视图是否需要在子视图滑动之前进行部分滑动处理。dispatchNestedPreScroll方法的定义如下:
java 复制代码
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)

其中,dx和dy分别表示在水平和垂直方向上的滑动距离;consumed是一个出参数组,如果父视图消费了部分滑动距离,会将消费的距离记录在这个数组中(consumed[0]表示水平方向消费的距离,consumed[1]表示垂直方向消费的距离);offsetInWindow是一个可选的数组,用于返回子视图在窗口中的位置偏移量。

在 RecyclerView 的实现中,dispatchNestedPreScroll方法会先检查是否存在嵌套滑动的父视图(mNestedScrollingParent != null),并且当前是否启用了嵌套滑动(isNestedScrollingEnabled())。如果满足条件,就会调用ViewParentCompat.onNestedPreScroll方法将滑动信息传递给父视图:

java 复制代码
// RecyclerView类中的dispatchNestedPreScroll方法简化示意
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {
            // 初始化consumed数组
            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            consumed[0] = 0;
            consumed[1] = 0;
            // 通知父视图预滑动
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, this, dx, dy, consumed);
            // 根据父视图消费的距离调整自身的滑动距离
            dx -= consumed[0];
            dy -= consumed[1];
            return consumed[0] != 0 || consumed[1] != 0;
        }
    }
    return false;
}
  1. 父视图处理预滑动:NestedScrollView 在接收到onNestedPreScroll方法的调用时,会根据自身的状态和业务逻辑来决定是否消费部分滑动距离。如果 NestedScrollView 需要滑动,它会将消费的距离记录在consumed数组中返回给 RecyclerView。例如,当 NestedScrollView 还没有滑动到顶部或底部边界时,它可能会消费部分垂直方向的滑动距离(dy),并将消费的距离记录在consumed[1]中。NestedScrollView 中onNestedPreScroll方法的简化实现如下:
java 复制代码
// NestedScrollView类中的onNestedPreScroll方法简化示意
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    if (dy > 0 && canScrollVertically(-1)) {
        // 向上滑动且自身可以向上滚动
        int delta = Math.min(dy, getScrollY());
        scrollBy(0, -delta);
        consumed[1] = delta;
    } else if (dy < 0 && canScrollVertically(1)) {
        // 向下滑动且自身可以向下滚动
        int delta = Math.min(-dy, getHeight() - getScrollY() - getChildAt(0).getHeight());
        scrollBy(0, delta);
        consumed[1] = -delta;
    }
}

在这个实现中,canScrollVertically方法用于判断 NestedScrollView 是否可以在垂直方向上滚动,getScrollY方法用于获取当前的滚动位置,scrollBy方法用于实际执行滚动操作。

  1. 子视图进行自身滑动:RecyclerView 根据父视图消费后的剩余滑动距离(dx 和 dy)进行自身的滑动处理。RecyclerView 会调用自身的scrollBy或scrollTo方法来实现滑动。例如:
java 复制代码
// RecyclerView根据剩余距离进行滑动
scrollBy(dx, dy);
  1. 通知父视图滑动结果:RecyclerView 在完成自身的滑动后,会调用dispatchNestedScroll方法,将已消费的滑动距离(dxConsumed 和 dyConsumed)和未消费的滑动距离(dxUnconsumed 和 dyUnconsumed)传递给父视图 NestedScrollView,让父视图进行进一步的处理。dispatchNestedScroll方法的定义如下:
java 复制代码
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)

其中,dxConsumed和dyConsumed分别表示子视图在水平和垂直方向上已经消费的滑动距离;dxUnconsumed和dyUnconsumed分别表示子视图在水平和垂直方向上未消费的滑动距离;offsetInWindow的作用与dispatchNestedPreScroll方法中的相同。

RecyclerView 中dispatchNestedScroll方法的简化实现如下:

java 复制代码
// RecyclerView类中的dispatchNestedScroll方法简化示意
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 通知父视图滑动结果
        ViewParentCompat.onNestedScroll(mNestedScrollingParent, this, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        return true;
    }
    return false;
}
  1. 父视图处理滑动结果:NestedScrollView 在接收到onNestedScroll方法的调用时,会根据子视图传递过来的滑动信息进行相应的处理。例如,如果子视图滑动到了边界,并且还有未消费的滑动距离,NestedScrollView 可能会继续滑动以消耗这些剩余距离。NestedScrollView 中onNestedScroll方法的简化实现如下:
java 复制代码
// NestedScrollView类中的onNestedScroll方法简化示意
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    if (dyUnconsumed != 0 && canScrollVertically(dyUnconsumed > 0? 1 : -1)) {
        // 子视图有未消费的垂直方向滑动距离且自身可以在该方向滚动
        int delta = Math.min(dyUnconsumed, dyUnconsumed > 0? getHeight() - getScrollY() - getChildAt(0).getHeight() : getScrollY());
        scrollBy(0, dyUnconsumed > 0? delta : -delta);
    }
}
3.2.3 ACTION_UP 阶段

当用户手指离开屏幕时,会产生一个 ACTION_UP 事件。RecyclerView 在接收到 ACTION_UP 事件后,会进行以下操作:

  1. 停止嵌套滑动流程:RecyclerView 会调用stopNestedScroll方法,通知父视图 NestedScrollView 嵌套滑动结束。stopNestedScroll方法会将相关的嵌套滑动状态清空,例如将mNestedScrollingParent设置为 null。RecyclerView 中stopNestedScroll方法的实现如下:
java 复制代码
// RecyclerView类中的stopNestedScroll方法
public void stopNestedScroll() {
    mNestedScrollingParent = null;
}
  1. 处理惯性滑动(如果有) :如果用户在滑动过程中产生了惯性滑动(fling),RecyclerView 会根据惯性滑动的速度和方向,调用dispatchNestedPreFling和dispatchNestedFling方法与父视图进行交互,以实现连贯的惯性滑动效果。这两个方法的作用与dispatchNestedPreScroll和dispatchNestedScroll方法类似,分别用于在惯性滑动前和惯性滑动后与父视图进行通信。

例如,dispatchNestedPreFling方法用于在子视图执行惯性滑动之前,将惯性滑动的速度(velocityX 和 velocityY)传递给父视图,询问父视图是否需要处理惯性滑动:

java 复制代码
// RecyclerView类中的dispatchNestedPreFling方法简化示意
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, this, velocityX, velocityY);
    }
    return false;
}

dispatchNestedFling方法用于在子视图执行惯性滑动之后,将惯性滑动的速度和子视图是否消费了惯性滑动(consumed)的信息传递给父视图,让父视图进行进一步的处理:

java 复制代码
// RecyclerView类中的dispatchNestedFling方法简化示意
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        return ViewParentCompat.onNestedFling(mNestedScrollingParent, this, velocityX, velocityY, consumed);
    }
    return false;
}

NestedScrollView 在接收到onNestedPreFling和onNestedFling方法的调用时,会根据自身的状态和业务逻辑来决定是否参与惯性滑动的处理。如果 NestedScrollView 需要处理惯性滑动,它会根据惯性滑动的速度和方向,以及自身的滚动边界等条件,来决定如何响应惯性滑动。例如,当 NestedScrollView 已经滑动到顶部或底部边界时,它可能会阻止惯性滑动的继续进行,或者根据惯性滑动的速度来调整自身的滚动位置。

通过以上详细的流程分析,我们可以清晰地看到在 RecyclerView 嵌套在 NestedScrollView 中的场景下,嵌套滑动事件是如何在子视图和父视图之间进行分发和处理的。这种机制使得子视图和父视图能够协同工作,实现更加流畅和自然的滑动交互效果。

四、源码深度解析

4.1 RecyclerView 中的嵌套滑动实现

RecyclerView 是 Android 开发中常用的列表展示控件,它对嵌套滑动的支持使得在复杂布局中能够实现流畅的滑动效果。下面我们从源码层面深入分析 RecyclerView 中嵌套滑动的实现原理。

4.1.1 onTouchEvent 方法分析

RecyclerView 的onTouchEvent方法是处理触摸事件的核心方法,其中包含了嵌套滑动相关的逻辑。下面是onTouchEvent方法中与嵌套滑动相关的主要代码片段及逐行分析:

java 复制代码
@Override
public boolean onTouchEvent(MotionEvent e) {
    // 获取当前触摸事件的动作
    final int action = e.getActionMasked();
    // 获取当前触摸点的ID
    final int index = e.findPointerIndex(mScrollPointerId);
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 记录当前触摸点的ID
            mScrollPointerId = e.getPointerId(0);
            // 记录初始触摸点的X坐标
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            // 记录初始触摸点的Y坐标
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
            // 初始化嵌套滚动轴
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            // 判断RecyclerView是否可以水平滚动
            if (canScrollHorizontally) {
                // 如果可以水平滚动,添加水平滚动轴标志
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            // 判断RecyclerView是否可以垂直滚动
            if (canScrollVertically) {
                // 如果可以垂直滚动,添加垂直滚动轴标志
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            // 开始嵌套滚动,查找愿意响应的父视图
            startNestedScroll(nestedScrollAxis, ViewCompat.TYPE_TOUCH);
            break;
        case MotionEvent.ACTION_MOVE:
            if (index < 0) {
                // 如果找不到当前触摸点的索引,打印错误日志并返回false
                Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                return false;
            }
            // 获取当前触摸点的X坐标
            final int x = (int) (e.getX(index) + 0.5f);
            // 获取当前触摸点的Y坐标
            final int y = (int) (e.getY(index) + 0.5f);
            // 计算水平方向的滑动距离
            int dx = mLastTouchX - x;
            // 计算垂直方向的滑动距离
            int dy = mLastTouchY - y;
            // 分发嵌套预滚动事件,通知父视图即将滑动
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
                // 如果父视图处理了预滚动事件,调整滑动距离
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                // 调整触摸事件的位置,考虑父视图滑动导致的偏移
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // 更新嵌套偏移量
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
            // 执行内部滚动操作
            if (scrollByInternal(canScrollHorizontally? dx : 0, canScrollVertically? dy : 0, vtev)) {
                // 如果滚动成功,请求父视图不拦截触摸事件
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            // 更新上一次触摸点的坐标
            mLastTouchX = x - mScrollOffset[0];
            mLastTouchY = y - mScrollOffset[1];
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            // 停止嵌套滚动
            stopNestedScroll(ViewCompat.TYPE_TOUCH);
            // 重置滚动相关的变量
            mScrollPointerId = INVALID_POINTER;
            mInitialTouchX = mLastTouchX = 0;
            mInitialTouchY = mLastTouchY = 0;
            break;
    }
    return true;
}

在ACTION_DOWN阶段:

  • 记录触摸点的 ID、初始坐标,以便后续计算滑动距离。

  • 根据 RecyclerView 是否支持水平和垂直滚动,确定嵌套滚动轴的方向。

  • 调用startNestedScroll方法,开启嵌套滑动流程,寻找愿意响应嵌套滑动的父视图。如果找到,父视图会进行相应的初始化操作,为后续的嵌套滑动交互做好准备。

在ACTION_MOVE阶段:

  • 计算本次滑动的水平和垂直距离dx和dy。

  • 调用dispatchNestedPreScroll方法,将滑动距离信息传递给父视图,询问父视图是否需要在子视图滑动之前进行部分滑动处理。如果父视图处理了预滚动事件,它会将消费的距离记录在mScrollConsumed数组中返回,RecyclerView 会根据父视图消费的距离调整自己的滑动距离dx和dy。同时,更新触摸事件的位置,以适应父视图滑动导致的子视图位置变化,并更新嵌套偏移量。

  • 调用scrollByInternal方法执行 RecyclerView 自身的滑动操作。如果滑动成功,请求父视图不拦截触摸事件,确保后续的滑动事件能够继续由 RecyclerView 处理。

  • 更新上一次触摸点的坐标,以便下一次计算滑动距离。

在ACTION_UP和ACTION_CANCEL阶段:

  • 调用stopNestedScroll方法,停止嵌套滑动流程,通知父视图嵌套滑动结束。

  • 重置滚动相关的变量,为下一次滑动操作做准备。

4.1.2 startNestedScroll 方法解析

startNestedScroll方法用于开始嵌套滑动流程,它的主要作用是查找愿意响应嵌套滑动的父视图,并进行相关的初始化操作。下面是startNestedScroll方法的源码及详细解析:

java 复制代码
public boolean startNestedScroll(int axes, int type) {
    // 获取NestedScrollingChildHelper对象
    NestedScrollingChildHelper childHelper = getScrollingChildHelper();
    // 调用NestedScrollingChildHelper的startNestedScroll方法
    return childHelper.startNestedScroll(axes, type);
}

在 RecyclerView 中,startNestedScroll方法实际上是调用了NestedScrollingChildHelper的startNestedScroll方法,因此我们来看NestedScrollingChildHelper中该方法的实现:

java 复制代码
public boolean startNestedScroll(int axes, int type) {
    // 如果已经存在嵌套滚动的父视图,直接返回true
    if (hasNestedScrollingParent(type)) {
        return true;
    }
    // 如果当前视图允许嵌套滚动
    if (isNestedScrollingEnabled()) {
        // 获取当前视图的父视图
        ViewParent p = mView.getParent();
        // 当前视图作为子视图
        View child = mView;
        // 向上遍历父视图层级
        while (p != null) {
            try {
                // 调用父视图的onStartNestedScroll方法,询问是否接受嵌套滚动
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    // 如果父视图接受嵌套滚动,记录该父视图
                    mNestedScrollingParent[type] = p;
                    // 调用父视图的onNestedScrollAccepted方法,进行初始化操作
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
            } catch (AbstractMethodError e) {
                // 如果在调用过程中出现AbstractMethodError异常,忽略该异常继续查找
            }
            // 如果父视图是View类型,更新子视图为当前父视图
            if (p instanceof View) {
                child = (View) p;
            }
            // 获取上一级父视图
            p = p.getParent();
        }
    }
    return false;
}

在NestedScrollingChildHelper的startNestedScroll方法中:

  • 首先检查是否已经存在嵌套滚动的父视图(通过hasNestedScrollingParent(type)方法判断),如果存在,则直接返回true,表示已经在进行嵌套滚动。

  • 然后检查当前视图是否允许嵌套滚动(通过isNestedScrollingEnabled方法判断)。如果允许,则向上遍历父视图层级。

  • 在遍历过程中,通过ViewParentCompat.onStartNestedScroll方法调用每个父视图的onStartNestedScroll方法,询问父视图是否接受嵌套滚动。如果某个父视图返回true,表示它愿意接受嵌套滚动,那么将该父视图记录为mNestedScrollingParent[type],并调用ViewParentCompat.onNestedScrollAccepted方法,让父视图进行相关的初始化操作,最后返回true。

  • 如果在遍历过程中没有找到接受嵌套滚动的父视图,或者出现AbstractMethodError异常(这种情况通常是由于版本兼容性问题导致的,在低版本中可能某些方法不存在),则继续向上查找,直到找到或遍历完所有父视图,最终返回false。

4.1.3 dispatchNestedPreScroll 和 dispatchNestedScroll 方法详解

dispatchNestedPreScroll和dispatchNestedScroll方法是 RecyclerView 与父视图进行嵌套滑动交互的关键方法,它们分别用于在子视图滑动前和滑动后与父视图进行通信。

dispatchNestedPreScroll 方法

dispatchNestedPreScroll方法用于在子视图滑动之前,将滑动的距离信息传递给父视图,让父视图有机会进行部分滑动处理。下面是该方法的源码及详细解析:

java 复制代码
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
    // 获取NestedScrollingChildHelper对象
    NestedScrollingChildHelper childHelper = getScrollingChildHelper();
    // 调用NestedScrollingChildHelper的dispatchNestedPreScroll方法
    return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}

同样,RecyclerView 中的dispatchNestedPreScroll方法调用了NestedScrollingChildHelper的dispatchNestedPreScroll方法,下面是NestedScrollingChildHelper中该方法的实现:

java 复制代码
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
    // 如果当前视图允许嵌套滚动且存在嵌套滚动的父视图
    if (isNestedScrollingEnabled() && mNestedScrollingParent[type] != null) {
        // 如果dx或dy不为0,说明有滑动距离需要处理
        if (dx != 0 || dy != 0) {
            // 记录滑动前子视图在窗口中的初始位置
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }
            // 如果consumed数组为null,创建一个临时数组
            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            // 初始化consumed数组,记录父视图消费的滑动距离
            consumed[0] = 0;
            consumed[1] = 0;
            // 调用父视图的onNestedPreScroll方法,通知父视图滑动信息
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent[type], mView, dx, dy, consumed, type);
            // 如果offsetInWindow不为null,计算并更新子视图在窗口中的位置偏移量
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            // 根据consumed数组判断父视图是否消费了滑动距离,如果消费了则返回true
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            // 如果dx和dy都为0,但offsetInWindow不为null,将其清零
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

在NestedScrollingChildHelper的dispatchNestedPreScroll方法中:

  • 首先检查当前视图是否允许嵌套滚动(通过isNestedScrollingEnabled方法判断)且存在嵌套滚动的父视图(mNestedScrollingParent[type] != null)。

  • 如果dx或dy不为 0,说明有滑动距离需要处理。记录滑动前子视图在窗口中的初始位置,以便后续计算位置偏移量。

  • 如果consumed数组为 null,创建一个临时数组用于记录父视图消费的滑动距离。初始化consumed数组为 0。

  • 调用父视图的onNestedPreScroll方法,将滑动距离dx和dy传递给父视图,父视图可以根据自身的状态和业务逻辑决定是否消费部分滑动距离,并将消费的距离记录在consumed数组中返回。

  • 如果offsetInWindow不为 null,再次获取子视图在窗口中的位置,计算并更新由于父视图滑动导致的子视图位置偏移量。

  • 根据consumed数组判断父视图是否消费了滑动距离,如果consumed[0]或consumed[1]不为 0,说明父视图消费了滑动距离,返回true;否则返回false。

dispatchNestedScroll 方法

dispatchNestedScroll方法用于在子视图滑动之后,将已消费和未消费的滑动距离信息传递给父视图,让父视图进行进一步的处理。下面是该方法的源码及详细解析:

java 复制代码
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
    // 获取NestedScrollingChildHelper对象
    NestedScrollingChildHelper childHelper = getScrollingChildHelper();
    // 调用NestedScrollingChildHelper的dispatchNestedScroll方法
    return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}

RecyclerView 中的dispatchNestedScroll方法调用了NestedScrollingChildHelper的dispatchNestedScroll方法,下面是NestedScrollingChildHelper中该方法的实现:

java 复制代码
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
    // 如果当前视图允许嵌套滚动且存在嵌套滚动的父视图
    if (isNestedScrollingEnabled() && mNestedScrollingParent[type] != null) {
        // 调用父视图的onNestedScroll方法,通知父视图滑动结果
        ViewParentCompat.onNestedScroll(mNestedScrollingParent[type], mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        // 如果offsetInWindow不为null,更新子视图在窗口中的位置偏移量(这里假设父视图滑动会影响子视图位置,实际计算逻辑可能更复杂,此处简化示意)
        if (offsetInWindow != null) {
            mView.getLocationInWindow(offsetInWindow);
            // 假设父视图滑动导致子视图位置变化,这里简单更新偏移量,实际可能需要更精确计算
            offsetInWindow[0] += dxUnconsumed;
            offsetInWindow[1] += dyUnconsumed;
        }
        return true;
    }
    return false;
}

在NestedScrollingChildHelper的dispatchNestedScroll方法中:

  • 首先检查当前视图是否允许嵌套滚动(通过isNestedScrollingEnabled方法判断)且存在嵌套滚动的父视图(mNestedScrollingParent[type] != null)。

  • 调用父视图的onNestedScroll方法,将子视图已消费的滑动距离dxConsumed和dyConsumed以及未消费的滑动距离dxUnconsumed和dyUnconsumed传递给父视图,父视图可以根据这些信息进行进一步的处理,比如继续滑动以消耗剩余的未消费距离。

  • 如果offsetInWindow不为 null,更新子视图在窗口中的位置偏移量。这里假设父视图滑动会影响子视图位置,简单地根据未消费的滑动距离更新偏移量,实际应用中可能需要更精确的计算逻辑来确定子视图位置的变化。

  • 如果成功调用了父视图的onNestedScroll方法,返回true;否则返回false。

结合实际滑动场景,当用户在 RecyclerView 上滑动时,dispatchNestedPreScroll方法会在 RecyclerView 滑动之前被调用,它将滑动距离信息传递给父视图,父视图可以根据自身的状态(比如是否已经滑动到边界)来决定是否消费部分滑动距离。如果父视图消费了部分距离,RecyclerView 会根据剩余的距离进行自身的滑动处理。在 RecyclerView 滑动结束后,dispatchNestedScroll方法会被调用,它将 RecyclerView 已消费和未消费的滑动距离传递给父视图,父视图可以根据这些信息进行进一步的处理,比如继续滑动以消耗剩余的未消费

五、嵌套滑动的应用场景与实践案例

5.1 吸顶效果的实现

吸顶效果是一种在应用中常见的交互设计,它可以提高用户体验,让用户在滚动页面时能够始终方便地访问重要信息。在 Android 开发中,通过嵌套滑动机制可以实现吸顶效果。下面我们将详细说明如何通过嵌套滑动实现吸顶效果,并给出具体的布局文件和代码实现,同时分析关键代码的逻辑。

布局文件

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    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="match_parent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <!-- 这里可以放置需要在滑动过程中折叠的头部视图,比如图片等 -->
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:src="@drawable/header_image"
                app:layout_collapseMode="parallax" />
            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tab_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:tabMode="fixed"
                app:tabTextColor="#000000"
                app:tabSelectedTextColor="#FF0000"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <!-- ViewPager的内容页面,这里可以是Fragment等 -->
    </androidx.viewpager.widget.ViewPager>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

在这个布局文件中:

  • CoordinatorLayout是整个布局的根容器,它提供了一种强大的协调子视图交互的机制,非常适合实现嵌套滑动和各种复杂的布局效果。

  • AppBarLayout用于管理应用栏相关的视图,它可以根据滑动事件来控制其子视图的显示和隐藏,以及动画效果。

  • CollapsingToolbarLayout是一个特殊的布局,它可以实现折叠和展开的效果。app:layout_scrollFlags="scroll|exitUntilCollapsed"表示该布局在滑动时会跟随滚动,并且在折叠时会逐渐消失,直到完全折叠。app:layout_collapseMode="parallax"用于设置图片在折叠时的视差效果,使图片的滚动速度与其他视图不同,增加视觉层次感。

  • TabLayout是一个选项卡布局,用于显示多个标签页。app:layout_collapseMode="pin"表示该布局在滑动时会固定在顶部,实现吸顶效果。

  • ViewPager用于展示多个页面,app:layout_behavior="@string/appbar_scrolling_view_behavior"表示它与AppBarLayout之间存在关联,能够根据AppBarLayout的滑动状态来调整自身的位置和显示效果。

代码实现

java 复制代码
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
public class MainActivity extends AppCompatActivity {
    private TabLayout tabLayout;
    private ViewPager viewPager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tabLayout = findViewById(R.id.tab_layout);
        viewPager = findViewById(R.id.view_pager);
        // 创建并设置ViewPager的适配器,这里假设ViewPagerAdapter已经实现
        ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
        viewPager.setAdapter(adapter);
        // 将TabLayout与ViewPager关联起来,实现选项卡和页面的同步切换
        tabLayout.setupWithViewPager(viewPager);
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        // 处理菜单点击事件
        return super.onOptionsItemSelected(item);
    }
}

在代码实现中:

  • 在onCreate方法中,首先通过findViewById方法获取布局文件中的TabLayout和ViewPager实例。

  • 然后创建一个ViewPagerAdapter并设置给ViewPager,ViewPagerAdapter负责管理ViewPager的页面数据和显示逻辑。

  • 最后调用tabLayout.setupWithViewPager(viewPager)方法,将TabLayout与ViewPager关联起来,这样当用户滑动ViewPager时,TabLayout的选项卡会同步切换,反之亦然。

关键代码逻辑分析

  1. 滑动事件的传递与处理:当用户滑动ViewPager时,ViewPager会首先处理滑动事件。由于ViewPager是嵌套在CoordinatorLayout中的,并且设置了app:layout_behavior="@string/appbar_scrolling_view_behavior",所以ViewPager在处理滑动事件时,会与AppBarLayout进行交互。ViewPager会根据滑动的距离和方向,通知AppBarLayout进行相应的折叠或展开操作。

  2. 吸顶效果的实现原理:TabLayout的吸顶效果是通过CollapsingToolbarLayout的app:layout_collapseMode="pin"属性实现的。当CollapsingToolbarLayout开始折叠时,TabLayout会逐渐固定在顶部,不再跟随CollapsingToolbarLayout一起折叠。这是因为pin模式会使TabLayout在折叠过程中保持在顶部位置,直到CollapsingToolbarLayout完全折叠或展开。

  3. 选项卡与页面的同步机制:tabLayout.setupWithViewPager(viewPager)方法实现了选项卡和页面的同步切换。在这个方法内部,TabLayout会监听ViewPager的页面切换事件,当ViewPager切换到新的页面时,TabLayout会自动选中对应的选项卡;同时,当用户点击TabLayout的选项卡时,ViewPager也会切换到对应的页面。这种同步机制通过TabLayout和ViewPager之间的事件监听和回调实现,确保了用户操作的一致性和流畅性。

通过以上布局文件和代码实现,以及对关键代码逻辑的分析,我们成功地通过嵌套滑动机制实现了吸顶效果。这种吸顶效果不仅提升了用户体验,还使应用的界面更加美观和易用。

5.2 多层嵌套滑动的处理

在实际的 Android 应用开发中,经常会遇到多层嵌套滑动的场景,例如 RecyclerView 嵌套在 NestedScrollView 中,而 NestedScrollView 又嵌套在 CoordinatorLayout 中。这种多层嵌套的结构可以实现复杂的界面交互效果,但同时也增加了滑动处理的难度。下面我们将以这种常见的多层嵌套结构为例,深入分析多层嵌套滑动的处理机制和实现方法。

布局文件

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    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="match_parent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <!-- 头部视图,例如图片等 -->
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:src="@drawable/header_image"
                app:layout_collapseMode="parallax" />
            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tab_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:tabMode="fixed"
                app:tabTextColor="#000000"
                app:tabSelectedTextColor="#FF0000"
                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">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!-- 其他视图,例如TextView等 -->
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Some text above RecyclerView"
                android:padding="16dp" />
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
            <!-- 更多视图 -->
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

在这个布局文件中:

  • CoordinatorLayout作为根布局,负责协调子视图之间的交互,特别是在嵌套滑动方面。

  • AppBarLayout包含一个CollapsingToolbarLayout,用于实现头部的折叠和展开效果,以及TabLayout的吸顶效果,这部分与前面吸顶效果实现中的布局类似。

  • NestedScrollView作为中间层,它可以嵌套其他可滚动的视图,并且支持嵌套滑动。通过app:layout_behavior="@string/appbar_scrolling_view_behavior"与AppBarLayout关联,在滑动时会与AppBarLayout协同工作。

  • RecyclerView作为最内层的可滚动视图,用于展示列表数据。它嵌套在NestedScrollView中,需要与NestedScrollView和CoordinatorLayout进行嵌套滑动的交互。

代码实现

java 复制代码
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
    private TabLayout tabLayout;
    private RecyclerView recyclerView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tabLayout = findViewById(R.id.tab_layout);
        recyclerView = findViewById(R.id.recycler_view);
        // 设置RecyclerView的布局管理器和适配器
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        List<String> data = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            data.add("Item " + i);
        }
        RecyclerViewAdapter adapter = new RecyclerViewAdapter(data);
        recyclerView.setAdapter(adapter);
        // 这里可以添加TabLayout与ViewPager关联的代码(如果有ViewPager的话)
    }
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        // 处理菜单点击事件
        return super.onOptionsItemSelected(item);
    }
}

在代码实现中:

  • 在onCreate方法中,首先获取布局文件中的TabLayout和RecyclerView实例。

  • 然后为RecyclerView设置LinearLayoutManager作为布局管理器,并创建一个包含数据的列表。这里通过循环生成了 50 个字符串数据,每个字符串表示列表中的一个项目。

  • 创建RecyclerViewAdapter并将其设置给RecyclerView,RecyclerViewAdapter负责管理RecyclerView的视图和数据绑定逻辑。

多层嵌套滑动的处理机制分析

  1. 滑动事件的传递顺序:当用户进行滑动操作时,触摸事件首先会传递到最外层的CoordinatorLayout。CoordinatorLayout会根据其内部的Behavior机制来决定是否拦截事件。在这个例子中,由于NestedScrollView设置了app:layout_behavior="@string/appbar_scrolling_view_behavior",CoordinatorLayout会将事件传递给NestedScrollView。

  2. NestedScrollView 与 RecyclerView 的交互:NestedScrollView在接收到滑动事件后,会首先调用RecyclerView的dispatchNestedPreScroll方法,通知RecyclerView即将进行滑动,并将滑动的距离等信息传递给RecyclerView。RecyclerView可以根据自身的状态(例如是否滑动到边界)来决定是否消费部分滑动距离。如果RecyclerView消费了部分距离,它会将消费的距离记录在consumed数组中返回给NestedScrollView。

  3. RecyclerView 的滑动处理:RecyclerView在接收到dispatchNestedPreScroll的通知后,会根据自身的逻辑进行滑动处理。如果RecyclerView没有滑动到边界,它会消费部分滑动距离,并更新自身的滚动位置。然后,RecyclerView会调用dispatchNestedScroll方法,将已消费的滑动距离和未消费的滑动距离传递给NestedScrollView。

  4. NestedScrollView 的后续处理:NestedScrollView在接收到RecyclerView传递过来的滑动信息后,会根据自身的状态(例如是否滑动到边界)来决定是否继续滑动。如果RecyclerView已经滑动到边界,并且还有未消费的滑动距离,NestedScrollView会继续滑动以消耗这些剩余距离。同时,NestedScrollView还会与AppBarLayout进行交互,根据滑动的距离和方向来控制AppBarLayout的折叠和展开。

  5. 与 AppBarLayout 的协同工作:在整个滑动过程中,NestedScrollView会与AppBarLayout协同工作。当NestedScrollView向上滑动时,如果AppBarLayout还没有完全折叠,AppBarLayout会随着NestedScrollView的滑动而逐渐折叠;当NestedScrollView向下滑动时,如果AppBarLayout已经折叠,它会随着NestedScrollView的滑动而逐渐展开。这种协同工作通过CoordinatorLayout的Behavior机制实现,确保了整个界面在滑动过程中的流畅性和一致性。

通过以上布局文件和代码实现,以及对多层嵌套滑动处理机制的分析,我们可以看到在这种复杂的多层嵌套结构中,各个视图之间通过嵌套滑动机制实现了协同工作,为用户提供了流畅的滑动体验和丰富的界面交互效果。在实际开发中,开发者需要深入理解这些机制,以便根据具体的需求进行灵活的布局和交互设计。

六、总结与展望

6.1 嵌套滑动原理的总结

通过前面的深入分析,我们对 Android View 的嵌套滑动原理有了全面且深入的理解。嵌套滑动的核心在于子视图和父视图之间的协同工作,打破了传统事件分发的单向模式,实现了双向通信和交互。

从相关接口和类来看,NestedScrollingChild和NestedScrollingParent接口分别定义了子视图和父视图在嵌套滑动中的职责和交互方法。NestedScrollingChildHelper和NestedScrollingParentHelper则为实现这些接口提供了便捷的辅助功能,大大简化了开发过程。例如,NestedScrollingChildHelper封装了与父视图交互的大部分逻辑,使得子视图在实现嵌套滑动时只需简单调用其方法即可完成复杂的操作,如startNestedScroll方法通过向上遍历父视图层级,寻找愿意响应嵌套滑动的父视图,并进行相关初始化操作,为后续的嵌套滑动交互奠定基础。

在事件分发流程方面,嵌套滑动事件的分发与传统事件分发有显著区别。在 ACTION_DOWN 阶段,子视图会开启嵌套滑动流程,寻找合适的父视图;ACTION_MOVE 阶段,子视图会先通知父视图预滑动,父视图根据自身状态决定是否消费部分滑动距离,子视图再根据父视图的处理结果进行自身滑动,并在滑动后通知父视图滑动结果;ACTION_UP 阶段,子视图停止嵌套滑动流程,并处理惯性滑动(如果有)。以 RecyclerView 嵌套在 NestedScrollView 中的场景为例,RecyclerView 在滑动过程中,通过dispatchNestedPreScroll和dispatchNestedScroll方法与 NestedScrollView 进行紧密交互,实现了流畅的滑动效果。

理解嵌套滑动原理对于 Android 开发至关重要。它能够帮助开发者解决复杂布局中的滑动冲突问题,实现各种炫酷的交互效果,如吸顶效果、多层嵌套滑动等。在实际开发中,掌握嵌套滑动原理可以让开发者更加灵活地设计界面,提升应用的用户体验,使应用在众多竞品中脱颖而出。

6.2 未来发展展望

随着 Android 技术的不断发展,嵌套滑动机制也有望在多个方面取得进一步的突破和改进。

在性能优化方面,未来可能会出现更高效的算法和数据结构来处理嵌套滑动中的事件分发和视图更新。例如,通过优化滑动过程中的计算逻辑,减少不必要的计算开销,提高滑动的流畅性和响应速度。同时,对于内存管理的优化也可能成为重点,确保在复杂的嵌套滑动场景下,应用能够合理地使用内存,避免内存泄漏和卡顿现象的发生。

在功能扩展方面,嵌套滑动机制可能会与其他新兴技术相结合,创造出更加丰富和强大的交互体验。例如,与虚拟现实(VR)和增强现实(AR)技术结合,实现沉浸式的嵌套滑动效果,让用户在虚拟环境中也能感受到自然流畅的滑动交互。此外,随着人工智能(AI)技术的发展,嵌套滑动机制可能会具备智能感知用户操作意图的能力,根据用户的使用习惯和当前场景,自动调整滑动的行为和效果,提供更加个性化的交互体验。

对于开发者来说,持续关注和学习嵌套滑动相关技术是跟上技术发展潮流的关键。随着嵌套滑动机制的不断演进,新的特性和功能将不断涌现,开发者需要及时掌握这些变化,以便在开发中充分利用它们。同时,开发者还可以积极参与到相关技术的讨论和开源项目中,与其他开发者交流经验,共同推动嵌套滑动技术的发展。

Android View 的嵌套滑动原理是 Android 开发中一个重要且富有挑战性的领域。通过深入理解其原理和不断关注其发展动态,开发者能够为用户带来更加优质、流畅和创新的应用体验。

相关推荐
蒟蒻小袁1 小时前
力扣面试150题--有效的括号和简化路径
算法·leetcode·面试
_一条咸鱼_3 小时前
Python 名称空间与作用域深度剖析(二十七)
人工智能·python·面试
_一条咸鱼_3 小时前
Python之函数对象+函数嵌套(二十六)
人工智能·python·面试
_一条咸鱼_3 小时前
Python 文件操作之修改(二十二)
人工智能·python·面试
_一条咸鱼_3 小时前
Python 闭包函数:原理、应用与深度解析(二十八)
人工智能·python·面试
Ya-Jun3 小时前
常用第三方库精讲:cached_network_image图片加载优化
android·flutter
_一条咸鱼_3 小时前
Python 之文件处理编码字符(二十)
人工智能·python·面试
_一条咸鱼_3 小时前
Python 装饰器:代码功能的优雅增强(二十九)
人工智能·python·面试
_一条咸鱼_3 小时前
Python 文件处理(二十一)
人工智能·python·面试