Activity、Window、DecorView

  • 每一个 Activity 都持有一个 Window 对象;

  • Window 是一个抽象类,只有一个唯一的实现 PhoneWindow;

  • 每一个 PhoneWindow 都持有一个 DecorView 实例,它继承自 FrameLayout,是 Activity 中最顶层的 View;

  • 在 Activity 中调用的 setContentView() 方法实际上就在将布局添加到了 DecorView 上;


触摸事件分发机制

什么是触摸事件?

Android 中几乎所有的交互都通过手指对屏幕的触摸来完成整个交互过程。每一次的点击、长按、移动等都是一个事件。

系统把事件封装为 MotionEvent 对象,包含了动作类型(如按下、移动、抬起)以及位置、时间等信息。MotionEvent 具有不同的类型:

  • 按下(ACTION_DOWN):手指按下,序列开始

  • 移动(ACTION_MOVE):手指移动,一般有若干个移动事件;

  • 抬起(ACTION_UP):手指抬起,序列结束;

  • 取消(ACTION_CANCEL):事件被系统中断。

事件序列是从手指按下屏幕开始,到手指离开屏幕所产生的一系列事件。一般包括一个 ACTION_DOWN + 多个 ACTION_MOVE + 一个 ACTION_UP

什么是事件分发机制?

事件分发的机制:某一个事件从屏幕传递到各个 View,由 View 来使用这一事件(消费事件)或者忽略这一事件(不消费事件),这整个过程的控制。事件分发的过程本质上就是 MotionEvent 分发的过程。

Avtivity 承载着视图的显示,事件总是最先从 Activity 产生,经过一系列的传递到达最终的 View。在 Android 中事件的传递层级是:

  • Activity → Window → DecorView → ViewGroup → View

Activity 的事件分发流程

在 Activity 中主要由两个核心方法处理事件的分发:

  • dispatchTouchEvent(MotionEvent ev):事件分发,决定事件的去向;

  • onTouchEvent(MotionEvent ev):事件处理,决定是否消费事件。

Activity 的事件分发流程如下图所示:

在 Activity 源码中,dispatchTouchEvent() 是触摸事件分发的入口方法:

// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); // 空方法,标记一次新的触摸序列开始
    }
    // 交给 Window(PhoneWindow)处理
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 如果 Window 没消费,自己尝试处理
    return onTouchEvent(ev);
}
  • onUserInteraction() 默认是空实现,系统在检测到新的 ACTION_DOWN 时会调用它,供开发者重写(比如在锁屏、休眠逻辑中)。

  • 真正的事件分发是交给 Window 去做的,Activity 自身只在 Window 没消费事件时兜底调用 onTouchEvent()

getWindow() 方法获取的是一个 Window 对象实例,而 Window 是一个抽象类,superDispatchTouchEvent() 也是一个抽象方法。

// Window.java
public abstract class Window {
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
}

那 Window 的具体实现类是什么呢?它只有一个的实现类 PhoneWindow,在 Activity 的 attach() 方法中创建:

// Activity.java
final void attach(Context context, ...) {
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setCallback(this); // Activity 自身作为回调处理事件
}

PhoneWindow 并不直接处理事件,而是把事件转交给它的顶层 View:DecorView。

// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

DecorView 继承自 FrameLayout,本质上就是应用窗口最顶层的 ViewGroup。它会把事件交给父类(ViewGroup)的 dispatchTouchEvent(),从而进入 ViewGroup → 子 View 的事件分发流程。

// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    // 调用父类 ViewGroup 的 dispatchTouchEvent 方法
    return super.dispatchTouchEvent(event); 
}

至此,一个触摸事件的向上传递链路是:

Activity.dispatchTouchEvent()
   → Window.superDispatchTouchEvent()
       → DecorView.superDispatchTouchEvent()
           → ViewGroup.dispatchTouchEvent()

当 ViewGroup 中的 dispatchTouchEvent() 方法的返回值是 true 时,Activity 的 dispatchTouchEvent() 方法就直接返回 true,表示事件被消费。否则会调用 Activity 的 onTouchEvent() 方法进行兜底处理。

// Activity.java
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;  // 消费事件
    }

    return false;  // 未消费事件
}

如果 shouldCloseOnTouch() 返回 true,Activity 会被关闭。满足下面三个条件时,Activity 会调用 finish() 关闭自己。

  • mCloseOnTouchOutside:允许点击外部关闭(常见于对话框式 Activity);

  • peekDecorView != null:表示当前 Window 中的 DecorView 不为空;

  • isOutside:点击事件在 DecorView 范围之外;

// Window.java
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
                    || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

ViewGroup 事件分发流程

在 ViewGroup 中主要由三个核心方法处理事件的分发:

  • dispatchTouchEvent(MotionEvent ev):该方法的调用标志着事件进入 ViewGroup 中,开始事件分发;

  • onInterceptTouchEvent(MotionEvent ev):该方法返回 true,表示当前的 ViewGroup 拦截事件,不会向下继续分发了。方法返回 false,表示不会拦截事件,继续向下分发;

  • onTouchEvent(MotionEvent ev):存在于父类 View 中,可以自定义处理事件的逻辑。

ViewGroup 的事件分发流程图如下:

在 ViewGroup 的源码中,dispatchTouchEvent() 方法表示开始分发触摸事件,主要就是做三件事情:

  1. 去判断是否需要拦截事件;

  2. 在当前 ViewGroup 中找到用户真正点击的 View;

  3. 分发事件到 View 上。

dispatchTouchEvent() 的源码很长,去除不影响事件分发逻辑的代码(调试、无障碍等)后,简化源码如下:

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 定义变量,表示该方法的返回值
    boolean handled = false;

    /*
      判断当前事件是否符合安全策略
      如果当前的 View 设置了不在顶部时不响应触摸事件,
      且当前 View 被其他 View 所遮挡了,则返回 false,不响应触摸事件。
      否则返回 true,开始事件分发。
     */
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 判断事件是否是 ACTION_DOWN 事件
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 针对 ACTION_DOWN 事件,执行一些初始化操作
            cancelAndClearTouchTargets(ev); // 清除所有的触摸目标
            resetTouchState();  // 重置触摸状态
        }

        // 检测是否拦截
        final boolean intercepted;
        // 如果当前事件是按下事件(新的事件),或者 mFirstTouchTarget 不为空(或当前已经存在处理事件的子 View)
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 判断当前的 ViewGroup 是否允许拦截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 当前的 ViewGroup 允许拦截事件
                intercepted = onInterceptTouchEvent(ev);    // 判断是否拦截事件
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        // 检查事件是否为取消事件
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // 判断当前事件是否要分发给多个子 View
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

        // 不是取消事件,且不拦截事件,进入事件分发代码
        if (!canceled && !intercepted) {
            View child = findChildUnder(ev.getX(), ev.getY());
            if (child != null) {
                // 分发给子 View 处理
                handled = child.dispatchTouchEvent(ev);
            }
        }
    }

    return handled;
}

拦截事件

onInterceptTouchEvent() 是 ViewGroup 特有的方法,决定是否拦截当前事件序列。主要判断以下四个条件,全满足时返回 true,拦截事件。否则返回 false,不拦截事件。

// ViewGroup.java
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 1. 当前输入设备是否是鼠标事件
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            // 2. 是否是按下事件
            && ev.getAction() == MotionEvent.ACTION_DOWN
            // 3. 是否是按下鼠标左键
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            // 4. 按下位置是否在滚动条上
            && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) { 
        return true;
    }
    return false;
}

可以看到一般来说我们在 Android 设备上不会使用鼠标设备,onInterceptTouchEvent() 方法大多数情况下都是返回 false,不会主动拦截事件。

从 ViewGroup 到子 View

在 ViewGroup 中找到目标子 View 后,会通过 dispatchTransformedTouchEvent() 方法将事件下发到子 View 中。

View 的事件分发流程

事件最终会从 ViewGroup 分发到 View 中,View 涉及事件分发的两个核心方法如下:

  • dispatchTouchEvent(MotionEvent ev):标志着事件到达 View 中;

  • onTouchEvent(MotionEvent ev):处理用户触摸事件的逻辑。

View 的事件分发流程图如下:

// —— View:触摸事件分发(精简版)——
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean handled = false;
    int action = event.getActionMasked();

    // 1) 新手势开始,防御性清理嵌套滚动状态
    if (action == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    // 2) 安全过滤(如窗口安全标志/来源校验等),不过滤才继续处理
    if (onFilterTouchEventForSecurity(event)) {
        // 3) 主通路:优先 OnTouchListener;否则走 onTouchEvent()
        handled = performOnTouchCallback(event);
    }

    // 4) 收尾:手势结束或 DOWN 未被消费 → 停止嵌套滚动,避免残留影响
    if (action == MotionEvent.ACTION_UP
            || action == MotionEvent.ACTION_CANCEL
            || (action == MotionEvent.ACTION_DOWN && !handled)) {
        stopNestedScroll();
    }

    // 5) 返回是否已被当前 View 消费
    return handled;
}

performOnTouchCallback() 的核心逻辑如下(保留重点):

// —— 优先监听器,未消费再走 onTouchEvent ——
// 说明:监听器需要 view.isEnabled() 才会触发(系统默认语义)
private boolean performOnTouchCallback(MotionEvent event) {
    // A. OnTouchListener 优先
    if (mOnTouchListener != null && isEnabled()
            && mOnTouchListener.onTouch(this, event)) {
        return true; // 监听器已消费
    }
    // B. 监听器未消费 → 交给 onTouchEvent 兜底
    return onTouchEvent(event);
}

performOnTouchCallback() 的源码可以发现:

  • 设置了 OnTouchListener 并且返回 true → 事件就不会走到 onTouchEvent

  • 返回 false 或没有设置 OnTouchListener → 事件会继续走 onTouchEvent


View 绘制流程

View 的绘制流程包括 measure、layout 和 draw。其中:

  • mesure 用来测量 View 的宽和高;

  • layout 用来确定 View 的位置;

  • draw 则用来绘制 View。

View 绘制的入口

在 Activity 的加载过程中,DecorView 被创建加载到 Window 中之后,会通过 ViewRootImpl 中的 performTraversals() 方法开启 View 的绘制。

onMesaure

首先来看一下 View 中的 onMeasure() 方法的源码:

// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

它调用了 setMeasuredDimension() 方法,

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

onLayout

onDraw

自定义 View


RecyclerView

RecyclerView 中的核心组件

  • RecyclerView:承载整个列表,处理触摸交互、边界判断,并协调 Recycler(回收器)与 Adapter(适配器)之间的工作。

  • LayoutManager:负责子 View 的测量与布局,决定何时回收 View。它通过 getViewForPosition() 向 Recycler 索取可复用的 View,并管理滚动逻辑与布局策略。

  • ViewHolder:对单个 item View 的封装,持有缓存的子控件引用,避免重复调用 findViewById,提升性能。

  • Recycler:RecyclerView 内部的“回收管理者”。它根据缓存策略,优先复用不同状态下的 ViewHolder,再将其交给 LayoutManager 使用。

  • Adapter:负责数据与 View 之间的桥接。当 Recycler 内部没有合适的 ViewHolder 时,会调用 onCreateViewHolder() 创建新的 ViewHolder,然后通过 onBindViewHolder() 为其绑定数据。

简易的运行机制

  1. LayoutManager 请求 View:通过 getViewForPosition() 向 Recycler 要求一个指定位置的 View。

  2. ViewHolder 创建/复用:如果 Recycler 内部没有合适的缓存,Adapter 会调用 onCreateViewHolder() 创建新的 ViewHolder。

  3. 数据绑定:Adapter 调用 onBindViewHolder(),为新创建或复用的 ViewHolder 绑定对应的数据。

  4. 布局展示:Recycler 将绑定好数据的 View 交给 LayoutManager,完成测量与摆放,显示在屏幕上。

RecyclerView 的复用机制

RecyclerView 的复用机制主要体现在两类场景:

  • 滑动场景:重复利用滑出屏幕的 ViewHolder,避免重复创建和绑定。

  • 数据更新场景:通过 Scrap 列表进行局部刷新,最大程度地复用已有 ViewHolder。

滑动场景的复用机制

当用户滚动列表时,部分 ViewHolder 会滑出/滑入屏幕。RecyclerView 使用 CachedViews 和 RecycledViewPool (回收池)两级缓存来管理复用:

  • 滑出屏幕 → 回收流程

    • 滑出的 ViewHolder 优先进入 mCachedViews(默认容量为 2)。

    • 超过容量后,会将最早进入的 ViewHolder 转存到 RecycledViewPool

    • RecycledViewPool 是一个 Map<viewType, List<ViewHolder>>,默认容量为 5。容量满时,新加入的 ViewHolder 会被直接丢弃。

  • 滑入屏幕 → 复用流程

    1. 先从 CachedViews 中查找。如果命中并且 position 相同,可以直接复用,不需要重新绑定数据。

    2. 如果未命中,再从 RecycledViewPool 中查找。如果存在相同 viewType 的 ViewHolder,则复用,但需要重新调用 onBindViewHolder() 绑定数据。

    3. 如果都未命中,才会走 onCreateViewHolder() 创建新的 ViewHolder。

数据更新的复用机制

当调用 Adapter 的 notifyItemChanged()notifyItemInserted() 等方法时,RecyclerView 会通过 Scrap 列表复用屏幕上已有的 ViewHolder,避免重复创建,流程如下:

  1. 分离当前屏幕上的 item

    • 将屏幕上的所有 ViewHolder 从布局中临时移除,放入 Scrap 列表中。

    • mAttachedScrap:保存正常分离的 ViewHolder。

    • mChangedScrap:保存那些因数据变化(如 notifyItemChanged)而需要做动画的 ViewHolder。

  2. 重新布局

    • LayoutManager 根据新的数据顺序,优先从 mAttachedScrap 中取出对应 ViewHolder。

    • 如果没有,再尝试从 CachedViews 或 RecycledViewPool 中复用。

    • 最后才会调用 Adapter 创建新的 ViewHolder。

  3. 清理工作

    • 布局完成后,Scrap 中未被重新利用的 ViewHolder,会被转存到 RecycledViewPool 中。

这样,RecyclerView 能够在数据局部更新的同时,最大化复用已有 ViewHolder,避免全量刷新,提高性能和动画体验。

ListView 与 RecyclerView 的区别

早期的 ListView 在 Android 开发中使用非常广泛,但它的适配器(Adapter)机制相对简单。
在数据刷新方面,ListView 只提供了 notifyDataSetChanged() 方法:

  • 调用该方法时,系统会认为整个数据集发生了变化;

  • ListView 会重新走一遍 getView(),对屏幕上所有可见的 item 进行数据绑定;

  • 即使只有一个 item 更新,ListView 也会对全部可见 item 重新绑定数据;

这会带来两个问题:

  1. 性能浪费:无效的 bind 次数增加,影响流畅度;

  2. 无法做精细动画:所有 item 一起刷新,没有插入、删除、移动的过渡效果。

相比之下,RecyclerView 在 Adapter 中提供了更精细的通知方法:

  • notifyItemChanged(position):更新单个 item;

  • notifyItemInserted(position):插入新的 item;

  • notifyItemRemoved(position):删除指定位置的 item;

  • notifyItemMoved(from, to):移动 item;

  • notifyItemRangeXXX(...):对一段范围做批量更新。

通过这些方法,RecyclerView 可以只刷新真正发生变化的 item,大幅度减少不必要的 bind 操作。同时,RecyclerView 还配合 ItemAnimator,在增删改的过程中提供平滑的动画过渡,用户体验显著优于 ListView。

因此,ListView 更适合小数据量的简单场景,而 RecyclerView 更适合大数据量高性能高交互的场景。

嵌套 RecyclerView 的滑动冲突问题

在实际开发中,我们经常会遇到 RecyclerView 内部再嵌套 RecyclerView 的场景。例如,一个竖向的父 RecyclerView,每个 item 中又包含一个固定高度的竖向 RecyclerView。当用户上下滑动时,默认情况下事件会被父 RecyclerView 拦截,导致子 RecyclerView 无法滚动。

问题溯源

Android 的事件分发机制是 自顶向下分发、自底向上传递 的过程。父容器(ViewGroup)在分发事件时会调用 onInterceptTouchEvent() 判断是否要拦截。如果父容器拦截了,那么子 View 就完全拿不到触摸事件。

onInterceptTouchEvent(MotionEvent ev) 是 ViewGroup 决定要不要把事件截留在自己这里处理的关键方法,默认实现是 返回 false(不拦截)RecyclerView 在内部重写了 onInterceptTouchEvent(),当检测到是竖直滑动 且自己能滚动时,就会返回 true,把事件截留在自己这里,从而开始列表的滑动逻辑。

因此在嵌套 RecyclerView 的场景中,父 RecyclerView 默认会优先消费上下滑动事件,因此子 RecyclerView 很难有机会响应。

解决思路

在分析 ViewGroup 的事件分发源码时可以发现,dispatchTouchEvent() 在调用 onInterceptTouchEvent() 之前,会先检查一个标志位 FLAG_DISALLOW_INTERCEPT

if (mGroupFlags & FLAG_DISALLOW_INTERCEPT) {
    // 已被 requestDisallowInterceptTouchEvent(true) 设置过了
    intercepted = false; // 直接禁止拦截
} else {
    intercepted = onInterceptTouchEvent(ev);
}

也就是说,如果子 View 调用了 requestDisallowInterceptTouchEvent(true),父容器的 onInterceptTouchEvent() 将不会再被执行,这相当于“强行禁止”了拦截。

另外,RecyclerViewonInterceptTouchEvent() 方法中默认不会拦截 ACTION_DOWN 事件,所以子 RecyclerView 能够顺利接收到手势的第一帧(DOWN)。这就给了子 View 一个“窗口期”:在 DOWN 或最早的 MOVE 阶段调用 requestDisallowInterceptTouchEvent(true),阻止父容器后续的拦截。这样整个触摸序列就能稳定地交给子 RecyclerView 处理。等到手势结束或子 View 滚动到边界,再把拦截权限还给父容器即可。

实现方案

下面是一个常见的实现方式:通过给子 RecyclerView 设置 OnTouchListener,在触摸过程中动态判断是否需要让父容器拦截事件。

holder.rv.layoutManager = LinearLayoutManager(holder.rv.context)
holder.rv.adapter = ChildAdapter2()

var lastY = 0f
holder.rv.setOnTouchListener { v, event ->
    val view = v as RecyclerView
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            lastY = event.y
            // 通知父容器:本次手势开始,先别拦截
            v.parent.requestDisallowInterceptTouchEvent(true)
        }

        MotionEvent.ACTION_MOVE -> {
            val dy = event.y - lastY
            lastY = event.y

            val draggingDown = dy > 0       // 手势向下
            val draggingUp = dy < 0         // 手势向上

            val canScrollUp = view.canScrollVertically(-1)   // 内容还能往下拖
            val canScrollDown = view.canScrollVertically(1)  // 内容还能往上拖

            val childCanConsume =
                (draggingDown && canScrollUp) || (draggingUp && canScrollDown)

            // 如果子 RV 能消费事件 -> 禁止父拦截
            // 如果子 RV 已经到边界 -> 允许父继续拦截
            v.parent.requestDisallowInterceptTouchEvent(childCanConsume)
        }

        MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
            // 手势结束,允许父容器恢复拦截
            v.parent.requestDisallowInterceptTouchEvent(false)
        }
    }
    false // 返回 false,不截断事件链,让 RV 的自身逻辑继续处理
}

RecyclerView 的局部刷新原理

RecyclerView 的卡顿优化

主要原因是 UI 线程被阻塞:

  1. item 布局层级过深,测量和绘制开销大。

  2. onBindViewHolder 里做了耗时操作(比如图片解码、复杂计算)。

  3. 图片加载未优化(大图未压缩、无缓存)。

  4. 使用 notifyDataSetChanged 导致全量刷新。

  5. 内存抖动频繁 GC。

  6. 动画、过度绘制开销大。

卡顿的优化思路:

  1. 布局优化:减少层级,优先用 ConstraintLayout,减少 measure/layout。

  2. 异步加载:图片、数据处理放后台线程,UI 层只做绑定。

  3. 缓存 & 复用:ViewHolder 缓存、setItemViewCacheSize()、图片缓存。

  4. 局部刷新:用 notifyItemChanged/Inserted/Removed 或 DiffUtil,避免全量刷新。

  5. 图片优化:用 Glide/Coil,设置占位图、合理缩放。

  6. 禁用不必要动画:recyclerView.setItemAnimator(null)。

  7. 减少过度绘制:避免多层背景、检查 GPU Overdraw。


ViewPager2