Skip to content

Android 的 View 体系是面试中另一个超级核心的主题,尤其对于应用开发岗位。考察点可以从 View 的基础知识、绘制流程、事件分发,深入到自定义 View 和性能优化。

下面我将为你系统地梳理关于 View 所能考察的所有知识点。


一、View 基础 (View Basics)

这部分是理解 View 的起点,通常用于考察基础是否扎实。

  1. View 的坐标体系

    • 相对坐标getTop(), getBottom(), getLeft(), GetRight()。这些是 View 相对于其父容器的坐标。
    • 绝对坐标getX(), getY()。表示的是触摸点相对于 View 左上角的坐标。
    • Raw 坐标getRawX(), getRawY()。表示的是触摸点相对于整个屏幕左上角的坐标。
  2. View 的滑动

    • layout():通过改变 left, top, right, bottom 来重新放置 View。
    • offsetLeftAndRight() / offsetTopAndBottom():对 View 进行偏移。
    • LayoutParams:改变 View 的布局参数(如 MarginLayoutParams)。
    • 动画:View 动画(补间动画)和属性动画(Animator)。注意 View 动画并不会改变 View 的真是位置,而属性动画(Android 3.0+)会。
    • scrollTo / scrollBy重要区别:这两个方法移动的是 View 的“内容” 或者说是 “视口”,而不是 View 本身的位置。常用于 ViewGroup(如 ScrollView)。理解 mScrollXmScrollY 的正负值。
  3. View 的生命周期

    • 指的是一个 View 从被创建(onAttachedToWindow)到被销毁(onDetachedFromWindow)的过程。
    • 关键方法:onAttachedToWindow -> onMeasure -> onLayout -> onDraw -> ... -> onDetachedFromWindow

二、View 的绘制流程 (View Drawing Process)

这是 View 体系的重中之重,必须深刻理解。整个过程始于 ViewRootImplperformTraversals() 方法。

  1. 三大流程:Measure -> Layout -> Draw

    • Measure (测量)测量 View 的宽高
      • onMeasure(int widthMeasureSpec, int heightMeasureSpec):View 在这里确定自己的尺寸。
      • MeasureSpec:一个 32 位 int 值,高 2 位是 Mode,低 30 位是 Size。这是理解测量的关键。
        • EXACTLY:精确值模式,对应 layout_width="100dp"match_parent
        • AT_MOST:最大值模式,对应 wrap_content
        • UNSPECIFIED:未指定,想多大就多大,常用于 ScrollView 等可滑动的容器。
      • ViewGroup 的测量ViewGroup(如 LinearLayout)需要先测量所有子 View,然后才能确定自己的尺寸。
    • Layout (布局)确定 View 在父容器中的位置
      • onLayout(boolean changed, int l, int t, int r, int b)ViewGroup 必须重写此方法,来安排子 View 的位置(调用子 View 的 layout 方法)。
    • Draw (绘制)将 View 绘制到屏幕上
      • onDraw(Canvas canvas):View 在这里进行具体的绘制操作(画背景、画内容)。
      • ViewGroup 默认不会调用 onDraw,除非调用了 setWillNotDraw(false)
      • 绘制顺序:背景 -> 自身内容(onDraw) -> 子 View(dispatchDraw) -> 装饰(如滚动条)。
  2. 自定义 View 时如何处理 wrap_content 和 padding?

    • 经典问题:如果不处理,自定义 View 在设置为 wrap_content 时会和 match_parent 效果一样。
    • 解决方案:在 onMeasure 中,当 MeasureSpec 的 mode 是 AT_MOST 时,计算一个默认的或者内容所需的最小尺寸,而不是直接使用父容器给出的 size
    • 处理 padding:在 onDraw 和计算尺寸时,要考虑到 getPaddingLeft(), getPaddingTop() 等值。

三、View 的事件分发机制 (Event Distribution)

另一个超级核心的考点,理解“责任链模式”在 Android 中的应用。

  1. 三个核心方法

    • dispatchTouchEvent(MotionEvent ev)进行事件分发。返回值表示是否消耗了当前事件。
    • onInterceptTouchEvent(MotionEvent ev)判断是否拦截某个事件。只有 ViewGroup 有此方法。
    • onTouchEvent(MotionEvent ev)处理点击事件。返回值表示是否消耗了当前事件。
  2. 事件分发流程 (就像一个U型管)

    1. DOWN 事件开始:事件从 Activity.dispatchTouchEvent 开始,传递给 Window,再传递给顶级 ViewGroup(通常是 DecorView)。
    2. 向下传递 (Intercept):事件从上到下传递。每一层的 ViewGroup 会先调用 onInterceptTouchEvent 判断是否拦截。如果拦截,则事件不再向下,转而交给该 ViewGrouponTouchEvent 处理。
    3. 向上传递 (Handle):如果没有任何 ViewGroup 拦截,事件最终会传递到最底层的 ViewonTouchEvent 方法。
    4. 事件消耗:如果某个 View 的 onTouchEvent 返回 true,表示它消耗了这个事件。那么后续的 MOVE、UP 等事件序列将直接交给它处理,不会再经历完整的向下传递和判断拦截过程。
    5. 事件回溯:如果所有子 View 和 ViewGroup 都不消耗事件(onTouchEvent 返回 false),事件会层层向上回溯,最终交给 Activity.onTouchEvent 处理。
  3. onTouchListener 与 onTouchEvent 的优先级

    • 如果一个 View 设置了 OnTouchListener,那么会先执行 listener.onTouch()
    • 如果 onTouch() 返回 true,则 onTouchEvent不会被执行
    • 否则,才会继续执行 onTouchEvent。在 onTouchEvent 中,如果 View 是 CLICKABLE 的,最后会执行 OnClickListener
  4. 滑动冲突

    • 场景:内外两层控件都可以滑动,如 ScrollView 里面套 ListView,或者 ViewPager 里面套 ListView
    • 解决思路
      • 外部拦截法:重写父容器onInterceptTouchEvent。在 DOWN 事件时不拦截,在 MOVE 事件时根据条件判断是否需要拦截。(推荐)
      • 内部拦截法:重写子 ViewdispatchTouchEvent,通过 requestDisallowInterceptTouchEvent 方法来请求父容器是否拦截。

四、自定义 View (Custom View)

考察实践能力和对原理的理解深度。

  1. 分类

    • 继承 View:需要自己处理 onMeasureonDraw。用于实现不规则形状的 View。
    • 继承 ViewGroup:需要自己处理 onMeasureonLayout。用于实现自定义布局。
    • 继承特定 View(如 TextView):在现有控件基础上进行功能扩展。
  2. 关键点

    • 支持 wrap_content 和 padding(如前所述)。
    • 处理自定义属性:在 res/values/attrs.xml 中定义属性,在构造方法中通过 TypedArray 获取。
    • 考虑性能:避免在 onDraw 中创建新对象(如 Paint, Path),应提前初始化。
  3. Canvas 和 Paint 的常用操作

    • CanvasdrawColor, drawCircle, drawRect, drawPath, drawText, drawBitmap 等。
    • Paint:设置颜色、样式(FILL, STROKE)、抗锯齿、文字大小等。

五、面试经典问题

  1. View 的测量流程中,match_parent 和 wrap_content 有什么区别?

    • 答:它们对应的 MeasureSpec 的 Mode 不同。match_parentEXACTLY,子 View 必须使用父容器给出的尺寸;wrap_contentAT_MOST,子 View 的尺寸不能超过父容器给出的尺寸。
  2. requestLayout() 和 invalidate() 的区别?

    • invalidate():只会触发 onDraw,用于刷新视图。必须在 UI 线程调用。
    • requestLayout():会触发完整的 measure -> layout 流程,不一定会触发 draw。当 View 的尺寸或位置可能发生变化时调用。
  3. postInvalidate() 和 invalidate() 的区别?

    • invalidate() 必须在 UI 线程调用。
    • postInvalidate() 可以在非 UI 线程中调用,它内部通过 Handler 将重绘请求发送到 UI 线程。
  4. 一个 MotionEvent 产生后,它的传递流程是怎样的?

    • 答:Activity -> Window -> DecorView -> 层层向下传递的 ViewGroup -> 最终的子 View。如果没人处理,再层层向上回溯到 Activity。
  5. 如何解决滑动冲突?你一般采用哪种方案,为什么?

    • 答:一般采用外部拦截法,因为它符合事件分发的逻辑,由父容器统一管理,逻辑更清晰,子 View 也无需关心。

总结

要应对关于 View 的所有考察,你需要:

  1. 能说清流程:清晰地描述 Measure、Layout、Draw 三大流程和事件分发流程。
  2. 能解释细节:理解 MeasureSpeconLayout 的作用、onTouchonClick 的优先级等。
  3. 能解决冲突:掌握滑动冲突的常见场景和解决方案。
  4. 能动手实践:了解自定义 View 的分类、注意事项和关键步骤。
  5. 能回答经典:对 requestLayout/invalidate 区别等问题对答如流。

把这些知识点串联起来,你就构建了一个完整的 Android View 知识体系。