Android View 生命周期原理源码分析

一、整体概述

在 Android 开发中,View 作为构建用户界面的基础组件,其生命周期的管理至关重要。理解 View 生命周期的每个阶段,不仅能帮助开发者优化布局性能,还能确保在合适的时机进行资源的分配与释放。本文将从源码层面出发,对 Android View 生命周期的各个阶段进行详尽的拆解和分析。

二、实例化阶段

2.1 构造函数的调用

在创建 View 实例时,构造函数是第一个被调用的部分。View 类提供了多个构造函数重载,以满足不同的使用场景。以下是几个常见的构造函数及其源码分析:

less 复制代码
public View(Context context) {
    this(context, null);
}

public View(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    // 保存上下文对象,后续操作会依赖该对象获取资源等
    mContext = context;
    mResources = context.getResources();

    // 初始化基本属性
    mLayoutDirection = View.LAYOUT_DIRECTION_INHERIT;
    mDisplayListProperties = new DisplayListProperties();

    // 解析 XML 属性
    TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
    try {
        // 处理各种属性设置,如背景、文本颜色等
        mBackground = a.getDrawable(com.android.internal.R.styleable.View_background);
        mTextColor = a.getColor(com.android.internal.R.styleable.View_textColor, Color.BLACK);
        // 其他属性处理...
    } finally {
        a.recycle();
    }

    // 调用 init 方法进行进一步初始化
    initView();
}
  • 参数传递 :从简单的只接收 Context 参数的构造函数开始,逐步调用更复杂的构造函数,将 AttributeSetdefStyleAttrdefStyleRes 等参数传递下去。
  • 属性解析 :通过 TypedArrayAttributeSet 中解析出 XML 中定义的属性。TypedArray 是一个临时数组,用于存储从资源中解析出的属性值。使用完后需要调用 recycle() 方法回收,以避免内存泄漏。
  • 初始化操作initView() 方法通常用于执行一些额外的初始化逻辑,例如设置默认值、初始化监听器等。

2.2 onFinishInflate 方法

当 View 从 XML 布局文件中加载完成后,会调用 onFinishInflate() 方法。该方法在 View 类中是一个空实现,主要用于开发者在自定义 View 时进行扩展。

scss 复制代码
protected void onFinishInflate() {
  super.onFinishInflate();
  // 在这里可以进行一些初始化操作,比如获取子 View 引用
  TextView textView = findViewById(R.id.text_view);
  if (textView != null) {
      textView.setText("Initial Text");
  }
}
  • 调用时机:在 XML 布局文件解析完成,所有子 View 都已经创建并添加到当前 View 中后调用。
  • 应用场景:可以在该方法中进行一些依赖于子 View 的初始化操作,例如设置子 View 的属性、添加监听器等。

三、测量阶段

3.1 measure 方法的调用流程

测量阶段的入口是 measure(int widthMeasureSpec, int heightMeasureSpec) 方法,该方法由父 View 调用,用于测量子 View 的大小。

ini 复制代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    // 生成一个唯一的 key,用于测量结果的缓存
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    // 判断是否需要重新测量
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        // 清除测量维度标志
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        // 解析 RTL 属性
        resolveRtlPropertiesIfNeeded();

        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // 调用 onMeasure 方法进行测量
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 从缓存中获取测量结果
            long value = mMeasureCache.valueAt(cacheIndex);
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        // 检查是否设置了测量维度
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    // 将测量结果存入缓存
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL);
}
  • 光学布局调整 :通过 isLayoutModeOptical 方法判断是否需要进行光学布局调整,如果需要则调用 MeasureSpec.adjust 方法对测量规格进行调整。
  • 缓存机制 :使用 LongSparseLongArray 作为测量结果的缓存,通过生成的唯一 key 进行查找。如果缓存中存在对应的测量结果,则直接使用,避免重复测量。
  • 重新测量判断 :如果设置了 PFLAG_FORCE_LAYOUT 标志,或者测量规格发生了变化,则需要重新测量。
  • 调用 onMeasure 方法 :如果需要重新测量,则调用 onMeasure 方法进行实际的测量操作。

3.2 MeasureSpec 详解

MeasureSpec 是一个 32 位的整数,高 2 位表示测量模式,低 30 位表示测量大小。测量模式有三种:

  • MeasureSpec.UNSPECIFIED:父容器不对 View 施加任何约束,View 可以任意大小。通常用于系统内部的一些测量场景。

  • MeasureSpec.AT_MOST :View 的大小不能超过父容器指定的最大值。对应 XML 中的 wrap_content 属性。

  • MeasureSpec.EXACTLY :父容器已经确定了 View 的精确大小。对应 XML 中的 match_parent 或具体的尺寸值。

以下是 MeasureSpec 相关的一些方法源码:

java 复制代码
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * 测量模式:未指定
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    /**
     * 测量模式:最多
     */
    public static final int AT_MOST     = 1 << MODE_SHIFT;
    /**
     * 测量模式:精确
     */
    public static final int EXACTLY     = 2 << MODE_SHIFT;

    /**
     * 根据大小和模式创建一个 MeasureSpec
     */
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * 获取 MeasureSpec 中的测量模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    /**
     * 获取 MeasureSpec 中的测量大小
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}
  • makeMeasureSpec 方法 :用于根据测量大小和模式创建一个 MeasureSpec
  • getMode 方法 :从 MeasureSpec 中提取测量模式。
  • getSize 方法 :从 MeasureSpec 中提取测量大小。

3.3 onMeasure 方法的实现

onMeasure 方法是 View 测量阶段的核心方法,需要在自定义 View 时重写该方法来确定 View 的大小。

java 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
  • getDefaultSize 方法:根据测量模式和建议的最小尺寸计算最终的尺寸。
  • getSuggestedMinimumWidthgetSuggestedMinimumHeight 方法:获取 View 的建议最小宽度和高度,考虑了背景的最小尺寸。
  • setMeasuredDimension 方法 :设置测量得到的宽度和高度,必须在 onMeasure 方法中调用,否则会抛出异常。

四、布局阶段

4.1 layout 方法的执行流程

测量完成后,会进入布局阶段,布局阶段的入口是 layout(int l, int t, int r, int b) 方法。

ini 复制代码
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
  • 重新测量检查 :如果设置了 PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 标志,则重新调用 onMeasure 方法进行测量。
  • 设置 View 的位置 :通过 setFramesetOpticalFrame 方法设置 View 的位置和大小。如果位置或大小发生了变化,changed 标志会被设置为 true
  • 调用 onLayout 方法 :如果位置或大小发生了变化,或者设置了 PFLAG_LAYOUT_REQUIRED 标志,则调用 onLayout 方法进行布局操作。
  • 布局变化监听器 :如果注册了 OnLayoutChangeListener,则在布局变化时会调用监听器的 onLayoutChange 方法。

4.2 onLayout 方法的实现

onLayout 方法用于确定子 View 的位置,对于 ViewGroup 来说,需要重写这个方法来布局子 View。

arduino 复制代码
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  // 空实现,需要在 ViewGroup 子类中重写
}

ViewGroup 的子类中,通常会遍历子 View 并调用它们的 layout 方法来确定每个子 View 的位置。例如,LinearLayoutonLayout 方法实现如下:

ini 复制代码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // 计算子 View 的位置
    final int width = right - left;
    int childRight = width - mPaddingRight;

    // 遍历子 View
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            childLeft = paddingLeft + getLocationOffset(child);
            childTop = calculateChildTop(i, childHeight);

            // 调用子 View 的 layout 方法
            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        }
    }
}
  • 布局方向判断 :根据 mOrientation 的值判断是垂直布局还是水平布局,然后调用相应的布局方法。
  • 子 View 位置计算:根据子 View 的测量大小和布局规则,计算每个子 View 的位置。
  • 调用子 View 的 layout 方法 :将计算得到的位置传递给子 View 的 layout 方法,完成子 View 的布局。

五、绘制阶段

5.1 draw 方法的绘制流程

绘制阶段的入口是 draw(Canvas canvas) 方法,该方法用于绘制 View 的内容。

java 复制代码
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step

接着draw 方法的绘制流程继续深入分析一下

五、绘制阶段(续)

5.1 draw 方法的绘制流程(深入分析)

1. 绘制背景(Draw the background)

java

scss 复制代码
if (!dirtyOpaque) {
    drawBackground(canvas);
}

draw 方法中,首先判断 dirtyOpaque 标志。如果 dirtyOpaquefalse,则调用 drawBackground(canvas) 方法绘制背景。dirtyOpaque 用于标记视图是否为不透明的脏区域,如果是不透明且脏区域,可能不需要重新绘制背景以节省性能。

drawBackground 方法的源码如下:

ini 复制代码
rivate void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    setBackgroundBounds();

    // 检查是否需要设置透明度
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
  • 首先检查背景 Drawable 是否为空,如果为空则直接返回。
  • 调用 setBackgroundBounds() 方法设置背景的边界,使其与视图的边界匹配。
  • 根据视图的滚动状态(mScrollXmScrollY),如果视图没有滚动,则直接在 Canvas 上绘制背景;如果有滚动,需要先将 Canvas 进行平移,绘制背景后再将 Canvas 平移回来。

2. 保存画布状态(Save the canvas' layers if necessary)

在某些情况下,需要保存画布的状态以准备渐变效果等。这部分代码通常在满足特定条件(如存在渐变边缘)时执行。

ini 复制代码
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (verticalEdges || horizontalEdges) {
    // 保存画布状态
    saveCount = canvas.getSaveCount();
    canvas.saveLayer(left, top, right, bottom, null, Canvas.ALL_SAVE_FLAG);
}
  • 通过 viewFlags 检查是否存在水平或垂直的渐变边缘。
  • 如果存在渐变边缘,调用 canvas.saveLayer() 方法保存画布的状态,该方法会创建一个新的图层,后续的绘制操作会在这个新图层上进行。

3. 绘制视图内容(Draw view's content)

scss 复制代码
if (!dirtyOpaque) {
    onDraw(canvas);
}

同样,先判断 dirtyOpaque 标志。如果视图不是不透明的脏区域,则调用 onDraw(canvas) 方法绘制视图的内容。onDraw 方法是一个空实现,需要在自定义视图时重写该方法来绘制具体的内容,例如绘制图形、文本等。

scss 复制代码
protected void onDraw(Canvas canvas) {
    // 自定义绘制逻辑
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, 50, paint);
}

在这个示例中,我们在视图的中心绘制了一个红色的圆形。

4. 绘制子视图(Draw children)

scss 复制代码
dispatchDraw(canvas);

dispatchDraw 方法用于绘制子视图。在 View 类中,dispatchDraw 方法是一个空实现,因为 View 本身没有子视图。而在 ViewGroup 类中,会重写该方法来遍历并绘制所有的子视图。

java 复制代码
@Override
protected void dispatchDraw(Canvas canvas) {
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
  • dispatchDraw 方法会遍历所有的子视图,检查子视图的可见性和动画状态。如果子视图可见或者正在执行动画,则调用 drawChild 方法绘制该子视图。
  • drawChild 方法会调用子视图的 draw 方法,从而递归地完成整个视图树的绘制。

5. 绘制渐变边缘并恢复图层(Draw the fading edges and restore layers if necessary)

如果之前保存了画布的图层,在绘制完子视图后,需要绘制渐变边缘并恢复图层。

scss 复制代码
if (verticalEdges || horizontalEdges) {
    // 绘制渐变边缘
    if (horizontalEdges) {
        drawHorizontalFadingEdge(canvas, top, bottom);
    }
    if (verticalEdges) {
        drawVerticalFadingEdge(canvas, left, right);
    }

    // 恢复画布状态
    canvas.restoreToCount(saveCount);
}
  • 根据是否存在水平或垂直的渐变边缘,调用相应的方法绘制渐变边缘。
  • 调用 canvas.restoreToCount(saveCount) 方法恢复之前保存的画布状态,将绘制操作切换回原来的图层。

6. 绘制装饰(Draw decorations)

scss 复制代码
onDrawForeground(canvas);

最后,调用 onDrawForeground(canvas) 方法绘制视图的装饰,如滚动条等。onDrawForeground 方法会先绘制前景 Drawable,然后绘制滚动条等装饰元素。

scss 复制代码
public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    final Drawable foreground = mForeground;
    if (foreground != null) {
        if (mForegroundInfo != null) {
            mForegroundInfo.mBounds.set(0, 0, getWidth(), getHeight());
            foreground.setBounds(mForegroundInfo.mBounds);
        }

        foreground.draw(canvas);
    }
}
  • 先调用 onDrawScrollIndicators(canvas)onDrawScrollBars(canvas) 方法绘制滚动指示器和滚动条。
  • 如果存在前景 Drawable,则设置其边界并绘制在 Canvas 上。

5.2 硬件加速与绘制优化

在 Android 中,硬件加速可以显著提高绘制性能。当启用硬件加速时,Canvas 的绘制操作会由 GPU 来处理。

硬件加速的开启方式

可以在 AndroidManifest.xml 中为整个应用、某个 Activity 或特定的 View 开启硬件加速。

ini 复制代码
<application
    android:hardwareAccelerated="true"
    ... >
    ...
</application>

绘制优化技巧

  • 减少不必要的重绘 :通过合理设置视图的 invalidaterequestLayout 方法,避免不必要的重绘操作。例如,只在视图的状态发生变化时调用 invalidate 方法。
  • 使用 Canvas 的裁剪和变换 :通过 Canvas 的裁剪和变换方法,可以减少不必要的绘制区域,提高绘制效率。例如,使用 canvas.clipRect() 方法裁剪绘制区域。
  • 避免在 onDraw 方法中创建对象onDraw 方法会频繁调用,在其中创建对象会导致频繁的垃圾回收,影响性能。可以将对象的创建移到 onCreateonFinishInflate 等方法中。

六、事件处理阶段

6.1 事件分发机制概述

当用户触摸屏幕时,触摸事件会从顶层的 View 开始向下传递,经过一系列的 ViewViewGroup,最终到达目标 View。事件分发机制主要涉及三个方法:dispatchTouchEventonInterceptTouchEventonTouchEvent

6.2 dispatchTouchEvent 方法

dispatchTouchEvent 方法是事件分发的入口,用于将触摸事件分发给子视图或自身处理。

csharp 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            return true;
        }

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    return false;
}
  • 首先检查输入事件的一致性验证器 mInputEventConsistencyVerifier,确保事件的合法性。
  • 调用 onFilterTouchEventForSecurity 方法过滤不安全的事件。
  • 如果视图是启用状态且正在处理滚动条拖动事件,则直接返回 true,表示事件已处理。
  • 检查是否设置了 OnTouchListener,如果设置了且 OnTouchListeneronTouch 方法返回 true,则表示事件已被处理,返回 true
  • 调用 onTouchEvent 方法处理事件,如果 onTouchEvent 方法返回 true,则表示事件已被处理,返回 true
  • 如果以上条件都不满足,则返回 false,表示事件未被处理,需要继续向上传递。

6.3 onInterceptTouchEvent 方法(仅适用于 ViewGroup)

onInterceptTouchEvent 方法是 ViewGroup 特有的方法,用于拦截触摸事件,阻止事件继续向下传递给子视图。

scss 复制代码
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

在默认情况下,onInterceptTouchEvent 方法返回 false,表示不拦截事件。可以在自定义的 ViewGroup 中重写该方法,根据需要拦截事件。

6.4 onTouchEvent 方法

onTouchEvent 方法用于处理触摸事件,例如点击、长按、滑动等。

scss 复制代码
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 处理按下事件
                setPressed(true);
                checkForLongClick(0, x, y);
                break;
            case MotionEvent.ACTION_UP:
                // 处理抬起事件
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        setPressed(true);
                    }

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 移除长按检测
                        removeLongPressCallback();

                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        mUnsetPressedState.run();
                    }

                    mIgnoreNextUpEvent = false;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                // 处理取消事件
                setPressed(false);
                removeLongPressCallback();
                mIgnoreNextUpEvent = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // 处理移动事件
                drawableHotspotChanged(x, y);

                if (!pointInView(x, y, mTouchSlop)) {
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                }
                break;
        }
        return true;
    }

    return false;
}
  • 首先检查视图是否禁用,如果禁用则根据点击状态进行相应处理。
  • 检查是否设置了触摸代理 mTouchDelegate,如果设置了且触摸代理处理了事件,则返回 true
  • 如果视图是可点击、可长按或可上下文点击的,则根据触摸事件的类型(按下、抬起、取消、移动)进行相应的处理。
  • 例如,在按下事件中,设置视图为按下状态并开始长按检测;在抬起事件中,检查是否触发了点击事件并执行相应的操作。

6.5 事件处理流程总结

  1. 触摸事件从顶层的 View 开始,调用 dispatchTouchEvent 方法进行事件分发。
  2. 如果是 ViewGroup,会先调用 onInterceptTouchEvent 方法判断是否拦截事件。如果拦截,则事件由该 ViewGroup 处理;如果不拦截,则继续向下传递给子视图。
  3. 子视图调用 dispatchTouchEvent 方法,重复上述步骤,直到找到目标 View
  4. 目标 View 调用 onTouchEvent 方法处理事件,如果处理成功则返回 true,表示事件已处理;如果处理失败则返回 false,事件会向上传递给父视图处理

七、销毁阶段

7.1 onDetachedFromWindow 方法

View 从窗口中分离时,会调用 onDetachedFromWindow 方法。该方法通常用于释放资源、注销监听器等操作,以避免内存泄漏。

typescript 复制代码
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();

    // 释放资源
    if (mAnimator != null) {
        mAnimator.cancel();
        mAnimator = null;
    }

    // 注销监听器
    if (mMyListener != null) {
        mSomeManager.unregisterListener(mMyListener);
        mMyListener = null;
    }
}
  • onDetachedFromWindow 方法中,首先调用父类的 onDetachedFromWindow 方法。
  • 释放 View 持有的资源,例如动画对象、定时器等。
  • 注销之前注册的监听器,避免在 View 销毁后仍然接收事件。
相关推荐
ljx140005255031 分钟前
Android AudioFlinger(一)——初识AndroidAudio Flinger
android
ljx140005255033 分钟前
Android AudioFlinger(四)—— 揭开PlaybackThread面纱
android
Codingwiz_Joy34 分钟前
Day04 模拟原生开发app过程 Androidstudio+逍遥模拟器
android·安全·web安全·安全性测试
叶羽西36 分钟前
Android15 Camera框架中的StatusTracker
android·camera框架
梦中千秋40 分钟前
安卓设备root检测与隐藏手段
android
buleideli1 小时前
CameraX学习2-关于录像、慢动作录像
android·camerax
stevenzqzq3 小时前
android paging使用教程
android
无敌发光大蟒蛇3 小时前
MySQL第一次作业
android·数据库·mysql
m0_748238924 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
技术蔡蔡4 小时前
Android多线程开发之线程安全
android·面试