Skip to content

自定义 View 面试题精选(带答案版)

一、基础概念类

1. 自定义 View 的基本步骤?

答案: 自定义 View 的三个基本步骤:

  1. 自定义属性

    • res/values/attrs.xml 中定义属性
    • 在构造方法中获取属性值
  2. 测量(onMeasure)

    • 测量 View 的宽高
    • 调用 setMeasuredDimension() 保存结果
  3. 布局(onLayout)

    • 确定子 View 的位置(ViewGroup 才需要)
    • 调用子 View 的 layout() 方法
  4. 绘制(onDraw)

    • 使用 Canvas 和 Paint 绘制内容
    • 调用 invalidate() 触发重绘

2. View 的绘制流程?

答案: View 的绘制流程分为三个阶段,由 ViewRootImplperformTraversals() 发起:

第一阶段:Measure(测量)

performTraversals() → performMeasure() → measure() → onMeasure()
  • 确定 View 的测量宽高
  • 父 View 通过 measure() 调用子 View 的 onMeasure()
  • 保存结果到 mMeasuredWidthmMeasuredHeight

第二阶段:Layout(布局)

performLayout() → layout() → onLayout()
  • 确定 View 在父容器中的位置
  • 设置 mLeftmTopmRightmBottom
  • ViewGroup 需要遍历调用子 View 的 layout()

第三阶段:Draw(绘制)

performDraw() → draw() → onDraw()
  • 绘制 View 的内容
  • 顺序:背景 → 自身内容 → 子 View → 装饰(滚动条等)

3. MeasureSpec 是什么?

答案:MeasureSpec 是一个 32 位 int 值,包含:

  • 高 2 位:测量模式(Mode)
  • 低 30 位:测量大小(Size)

三种测量模式

java
// 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 等可滚动的容器

获取和解析

java
int widthSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int mode = MeasureSpec.getMode(widthSpec);  // 获取模式
int size = MeasureSpec.getSize(widthSpec);  // 获取大小

二、测量与布局

4. onMeasure() 中如何正确测量?

答案:基本流程

java
@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:

java
@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 的位置:

java
@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 是画布,提供各种绘制方法:

java
@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 的常用属性?

答案:颜色与样式

java
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);

文字相关

java
// 文字大小
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);

效果与标志

java
// 抗锯齿
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树可能重新布局)

使用示例

java
// 只是内容变化,使用 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
<?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:在布局中使用

xml
<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 中获取属性

java
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. 触摸事件的分发机制?

答案:三个核心方法

  1. dispatchTouchEvent():事件分发

    • 返回 true:事件被消费
    • 返回 false:事件继续传递
  2. onInterceptTouchEvent():事件拦截(ViewGroup 特有)

    • 返回 true:拦截事件,不再传递给子 View
    • 返回 false:不拦截,继续传递给子 View
  3. onTouchEvent():事件处理

    • 返回 true:事件被消费,不再传递
    • 返回 false:事件未消费,继续传递

事件分发流程图

Activity.dispatchTouchEvent()

Window.superDispatchTouchEvent()

ViewGroup.dispatchTouchEvent()
    ↓(询问是否拦截)
ViewGroup.onInterceptTouchEvent()

    找到接收事件的子View

View.dispatchTouchEvent()

View.onTouchEvent()

12. 如何处理多点触控?

答案: 使用 MotionEvent 的多点触控相关方法:

java
@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 的使用?

答案:

java
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(属性动画)

java
// 创建动画
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(更简单)

java
// 自定义 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 动画

java
@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(需要支持库):

java
// 添加依赖
// 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() 中的对象创建

java
// 错误做法:每次 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()

java
@Override
protected void onDraw(Canvas canvas) {
    // 只绘制可见区域
    canvas.clipRect(mLeft, mTop, mRight, mBottom);
    
    // 绘制内容
    drawContent(canvas);
}

优化点3:使用硬件加速

xml
<!-- 在布局中启用硬件加速 -->
<View
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layerType="hardware" />  <!-- 或 software、none -->
java
// 在代码中设置
setLayerType(View.LAYER_TYPE_HARDWARE, null);

优化点4:避免过度绘制

  • 减少背景绘制
  • 合并绘制操作
  • 使用 canvas.quickReject() 判断是否需要绘制

优化点5:使用 View 缓存

java
// 开启绘图缓存
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 分析

bash
# 命令行使用
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. 如何实现一个流式布局?

答案:

java
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. 如何实现一个环形进度条?

答案:

java
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() 可能被调用多次?

答案:原因

  1. 布局嵌套:父 View 需要多次测量来确定子 View 的尺寸
  2. wrap_content:需要测量子 View 后才能确定自己的尺寸
  3. 权重(weight):LinearLayout 会测量两次
  4. margin/padding 变化
  5. requestLayout() 被调用

优化建议

  • onMeasure() 中避免耗时操作
  • 缓存测量结果(如果尺寸没变化)
  • 使用 getMeasuredWidth() 而不是 getWidth()

21. getWidth() 和 getMeasuredWidth() 的区别?

答案:

对比项getMeasuredWidth()getWidth()
获取时机onMeasure() 之后onLayout() 之后
值来源setMeasuredDimension() 设置的值mRight - mLeft 计算的值
是否变化测量期间可能变化布局完成后固定
使用场景onMeasure()onLayout()onDraw() 和事件处理中
关系通常相等,但可能不同(如动画缩放时)最终显示的宽度
java
// 正常情况下两者相等
// 但如果设置了缩放:
view.setScaleX(0.5f);
// getWidth() = 原始宽度
// getMeasuredWidth() = 原始宽度
// 实际显示宽度 = getWidth() * 0.5f

十、实战经验

22. 自定义 View 的注意事项?

答案:

  1. 构造函数要完整

    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);
    }
  2. 处理好 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);
    }
  3. 支持 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);
    }
  4. 避免内存泄漏

    java
    // 及时停止动画
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (animator != null && animator.isRunning()) {
            animator.cancel();
        }
    }
  5. 做好兼容性处理

    java
    // 版本兼容
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // 使用新 API
    } else {
        // 使用兼容实现
    }

这些是自定义 View 的核心知识点,掌握这些内容可以应对绝大多数面试问题。