自定义 View 面试题精选(带答案版)
一、基础概念类
1. 自定义 View 的基本步骤?
答案: 自定义 View 的三个基本步骤:
自定义属性
- 在
res/values/attrs.xml中定义属性 - 在构造方法中获取属性值
- 在
测量(onMeasure)
- 测量 View 的宽高
- 调用
setMeasuredDimension()保存结果
布局(onLayout)
- 确定子 View 的位置(ViewGroup 才需要)
- 调用子 View 的
layout()方法
绘制(onDraw)
- 使用 Canvas 和 Paint 绘制内容
- 调用
invalidate()触发重绘
2. View 的绘制流程?
答案: View 的绘制流程分为三个阶段,由 ViewRootImpl 的 performTraversals() 发起:
第一阶段:Measure(测量)
performTraversals() → performMeasure() → measure() → onMeasure()- 确定 View 的测量宽高
- 父 View 通过
measure()调用子 View 的onMeasure() - 保存结果到
mMeasuredWidth和mMeasuredHeight
第二阶段:Layout(布局)
performLayout() → layout() → onLayout()- 确定 View 在父容器中的位置
- 设置
mLeft、mTop、mRight、mBottom - ViewGroup 需要遍历调用子 View 的
layout()
第三阶段:Draw(绘制)
performDraw() → draw() → onDraw()- 绘制 View 的内容
- 顺序:背景 → 自身内容 → 子 View → 装饰(滚动条等)
3. MeasureSpec 是什么?
答案:MeasureSpec 是一个 32 位 int 值,包含:
- 高 2 位:测量模式(Mode)
- 低 30 位:测量大小(Size)
三种测量模式:
// 1. EXACTLY(精确模式)
// 父容器已确定子 View 的精确大小
// 对应布局参数:match_parent 或具体数值
// 例如:android:layout_width="100dp"
// 2. AT_MOST(最大模式)
// 子 View 不能超过指定大小
// 对应布局参数:wrap_content
// 例如:android:layout_width="wrap_content"
// 3. UNSPECIFIED(未指定模式)
// 父容器不对子 View 做限制
// 通常用于 ScrollView、ListView 等可滚动的容器获取和解析:
int widthSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int mode = MeasureSpec.getMode(widthSpec); // 获取模式
int size = MeasureSpec.getSize(widthSpec); // 获取大小二、测量与布局
4. onMeasure() 中如何正确测量?
答案:基本流程:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 解析测量规格
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 2. 计算期望尺寸
int desiredWidth = getDesiredWidth();
int desiredHeight = getDesiredHeight();
// 3. 根据模式调整
int finalWidth, finalHeight;
// 宽度计算
if (widthMode == MeasureSpec.EXACTLY) {
finalWidth = widthSize; // 使用精确值
} else if (widthMode == MeasureSpec.AT_MOST) {
finalWidth = Math.min(desiredWidth, widthSize); // 不能超过最大值
} else { // UNSPECIFIED
finalWidth = desiredWidth; // 使用期望值
}
// 高度计算(同理)
if (heightMode == MeasureSpec.EXACTLY) {
finalHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
finalHeight = Math.min(desiredHeight, heightSize);
} else {
finalHeight = desiredHeight;
}
// 4. 保存测量结果(必须调用)
setMeasuredDimension(finalWidth, finalHeight);
}重要原则:
- 必须调用
setMeasuredDimension() - 考虑
padding的影响 - 对于 ViewGroup,需要测量所有子 View
5. ViewGroup 的 onMeasure() 如何处理?
答案: ViewGroup 需要遍历测量所有子 View:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 遍历所有子 View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 2. 跳过 GONE 状态的子 View
if (child.getVisibility() == View.GONE) {
continue;
}
// 3. 获取子 View 的 LayoutParams
LayoutParams lp = child.getLayoutParams();
// 4. 根据 LayoutParams 生成 MeasureSpec
int childWidthSpec = getChildMeasureSpec(
widthMeasureSpec,
getPaddingLeft() + getPaddingRight(),
lp.width
);
int childHeightSpec = getChildMeasureSpec(
heightMeasureSpec,
getPaddingTop() + getPaddingBottom(),
lp.height
);
// 5. 测量子 View
child.measure(childWidthSpec, childHeightSpec);
}
// 6. 根据所有子 View 的测量结果,计算自己的尺寸
int totalWidth = calculateTotalWidth();
int totalHeight = calculateTotalHeight();
// 7. 考虑父容器的限制
int finalWidth = resolveSize(totalWidth, widthMeasureSpec);
int finalHeight = resolveSize(totalHeight, heightMeasureSpec);
// 8. 保存结果
setMeasuredDimension(finalWidth, finalHeight);
}6. onLayout() 的实现?
答案:对于普通 View:通常不需要重写,因为默认实现已足够。
对于 ViewGroup:必须重写,确定子 View 的位置:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// l,t,r,b 是 ViewGroup 相对于父容器的位置
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
// 获取子 View 的测量宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 计算子 View 的位置
int childRight = childLeft + childWidth;
int childBottom = childTop + childHeight;
// 布局子 View
child.layout(childLeft, childTop, childRight, childBottom);
// 更新下一个子 View 的位置(根据布局规则)
childLeft = childRight + getChildMargin();
}
}三、绘制与渲染
7. onDraw() 中如何使用 Canvas?
答案: Canvas 是画布,提供各种绘制方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true); // 抗锯齿
// 1. 绘制矩形
paint.setColor(Color.RED);
canvas.drawRect(0, 0, 100, 100, paint);
// 2. 绘制圆形
paint.setColor(Color.BLUE);
canvas.drawCircle(150, 150, 50, paint);
// 3. 绘制文字
paint.setTextSize(48);
paint.setColor(Color.BLACK);
canvas.drawText("Hello", 200, 200, paint);
// 4. 绘制路径
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 100);
canvas.drawPath(path, paint);
// 5. 绘制 Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
canvas.drawBitmap(bitmap, 0, 0, paint);
}8. Paint 的常用属性?
答案:颜色与样式:
Paint paint = new Paint();
// 颜色
paint.setColor(Color.RED);
paint.setAlpha(128); // 透明度 0-255
paint.setARGB(128, 255, 0, 0);
// 样式
paint.setStyle(Paint.Style.FILL); // 填充
paint.setStyle(Paint.Style.STROKE); // 描边
paint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充+描边
// 描边宽度
paint.setStrokeWidth(5f);文字相关:
// 文字大小
paint.setTextSize(48f);
// 文字对齐
paint.setTextAlign(Paint.Align.LEFT); // 左对齐
paint.setTextAlign(Paint.Align.CENTER); // 居中
paint.setTextAlign(Paint.Align.RIGHT); // 右对齐
// 字体
Typeface typeface = Typeface.create("宋体", Typeface.BOLD);
paint.setTypeface(typeface);效果与标志:
// 抗锯齿
paint.setAntiAlias(true);
// 抖动(颜色过渡更平滑)
paint.setDither(true);
// 线性文本(优化小字体显示)
paint.setLinearText(true);
// 阴影
paint.setShadowLayer(10, 5, 5, Color.GRAY);9. invalidate() 和 requestLayout() 的区别?
答案:
| 对比项 | invalidate() | requestLayout() |
|---|---|---|
| 作用 | 请求重绘 | 请求重新布局和重绘 |
| 触发方法 | 调用 onDraw() | 调用 onMeasure() → onLayout() → onDraw() |
| 使用场景 | 内容变化但尺寸不变 | 尺寸或位置发生变化 |
| 性能消耗 | 较小 | 较大(需要重新测量和布局) |
| 是否影响子View | 否 | 是(整个View树可能重新布局) |
使用示例:
// 只是内容变化,使用 invalidate()
public void updateContent() {
mContent = "New Content";
invalidate(); // 只触发重绘
}
// 尺寸变化,使用 requestLayout()
public void updateSize(int newWidth, int newHeight) {
mWidth = newWidth;
mHeight = newHeight;
requestLayout(); // 触发重新测量和布局
}
// 内容、尺寸都变化,使用两者
public void updateAll() {
mContent = "New Content";
mWidth = 200;
mHeight = 200;
requestLayout(); // requestLayout() 已经包含重绘
}四、自定义属性
10. 如何定义和使用自定义属性?
答案:步骤1:定义属性(attrs.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<!-- 颜色属性 -->
<attr name="circleColor" format="color|reference" />
<!-- 尺寸属性 -->
<attr name="circleRadius" format="dimension|reference" />
<!-- 枚举属性 -->
<attr name="circleStyle" format="enum">
<enum name="fill" value="0" />
<enum name="stroke" value="1" />
<enum name="fillAndStroke" value="2" />
</attr>
<!-- 布尔属性 -->
<attr name="showText" format="boolean" />
<!-- 字符串属性 -->
<attr name="textContent" format="string|reference" />
<!-- 整型属性 -->
<attr name="maxLength" format="integer" />
<!-- 浮点型属性 -->
<attr name="strokeWidth" format="float" />
<!-- 引用类型 -->
<attr name="backgroundDrawable" format="reference" />
</declare-styleable>
</resources>步骤2:在布局中使用
<com.example.CircleView
android:layout_width="200dp"
android:layout_height="200dp"
app:circleColor="#FF0000"
app:circleRadius="50dp"
app:circleStyle="stroke"
app:strokeWidth="2.5"
app:showText="true"
app:textContent="Hello" />步骤3:在自定义 View 中获取属性
public class CircleView extends View {
private int mCircleColor;
private float mCircleRadius;
private int mCircleStyle;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
// 获取 TypedArray
TypedArray ta = context.obtainStyledAttributes(
attrs,
R.styleable.CircleView,
defStyleAttr,
0
);
try {
// 读取属性值,第二个参数是默认值
mCircleColor = ta.getColor(
R.styleable.CircleView_circleColor,
Color.RED
);
mCircleRadius = ta.getDimension(
R.styleable.CircleView_circleRadius,
50f
);
mCircleStyle = ta.getInt(
R.styleable.CircleView_circleStyle,
0
);
boolean showText = ta.getBoolean(
R.styleable.CircleView_showText,
false
);
String textContent = ta.getString(
R.styleable.CircleView_textContent
);
} finally {
// 必须回收 TypedArray
ta.recycle();
}
}
}五、触摸事件处理
11. 触摸事件的分发机制?
答案:三个核心方法:
dispatchTouchEvent():事件分发
- 返回 true:事件被消费
- 返回 false:事件继续传递
onInterceptTouchEvent():事件拦截(ViewGroup 特有)
- 返回 true:拦截事件,不再传递给子 View
- 返回 false:不拦截,继续传递给子 View
onTouchEvent():事件处理
- 返回 true:事件被消费,不再传递
- 返回 false:事件未消费,继续传递
事件分发流程图:
Activity.dispatchTouchEvent()
↓
Window.superDispatchTouchEvent()
↓
ViewGroup.dispatchTouchEvent()
↓(询问是否拦截)
ViewGroup.onInterceptTouchEvent()
↓
找到接收事件的子View
↓
View.dispatchTouchEvent()
↓
View.onTouchEvent()12. 如何处理多点触控?
答案: 使用 MotionEvent 的多点触控相关方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取事件类型
int action = event.getAction() & MotionEvent.ACTION_MASK;
// 获取手指数量
int pointerCount = event.getPointerCount();
// 处理不同的事件类型
switch (action) {
case MotionEvent.ACTION_DOWN:
// 第一根手指按下
handleTouchDown(event.getX(0), event.getY(0));
break;
case MotionEvent.ACTION_POINTER_DOWN:
// 非第一根手指按下
int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
float x = event.getX(index);
float y = event.getY(index);
handlePointerDown(index, x, y);
break;
case MotionEvent.ACTION_MOVE:
// 移动事件,可能有多个手指
for (int i = 0; i < pointerCount; i++) {
float x = event.getX(i);
float y = event.getY(i);
handleMove(i, x, y);
}
break;
case MotionEvent.ACTION_POINTER_UP:
// 非最后一根手指抬起
break;
case MotionEvent.ACTION_UP:
// 最后一根手指抬起
break;
}
return true;
}13. 手势识别 GestureDetector 的使用?
答案:
public class GestureView extends View {
private GestureDetector mGestureDetector;
public GestureView(Context context) {
super(context);
initGestureDetector(context);
}
private void initGestureDetector(Context context) {
mGestureDetector = new GestureDetector(context,
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
// 按下事件
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
// 单击抬起
return true;
}
@Override
public void onLongPress(MotionEvent e) {
// 长按
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
// 滚动
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
// 快速滑动(抛掷)
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
// 双击
return true;
}
});
// 解决长按和滚动的冲突
mGestureDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 将事件交给 GestureDetector 处理
boolean handled = mGestureDetector.onTouchEvent(event);
// 如果需要,可以添加自己的处理逻辑
if (!handled) {
// 自己的处理逻辑
}
return true;
}
}六、动画与过渡
14. 自定义 View 中实现动画?
答案:方法1:ValueAnimator(属性动画)
// 创建动画
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
// 监听动画值变化
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
// 根据 value 更新 View 状态
updateViewState(value);
invalidate(); // 重绘
}
});
// 开始动画
animator.start();
// 停止动画
animator.cancel();方法2:ObjectAnimator(更简单)
// 自定义 View 需要提供属性的 getter/setter
public class CircleView extends View {
private float mRadius;
public float getRadius() {
return mRadius;
}
public void setRadius(float radius) {
mRadius = radius;
invalidate(); // 设置新值时重绘
}
}
// 使用 ObjectAnimator 动画
ObjectAnimator animator = ObjectAnimator.ofFloat(
circleView, "radius", 0f, 100f
);
animator.setDuration(1000);
animator.start();方法3:使用 Canvas 动画
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 根据动画进度计算当前值
float currentRadius = mMinRadius + (mMaxRadius - mMinRadius) * mAnimProgress;
// 绘制
canvas.drawCircle(centerX, centerY, currentRadius, paint);
}
// 启动动画
private void startAnimation() {
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(animation -> {
mAnimProgress = (float) animation.getAnimatedValue();
invalidate();
});
animator.start();
}15. 如何实现弹性动画?
答案: 使用 SpringAnimation(需要支持库):
// 添加依赖
// implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
// 创建弹性动画
final SpringAnimation anim = new SpringAnimation(view,
DynamicAnimation.TRANSLATION_Y, 0f);
// 配置 SpringForce(弹簧)
SpringForce spring = new SpringForce();
spring.setFinalPosition(0f); // 最终位置
spring.setStiffness(SpringForce.STIFFNESS_MEDIUM); // 刚度
spring.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY); // 阻尼比
anim.setSpring(spring);
// 启动动画
anim.start();
// 也可以使用自定义值
SpringAnimation scaleAnim = new SpringAnimation(view,
new DynamicAnimation.ViewProperty() {
@Override
public void setValue(View view, float value) {
view.setScaleX(value);
view.setScaleY(value);
}
@Override
public float getValue(View view) {
return view.getScaleX();
}
}, 1f);七、性能优化
16. 自定义 View 的性能优化点?
答案:优化点1:减少 onDraw() 中的对象创建
// 错误做法:每次 onDraw 都创建新对象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每次创建,导致GC
canvas.drawRect(0, 0, 100, 100, paint);
}
// 正确做法:复用对象
private Paint mPaint;
public MyView(Context context) {
super(context);
mPaint = new Paint(); // 只创建一次
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRect(0, 0, 100, 100, mPaint); // 复用
}优化点2:使用 Canvas.clipRect()
@Override
protected void onDraw(Canvas canvas) {
// 只绘制可见区域
canvas.clipRect(mLeft, mTop, mRight, mBottom);
// 绘制内容
drawContent(canvas);
}优化点3:使用硬件加速
<!-- 在布局中启用硬件加速 -->
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layerType="hardware" /> <!-- 或 software、none -->// 在代码中设置
setLayerType(View.LAYER_TYPE_HARDWARE, null);优化点4:避免过度绘制
- 减少背景绘制
- 合并绘制操作
- 使用
canvas.quickReject()判断是否需要绘制
优化点5:使用 View 缓存
// 开启绘图缓存
setDrawingCacheEnabled(true);
setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
// 获取缓存 Bitmap
Bitmap cache = getDrawingCache();17. 如何检测自定义 View 的性能?
答案:工具1:GPU 过度绘制检测
- 开发者选项 → 显示GPU过度绘制
- 颜色说明:
- 无色:无过度绘制(最优)
- 蓝色:1次过度绘制
- 绿色:2次过度绘制
- 粉色:3次过度绘制
- 红色:4次或以上过度绘制(需要优化)
工具2:GPU 渲染分析
- 开发者选项 → GPU渲染模式分析 → 在屏幕上显示为条形图
- 每条柱状图代表一帧的渲染时间
- 绿色横线:16.67ms(60FPS的每帧时间)
- 超过绿线表示丢帧
工具3:Systrace 分析
# 命令行使用
python systrace.py -t 10 sched gfx view wm am app工具4:Profile GPU Rendering
- Android Studio → Tools → Layout Inspector
- 或使用
Debug.startMethodTracing()和Debug.stopMethodTracing()
八、高级技巧
18. 如何实现一个流式布局?
答案:
public class FlowLayout extends ViewGroup {
private List<List<View>> mAllViews = new ArrayList<>(); // 所有行的View
private List<Integer> mLineHeight = new ArrayList<>(); // 每行高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 已使用宽度和高度
int usedWidth = 0;
int usedHeight = 0;
int lineWidth = 0;
int lineHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
// 测量子View
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 判断是否需要换行
if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
// 换行
usedWidth = Math.max(usedWidth, lineWidth);
usedHeight += lineHeight;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
// 不换行
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 处理最后一个View
if (i == childCount - 1) {
usedWidth = Math.max(usedWidth, lineWidth);
usedHeight += lineHeight;
}
}
// 考虑padding
int finalWidth = widthMode == MeasureSpec.EXACTLY ? widthSize
: usedWidth + getPaddingLeft() + getPaddingRight();
int finalHeight = heightMode == MeasureSpec.EXACTLY ? heightSize
: usedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(finalWidth, finalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mLineHeight.clear();
int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;
List<View> lineViews = new ArrayList<>();
int childCount = getChildCount();
// 遍历所有子View,按行分组
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 需要换行
if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin
> width - getPaddingLeft() - getPaddingRight()) {
mAllViews.add(lineViews);
mLineHeight.add(lineHeight);
lineWidth = 0;
lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
lineViews = new ArrayList<>();
}
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);
lineViews.add(child);
}
// 处理最后一行
mAllViews.add(lineViews);
mLineHeight.add(lineHeight);
// 开始布局
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < mAllViews.size(); i++) {
lineViews = mAllViews.get(i);
lineHeight = mLineHeight.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int cl = left + lp.leftMargin;
int ct = top + lp.topMargin;
int cr = cl + child.getMeasuredWidth();
int cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
left += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
left = getPaddingLeft();
top += lineHeight;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}19. 如何实现一个环形进度条?
答案:
public class CircleProgressBar extends View {
private Paint mPaint;
private int mProgress = 0;
private int mMaxProgress = 100;
private int mStrokeWidth = 10;
private int mBackgroundColor = Color.GRAY;
private int mProgressColor = Color.BLUE;
public CircleProgressBar(Context context) {
super(context);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = Math.min(centerX, centerY) - mStrokeWidth / 2;
// 绘制背景圆
mPaint.setColor(mBackgroundColor);
canvas.drawCircle(centerX, centerY, radius, mPaint);
// 绘制进度圆弧
mPaint.setColor(mProgressColor);
float sweepAngle = 360f * mProgress / mMaxProgress;
RectF rectF = new RectF(
centerX - radius, centerY - radius,
centerX + radius, centerY + radius
);
canvas.drawArc(rectF, -90, sweepAngle, false, mPaint);
// 绘制进度文字
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(48);
mPaint.setStyle(Paint.Style.FILL);
String text = mProgress + "%";
float textWidth = mPaint.measureText(text);
float textHeight = mPaint.descent() - mPaint.ascent();
canvas.drawText(text,
centerX - textWidth / 2,
centerY + textHeight / 2 - mPaint.descent(),
mPaint);
}
public void setProgress(int progress) {
mProgress = Math.max(0, Math.min(progress, mMaxProgress));
invalidate();
}
public void setMaxProgress(int maxProgress) {
mMaxProgress = maxProgress;
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredSize = 200; // 默认大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredSize, widthSize);
} else {
width = desiredSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredSize, heightSize);
} else {
height = desiredSize;
}
// 确保宽高相等(圆形)
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
}九、常见问题
20. 为什么 onMeasure() 可能被调用多次?
答案:原因:
- 布局嵌套:父 View 需要多次测量来确定子 View 的尺寸
- wrap_content:需要测量子 View 后才能确定自己的尺寸
- 权重(weight):LinearLayout 会测量两次
- margin/padding 变化
- requestLayout() 被调用
优化建议:
- 在
onMeasure()中避免耗时操作 - 缓存测量结果(如果尺寸没变化)
- 使用
getMeasuredWidth()而不是getWidth()
21. getWidth() 和 getMeasuredWidth() 的区别?
答案:
| 对比项 | getMeasuredWidth() | getWidth() |
|---|---|---|
| 获取时机 | onMeasure() 之后 | onLayout() 之后 |
| 值来源 | setMeasuredDimension() 设置的值 | mRight - mLeft 计算的值 |
| 是否变化 | 测量期间可能变化 | 布局完成后固定 |
| 使用场景 | onMeasure() 和 onLayout() 中 | onDraw() 和事件处理中 |
| 关系 | 通常相等,但可能不同(如动画缩放时) | 最终显示的宽度 |
// 正常情况下两者相等
// 但如果设置了缩放:
view.setScaleX(0.5f);
// getWidth() = 原始宽度
// getMeasuredWidth() = 原始宽度
// 实际显示宽度 = getWidth() * 0.5f十、实战经验
22. 自定义 View 的注意事项?
答案:
构造函数要完整
java// 必须实现所有构造函数 public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); }处理好 padding
java@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 考虑 padding int left = getPaddingLeft(); int top = getPaddingTop(); int right = getWidth() - getPaddingRight(); int bottom = getHeight() - getPaddingBottom(); // 在考虑 padding 的区域绘制 canvas.drawRect(left, top, right, bottom, paint); }支持 wrap_content
java@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 必须处理 AT_MOST 模式 int desiredWidth = calculateDesiredWidth(); int desiredHeight = calculateDesiredHeight(); int width = resolveSize(desiredWidth, widthMeasureSpec); int height = resolveSize(desiredHeight, heightMeasureSpec); setMeasuredDimension(width, height); }避免内存泄漏
java// 及时停止动画 @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (animator != null && animator.isRunning()) { animator.cancel(); } }做好兼容性处理
java// 版本兼容 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 使用新 API } else { // 使用兼容实现 }
这些是自定义 View 的核心知识点,掌握这些内容可以应对绝大多数面试问题。