一、View绘制流程核心考点

在Android面试中,View绘制流程是必问的基础题。面试官通常要求候选人详细描述从Activity启动到View最终渲染在屏幕上的全过程。

1.1 三大方法:measure, layout, draw

理解 View 的绘制过程,关键在于掌握 measure()、layout() 和 draw() 这三个核心方法。

Step 1: Measure

View测量:通过 onMeasure() 确定View的宽和高。如果是自定义View,通常需要重写此方法,根据传入的 MeasureSpec 来计算自身的尺寸。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 自定义测量逻辑
    int width = resolveSize(size, widthMeasureSpec);
    setMeasuredDimension(width, height);
}
                                
Step 2: Layout

View布局:通过 onLayout() 确定View在父容器中的位置。对于 ViewGroup,需要遍历子View并调用其 layout() 方法。

Step 3: Draw

View绘制:通过 onDraw() 将View绘制到Canvas上。这是自定义UI效果的核心环节,可以使用 Paint 和 Canvas 进行各种绘图操作。

⚡ 面试加分项: 了解 MeasureSpec 的三种模式:UNSPECIFIED, EXACTLY, AT_MOST。理解父容器如何传递约束给子View。

二、View事件分发机制深度解析

事件分发是Android交互的核心,也是面试中的高频难点。主要涉及三个方法:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。

ViewGroup
View
冲突解决

ViewGroup 事件分发流程

  • dispatchTouchEvent:负责事件的分发。如果返回 true,表示消费该事件;返回 false 或 super,则交由子View处理或传递给父容器。
  • onInterceptTouchEvent:负责事件的拦截。只有ViewGroup才有此方法。如果返回 true,表示拦截事件,后续事件将直接交给ViewGroup的 onTouchEvent 处理。
  • onTouchEvent:负责处理事件。如果返回 true,表示消费;返回 false,表示事件传递给父容器的 onTouchEvent。

口诀:“从上往下分发,从下往上传递”。分发看 dispatch,拦截看 intercept,处理看 onTouch。

View 事件处理流程

View 没有 onInterceptTouchEvent。其事件处理流程相对简单:

  1. dispatchTouchEvent:将事件分发给 onTouchEvent 或 OnTouchListener。
  2. setOnTouchListener 的 onTouch 方法优先级高于 onTouchEvent。
  3. onTouchEvent:处理点击、滑动等事件。如果View可点击且事件为点击,则返回 true。

常见事件冲突与解决

在实际开发中,ViewPager 与 ScrollView 的滑动冲突最常见。解决方案通常包括:

  • 外部拦截法:在父View的 onInterceptTouchEvent 中判断,如果是横向滑动则拦截,纵向滑动不拦截。
  • 内部拦截法:配合 requestDisallowInterceptTouchEvent 使用,子View根据滑动方向请求父View是否拦截事件。

三、属性动画与ValueAnimator

属性动画是Android 3.0引入的强大动画系统,相比传统的 Tween动画,属性动画可以真正改变View的属性值,并支持自定义插值器和估值器。

3.1 ObjectAnimator 核心用法

使用 ObjectAnimator 可以非常方便地实现平移、旋转、缩放、透明度等动画效果。需要注意的是,目标属性必须有对应的 setXxx() 方法。


ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0, 100, 0);
animator.setDuration(500);
animator.start();
                        

3.2 自定义估值器 (TypeEvaluator)

对于非基础类型(如颜色、贝塞尔曲线点),需要自定义 TypeEvaluator 来计算中间值。这是实现复杂自定义View动画的关键。

动画类型 对应属性 说明
Alpha alpha 透明度变化,0.0 - 1.0
Translation translationX/Y 位置移动
Scale scaleX/Y 缩放比例
Rotation rotation 旋转角度

四、高阶实战:贝塞尔曲线与性能优化

4.1 贝塞尔曲线在自定义View中的应用

贝塞尔曲线常用于实现水波纹、下拉刷新、波浪效果等。Android中的 Path 类提供了 quadTo(二阶)和 cubicTo(三阶)方法。

面试中常问:如何结合属性动画实现动态的波浪效果?

解答思路:利用 ValueAnimator 改变控制点的坐标,每一帧重新绘制 Path,从而实现动态效果。

4.2 自定义View性能优化

在 onDraw 方法中,严禁进行对象分配、数据库操作或网络请求。应遵循以下原则:

  • 避免创建对象:在 onDraw 中复用 Paint 和 Path 对象。
  • 减少绘制范围:使用 canvas.clipRect() 裁剪不需要绘制的区域。
  • 硬件加速:了解哪些API支持硬件加速,哪些不支持,避免在复杂View中频繁切换。
  • 双层缓冲:对于极其复杂的绘制,可以使用 Bitmap 离屏绘制,减少重绘开销。

五、网友们还关心:高频面试Q&A

以下是求职者最常搜索的 Android自定义View面试题 及其深度解答。

Q1: View的滑动冲突有哪些场景?如何解决?

场景主要分为三种:外部滑动与内部滑动方向不一致、外部滑动与内部滑动方向一致、外部滑动与内部滑动的嵌套。

解决方法主要推荐外部拦截法,因为逻辑更清晰,符合Android事件分发机制的设计初衷。内部拦截法需要配合 requestDisallowInterceptTouchEvent,逻辑较为复杂。

Q2: 自定义ViewGroup需要注意什么?

1. 必须正确处理子View的 measure 和 layout。
2. 注意 onMeasure 中 child.measure() 的调用。
3. 处理 onLayout 时的边界情况,如子View超出范围。
4. 考虑 Margin 和 Padding 的影响。

Q3: 如何自定义一个支持Wrap_Content的View?

在 onMeasure 中,当 MeasureSpecMode 为 AT_MOST 时,需要遍历子View或计算自身内容尺寸,取子View最大尺寸与父容器限制的最小值作为最终尺寸。