深度揭秘:Android NestedScrollView 拖动原理全解析

深度揭秘:Android NestedScrollView 拖动原理全解析

一、引言

在 Android 开发的广袤天地中,用户界面的流畅交互是吸引用户的关键要素。NestedScrollView 作为 Android 系统里极为重要的滚动视图组件,它具备处理嵌套滚动的强大能力,能够让用户在界面上实现平滑的滚动操作。理解 NestedScrollView 的拖动原理,不仅有助于开发者解决实际开发中遇到的滚动问题,还能助力开发者打造出更加优质、流畅的用户界面。本文将深入到 NestedScrollView 的源码层面,全面剖析其拖动原理,带领大家领略其背后的精妙机制。

二、NestedScrollView 概述

2.1 基本概念

NestedScrollView 是 Android 提供的一个支持嵌套滚动的滚动视图组件,它继承自 FrameLayout,因此可以包含一个子视图,并且允许这个子视图的高度超过 NestedScrollView 自身的高度,从而实现滚动效果。与普通的 ScrollView 不同的是,NestedScrollView 能够与其他支持嵌套滚动的视图(如 RecyclerView)协同工作,处理复杂的滚动场景,避免滚动冲突,为用户带来流畅的滚动体验。

2.2 应用场景

NestedScrollView 在实际开发中有着广泛的应用场景,例如:

  • 长列表与头部布局的组合 :在很多应用中,页面顶部可能有一个固定的头部布局,下方是一个长列表。使用 NestedScrollView 可以实现头部布局随着列表的滚动而产生相应的变化,如渐变、缩放等效果,提升用户体验。
  • 嵌套滚动的界面设计 :当界面中存在多个可滚动的视图嵌套时,NestedScrollView 能够协调这些视图之间的滚动操作,确保滚动的流畅性和一致性。例如,在一个包含 RecyclerViewNestedScrollView 中,用户可以在不同的滚动区域之间平滑切换。

三、NestedScrollView 的继承关系与接口实现

3.1 继承关系

NestedScrollView 继承自 FrameLayout,这意味着它具备 FrameLayout 的所有特性,并且可以作为一个容器来包含其他视图。以下是 NestedScrollView 继承关系的代码示例:

java 复制代码
// NestedScrollView 类继承自 FrameLayout
public class NestedScrollView extends FrameLayout implements NestedScrollingParent2, NestedScrollingChild2 {
    // 类的具体实现
    // ...
}

通过继承 FrameLayoutNestedScrollView 可以利用 FrameLayout 的布局特性来管理其子视图的布局。

3.2 接口实现

NestedScrollView 实现了 NestedScrollingParent2NestedScrollingChild2 接口,这两个接口是 Android 提供的用于处理嵌套滚动的关键接口。

  • NestedScrollingParent2 接口 :该接口定义了作为嵌套滚动父视图的一系列方法,用于处理子视图在滚动过程中传递的滚动信息。以下是 NestedScrollingParent2 接口的部分方法:
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 onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type);
    // 嵌套滚动过程中,父视图处理滚动
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type);
    // 结束嵌套滚动
    void onStopNestedScroll(@NonNull View target, int type);
}
  • NestedScrollingChild2 接口 :该接口定义了作为嵌套滚动子视图的一系列方法,用于向父视图传递滚动信息。以下是 NestedScrollingChild2 接口的部分方法:
java 复制代码
// NestedScrollingChild2 接口定义
public interface NestedScrollingChild2 extends NestedScrollingChild {
    // 开始嵌套滚动
    boolean startNestedScroll(int axes, int type);
    // 停止嵌套滚动
    void stopNestedScroll(int type);
    // 判断是否有嵌套滚动的父视图
    boolean hasNestedScrollingParent(int type);
    // 分发预滚动信息给父视图
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type);
    // 分发滚动信息给父视图
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type);
}

通过实现这两个接口,NestedScrollView 能够与其他支持嵌套滚动的视图进行有效的通信和协作,实现复杂的滚动效果。

四、NestedScrollView 的初始化与布局

4.1 构造函数

NestedScrollView 提供了多个构造函数,用于在不同的场景下初始化 NestedScrollView 对象。以下是其中一个构造函数的代码示例:

java 复制代码
// NestedScrollView 的构造函数
public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    // 调用父类 FrameLayout 的构造函数
    super(context, attrs, defStyleAttr);

    // 初始化滚动条
    initScrollbars(context);
    // 初始化嵌套滚动相关的成员变量
    initNestedScrolling();
    // 初始化滚动监听器
    mScrollListener = new ScrollListener();
}

在构造函数中,首先调用父类 FrameLayout 的构造函数进行基本的初始化操作。然后,调用 initScrollbars 方法初始化滚动条,initNestedScrolling 方法初始化嵌套滚动相关的成员变量,最后创建一个 ScrollListener 对象用于监听滚动事件。

4.2 测量与布局

4.2.1 测量过程

在测量阶段,NestedScrollView 会根据父容器传递的测量规格(MeasureSpec)来确定自己的大小。以下是 NestedScrollViewonMeasure 方法的代码示例:

java 复制代码
// NestedScrollView 的 onMeasure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 调用父类的 onMeasure 方法进行基本的测量操作
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 获取子视图的数量
    final int count = getChildCount();
    if (count > 1) {
        // 如果子视图数量大于 1,抛出异常,因为 NestedScrollView 只能包含一个子视图
        throw new IllegalStateException("NestedScrollView can host only one direct child");
    }
    if (count == 1) {
        // 如果有子视图
        final View child = getChildAt(0);
        if (getMeasureMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
            // 如果高度测量模式为 UNSPECIFIED
            final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            // 测量子视图的高度
            child.measure(widthMeasureSpec, childHeightMeasureSpec);
        } else {
            // 如果高度测量模式不为 UNSPECIFIED
            int heightPadding = getPaddingTop() + getPaddingBottom();
            int height = getMeasuredHeight() - heightPadding;
            if (height < 0) {
                // 如果高度小于 0,将高度设为 0
                height = 0;
            }
            final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
            // 测量子视图的高度
            child.measure(widthMeasureSpec, childHeightMeasureSpec);
        }
    }
    // 计算滚动范围
    computeScrollRange();
    // 计算滚动条的大小
    computeScrollBarSizes();
}

onMeasure 方法中,首先调用父类的 onMeasure 方法进行基本的测量操作。然后,检查子视图的数量,如果子视图数量大于 1,则抛出异常,因为 NestedScrollView 只能包含一个子视图。接着,根据高度测量模式的不同,对唯一的子视图进行测量。最后,调用 computeScrollRange 方法计算滚动范围,调用 computeScrollBarSizes 方法计算滚动条的大小。

4.2.2 布局过程

在布局阶段,NestedScrollView 会根据测量结果来确定子视图的位置。以下是 NestedScrollViewonLayout 方法的代码示例:

java 复制代码
// NestedScrollView 的 onLayout 方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 调用父类的 onLayout 方法进行基本的布局操作
    super.onLayout(changed, l, t, r, b);
    // 检查是否需要调整滚动位置
    ensureScrollYRange();
    // 计算滚动范围
    computeScrollRange();
    // 计算滚动条的大小
    computeScrollBarSizes();
    // 调整滚动条的位置
    adjustScrollbars();
    // 检查滚动位置是否发生变化
    if (mScrollY != mLastScrollY) {
        // 如果滚动位置发生变化,调用滚动监听器的 onScrollChange 方法
        mScrollListener.onScrollChange(this, mScrollX, mScrollY, mLastScrollX, mLastScrollY);
        mLastScrollX = mScrollX;
        mLastScrollY = mScrollY;
    }
}

onLayout 方法中,首先调用父类的 onLayout 方法进行基本的布局操作。然后,调用 ensureScrollYRange 方法检查是否需要调整滚动位置,调用 computeScrollRange 方法计算滚动范围,调用 computeScrollBarSizes 方法计算滚动条的大小,调用 adjustScrollbars 方法调整滚动条的位置。最后,检查滚动位置是否发生变化,如果发生变化,则调用滚动监听器的 onScrollChange 方法。

五、NestedScrollView 的触摸事件处理

5.1 事件分发

NestedScrollView 的事件分发过程是处理触摸事件的第一步。以下是 NestedScrollViewdispatchTouchEvent 方法的代码示例:

java 复制代码
// NestedScrollView 的 dispatchTouchEvent 方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 如果嵌套滚动启用
    if (isNestedScrollingEnabled()) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 当触摸事件为 ACTION_DOWN 时
                mLastMotionY = (int) ev.getY();
                // 开始嵌套滚动,指定滚动方向为垂直方向
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 当触摸事件为 ACTION_UP 或 ACTION_CANCEL 时
                // 停止嵌套滚动
                stopNestedScroll(ViewCompat.TYPE_TOUCH);
                break;
        }
    }
    // 调用父类的 dispatchTouchEvent 方法继续分发事件
    return super.dispatchTouchEvent(ev);
}

dispatchTouchEvent 方法中,首先检查嵌套滚动是否启用。如果启用,当触摸事件为 ACTION_DOWN 时,记录触摸点的纵坐标,并调用 startNestedScroll 方法开始嵌套滚动,指定滚动方向为垂直方向。当触摸事件为 ACTION_UPACTION_CANCEL 时,调用 stopNestedScroll 方法停止嵌套滚动。最后,调用父类的 dispatchTouchEvent 方法继续分发事件。

5.2 事件拦截

NestedScrollView 的事件拦截机制用于决定是否拦截触摸事件。以下是 NestedScrollViewonInterceptTouchEvent 方法的代码示例:

java 复制代码
// NestedScrollView 的 onInterceptTouchEvent 方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 获取触摸事件的动作
    final int action = ev.getActionMasked();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        // 如果触摸事件为 ACTION_MOVE 且正在被拖动
        return true;
    }
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // 当触摸事件为 ACTION_DOWN 时
            mLastMotionY = (int) ev.getY();
            mIsBeingDragged = false;
            // 检查是否有嵌套滚动的父视图
            final ViewParent parent = getParent();
            if (parent != null) {
                // 请求父视图不要拦截触摸事件
                parent.requestDisallowInterceptTouchEvent(true);
            }
            // 检查是否可以滚动
            if (canScrollVertically(1) || canScrollVertically(-1)) {
                // 如果可以滚动,初始化 Scroller
                mScroller.forceFinished(true);
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            // 当触摸事件为 ACTION_MOVE 时
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                // 如果活动指针 ID 无效,跳出
                break;
            }
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            if (pointerIndex == -1) {
                // 如果指针索引为 -1,跳出
                break;
            }
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop) {
                // 如果垂直方向的移动距离超过触摸阈值
                mIsBeingDragged = true;
                mLastMotionY = y;
                // 检查是否有嵌套滚动的父视图
                final ViewParent parent = getParent();
                if (parent != null) {
                    // 请求父视图不要拦截触摸事件
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        }
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            // 当触摸事件为 ACTION_UP 或 ACTION_CANCEL 时
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        }
    }
    return mIsBeingDragged;
}

onInterceptTouchEvent 方法中,首先检查触摸事件的动作。如果触摸事件为 ACTION_MOVE 且正在被拖动,则返回 true 表示拦截事件。当触摸事件为 ACTION_DOWN 时,记录触摸点的纵坐标,将 mIsBeingDragged 标志设为 false,请求父视图不要拦截触摸事件,并检查是否可以滚动,如果可以则初始化 Scroller。当触摸事件为 ACTION_MOVE 时,计算垂直方向的移动距离,如果超过触摸阈值,则将 mIsBeingDragged 标志设为 true,并请求父视图不要拦截触摸事件。当触摸事件为 ACTION_UPACTION_CANCEL 时,将 mIsBeingDragged 标志设为 false,并将活动指针 ID 设为无效。最后,返回 mIsBeingDragged 标志。

5.3 事件处理

NestedScrollView 的事件处理过程是实现滚动效果的关键。以下是 NestedScrollViewonTouchEvent 方法的代码示例:

java 复制代码
// NestedScrollView 的 onTouchEvent 方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 如果嵌套滚动启用
    if (isNestedScrollingEnabled()) {
        // 处理嵌套滚动的触摸事件
        return onTouchEventNestedScrolling(ev);
    }
    // 处理非嵌套滚动的触摸事件
    return onTouchEventNonNestedScrolling(ev);
}

// 处理嵌套滚动的触摸事件
private boolean onTouchEventNestedScrolling(MotionEvent ev) {
    final int action = ev.getActionMasked();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // 当触摸事件为 ACTION_DOWN 时
            mLastMotionY = (int) ev.getY();
            // 开始嵌套滚动,指定滚动方向为垂直方向
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            // 当触摸事件为 ACTION_MOVE 时
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                // 如果活动指针 ID 无效,跳出
                break;
            }
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            if (pointerIndex == -1) {
                // 如果指针索引为 -1,跳出
                break;
            }
            final int y = (int) ev.getY(pointerIndex);
            int dy = mLastMotionY - y;
            // 分发预滚动信息给父视图
            if (dispatchNestedPreScroll(0, dy, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
                dy -= mScrollConsumed[1];
                mLastMotionY = y - mScrollOffset[1];
            }
            if (Math.abs(dy) > mTouchSlop) {
                // 如果垂直方向的移动距离超过触摸阈值
                if (!mIsBeingDragged) {
                    mIsBeingDragged = true;
                    // 请求父视图不要拦截触摸事件
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                // 滚动视图
                scrollBy(0, dy);
                mLastMotionY = y;
            }
            break;
        }
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            // 当触摸事件为 ACTION_UP 或 ACTION_CANCEL 时
            // 停止嵌套滚动
            stopNestedScroll(ViewCompat.TYPE_TOUCH);
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        }
    }
    return true;
}

// 处理非嵌套滚动的触摸事件
private boolean onTouchEventNonNestedScrolling(MotionEvent ev) {
    final int action = ev.getActionMasked();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // 当触摸事件为 ACTION_DOWN 时
            mLastMotionY = (int) ev.getY();
            mIsBeingDragged = false;
            // 检查是否可以滚动
            if (canScrollVertically(1) || canScrollVertically(-1)) {
                // 如果可以滚动,初始化 Scroller
                mScroller.forceFinished(true);
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            // 当触摸事件为 ACTION_MOVE 时
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                // 如果活动指针 ID 无效,跳出
                break;
            }
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            if (pointerIndex == -1) {
                // 如果指针索引为 -1,跳出
                break;
            }
            final int y = (int) ev.getY(pointerIndex);
            int dy = mLastMotionY - y;
            if (Math.abs(dy) > mTouchSlop) {
                // 如果垂直方向的移动距离超过触摸阈值
                if (!mIsBeingDragged) {
                    mIsBeingDragged = true;
                    // 请求父视图不要拦截触摸事件
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                // 滚动视图
                scrollBy(0, dy);
                mLastMotionY = y;
            }
            break;
        }
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            // 当触摸事件为 ACTION_UP 或 ACTION_CANCEL 时
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        }
    }
    return true;
}

onTouchEvent 方法中,首先检查嵌套滚动是否启用。如果启用,则调用 onTouchEventNestedScrolling 方法处理嵌套滚动的触摸事件;否则,调用 onTouchEventNonNestedScrolling 方法处理非嵌套滚动的触摸事件。在 onTouchEventNestedScrolling 方法中,当触摸事件为 ACTION_DOWN 时,记录触摸点的纵坐标,并开始嵌套滚动。当触摸事件为 ACTION_MOVE 时,计算垂直方向的移动距离,分发预滚动信息给父视图,根据父视图消耗的滚动距离调整剩余的滚动距离,当垂直方向的移动距离超过触摸阈值时,将 mIsBeingDragged 标志设为 true,并滚动视图。当触摸事件为 ACTION_UPACTION_CANCEL 时,停止嵌套滚动,将 mIsBeingDragged 标志设为 false,并将活动指针 ID 设为无效。在 onTouchEventNonNestedScrolling 方法中,处理逻辑与 onTouchEventNestedScrolling 方法类似,但不涉及嵌套滚动的分发操作。

六、NestedScrollView 的滚动实现

6.1 滚动方法

NestedScrollView 提供了 scrollByscrollTo 方法来实现滚动操作。以下是这两个方法的代码示例:

java 复制代码
// NestedScrollView 的 scrollBy 方法
@Override
public void scrollBy(int x, int y) {
    // 调用 scrollTo 方法进行滚动
    scrollTo(mScrollX + x, mScrollY + y);
}

// NestedScrollView 的 scrollTo 方法
@Override
public void scrollTo(int x, int y) {
    // 获取当前的滚动范围
    int scrollRange = getScrollRange();
    if (y < 0) {
        // 如果滚动位置小于 0,将滚动位置设为 0
        y = 0;
    } else if (y > scrollRange) {
        // 如果滚动位置大于滚动范围,将滚动位置设为滚动范围
        y = scrollRange;
    }
    if (y != mScrollY) {
        // 如果滚动位置发生变化
        mScrollY = y;
        // 调用父类的 scrollTo 方法进行滚动
        super.scrollTo(x, y);
        // 调整滚动条的位置
        adjustScrollbars();
        // 检查滚动位置是否发生变化
        if (mScrollY != mLastScrollY) {
            // 如果滚动位置发生变化,调用滚动监听器的 onScrollChange 方法
            mScrollListener.onScrollChange(this, mScrollX, mScrollY, mLastScrollX, mLastScrollY);
            mLastScrollX = mScrollX;
            mLastScrollY = mScrollY;
        }
    }
}

scrollBy 方法中,调用 scrollTo 方法进行滚动,将当前的滚动位置加上偏移量。在 scrollTo 方法中,首先获取当前的滚动范围,然后检查滚动位置是否超出了滚动范围,如果超出则将滚动位置限制在滚动范围内。如果滚动位置发生变化,则更新 mScrollY 的值,调用父类的 scrollTo 方法进行滚动,调整滚动条的位置,最后检查滚动位置是否发生变化,如果发生变化则调用滚动监听器的 onScrollChange 方法。

6.2 平滑滚动

NestedScrollView 还提供了 smoothScrollBysmoothScrollTo 方法来实现平滑滚动效果。以下是这两个方法的代码示例:

java 复制代码
// NestedScrollView 的 smoothScrollBy 方法
public final void smoothScrollBy(int dx, int dy) {
    // 获取当前的滚动位置
    int scrollX = getScrollX();
    int scrollY = getScrollY();
    // 调用 smoothScrollTo 方法进行平滑滚动
    smoothScrollTo(scrollX + dx, scrollY + dy);
}

// NestedScrollView 的 smoothScrollTo 方法
public final void smoothScrollTo(int x, int y) {
    // 获取当前的滚动位置
    int scrollX = getScrollX();
    int scrollY = getScrollY();
    // 计算滚动的偏移量
    int deltaX = x - scrollX;
    int deltaY = y - scrollY;
    if (deltaX != 0 || deltaY != 0) {
        // 如果有滚动偏移量
        // 启动 Scroller 进行平滑滚动
        mScroller.startScroll(scrollX, scrollY, deltaX, deltaY);
        // 使视图无效,触发重绘
        invalidate();
    }
}

smoothScrollBy 方法中,调用 smoothScrollTo 方法进行平滑滚动,将当前的滚动位置加上偏移量。在 smoothScrollTo 方法中,首先获取当前的滚动位置,计算滚动的偏移量,如果有滚动偏移量,则启动 Scroller 进行平滑滚动,并使视图无效,触发重绘。

6.3 Scroller 类的作用

Scroller 类是 Android 提供的一个用于实现平滑滚动效果的辅助类。NestedScrollView 在实现平滑滚动时,会使用 Scroller 类来计算滚动的位置和时间。以下是 Scroller 类的部分代码示例:

java 复制代码
// Scroller 类的部分代码
public class Scroller {
    // 滚动的起始位置
    private int mStartX;
    private int mStartY;
    // 滚动的偏移量
    private int mFinalX;
    private int mFinalY;
    // 滚动的持续时间
    private int mDuration;
    // 滚动的开始时间
    private long mStartTime;
    // 插值器
    private Interpolator mInterpolator;

    // 构造函数
    public Scroller(Context context) {
        this(context, null);
    }

    // 构造函数
    public Scroller(Context context, Interpolator interpolator) {
        mInterpolator = interpolator;
        if (mInterpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        }
    }

    // 启动滚动
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
    }

    // 计算滚动的位置
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }
        int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
        if (timePassed < mDuration) {
            switch (mMode) {
                case SCROLL_MODE:
                    float x = mInterpolator.getInterpolation(timePassed * 1f / mDuration);
                    mCurrX = mStartX + Math.round(x * (mFinalX - mStartX));
                    mCurrY = mStartY + Math.round(x * (mFinalY - mStartY));
                    break;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }
}

Scroller 类中,startScroll 方法用于启动滚动,记录滚动的起始位置、偏移量和持续时间。computeScrollOffset 方法用于计算滚动的位置,根据插值器和时间来计算当前的滚动位置。在 NestedScrollViewcomputeScroll 方法中,会调用 ScrollercomputeScrollOffset 方法来更新滚动位置。

java 复制代码
// NestedScrollView 的 computeScroll 方法
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        // 如果 Scroller 还在滚动
        int oldX = mScrollX;
        int oldY = mScrollY;
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();
        // 调用 scrollTo 方法进行滚动
        scrollTo(x, y);
        // 检查滚动位置是否发生变化
        if (oldX != x || oldY != y) {
            // 如果滚动位置发生变化,调用滚动监听器的 onScrollChange 方法
            mScrollListener.onScrollChange(this, mScrollX, mScrollY, oldX, oldY);
        }
        // 使视图无效,触发重绘
        postInvalidateOnAnimation();
    }
}

computeScroll 方法中,首先调用 ScrollercomputeScrollOffset 方法来计算滚动位置。如果 Scroller 还在滚动,则获取当前的滚动位置,调用 scrollTo 方法进行滚动,检查滚动位置是否发生变化,如果发生变化则调用滚动监听器的 onScrollChange 方法,最后使视图无效,触发重绘。

七、NestedScrollView 的嵌套滚动机制

7.1 嵌套滚动的基本原理

嵌套滚动是指在一个滚动视图内部嵌套另一个滚动视图时,两个滚动视图能够协同工作,实现平滑的滚动效果。NestedScrollView 通过实现 NestedScrollingParent2NestedScrollingChild2 接口,与其他支持嵌套滚动的视图进行通信和协作。在嵌套滚动过程中,子视图在滚动前会先将滚动信息传递给父视图,父视图根据自身的状态决定是否消耗部分滚动距离,然后子视图再根据父视图消耗的距离进行剩余的滚动操作。

7.2 嵌套滚动的接口方法

7.2.1 父视图接口方法

NestedScrollingParent2 接口定义了一系列父视图处理嵌套滚动的方法,以下是部分方法的代码示例:

java 复制代码
// NestedScrollingParent2 接口的部分方法
@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) {
    // 当嵌套滚动被接受时,初始化嵌套滚动的相关参数
    startNestedScroll(axes, type);
}

@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
    // 在子视图滚动前,父视图处理预滚动
    if (dy > 0) {
        // 向上滚动
        if (mScrollY > 0) {
            // 如果父视图还可以向上滚动
            int delta = Math.min(dy, mScrollY);
            // 父视图消耗滚动距离
            scrollBy(0, -delta);
            consumed[1] = delta;
        }
    } else if (dy < 0) {
        // 向下滚动
        int scrollRange = getScrollRange();
        if (mScrollY < scrollRange) {
            // 如果父视图还可以向下滚动
            int delta = Math.min(-dy, scrollRange - mScrollY);
            // 父视图消耗滚动距离
            scrollBy(0, delta);
            consumed[1] = -delta;
        }
    }
}

@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
    // 在子视图滚动后,父视图处理剩余的滚动
    if (dyUnconsumed > 0) {
        // 向上滚动
        int scrollRange = getScrollRange();
        if (mScrollY < scrollRange) {
            // 如果父视图还可以向上滚动
            int delta = Math.min(dyUnconsumed, scrollRange - mScrollY);
            // 父视图继续滚动
            scrollBy(0, delta);
        }
    } else if (dyUnconsumed < 0) {
        // 向下滚动
        if (mScrollY > 0) {
            // 如果父视图还可以向下滚动
            int delta = Math.min(-dyUnconsumed, mScrollY);
            // 父视图继续滚动
            scrollBy(0, -delta);
        }
    }
}

@Override
public void onStopNestedScroll(@NonNull View target, int type) {
    // 当嵌套滚动结束时,停止嵌套滚动
    stopNestedScroll(type);
}

onStartNestedScroll 方法中,判断是否开始嵌套滚动,这里只处理垂直方向的滚动。在 onNestedScrollAccepted 方法中,当嵌套滚动被接受时,初始化嵌套滚动的相关参数。在 onNestedPreScroll 方法中,在子视图滚动前,父视图根据滚动方向和自身的滚动位置决定是否消耗部分滚动距离。在 onNestedScroll 方法中,在子视图滚动后,父视图处理剩余的滚动。在 onStopNestedScroll 方法中,当嵌套滚动结束时,停止嵌套滚动。

7.2.2 子视图接口方法

NestedScrollingChild2 接口定义了一系列子视图处理嵌套滚动的方法,以下是部分方法的代码示例:

java 复制代码
// NestedScrollingChild2 接口的部分方法
@Override
public boolean startNestedScroll(int axes, int type) {
    // 开始嵌套滚动
    return mNestedScrollingParentHelper.startNestedScroll(this, axes, type);
}

@Override
public void stopNestedScroll(int type) {
    // 停止嵌套滚动
    mNestedScrollingParentHelper.stopNestedScroll(this, type);
}

@Override
public boolean hasNestedScrollingParent(int type) {
    // 判断是否有嵌套滚动的父视图
    return mNestedScrollingParentHelper.hasNestedScrollingParent(this, type);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
    // 分发预滚动信息给父视图
    return mNestedScrollingParentHelper.dispatchNestedPreScroll(this, dx, dy, consumed, offsetInWindow
java 复制代码
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
        @Nullable int[] offsetInWindow, int type) {
    // 分发滚动信息给父视图
    return mNestedScrollingParentHelper.dispatchNestedScroll(this, dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}

startNestedScroll 方法中,调用 mNestedScrollingParentHelperstartNestedScroll 方法来开始嵌套滚动。mNestedScrollingParentHelperNestedScrollingChildHelper 类型的辅助类,用于帮助子视图处理嵌套滚动相关的逻辑。

java 复制代码
public class NestedScrollingChildHelper {
    // 嵌套滚动的父视图
    private ViewParent mNestedScrollingParent;

    public boolean startNestedScroll(@NonNull View child, int axes, int type) {
        if (hasNestedScrollingParent(type)) {
            // 如果已经有嵌套滚动的父视图,直接返回 true
            return true;
        }
        if (isNestedScrollingEnabled()) {
            // 如果嵌套滚动启用
            ViewParent p = child.getParent();
            View childToUse = child;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, childToUse, child, axes, type)) {
                    // 如果父视图接受嵌套滚动
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, childToUse, child, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    childToUse = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
}

NestedScrollingChildHelperstartNestedScroll 方法中,首先检查是否已经有嵌套滚动的父视图,如果有则直接返回 true。然后检查嵌套滚动是否启用,如果启用则从当前子视图的父视图开始向上遍历,调用 ViewParentCompat.onStartNestedScroll 方法询问父视图是否接受嵌套滚动。如果父视图接受,则记录该父视图,并调用 ViewParentCompat.onNestedScrollAccepted 方法通知父视图嵌套滚动已被接受。

stopNestedScroll 方法中,调用 mNestedScrollingParentHelperstopNestedScroll 方法来停止嵌套滚动。

java 复制代码
public void stopNestedScroll(@NonNull View child, int type) {
    if (mNestedScrollingParent != null) {
        // 如果有嵌套滚动的父视图
        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, child, type);
        mNestedScrollingParent = null;
    }
}

当调用 stopNestedScroll 方法时,会通知嵌套滚动的父视图停止嵌套滚动,并将 mNestedScrollingParent 置为 null

hasNestedScrollingParent 方法用于判断是否有嵌套滚动的父视图,它直接调用 mNestedScrollingParentHelperhasNestedScrollingParent 方法。

java 复制代码
public boolean hasNestedScrollingParent(@NonNull View child, int type) {
    return mNestedScrollingParent != null;
}

如果 mNestedScrollingParent 不为 null,则表示有嵌套滚动的父视图。

dispatchNestedPreScroll 方法用于分发预滚动信息给父视图。

java 复制代码
public boolean dispatchNestedPreScroll(@NonNull View child, int dx, int dy,
        @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 如果嵌套滚动启用且有嵌套滚动的父视图
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                child.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;
            // 调用父视图的 onNestedPreScroll 方法
            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, child, dx, dy, consumed, type);

            if (offsetInWindow != null) {
                child.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;
}

dispatchNestedPreScroll 方法中,首先检查嵌套滚动是否启用且有嵌套滚动的父视图。如果有,则记录子视图的起始位置,将 consumed 数组初始化为 [0, 0],然后调用父视图的 onNestedPreScroll 方法。父视图会根据自身的状态决定是否消耗部分滚动距离,并将消耗的距离记录在 consumed 数组中。最后,更新 offsetInWindow 数组,并返回父视图是否消耗了滚动距离。

dispatchNestedScroll 方法用于分发滚动信息给父视图。

java 复制代码
public boolean dispatchNestedScroll(@NonNull View child, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 如果嵌套滚动启用且有嵌套滚动的父视图
        if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                child.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }
            // 调用父视图的 onNestedScroll 方法
            ViewParentCompat.onNestedScroll(mNestedScrollingParent, child, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed, type);

            if (offsetInWindow != null) {
                child.getLocationInWindow(offsetInWindow);
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            return true;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

dispatchNestedScroll 方法中,首先检查嵌套滚动是否启用且有嵌套滚动的父视图。如果有,则记录子视图的起始位置,然后调用父视图的 onNestedScroll 方法。父视图会根据子视图的滚动信息处理剩余的滚动。最后,更新 offsetInWindow 数组,并返回是否成功分发滚动信息。

7.3 嵌套滚动的工作流程

嵌套滚动的工作流程可以分为以下几个步骤:

  1. 开始嵌套滚动 :当子视图接收到 ACTION_DOWN 触摸事件时,会调用 startNestedScroll 方法开始嵌套滚动。子视图会向上遍历父视图,询问父视图是否接受嵌套滚动。如果父视图接受,则记录该父视图,并通知父视图嵌套滚动已被接受。
  2. 预滚动处理 :当子视图接收到 ACTION_MOVE 触摸事件时,会计算滚动的偏移量,并调用 dispatchNestedPreScroll 方法将预滚动信息分发给父视图。父视图会根据自身的状态决定是否消耗部分滚动距离,并将消耗的距离记录在 consumed 数组中。子视图会根据父视图消耗的距离进行剩余的滚动操作。
  3. 滚动处理 :子视图进行滚动操作后,会调用 dispatchNestedScroll 方法将滚动信息分发给父视图。父视图会根据子视图的滚动信息处理剩余的滚动。
  4. 停止嵌套滚动 :当子视图接收到 ACTION_UPACTION_CANCEL 触摸事件时,会调用 stopNestedScroll 方法停止嵌套滚动,并通知父视图停止嵌套滚动。

通过这种方式,NestedScrollView 可以与其他支持嵌套滚动的视图协同工作,实现平滑的嵌套滚动效果。

八、NestedScrollView 的滚动监听

8.1 滚动监听接口

NestedScrollView 提供了 OnScrollChangeListener 接口,用于监听 NestedScrollView 的滚动事件。以下是该接口的定义:

java 复制代码
// OnScrollChangeListener 接口定义
public interface OnScrollChangeListener {
    /**
     * 当滚动位置发生变化时调用
     * @param v NestedScrollView 实例
     * @param scrollX 水平滚动位置
     * @param scrollY 垂直滚动位置
     * @param oldScrollX 旧的水平滚动位置
     * @param oldScrollY 旧的垂直滚动位置
     */
    void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
}

该接口只有一个方法 onScrollChange,当 NestedScrollView 的滚动位置发生变化时,会调用该方法。

8.2 设置滚动监听器

可以通过 setOnScrollChangeListener 方法为 NestedScrollView 设置滚动监听器。以下是示例代码:

java 复制代码
// 获取 NestedScrollView 实例
NestedScrollView nestedScrollView = findViewById(R.id.nestedScrollView);
// 设置滚动监听器
nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
    @Override
    public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        // 处理滚动事件
        if (scrollY > oldScrollY) {
            // 向下滚动
            Log.d("NestedScrollView", "Scrolling down");
        } else if (scrollY < oldScrollY) {
            // 向上滚动
            Log.d("NestedScrollView", "Scrolling up");
        }
    }
});

在上述代码中,通过 setOnScrollChangeListener 方法为 NestedScrollView 设置了一个滚动监听器。当 NestedScrollView 的滚动位置发生变化时,会调用 onScrollChange 方法,在该方法中可以根据滚动位置的变化进行相应的处理。

8.3 滚动监听的实现原理

NestedScrollViewscrollTo 方法和 computeScroll 方法中,会检查滚动位置是否发生变化,如果发生变化则调用滚动监听器的 onScrollChange 方法。以下是相关代码:

java 复制代码
// NestedScrollView 的 scrollTo 方法
@Override
public void scrollTo(int x, int y) {
    // 获取当前的滚动范围
    int scrollRange = getScrollRange();
    if (y < 0) {
        // 如果滚动位置小于 0,将滚动位置设为 0
        y = 0;
    } else if (y > scrollRange) {
        // 如果滚动位置大于滚动范围,将滚动位置设为滚动范围
        y = scrollRange;
    }
    if (y != mScrollY) {
        // 如果滚动位置发生变化
        mScrollY = y;
        // 调用父类的 scrollTo 方法进行滚动
        super.scrollTo(x, y);
        // 调整滚动条的位置
        adjustScrollbars();
        // 检查滚动位置是否发生变化
        if (mScrollY != mLastScrollY) {
            // 如果滚动位置发生变化,调用滚动监听器的 onScrollChange 方法
            if (mOnScrollChangeListener != null) {
                mOnScrollChangeListener.onScrollChange(this, mScrollX, mScrollY, mLastScrollX, mLastScrollY);
            }
            mLastScrollX = mScrollX;
            mLastScrollY = mScrollY;
        }
    }
}

// NestedScrollView 的 computeScroll 方法
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        // 如果 Scroller 还在滚动
        int oldX = mScrollX;
        int oldY = mScrollY;
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();
        // 调用 scrollTo 方法进行滚动
        scrollTo(x, y);
        // 检查滚动位置是否发生变化
        if (oldX != x || oldY != y) {
            // 如果滚动位置发生变化,调用滚动监听器的 onScrollChange 方法
            if (mOnScrollChangeListener != null) {
                mOnScrollChangeListener.onScrollChange(this, mScrollX, mScrollY, oldX, oldY);
            }
        }
        // 使视图无效,触发重绘
        postInvalidateOnAnimation();
    }
}

scrollTo 方法中,当滚动位置发生变化时,会检查 mOnScrollChangeListener 是否为空,如果不为空则调用其 onScrollChange 方法。在 computeScroll 方法中,当 Scroller 还在滚动且滚动位置发生变化时,也会检查 mOnScrollChangeListener 是否为空,如果不为空则调用其 onScrollChange 方法。

九、NestedScrollView 的性能优化

9.1 减少不必要的重绘

NestedScrollView 的滚动过程中,频繁的重绘会影响性能。可以通过合理设置视图的属性和优化绘制逻辑来减少不必要的重绘。例如,设置视图的 android:layerType 属性为 hardware 可以开启硬件加速,提高绘制效率。

xml 复制代码
<androidx.core.widget.NestedScrollView
    android:id="@+id/nestedScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layerType="hardware">
    <!-- 子视图 -->
</androidx.core.widget.NestedScrollView>

开启硬件加速后,NestedScrollView 的绘制将由 GPU 来完成,从而提高绘制效率。

9.2 优化滚动计算

NestedScrollView 的滚动计算过程中,避免进行复杂的计算和频繁的内存分配。例如,在 onScrollChange 方法中,避免进行大量的计算和创建新的对象。可以提前计算好需要的数据,避免在滚动过程中重复计算。

java 复制代码
// 提前计算好需要的数据
private int mScrollThreshold = 100;

nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
    @Override
    public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        if (scrollY - oldScrollY > mScrollThreshold) {
            // 向下滚动超过阈值
            Log.d("NestedScrollView", "Scrolling down more than threshold");
        } else if (oldScrollY - scrollY > mScrollThreshold) {
            // 向上滚动超过阈值
            Log.d("NestedScrollView", "Scrolling up more than threshold");
        }
    }
});

在上述代码中,提前计算好滚动阈值 mScrollThreshold,在 onScrollChange 方法中直接使用该阈值进行判断,避免了在滚动过程中重复计算。

9.3 合理使用缓存

NestedScrollView 中,可以合理使用缓存来提高性能。例如,对于一些频繁使用的视图或数据,可以进行缓存,避免重复创建和加载。

java 复制代码
// 缓存子视图
private View mCachedChildView;

if (mCachedChildView == null) {
    // 如果缓存的子视图为空,创建新的子视图
    mCachedChildView = LayoutInflater.from(context).inflate(R.layout.child_view, nestedScrollView, false);
    nestedScrollView.addView(mCachedChildView);
} else {
    // 如果缓存的子视图不为空,直接使用
    nestedScrollView.addView(mCachedChildView);
}

在上述代码中,通过缓存子视图,避免了重复创建子视图,提高了性能。

十、常见问题及解决方案

10.1 滚动冲突问题

NestedScrollView 与其他可滚动视图(如 RecyclerView)嵌套使用时,可能会出现滚动冲突问题。例如,当在 NestedScrollView 中嵌套 RecyclerView 时,可能会出现滚动不流畅或滚动方向不一致的问题。

解决方案

  • 使用 NestedScrollingChild 接口 :确保 RecyclerView 实现了 NestedScrollingChild 接口,这样 RecyclerView 可以与 NestedScrollView 进行嵌套滚动协作。在 Android Support Library 23.2 及以上版本中,RecyclerView 已经默认实现了该接口。
  • 设置 android:nestedScrollingEnabled 属性 :在布局文件中,为 RecyclerView 设置 android:nestedScrollingEnabled="true",确保 RecyclerView 的嵌套滚动功能启用。
xml 复制代码
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:nestedScrollingEnabled="true"/>
  • 自定义滚动处理逻辑 :如果上述方法无法解决滚动冲突问题,可以自定义滚动处理逻辑。例如,在 NestedScrollViewonInterceptTouchEvent 方法中,根据滚动位置和手势判断是否拦截触摸事件。
java 复制代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 获取触摸事件的动作
    final int action = ev.getActionMasked();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        // 如果触摸事件为 ACTION_MOVE 且正在被拖动
        return true;
    }
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            // 当触摸事件为 ACTION_DOWN 时
            mLastMotionY = (int) ev.getY();
            mIsBeingDragged = false;
            // 检查是否有嵌套滚动的父视图
            final ViewParent parent = getParent();
            if (parent != null) {
                // 请求父视图不要拦截触摸事件
                parent.requestDisallowInterceptTouchEvent(true);
            }
            // 检查 RecyclerView 是否可以滚动
            RecyclerView recyclerView = findViewById(R.id.recyclerView);
            if (recyclerView != null && canRecyclerViewScrollVertically(recyclerView)) {
                // 如果 RecyclerView 可以滚动,不拦截触摸事件
                return false;
            }
            // 检查是否可以滚动
            if (canScrollVertically(1) || canScrollVertically(-1)) {
                // 如果可以滚动,初始化 Scroller
                mScroller.forceFinished(true);
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
            }
            break;
        }
        // 其他动作处理逻辑
    }
    return mIsBeingDragged;
}

private boolean canRecyclerViewScrollVertically(RecyclerView recyclerView) {
    if (recyclerView == null) {
        return false;
    }
    return recyclerView.canScrollVertically(1) || recyclerView.canScrollVertically(-1);
}

在上述代码中,在 onInterceptTouchEvent 方法中,检查 RecyclerView 是否可以滚动,如果可以滚动,则不拦截触摸事件,将事件传递给 RecyclerView 处理。

10.2 滚动到指定位置不准确问题

有时候,使用 scrollTosmoothScrollTo 方法将 NestedScrollView 滚动到指定位置时,可能会出现滚动位置不准确的问题。

解决方案

  • 延迟滚动操作:在某些情况下,视图的布局可能还没有完全完成,此时进行滚动操作可能会导致滚动位置不准确。可以通过延迟滚动操作来确保视图的布局已经完成。
java 复制代码
nestedScrollView.postDelayed(new Runnable() {
    @Override
    public void run() {
        // 延迟一段时间后进行滚动操作
        nestedScrollView.smoothScrollTo(0, targetY);
    }
}, 200);

在上述代码中,使用 postDelayed 方法延迟 200 毫秒后进行滚动操作,确保视图的布局已经完成。

  • 检查滚动范围:在进行滚动操作前,检查滚动范围,确保滚动位置在滚动范围内。
java 复制代码
int scrollRange = nestedScrollView.getScrollRange();
if (targetY < 0) {
    targetY = 0;
} else if (targetY > scrollRange) {
    targetY = scrollRange;
}
nestedScrollView.smoothScrollTo(0, targetY);

在上述代码中,检查 targetY 是否在滚动范围内,如果不在则将其调整到滚动范围内,然后进行滚动操作。

10.3 滚动时闪烁问题

NestedScrollView 滚动过程中,可能会出现闪烁问题,影响用户体验。

解决方案

  • 开启硬件加速 :如前面所述,设置 android:layerType 属性为 hardware 可以开启硬件加速,提高绘制效率,减少闪烁问题。
xml 复制代码
<androidx.core.widget.NestedScrollView
    android:id="@+id/nestedScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layerType="hardware">
    <!-- 子视图 -->
</androidx.core.widget.NestedScrollView>
  • 优化绘制逻辑 :检查 NestedScrollView 及其子视图的绘制逻辑,避免在滚动过程中进行不必要的重绘。例如,在 onDraw 方法中,添加必要的判断条件,只在需要绘制时进行绘制。
java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    if (isNeedToDraw()) {
        // 进行绘制操作
        super.onDraw(canvas);
    }
}

private boolean isNeedToDraw() {
    // 判断是否需要绘制
    return true;
}

在上述代码中,通过 isNeedToDraw 方法判断是否需要绘制,避免不必要的重绘。

十一、总结与展望

11.1 总结

通过对 Android NestedScrollView 拖动原理的深入源码分析,我们全面了解了其工作机制和相关特性。NestedScrollView 作为一个支持嵌套滚动的滚动视图组件,通过继承 FrameLayout 并实现 NestedScrollingParent2NestedScrollingChild2 接口,能够与其他支持嵌套滚动的视图协同工作,处理复杂的滚动场景。

在初始化与布局阶段,NestedScrollView 会根据父容器传递的测量规格确定自身大小,并对唯一的子视图进行测量和布局。在触摸事件处理方面,NestedScrollView 通过事件分发、拦截和处理机制,实现了滚动效果。在滚动实现上,提供了 scrollByscrollTosmoothScrollBysmoothScrollTo 等方法,结合 Scroller 类实现了平滑滚动。嵌套滚动机制使得 NestedScrollView 能够与其他可滚动视图协作,通过接口方法进行滚动信息的传递和处理。滚动监听功能允许开发者监听 NestedScrollView 的滚动事件,进行相应的处理。同时,我们也探讨了性能优化的方法和常见问题的解决方案。

11.2 展望

随着 Android 技术的不断发展,NestedScrollView 在未来可能会有更多的改进和应用。

  • 更强大的嵌套滚动支持:未来可能会进一步优化嵌套滚动机制,支持更多复杂的滚动场景。例如,支持多方向的嵌套滚动,或者在嵌套滚动过程中实现更精细的滚动控制。
  • 与其他组件的深度集成NestedScrollView 可能会与更多的 Android 组件进行深度集成,提供更便捷的开发方式。例如,与 CoordinatorLayout 结合,实现更多炫酷的滚动效果和交互体验。
  • 性能优化的进一步提升 :随着 Android 系统性能的不断提升,NestedScrollView 的性能也会得到进一步优化。例如,在滚动过程中减少内存占用和 CPU 消耗,提高滚动的流畅性。
  • 跨平台兼容性 :随着跨平台开发的需求不断增加,NestedScrollView 可能会提供更好的跨平台兼容性,使得开发者可以在不同的平台上使用相同的代码实现类似的滚动效果。

深入理解 NestedScrollView 的拖动原理,不仅有助于解决当前开发中的问题,还为未来的 Android 应用开发提供了更多的可能性。开发者可以根据这些原理和特性,创造出更加出色的用户界面和交互体验。

相关推荐
感谢地心引力8 分钟前
安卓、苹果手机无线投屏到Windows
android·windows·ios·智能手机·安卓·苹果·投屏
xiaoxue..4 小时前
React 手写实现的 KeepAlive 组件
前端·javascript·react.js·面试
快乐非自愿4 小时前
【面试题】MySQL 的索引类型有哪些?
数据库·mysql·面试
南风知我意9574 小时前
【前端面试2】基础面试(杂项)
前端·面试·职场和发展
优雅的潮叭4 小时前
cud编程之 reduce
android·redis·缓存
2601_949613025 小时前
flutter_for_openharmony家庭药箱管理app实战+用药知识详情实现
android·javascript·flutter
一起养小猫5 小时前
Flutter for OpenHarmony 实战 表单处理与验证完整指南
android·开发语言·前端·javascript·flutter·harmonyos
2601_949975085 小时前
flutter_for_openharmony城市井盖地图app实战+附近井盖实现
android·flutter
倾云鹤5 小时前
通用Digest认证
android·digest
我是阿亮啊6 小时前
Android 自定义 View 完全指南
android·自定义·自定义view·viewgroup