🚀 第一梯队:高频核心考点(必问 / 实战难点)
Q1:自定义 View 的绘制流程(测量、布局、绘制)是怎样的?核心方法有哪些?如何正确处理 wrap_content 和 padding?
核心答案
自定义 View 的绘制流程由 ViewRootImpl.performTraversals() 触发,依次执行三大步骤:
- 测量 (Measure):
measure()→onMeasure(),根据父容器约束和自身尺寸要求确定宽高。wrap_content对应AT_MOST:必须主动计算内容所需尺寸,并通过resolveSizeAndState()取不超过父容器的值。padding处理:内容尺寸需加上getPaddingLeft/Right/Top/Bottom()。
- 布局 (Layout):
layout()→onLayout()(ViewGroup 需实现),确定自身及子 View 在父容器中的位置。 - 绘制 (Draw):
draw()→onDraw(),使用 Canvas 绘制内容,绘制坐标需偏移 padding。
流程图
精简源码(支持 wrap_content 和 padding 的圆形 View)
public class CircleView extends View {
private int radius = 100;
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int contentWidth = radius * 2 + getPaddingLeft() + getPaddingRight();
int contentHeight = radius * 2 + getPaddingTop() + getPaddingBottom();
int width = resolveSizeAndState(contentWidth, widthMeasureSpec, 0);
int height = resolveSizeAndState(contentHeight, heightMeasureSpec, 0);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int cx = getPaddingLeft() + radius;
int cy = getPaddingTop() + radius;
canvas.drawCircle(cx, cy, radius, paint);
}
}
Q2:详述 ViewGroup 的事件分发机制,三个核心方法的作用?如何解决滑动冲突?
核心答案
- 三大方法:
dispatchTouchEvent:分发事件,最先到达。决定交给onInterceptTouchEvent还是子 View。onInterceptTouchEvent:仅 ViewGroup 有,返回true则拦截,交由自己的onTouchEvent;返回false则传递给子 View。onTouchEvent:消费事件,返回true表示消费,流程结束;返回false则回传给父 View。
- 滑动冲突解决:
- 外部拦截法(推荐):父容器在
onInterceptTouchEvent中根据滑动方向判断。横向滑动不拦截(给子 View),纵向滑动拦截(父容器处理)。 - 内部拦截法:子 View 调用
parent.requestDisallowInterceptTouchEvent(true)禁止父容器拦截。
- 外部拦截法(推荐):父容器在
流程图
graph TD
A[Activity.dispatchTouchEvent] --> B[ViewGroup.dispatchTouchEvent]
B --> C{onInterceptTouchEvent}
C -->|true| D[ViewGroup.onTouchEvent]
C -->|false| E[子View.dispatchTouchEvent]
E --> F{子View.onTouchEvent}
F -->|true| G[事件消费]
F -->|false| H[回溯给父View.onTouchEvent]
D --> G
精简源码(外部拦截法示例)
// 父容器
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false; // 必须 false
}
float dx = ev.getX() - lastX;
float dy = ev.getY() - lastY;
if (Math.abs(dx) > Math.abs(dy)) {
return false; // 横向,不拦截
} else {
return true; // 纵向,拦截
}
}
Q3:如何优化自定义 View 的性能?(避免过度绘制与卡顿)
核心答案
- 绘制优化:
- 严禁在
onDraw中new对象(Paint,Path,Rect),应在构造函数中初始化复用,避免 GC 卡顿。 - 使用
canvas.clipRect()裁剪不可见区域,canvas.quickReject()快速拒绝。 - 复杂图形使用
Bitmap缓存或Path复用。 - 开启硬件加速:
setLayerType(LAYER_TYPE_HARDWARE, null)。
- 严禁在
- 刷新优化:使用
invalidate(Rect)局部刷新,代替全屏刷新。 - 布局优化:减少布局层级(
<merge>标签),使用ConstraintLayout扁平化,按需加载(ViewStub)。 - 测量优化:避免在
onMeasure中执行耗时计算,使用缓存结果。
流程图
graph TD
A[性能问题诊断] --> B{问题类型}
B -->|过度绘制| C[减少层级 / clipRect / 避免重叠背景]
B -->|动画卡顿| D[硬件加速 / 局部刷新 / 缓存计算]
B -->|内存抖动| E[onDraw中避免new对象 / 复用]
B -->|布局复杂| F[扁平化 / ViewStub / merge]
精简源码(绘制优化示例)
@Override
protected void onDraw(Canvas canvas) {
if (canvas.quickReject(mPath.getBounds(), Canvas.EdgeType.ANTI_ALIAS)) return;
canvas.save();
canvas.clipRect(getLeft(), getTop(), getRight(), getBottom());
canvas.drawPath(mPath, mPaint); // mPaint 在构造函数中初始化
canvas.restore();
}
Q4:属性动画 ObjectAnimator 的原理是什么?与 View 动画有何区别?
核心答案
- 原理:
- 基于
ValueAnimator,通过Choreographer监听 VSYNC 信号驱动。 - 计算动画分数(0~1)→ 插值器调整速率 → 估值器计算属性值 → 反射调用 Setter 方法 →
invalidate()重绘。
- 基于
- 与 View 动画区别:
- View 动画(补间)只改变视觉效果(矩阵变换),不改变 View 的实际属性(点击事件位置不变)。
- 属性动画真正改变对象属性,交互正常,支持任意对象。
流程图
graph TD
A[start] --> B[AnimationHandler注册帧回调]
B --> C[Choreographer每帧触发]
C --> D[fraction = currentTime-startTime/duration]
D --> E[interpolatedFraction = interpolator.getInterpolation]
E --> F[value = evaluator.evaluate]
F --> G[反射调用setter / 更新属性]
G --> H{动画完成?}
H -->|否| C
H -->|是| I[end]
精简源码(自定义抛物线估值器)
ObjectAnimator anim = ObjectAnimator.ofFloat(view, "translationY", 0f);
anim.setDuration(1000);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.setEvaluator(new ParabolaEvaluator(500f));
anim.start();
Q5:invalidate() 与 requestLayout() 的区别是什么?为什么 invalidate() 有时候不会回调 onDraw()?
核心答案
- 区别:
| 方法 | 触发流程 | 影响范围 | 适用场景 |
|---|---|---|---|
invalidate() | 标记脏 → 向上找根 View → performDraw() | 仅重绘 | 内容改变,位置大小不变 |
postInvalidate() | 同上,通过 Handler 切到主线程 | 仅重绘 | 子线程中调用重绘 |
requestLayout() | 标记 View 及父容器 → measure + layout + draw | 重新测量+布局+重绘 | 位置或大小改变(开销大) |
invalidate()不回调onDraw()的原因:- 当前 View 不可见(
getVisibility()为 GONE 或动画未开始)。 - 请求重绘的区域被其他 View 完全覆盖且父容器未标记
willNotDraw(false)。 - 多次连续调用被系统优化合并,仅重绘一次。
- View 尚未 attached 到窗口(例如
onCreate中调用invalidate)。
- 当前 View 不可见(
流程图
graph TD
A[invalidate] --> B[添加脏区域到ViewRootImpl]
B --> C[scheduleTraversals]
C --> D[合并多次请求]
D --> E[performTraversals]
E --> F[performDraw]
F --> G{View可见且脏区域非空?}
G -->|是| H[onDraw]
G -->|否| I[跳过onDraw]
精简源码
// postInvalidate 实现原理(简化)
public void postInvalidate() {
getRunQueue().postDelayed(new Runnable() {
@Override
public void run() {
invalidate();
}
}, 0);
}
🏗️ 第二梯队:底层原理与系统启动(架构视角)
Q6:Android 视图从 setContentView 到最终显示在屏幕上的完整底层链路?
核心答案
Activity.attach():创建PhoneWindow。setContentView():PhoneWindow创建DecorView,通过LayoutInflater将布局添加到android.R.id.content。ActivityThread.handleResumeActivity():执行onResume,调用WindowManagerImpl.addView(DecorView)。ViewRootImpl.setView():绘制起点。调用requestLayout()→scheduleTraversals()。Choreographer& VSYNC:收到 VSYNC 信号 →doTraversal()→performTraversals()。- 三大流程:
measure→layout→draw。 - 上屏:通过
Surface.lockCanvas()和unlockCanvasAndPost()将图形数据交给SurfaceFlinger,合成后显示。
流程图
graph TD
A[Activity.setContentView] --> B[PhoneWindow创建DecorView]
B --> C[LayoutInflater加载布局到content]
C --> D[Activity.onResume]
D --> E[WindowManager.addView]
E --> F[创建ViewRootImpl]
F --> G[ViewRootImpl.setView]
G --> H[requestLayout]
H --> I[scheduleTraversals]
I --> J[等待VSYNC]
J --> K[doTraversal]
K --> L[performTraversals: measure/layout/draw]
L --> M[Surface.unlockCanvasAndPost]
M --> N[SurfaceFlinger合成]
N --> O[屏幕显示]
精简源码(模拟绘制核心)
// ViewRootImpl 绘制部分
private void performDraw() {
Canvas canvas = surface.lockCanvas(null);
try {
draw(canvas);
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
Q7:为什么子线程不能更新 UI?ViewRootImpl 起了什么作用?如何正确更新?
核心答案
- 直接原因:
ViewRootImpl.checkThread()对比当前线程与创建ViewRootImpl时的线程(主线程),不一致则抛出CalledFromWrongThreadException。 - 深层原因:Android UI 控件非线程安全,多线程并发修改会导致状态混乱;加锁会严重降低渲染效率。
- 特例:在
ViewRootImpl创建之前(onResume前),子线程更新 UI 不会报错。 - 正确更新方式:
Activity.runOnUiThread、View.post、Handler绑定主线程 Looper。
流程图
graph TD
A[子线程调用textView.setText] --> B[ViewRootImpl.checkThread]
B --> C{mThread == currentThread?}
C -->|否| D[抛出CalledFromWrongThreadException]
C -->|是| E[正常更新]
F[正确做法] --> G[通过Handler.post]
G --> H[UI线程执行更新]
精简源码
// 错误写法
new Thread(() -> textView.setText("error")).start();
// 正确写法
new Thread(() -> {
final String data = loadData();
textView.post(() -> textView.setText(data));
}).start();
Q8:Activity、Window、View 三者之间的联系和区别?
核心答案
- Activity:UI 的管理者,负责生命周期、事件分发、与系统交互,内部持有一个
Window。 - Window:抽象类(实现类
PhoneWindow),代表一个“窗口”,负责管理View的添加、移除以及接收系统事件。每个 Activity 对应一个 Window。 - View:UI 的基本单元,处理自身绘制和事件。Window 通过
ViewRootImpl将 DecorView 附着到屏幕上。 - 联系:Activity 创建 Window → Window 创建 DecorView → DecorView 加载
setContentView的布局。
流程图
graph TD
A[Activity.attach] --> B[创建PhoneWindow]
B --> C[setContentView]
C --> D[PhoneWindow生成DecorView]
D --> E[LayoutInflater加载布局到content]
E --> F[onResume]
F --> G[WindowManager.addView]
G --> H[创建ViewRootImpl]
H --> I[ViewRootImpl控制DecorView的测量/布局/绘制]
精简源码(伪代码)
// Activity 内部
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
}
// PhoneWindow 内部
public void setContentView(int layoutResID) {
if (mDecor == null) installDecor();
mLayoutInflater.inflate(layoutResID, mContentParent);
}
📏 第三梯队:测量与布局核心(MeasureSpec / 宽高)
Q9:MeasureSpec 的工作原理是什么?三种模式分别对应什么场景?为什么 wrap_content 会失效?如何解决?
核心答案
MeasureSpec:32 位 int,高 2 位为模式(SpecMode),低 30 位为大小(SpecSize),由父容器的MeasureSpec和子 View 的LayoutParams组合而成。- 三种模式:
EXACTLY(精确):match_parent或固定数值。父容器已确定大小,子 View 必须遵守。AT_MOST(最大):wrap_content。父容器给出最大值,子 View 不能超过此值。UNSPECIFIED(未指定):系统内部使用(如ScrollView),子 View 想要多大就多大。
wrap_content失效原因:自定义 View 在onMeasure()中没有针对AT_MOST模式做特殊处理,直接使用父容器给的尺寸(即match_parent的效果)。- 解决方案:在
onMeasure()中判断模式,当为AT_MOST时,根据内容计算出期望尺寸,然后取min(期望尺寸, 父容器尺寸)。
流程图
graph TD
A[父容器传递childMeasureSpec] --> B[onMeasure]
B --> C[提取mode和size]
C --> D{mode?}
D -->|EXACTLY| E[finalSize = size]
D -->|AT_MOST| F[finalSize = min(内容尺寸, size)]
D -->|UNSPECIFIED| G[finalSize = 内容尺寸]
E --> H[setMeasuredDimension]
F --> H
G --> H
精简源码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = getDesiredWidth(); // 根据内容计算期望宽
int desiredHeight = getDesiredHeight();
int finalWidth = 0, finalHeight = 0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
finalWidth = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
finalWidth = Math.min(desiredWidth, widthSize);
} else {
finalWidth = desiredWidth;
}
// 高度同理...
setMeasuredDimension(finalWidth, finalHeight);
}
Q10:getMeasuredWidth() 与 getWidth() 有什么区别?在 onResume 中能获取到宽高吗?
核心答案
- 区别:
| 方法 | 数据来源 | 赋值时机 | 含义 |
|---|---|---|---|
getMeasuredWidth() | mMeasuredWidth | onMeasure 结束 (setMeasuredDimension) | 测量出来的理论宽高 |
getWidth() | mRight - mLeft | onLayout 阶段 (setFrame) | 实际在屏幕上占据的宽高 |
- 在
onResume中获取宽高的结果:首次启动不能,因为测量布局在onResume之后才执行;再次进入时可以。
流程图
graph TD
A[onResume执行] --> B{ViewRootImpl是否已执行measure?}
B -->|首次启动| C[尚未执行,宽高为0]
B -->|再次进入| D[已存在测量结果,可获取]
E[正确获取方式] --> F[view.post]
E --> G[OnGlobalLayoutListener]
E --> H[onWindowFocusChanged]
精简源码(正确获取方式)
view.post(() -> {
int w = view.getWidth();
});
Q11:ViewGroup 的 onDraw 方法默认为什么不执行?如何让它执行?
核心答案
- 原因:为了优化性能,
ViewGroup默认设置了PFLAG_SKIP_DRAW标志位,即setWillNotDraw(true)。 - 触发执行:调用
setWillNotDraw(false),或设置背景 (setBackground)、前景等属性。
流程图
graph TD
A[ViewGroup默认] --> B[setWillNotDraw(true)]
B --> C[跳过onDraw]
D[需要执行onDraw] --> E[调用setWillNotDraw(false)]
D --> F[设置背景setBackground]
D --> G[设置前景setForeground]
E --> H[onDraw可执行]
F --> H
G --> H
🛠️ 第四梯队:进阶实战与细节(资深必备)
Q12:自定义 ViewGroup 时如何正确实现 onMeasure 和 onLayout?需要处理哪些边界情况?自定义 View 与 ViewGroup 的核心区别是什么?
核心答案
- 自定义 ViewGroup 核心步骤:
onMeasure:遍历子 View,调用measureChildWithMargins(),累加宽高,加上 padding,setMeasuredDimension()。onLayout:遍历子 View,计算位置,调用child.layout()。
- 边界情况:处理
GONE、margin、gravity、MATCH_PARENT/WRAP_CONTENT。 - 区别:View 只需
onMeasure+onDraw,ViewGroup 还需onLayout管理子 View。
流程图
graph TD
A[onMeasure] --> B[遍历子View]
B --> C[measureChildWithMargins]
C --> D[累加宽高, 记录最大值]
D --> E[加上padding]
E --> F[setMeasuredDimension]
F --> G[onLayout]
G --> H[遍历子View]
H --> I[计算left, top]
I --> J[child.layout]
精简源码(简易垂直 LinearLayout 核心)
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int totalHeight = 0, maxWidth = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
measureChildWithMargins(child, widthSpec, 0, heightSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
totalHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
}
totalHeight += getPaddingTop() + getPaddingBottom();
maxWidth += getPaddingLeft() + getPaddingRight();
setMeasuredDimension(resolveSize(maxWidth, widthSpec),
resolveSize(totalHeight, heightSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int currentTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int left = getPaddingLeft() + lp.leftMargin;
int right = left + child.getMeasuredWidth();
int bottom = currentTop + lp.topMargin + child.getMeasuredHeight();
child.layout(left, currentTop + lp.topMargin, right, bottom);
currentTop = bottom + lp.bottomMargin;
}
}
Q13:自定义 View 中如何处理触摸事件(单点拖拽、双指缩放)?如何与 ViewGroup 的事件分发协作?
核心答案
- 单点拖拽:记录坐标,MOVE 中更新位置。
- 双指缩放:使用
ScaleGestureDetector。 - 事件协作:调用
getParent().requestDisallowInterceptTouchEvent(true)阻止父容器拦截。
流程图
graph TD
A[ACTION_DOWN] --> B[记录起始坐标]
B --> C[requestDisallowInterceptTouchEvent true]
C --> D[ACTION_MOVE]
D --> E[计算偏移量]
E --> F[setX/setY更新位置]
F --> G[ACTION_UP]
G --> H[清理状态]
I[双指缩放] --> J[ScaleGestureDetector]
J --> K[onScale回调]
K --> L[setScaleX/setScaleY]
精简源码(拖拽+缩放)
public class DragScaleView extends View {
private float lastX, lastY;
private ScaleGestureDetector scaleDetector;
// ...
@Override
public boolean onTouchEvent(MotionEvent event) {
scaleDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getRawX();
lastY = event.getRawY();
getParent().requestDisallowInterceptTouchEvent(true);
return true;
case MotionEvent.ACTION_MOVE:
float dx = event.getRawX() - lastX;
float dy = event.getRawY() - lastY;
setX(getX() + dx);
setY(getY() + dy);
lastX = event.getRawX();
lastY = event.getRawY();
return true;
}
return super.onTouchEvent(event);
}
}
Q14:如何在自定义 View 中高效实现沿贝塞尔曲线移动的动画?需要注意哪些性能问题?
核心答案
- 实现:
Path定义曲线 →PathMeasure获取长度和坐标 →ValueAnimator驱动 →getPosTan获取坐标 →setTranslationX/Y。 - 性能注意:对象复用、硬件加速、局部刷新。
流程图
graph TD
A[创建Path贝塞尔曲线] --> B[创建PathMeasure]
B --> C[ValueAnimator 0..length]
C --> D[每帧onAnimationUpdate]
D --> E[getPosTan获取坐标]
E --> F[setTranslationX/Y]
F --> G[invalidate重绘]
精简源码
public class PathMoveView extends View {
private Path path;
private PathMeasure pathMeasure;
private float[] pos = new float[2];
// ...
public PathMoveView(Context context, AttributeSet attrs) {
super(context, attrs);
path = new Path();
path.moveTo(100, 500);
path.cubicTo(300, 100, 600, 900, 800, 500);
pathMeasure = new PathMeasure(path, false);
ValueAnimator anim = ValueAnimator.ofFloat(0, pathMeasure.getLength());
anim.setDuration(3000);
anim.addUpdateListener(animation -> {
float distance = (float) animation.getAnimatedValue();
pathMeasure.getPosTan(distance, pos, null);
setTranslationX(pos[0] - getWidth()/2f);
setTranslationY(pos[1] - getHeight()/2f);
invalidate();
});
anim.start();
}
}
Q15:如何定义和使用自定义属性?在自定义 View 中如何获取这些属性?
核心答案
- 在
attrs.xml中定义<declare-styleable>。 - 布局中使用
app:赋值。 - 构造函数中
obtainStyledAttributes获取,最后recycle()。
流程图
graph TD
A[attrs.xml定义declare-styleable] --> B[布局文件使用app:customAttr]
B --> C[构造函数obtainStyledAttributes]
C --> D[读取属性值]
D --> E[存储为成员变量]
E --> F[调用recycle]
精简源码
// attrs.xml
<declare-styleable name="CircleView">
<attr name="circleColor" format="color" />
</declare-styleable>
// CircleView.java
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
int color = ta.getColor(R.styleable.CircleView_circleColor, Color.RED);
ta.recycle();
}
Q16:自定义 View 的内存优化有哪些注意事项?
核心答案
- 避免内存泄漏:静态内部类+弱引用。
- 资源释放:
onDetachedFromWindow中停止动画、回收 Bitmap。 - 大图采样压缩。
流程图
graph TD
A[内存优化] --> B[避免泄漏]
A --> C[资源释放]
A --> D[大图压缩]
B --> E[静态内部类+弱引用]
C --> F[onDetachedFromWindow停止动画/回收Bitmap]
D --> G[inSampleSize采样]
Q17:onMeasure 方法通常会执行几次?为什么?
核心答案
最少 2 次,最多 4 次。默认预测量+正式测量=2次;弹窗场景预测量可能2-3次,加上正式测量达4次。
流程图
graph TD
A[测量开始] --> B[预测量]
B --> C{是否弹窗且WRAP_CONTENT?}
C -->|是| D[预测量协商2-3次]
C -->|否| E[预测量1次]
D --> F[正式测量1次]
E --> F
F --> G[测量结束]
Q18:View 的 post、postDelayed 与 Handler 的关系?如何利用它们实现延迟任务?
核心答案
View 的 post 内部通过 ViewRootImpl.getHandler() 获取主线程 Handler 执行。可用于延迟获取宽高、延迟动画等。
流程图
graph TD
A[view.postDelayed] --> B[ViewRootImpl.getHandler]
B --> C[主线程Handler]
C --> D[延时后执行Runnable]
D --> E[更新UI或执行任务]
精简源码
view.postDelayed(() -> {
view.animate().translationX(100f).setDuration(300).start();
}, 1000);
Q19:如何实现一个自定义 View 的“限行数”效果(例如给 TextView 限定最多显示 3 行)?应该在哪个步骤处理?
核心答案
在 onMeasure 阶段使用 StaticLayout 计算行数,超过限制则调整高度,onDraw 中只绘制前 maxLines 行。
流程图
graph TD
A[onMeasure] --> B[创建StaticLayout]
B --> C[获取lineCount]
C --> D{lineCount > maxLines?}
D -->|是| E[计算maxLines高度]
D -->|否| F[使用完整高度]
E --> G[setMeasuredDimension]
F --> G
G --> H[onDraw绘制前maxLines行]
精简源码(核心片段)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int availableWidth = width - getPaddingLeft() - getPaddingRight();
StaticLayout layout = new StaticLayout(text, paint, availableWidth, ...);
int lineCount = layout.getLineCount();
int desiredHeight;
if (lineCount > maxLines) {
desiredHeight = layout.getLineBottom(maxLines - 1) + getPaddingTop() + getPaddingBottom();
} else {
desiredHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
}
setMeasuredDimension(width, resolveSize(desiredHeight, heightMeasureSpec));
}
Q20:什么是多点触控?如何在自定义 View 中处理?
核心答案
多点触控指屏幕同时支持多个手指操作。通过 event.getPointerCount()、event.getX(int index)、event.getActionIndex() 处理。
流程图
graph TD
A[多点触控事件] --> B[event.getPointerCount]
B --> C[遍历每个触点]
C --> D[event.getX(index), getY(index)]
D --> E[处理多指逻辑]
E --> F[常见应用: 缩放、多指绘图]
Q21:简述 Android 中 View 的绘制性能分析工具及常见问题(过度绘制、丢帧)的排查方法。
核心答案
工具:GPU 过度绘制、Profile GPU Rendering、Systrace、Layout Inspector。
常见问题:过度绘制、丢帧。解决:减少层级、clipRect、硬件加速、局部刷新。
流程图
graph TD
A[发现卡顿/掉帧] --> B[开启Profile GPU Rendering]
A --> C[开启GPU过度绘制]
A --> D[使用Layout Inspector]
B --> E[定位耗时帧]
C --> F[找到红色过度绘制区域]
D --> G[查看层级过深]
E --> H[优化手段]
F --> H
G --> H
H --> I[减少背景/clipRect/硬件加速/局部刷新]
Q22:View 的硬件加速是什么?如何开启?有什么注意事项?
核心答案
硬件加速利用 GPU 进行绘制,提升性能。开启方式:应用级、Activity 级、View 级。注意:某些 Canvas 操作不支持,过度使用会占用 GPU 内存。
流程图
graph TD
A[硬件加速] --> B[开启方式]
B --> C[应用级: manifest]
B --> D[Activity级: setFlags]
B --> E[View级: setLayerType]
A --> F[注意事项]
F --> G[clipPath/drawPicture可能不支持]
F --> H[过度使用占GPU内存]
F --> I[简单绘制反而降低性能]
Q23:你做过哪些高级的自定义 View 和动画结合的项目?请具体说明。
核心答案(示例)
项目一:可拖拽环形菜单 + 弹性动画;项目二:波浪进度条 + 颜色渐变动画。
流程图
graph TD
A[高级自定义View项目] --> B[环形菜单]
A --> C[波浪进度条]
B --> D[拖拽+Scroller惯性]
B --> E[ValueAnimator弹性回弹]
C --> F[Path绘制正弦波]
C --> G[ValueAnimator相位偏移]
C --> H[ArgbEvaluator颜色渐变]
精简源码(波浪动画核心)
ValueAnimator animator = ValueAnimator.ofFloat(0f, 2f * (float) Math.PI);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(animation -> {
phase = (float) animation.getAnimatedValue();
postInvalidate();
});
animator.start();
Q24:自定义 View 的 onDraw 中如何避免频繁 GC?ClipRect 和 QuickReject 如何使用?
核心答案
- 避免 GC:成员变量复用,不在
onDraw中new对象。 clipRect:裁剪绘制区域。quickReject:快速判断是否在裁剪区域外,若是则跳过绘制。
流程图
graph TD
A[onDraw优化] --> B[对象复用]
A --> C[clipRect裁剪]
A --> D[quickReject快速拒绝]
B --> E[Paint/Path/Rect声明为成员变量]
C --> F[canvas.clipRect限制绘制区域]
D --> G[if quickReject返回true则跳过绘制]
精简源码
@Override
protected void onDraw(Canvas canvas) {
if (canvas.quickReject(mPath.getBounds(), Canvas.EdgeType.AA)) return;
canvas.save();
canvas.clipRect(visibleRect);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
Q25:onWindowFocusChanged、onAttachedToWindow、onDetachedFromWindow 在自定义 View 中的使用场景?
核心答案
onWindowFocusChanged:窗口焦点变化,可获取宽高、启停动画。onAttachedToWindow:View 添加到窗口,注册监听、启动动画。onDetachedFromWindow:View 移除窗口,取消注册、停止动画、回收资源。
流程图
graph TD
A[自定义View生命周期回调] --> B[onAttachedToWindow]
A --> C[onWindowFocusChanged]
A --> D[onDetachedFromWindow]
B --> E[注册监听器/启动动画]
C --> F[获取宽高/启停动画]
D --> G[取消注册/停止动画/回收Bitmap]
精简源码
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(mListener);
mAnimator.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnGlobalLayoutListener(mListener);
mAnimator.cancel();
if (mBitmap != null) mBitmap.recycle();
}