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 销毁后仍然接收事件。
相关推荐
Devil枫1 小时前
Kotlin高级特性深度解析
android·开发语言·kotlin
ChinaDragonDreamer1 小时前
Kotlin:2.1.20 的新特性
android·开发语言·kotlin
雨白12 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹13 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空15 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭15 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日16 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安16 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑16 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟21 小时前
CTF Web的数组巧用
android