5. 2026金三银四 吐血整理!Android高级UI 自定义view面试25题,覆盖90%大厂考点

🚀 第一梯队:高频核心考点(必问 / 实战难点)

Q1:自定义 View 的绘制流程(测量、布局、绘制)是怎样的?核心方法有哪些?如何正确处理 wrap_content 和 padding?

核心答案

自定义 View 的绘制流程由 ViewRootImpl.performTraversals() 触发,依次执行三大步骤:

  1. 测量 (Measure)measure()onMeasure(),根据父容器约束和自身尺寸要求确定宽高。
    • wrap_content 对应 AT_MOST:必须主动计算内容所需尺寸,并通过 resolveSizeAndState() 取不超过父容器的值。
    • padding 处理:内容尺寸需加上 getPaddingLeft/Right/Top/Bottom()
  2. 布局 (Layout)layout()onLayout()(ViewGroup 需实现),确定自身及子 View 在父容器中的位置。
  3. 绘制 (Draw)draw()onDraw(),使用 Canvas 绘制内容,绘制坐标需偏移 padding。

流程图

精简源码(支持 wrap_content 和 padding 的圆形 View)

java 复制代码
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

精简源码(外部拦截法示例)

java 复制代码
// 父容器
@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 的性能?(避免过度绘制与卡顿)

核心答案

  • 绘制优化
    • 严禁在 onDrawnew 对象(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]

精简源码(绘制优化示例)

java 复制代码
@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]

精简源码(自定义抛物线估值器)

java 复制代码
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)。

流程图

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]

精简源码

java 复制代码
// postInvalidate 实现原理(简化)
public void postInvalidate() {
    getRunQueue().postDelayed(new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    }, 0);
}

🏗️ 第二梯队:底层原理与系统启动(架构视角)

Q6:Android 视图从 setContentView 到最终显示在屏幕上的完整底层链路?

核心答案

  1. Activity.attach():创建 PhoneWindow
  2. setContentView()PhoneWindow 创建 DecorView,通过 LayoutInflater 将布局添加到 android.R.id.content
  3. ActivityThread.handleResumeActivity():执行 onResume,调用 WindowManagerImpl.addView(DecorView)
  4. ViewRootImpl.setView():绘制起点。调用 requestLayout()scheduleTraversals()
  5. Choreographer & VSYNC:收到 VSYNC 信号 → doTraversal()performTraversals()
  6. 三大流程:measurelayoutdraw
  7. 上屏:通过 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[屏幕显示]

精简源码(模拟绘制核心)

java 复制代码
// 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.runOnUiThreadView.postHandler 绑定主线程 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线程执行更新]

精简源码

java 复制代码
// 错误写法
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的测量/布局/绘制]

精简源码(伪代码)

java 复制代码
// 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

精简源码

java 复制代码
@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]

精简源码(正确获取方式)

java 复制代码
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 时如何正确实现 onMeasureonLayout?需要处理哪些边界情况?自定义 View 与 ViewGroup 的核心区别是什么?

核心答案

  • 自定义 ViewGroup 核心步骤
    1. onMeasure:遍历子 View,调用 measureChildWithMargins(),累加宽高,加上 padding,setMeasuredDimension()
    2. onLayout:遍历子 View,计算位置,调用 child.layout()
  • 边界情况 :处理 GONEmargingravityMATCH_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 核心)

java 复制代码
@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]

精简源码(拖拽+缩放)

java 复制代码
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重绘]

精简源码

java 复制代码
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 中如何获取这些属性?

核心答案

  1. attrs.xml 中定义 <declare-styleable>
  2. 布局中使用 app: 赋值。
  3. 构造函数中 obtainStyledAttributes 获取,最后 recycle()

流程图

graph TD A[attrs.xml定义declare-styleable] --> B[布局文件使用app:customAttr] B --> C[构造函数obtainStyledAttributes] C --> D[读取属性值] D --> E[存储为成员变量] E --> F[调用recycle]

精简源码

java 复制代码
// 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 的 postpostDelayedHandler 的关系?如何利用它们实现延迟任务?

核心答案

View 的 post 内部通过 ViewRootImpl.getHandler() 获取主线程 Handler 执行。可用于延迟获取宽高、延迟动画等。

流程图

graph TD A[view.postDelayed] --> B[ViewRootImpl.getHandler] B --> C[主线程Handler] C --> D[延时后执行Runnable] D --> E[更新UI或执行任务]

精简源码

java 复制代码
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行]

精简源码(核心片段)

java 复制代码
@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颜色渐变]

精简源码(波浪动画核心)

java 复制代码
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:成员变量复用,不在 onDrawnew 对象。
  • 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则跳过绘制]

精简源码

java 复制代码
@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:onWindowFocusChangedonAttachedToWindowonDetachedFromWindow 在自定义 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]

精简源码

java 复制代码
@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();
}

相关推荐
兄弟加油,别颓废了。2 小时前
XSS-Labs 前 5 关 超详细通关全解
前端·xss
telllong2 小时前
深入理解React Fiber架构:从栈调和到时间切片
前端·react.js·架构
英俊潇洒美少年2 小时前
React18 Hooks 项目重构为 Vue3 组合式API的坑
前端·javascript·重构
计算机安禾2 小时前
【Linux从入门到精通】第14篇:Linux引导流程浅析——从按下电源到登录界面
linux·服务器·人工智能·面试·知识图谱
雕刻刀2 小时前
服务器模拟断网
linux·服务器·前端
zs宝来了2 小时前
Vite 构建原理:ESBuild 与模块热更新
前端·javascript·框架
2301_814809862 小时前
实战分享Flutter Web 开发:解决跨域(CORS)问题的终极指南
前端·flutter
ayqy贾杰3 小时前
GPT-5.5+Codex全自动搓出macOS游戏,创作链路首次真正连续
前端·面试·游戏开发
不会写DN5 小时前
其实跨域问题是后端来解决的? CORS
服务器·网络·面试·go