Android 的 View 体系是面试中另一个超级核心的主题,尤其对于应用开发岗位。考察点可以从 View 的基础知识、绘制流程、事件分发,深入到自定义 View 和性能优化。
下面我将为你系统地梳理关于 View 所能考察的所有知识点。
一、View 基础 (View Basics)
这部分是理解 View 的起点,通常用于考察基础是否扎实。
View 的坐标体系
- 相对坐标:
getTop(),getBottom(),getLeft(),GetRight()。这些是 View 相对于其父容器的坐标。 - 绝对坐标:
getX(),getY()。表示的是触摸点相对于 View 左上角的坐标。 - Raw 坐标:
getRawX(),getRawY()。表示的是触摸点相对于整个屏幕左上角的坐标。
- 相对坐标:
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)。理解mScrollX和mScrollY的正负值。
- layout():通过改变
View 的生命周期
- 指的是一个 View 从被创建(
onAttachedToWindow)到被销毁(onDetachedFromWindow)的过程。 - 关键方法:
onAttachedToWindow->onMeasure->onLayout->onDraw-> ... ->onDetachedFromWindow。
- 指的是一个 View 从被创建(
二、View 的绘制流程 (View Drawing Process)
这是 View 体系的重中之重,必须深刻理解。整个过程始于 ViewRootImpl 的 performTraversals() 方法。
三大流程: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) -> 装饰(如滚动条)。
- Measure (测量):测量 View 的宽高。
自定义 View 时如何处理 wrap_content 和 padding?
- 经典问题:如果不处理,自定义 View 在设置为
wrap_content时会和match_parent效果一样。 - 解决方案:在
onMeasure中,当MeasureSpec的 mode 是AT_MOST时,计算一个默认的或者内容所需的最小尺寸,而不是直接使用父容器给出的size。 - 处理 padding:在
onDraw和计算尺寸时,要考虑到getPaddingLeft(),getPaddingTop()等值。
- 经典问题:如果不处理,自定义 View 在设置为
三、View 的事件分发机制 (Event Distribution)
另一个超级核心的考点,理解“责任链模式”在 Android 中的应用。
三个核心方法
dispatchTouchEvent(MotionEvent ev):进行事件分发。返回值表示是否消耗了当前事件。onInterceptTouchEvent(MotionEvent ev):判断是否拦截某个事件。只有ViewGroup有此方法。onTouchEvent(MotionEvent ev):处理点击事件。返回值表示是否消耗了当前事件。
事件分发流程 (就像一个U型管)
- DOWN 事件开始:事件从
Activity.dispatchTouchEvent开始,传递给Window,再传递给顶级ViewGroup(通常是DecorView)。 - 向下传递 (Intercept):事件从上到下传递。每一层的
ViewGroup会先调用onInterceptTouchEvent判断是否拦截。如果拦截,则事件不再向下,转而交给该ViewGroup的onTouchEvent处理。 - 向上传递 (Handle):如果没有任何
ViewGroup拦截,事件最终会传递到最底层的View的onTouchEvent方法。 - 事件消耗:如果某个 View 的
onTouchEvent返回true,表示它消耗了这个事件。那么后续的 MOVE、UP 等事件序列将直接交给它处理,不会再经历完整的向下传递和判断拦截过程。 - 事件回溯:如果所有子 View 和
ViewGroup都不消耗事件(onTouchEvent返回false),事件会层层向上回溯,最终交给Activity.onTouchEvent处理。
- DOWN 事件开始:事件从
onTouchListener 与 onTouchEvent 的优先级
- 如果一个 View 设置了
OnTouchListener,那么会先执行listener.onTouch()。 - 如果
onTouch()返回true,则onTouchEvent将不会被执行。 - 否则,才会继续执行
onTouchEvent。在onTouchEvent中,如果 View 是CLICKABLE的,最后会执行OnClickListener。
- 如果一个 View 设置了
滑动冲突
- 场景:内外两层控件都可以滑动,如
ScrollView里面套ListView,或者ViewPager里面套ListView。 - 解决思路:
- 外部拦截法:重写父容器的
onInterceptTouchEvent。在 DOWN 事件时不拦截,在 MOVE 事件时根据条件判断是否需要拦截。(推荐) - 内部拦截法:重写子 View 的
dispatchTouchEvent,通过requestDisallowInterceptTouchEvent方法来请求父容器是否拦截。
- 外部拦截法:重写父容器的
- 场景:内外两层控件都可以滑动,如
四、自定义 View (Custom View)
考察实践能力和对原理的理解深度。
分类
- 继承 View:需要自己处理
onMeasure和onDraw。用于实现不规则形状的 View。 - 继承 ViewGroup:需要自己处理
onMeasure和onLayout。用于实现自定义布局。 - 继承特定 View(如 TextView):在现有控件基础上进行功能扩展。
- 继承 View:需要自己处理
关键点
- 支持 wrap_content 和 padding(如前所述)。
- 处理自定义属性:在
res/values/attrs.xml中定义属性,在构造方法中通过TypedArray获取。 - 考虑性能:避免在
onDraw中创建新对象(如Paint,Path),应提前初始化。
Canvas 和 Paint 的常用操作
Canvas:drawColor,drawCircle,drawRect,drawPath,drawText,drawBitmap等。Paint:设置颜色、样式(FILL,STROKE)、抗锯齿、文字大小等。
五、面试经典问题
View 的测量流程中,match_parent 和 wrap_content 有什么区别?
- 答:它们对应的
MeasureSpec的 Mode 不同。match_parent是EXACTLY,子 View 必须使用父容器给出的尺寸;wrap_content是AT_MOST,子 View 的尺寸不能超过父容器给出的尺寸。
- 答:它们对应的
requestLayout() 和 invalidate() 的区别?
invalidate():只会触发onDraw,用于刷新视图。必须在 UI 线程调用。requestLayout():会触发完整的measure->layout流程,不一定会触发draw。当 View 的尺寸或位置可能发生变化时调用。
postInvalidate() 和 invalidate() 的区别?
invalidate()必须在 UI 线程调用。postInvalidate()可以在非 UI 线程中调用,它内部通过Handler将重绘请求发送到 UI 线程。
一个 MotionEvent 产生后,它的传递流程是怎样的?
- 答:Activity -> Window -> DecorView -> 层层向下传递的 ViewGroup -> 最终的子 View。如果没人处理,再层层向上回溯到 Activity。
如何解决滑动冲突?你一般采用哪种方案,为什么?
- 答:一般采用外部拦截法,因为它符合事件分发的逻辑,由父容器统一管理,逻辑更清晰,子 View 也无需关心。
总结
要应对关于 View 的所有考察,你需要:
- 能说清流程:清晰地描述 Measure、Layout、Draw 三大流程和事件分发流程。
- 能解释细节:理解
MeasureSpec、onLayout的作用、onTouch和onClick的优先级等。 - 能解决冲突:掌握滑动冲突的常见场景和解决方案。
- 能动手实践:了解自定义 View 的分类、注意事项和关键步骤。
- 能回答经典:对
requestLayout/invalidate区别等问题对答如流。
把这些知识点串联起来,你就构建了一个完整的 Android View 知识体系。