深入剖析 Android NestedScrollView 使用原理

深入剖析 Android NestedScrollView 使用原理

一、引言

在 Android 开发中,滚动视图是非常常见的 UI 组件,用于展示超出屏幕范围的内容。NestedScrollView 作为 Android 系统提供的一个重要滚动视图组件,它支持嵌套滚动机制,使得开发者可以更灵活地实现复杂的滚动效果。本文将从源码级别深入分析 NestedScrollView 的使用原理,帮助开发者更好地理解和运用这个组件。

二、NestedScrollView 概述

2.1 基本介绍

NestedScrollView 是 Android 提供的一个垂直滚动的视图容器,它继承自 FrameLayout,并实现了 NestedScrollingParentNestedScrollingChild 接口,从而支持嵌套滚动功能。嵌套滚动允许父视图和子视图之间协同处理滚动事件,实现更流畅的滚动效果。

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 实现了 NestedScrollingParent2NestedScrollingChild2 接口,这两个接口定义了嵌套滚动的相关方法,使得 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 嵌套滚动流程

嵌套滚动的流程主要包括以下几个步骤:

  1. 开始嵌套滚动 :子视图调用 startNestedScroll 方法通知父视图开始嵌套滚动,父视图通过 onStartNestedScroll 方法决定是否接受嵌套滚动。
  2. 预滚动处理 :子视图在滚动前调用 dispatchNestedPreScroll 方法分发预滚动事件,父视图通过 onNestedPreScroll 方法处理预滚动事件,并返回消费的滚动距离。
  3. 滚动处理 :子视图处理剩余的滚动距离,并调用 dispatchNestedScroll 方法分发滚动事件,父视图通过 onNestedScroll 方法处理滚动事件。
  4. 停止嵌套滚动 :子视图调用 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 内部,通过设置 RecyclerViewnestedScrollingEnabled 属性为 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 外部,通过设置 AppBarLayoutScrollingViewBehavior,可以实现滚动联动。

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 作为一个支持嵌套滚动的视图容器,通过实现 NestedScrollingParentNestedScrollingChild 接口,与父视图和子视图之间协同处理滚动事件,实现了流畅的滚动效果。其触摸事件处理机制、嵌套滚动机制和滚动动画处理机制相互配合,为开发者提供了强大的滚动功能。

8.2 展望

随着 Android 系统的不断发展,NestedScrollView 可能会在以下方面得到进一步的优化和扩展:

  • 性能优化:进一步优化滚动性能,减少滚动时的卡顿现象,提高用户体验。
  • 功能扩展:增加更多的滚动效果和交互方式,如弹性滚动、阻尼滚动等。
  • 兼容性提升:更好地兼容不同版本的 Android 系统和各种设备,确保在不同环境下都能正常使用。

总之,NestedScrollView 作为 Android 开发中重要的滚动视图组件,将在未来的开发中发挥更加重要的作用。开发者可以根据实际需求,灵活运用 NestedScrollView 的特性,实现各种复杂的滚动效果。

以上是一篇关于 Android NestedScrollView 使用原理的技术博客,通过对源码的详细分析,帮助开发者深入理解 NestedScrollView 的工作原理和使用方法。希望对你有所帮助!

相关推荐
刘龙超6 小时前
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(一)基础搭建
android jetpack
小悟空7 小时前
[AI 生成] Flink 面试题
大数据·面试·flink
Jackilina_Stone9 小时前
【faiss】用于高效相似性搜索和聚类的C++库 | 源码详解与编译安装
android·linux·c++·编译·faiss
Sherry0079 小时前
CSS Grid 交互式指南(译)(下)
css·面试
一只毛驴10 小时前
浏览器中的事件冒泡,事件捕获,事件委托
前端·面试
一只叫煤球的猫10 小时前
你真的处理好 null 了吗?——11种常见但容易被忽视的空值处理方式
java·后端·面试
棒棒AIT10 小时前
mac 苹果电脑 Intel 芯片(Mac X86) 安卓虚拟机 Android模拟器 的救命稻草(下载安装指南)
android·游戏·macos·安卓·mac
KarrySmile10 小时前
Day04–链表–24. 两两交换链表中的节点,19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II
算法·链表·面试·双指针法·虚拟头结点·环形链表
fishwheel10 小时前
Android:Reverse 实战 part 2 番外 IDA python
android·python·安全
一只毛驴11 小时前
谈谈浏览器的DOM事件-从0级到2级
前端·面试