在Android中高级开发岗位的面试中,自定义View几乎是绕不开的环节。面试官关注的不仅仅是你能否画出复杂的图形,更看重你对View体系的理解深度、对性能的关注度以及解决复杂交互问题的能力。以下是目前互联网大厂最关注的四大核心模块:
① View绘制流程
理解measure、layout、draw三大流程的源码实现,掌握MeasureSpec的三种模式,以及如何重写onMeasure处理wrap_content。
② 事件分发机制
深入解析dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的传递规则,解决ViewGroup与View之间的冲突,以及滑动嵌套冲突。
③ 属性动画系统
区别于补间动画,ValueAnimator与ObjectAnimator的原理,插值器与估值器的应用,以及ViewPropertyAnimator的高效用法。
④ 性能优化与进阶
硬件加速开启与关闭,离屏缓存的使用,自定义ViewGroup的布局优化,以及如何在Recyclerview中高效绘制复杂图表。
自定义View的第一步,是理解系统是如何“画”出View的。这一过程分为三个阶段:Measure(测量)、Layout(布局)和Draw(绘制)。
1. Measure流程核心逻辑
View的测量过程是由上至下递归进行的。根ViewGroup通过measureChild遍历子View,子View再调用自身的measure方法。
// View的measure方法核心逻辑伪代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if (forceLayout || mLayout) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
在onMeasure中,我们需要处理三种MeasureSpec模式:
- EXACTLY:对应match_parent或具体数值,父容器已经确定了大小。
- AT_MOST:对应wrap_content,子View的大小不能超过父容器剩余空间。
- UNSPECIFIED:父容器不对View大小做限制,常用于系统内部测量。
面试高频点:为什么重写onMeasure时要调用setMeasuredDimension?因为measure是final方法,子类无法修改,必须在onMeasure中设置最终测量结果,否则抛出异常。
2. Layout流程核心逻辑
Layout过程确定View在容器中的位置。ViewGroup通过layout方法确定自身位置,并通过onLayout方法遍历子View,调用子View的layout方法确定子View的位置。
关键点在于onLayout方法。对于自定义ViewGroup,必须重写此方法,根据子View的测量结果计算其left、top、right、bottom坐标。
// 自定义ViewGroup示例
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 简单线性排列示例
child.layout(left, top, left + width, top + height);
}
}
3. Draw流程核心逻辑
Draw过程将View绘制到屏幕上。流程如下:
- 绘制背景:drawBackground(canvas)
- 绘制自己:onDraw(canvas)
- 绘制子View:dispatchDraw(canvas)
- 绘制装饰:onDrawForeground(canvas)
重点:大多数情况下,我们只需重写onDraw方法。Canvas是绘制的核心工具,Paint是画笔配置。注意Canvas的save()和restore()方法,用于保存和恢复画布状态,避免绘制相互影响。
Android中的点击事件本质是Touch事件,由MotionEvent描述。事件分发涉及三个核心方法,理解它们的返回值和职责是解决滑动冲突的关键。
| 方法 | 所属类 | 职责 | 返回值含义 |
|---|---|---|---|
| dispatchTouchEvent | ViewGroup/View | 事件分发,决定事件传递给谁 | true: 事件被消费;false: 事件继续向上传递 |
| onInterceptTouchEvent | ViewGroup | 事件拦截,决定是否拦截事件 | true: 拦截,自身处理;false: 不拦截,传给子View |
| onTouchEvent | ViewGroup/View | 事件处理,消费点击事件 | true: 消费事件;false: 事件向上传递给父View |
属性动画(Property Animation)是Android 3.0引入的,它通过不断修改对象的属性值来实现动画效果,比补间动画更强大、更灵活。
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
view.setTranslationX(value 100);
});
animator.start();
view.animate().translationX(100).alpha(0.5).setDuration(300).start();
插值器与估值器
插值器(Interpolator)决定了属性值随时间变化的速率(如加速、减速、弹跳)。估值器(TypeEvaluator)决定了如何根据当前时间进度计算属性值。默认情况下,系统提供了LinearInterpolator和IntEvaluator等,对于复杂对象(如Color、Point),需要自定义TypeEvaluator。
在面试中,能够主动提及性能优化方案,是区分初级与高级开发的重要标志。以下是自定义View中常见的优化点:
1. 避免在onDraw中创建对象
onDraw方法可能会被频繁调用(如滑动、刷新时),在其中new对象会导致频繁的GC,引起界面卡顿。应将Paint、Rect等对象定义为成员变量并复用。
2. 使用硬件加速开关
对于复杂的绘制,开启硬件加速(setLayerType(View.LAYER_TYPE_HARDWARE, null))可以提升性能。但对于某些不支持硬件加速的API(如某些PathEffect),需要关闭硬件加速(LAYER_TYPE_SOFTWARE)以避免异常。
3. 减少onDraw中的复杂计算
将计算量大的操作放在onMeasure或专门的初始化方法中完成。如果View内容不经常变化,可以使用缓存机制,将绘制结果保存到Bitmap中,下次直接绘制Bitmap。
4. 优化ViewGroup的onLayout
避免在onLayout中进行复杂的计算或递归。对于复杂的布局,考虑使用ConstraintLayout或自定义LayoutManager(在RecyclerView中)来优化性能。
Q1: 为什么自定义View继承ViewGroup时,onMeasure和onLayout必须重写?
ViewGroup本身不绘制内容,但它是容器的管理者。不重写onMeasure,子View的大小无法确定;不重写onLayout,子View的位置无法确定,导致界面空白或错乱。
Q2: wrap_content在自定义View中如何实现?
在onMeasure中,当MeasureSpec.mode为AT_MOST时,需要根据子View的测量结果或默认尺寸,结合父容器剩余空间,计算出合适的宽高,并调用setMeasuredDimension设置。
Q3: 点击事件冲突如何解决?
核心思路是控制onInterceptTouchEvent的返回值。在父View中,根据滑动方向或距离,决定是拦截事件(返回true)还是传递给子View(返回false)。
Q4: onDraw中的Canvas和View.draw中的Canvas有什么区别?
在onDraw中,Canvas已经经过了平移、裁剪等处理,直接绘制即可。而在View.draw中,Canvas是原始的,需要手动处理平移和保存状态。
Q5: 什么是离屏缓存(Off-screen Buffer)?
当View开启了硬件加速且使用了不支持硬件加速的API(如PorterDuffXfermode)时,系统会为View创建一个离屏Bitmap进行绘制,然后再将其绘制到屏幕上。这会带来性能开销,应谨慎使用。
Q6: ValueAnimator和ObjectAnimator的区别?
ValueAnimator是底层核心,只提供数值生成;ObjectAnimator继承自ValueAnimator,直接操作对象属性,但要求对象有setter方法。
Q7: 如何实现一个圆角矩形?
在onDraw中,使用Path绘制圆角矩形,或者使用Canvas.drawRoundRect。注意处理抗锯齿(setAntiAlias(true))。
Q8: 自定义ViewGroup如何支持子View的margin?
在onMeasure中,计算子View大小时,需要获取子View的LayoutParams并判断是否为MarginLayoutParams,如果是,则在计算时加上margin值。
Q9: 如何绘制文字并获取其宽度?
使用Paint.measureText(text)获取文字宽度。注意,measureText返回的是逻辑宽度,实际绘制时可能因字体不同而有细微差异。
Q10: 属性动画结束后,View的属性值会恢复吗?
不会。属性动画修改的是对象的实际属性值。如果希望动画结束后恢复原状,需要在动画监听器中手动设置,或使用ViewPropertyAnimator的withEndAction。
除了核心的自定义View,以下知识点也是面试中常被关联考察的内容,建议一并掌握:
RecyclerView优化
了解ViewHolder模式、DiffUtil的使用、多Type布局的实现,以及如何优化长列表的滑动性能。
Jetpack Compose
作为Android UI的未来,Compose基于声明式UI,不再需要传统View体系。了解其原理(如Composition、Recomposition)是加分项。
Canvas高级绘制
掌握Matrix变换、Shader(渐变、位图、路径着色器)、Xfermode(图像混合模式)等高级API,实现炫酷的视觉效果。
ViewStub与include
了解ViewStub的延迟加载机制,如何在布局复杂时减少内存占用,提高启动速度。