当前位置: 首页 > news >正文

Android自定义View学习总结

面试官​:能说说自定义 View 的绘制流程,比如从 onMeasure()onDraw() 是怎么串联起来的吗?

​:
嗯,这个问题其实有点像问“盖房子需要哪些步骤”。自定义 View 的绘制流程,简单说就是先量尺寸、再摆位置、最后画内容。不过背后其实是 Android 系统通过 ViewRootImpl 这个“总指挥”来调度的。

我之前在做一个自定义进度条的时候,遇到过布局不生效的问题,后来通过看源码才搞明白。比如调用 requestLayout() 的时候,系统会从当前 View 往上找父容器,直到 ViewRootImpl,然后触发 performTraversals() 方法。这个方法就像项目经理,统筹安排测量(measure)、布局(layout)、绘制(draw)三个阶段。

测量阶段就像量房,父容器会给子 View 一个“预算”(比如最大宽度),子 View 在 onMeasure() 里确定自己需要多大空间。比如我的进度条需要根据文字长度动态调整宽度,就得在这里计算好,然后调用 setMeasuredDimension() 提交结果。如果忘了调用这个方法,系统会直接抛异常,就像施工队没交图纸就跑了。

布局阶段就是摆家具,确定每个 View 的位置。父容器在 onLayout() 里告诉子 View 该放哪儿(left, top, right, bottom)。比如我做过一个流式布局标签组件,这里要遍历所有子 View,计算每行能放几个标签,然后累加位置。

绘制阶段就是刷墙装修了,onDraw() 里用 Canvas 画内容。比如进度条要画背景、进度条和文字,这里得注意别画到边界外面。但这里有个坑:如果直接在主线程做复杂计算(比如实时绘制图表),会导致掉帧,后来我改成在子线程计算数据,主线程只负责绘制,才解决了卡顿问题。

对了,说到源码,ViewRootImplperformTraversals() 方法会根据需要决定是否触发这三个阶段。比如只有尺寸变化了才会重新测量,否则会复用之前的测量结果,这点对性能优化很重要。

举个实际例子
之前做项目时,发现一个自定义按钮在动态更新文字时偶尔会错位。后来发现是因为在子线程调了 requestLayout(),结果 ViewRootImpl 的线程检查没通过,导致布局没生效。最后改成用 post() 方法切回主线程,问题才解决。这也让我理解了为什么系统要求 UI 操作必须在主线程。


面试官​:那 invalidate()requestLayout() 有什么区别?什么时候用哪个?

​:
这两个方法有点像“刷新”和“重建”的区别。invalidate() 是局部刷新,比如按钮颜色变了,但大小没变,这时候只需要重绘(onDraw());而 requestLayout() 是整体重建,比如按钮大小变了,得重新测量和布局。

举个例子,我做过一个支持动态调整大小的图表 View。当用户拖拽边缘调整大小时,需要调用 requestLayout(),因为宽高变了;而只是数据更新(比如折线图的点变了),只需要 invalidate() 触发重绘。

但这里有个细节:invalidate() 会标记脏区域,系统在下一帧绘制时只刷新这部分。源码里能看到,如果脏区域积累过多(比如频繁调用 invalidate()),反而会浪费性能。所以后来我做动画时,会用 ValueAnimator 控制刷新频率,避免无意义的重复绘制。

踩坑经历
之前有次在 onDraw() 里调了 requestLayout(),结果陷入死循环——重绘触发重新布局,布局又触发重绘……最后用 Handler 延迟调用才绕过去。这也让我明白,绘制流程的方法不能随便嵌套调用。


面试官​:如果自定义 View 需要处理触摸事件,怎么避免手势冲突?

​:
手势冲突就像两个人同时说话,得有个优先级。比如我做过一个支持双击放大的图片 View,同时还要支持长按显示菜单。

解决方案是结合 GestureDetector 和自定义判断逻辑:

  1. onTouchEvent() 里,ACTION_DOWN 时启动一个延迟任务,500ms 后触发长按回调。
  2. 如果在这期间手指移动了(ACTION_MOVE 超过阈值),就取消长按任务,转为拖拽模式。
  3. 记录两次 ACTION_DOWN 的时间差,小于 300ms 且坐标接近时触发双击。

但这里有个问题:长按和双击会冲突。后来参考了系统 ImageView 的源码,发现长按触发后要标记状态,屏蔽后续的单击事件。

实际案例
在做画图 App 的橡皮擦功能时,长按激活橡皮擦,双击切换工具。一开始两个事件总打架,后来在长按回调里加了 isLongPressHandled 标记,如果长按已处理,就拦截后续的双击事件,这才让两者和谐共存。

扩展追问:

自定义 View 里的手势冲突啊,这个确实是个挺常见也挺头疼的问题,尤其是在那种嵌套滑动的场景里,比如说外面是一个上下滑动的列表 RecyclerView,里面又嵌了一个可以左右滑动的自定义 View,手指一搓,到底是上下滑还是左右滑,就很容易打架。

要避免这种冲突,关键就是要搞明白安卓那个触摸事件是怎么一层层往下传(dispatchTouchEvent),又是怎么决定谁来处理(onInterceptTouchEventonTouchEvent)的。这里面最重要的“裁判”和“沟通”机制,我理解有这么几个点:

  1. onInterceptTouchEvent(MotionEvent ev) 方法(主要在父 View/ViewGroup 里用):

    • 这个方法是父 View 的一个“特权”。当触摸事件从上往下传递的时候,会先经过父 View 的 dispatchTouchEvent,然后就会调用到这个 onInterceptTouchEvent
    • 父 View 就在这里“瞅一眼”这个事件(比如是 ACTION_DOWNACTION_MOVE),然后决定要不要“截胡”。
      • 如果它觉得这个手势应该是它来处理(比如用户开始明显地上下滑动了,而它本身就是个上下滑动的列表),它就可以从 onInterceptTouchEvent 返回 true。一旦返回 true,就表示“这个事件以及后续的同一系列事件,我收了!”,这些事件就不会再传给它的子 View 了(子 View 会收到一个 ACTION_CANCEL 事件,告诉它别等了)。
      • 如果它返回 false,就表示“我先不管,让孩子们试试看”。事件就会继续往下传给子 View。
    • 所以,父 View 可以通过这个方法来判断什么时候该自己处理,什么时候该放给子 View。
  2. onTouchEvent(MotionEvent event) 方法(父 View 和子 View 都有):

    • 如果事件没有被父 View 的 onInterceptTouchEvent 拦截掉,那么它就会传到子 View 的 dispatchTouchEvent,如果子 View 也是个 ViewGroup,它也会有 onInterceptTouchEvent 的机会。如果最终事件到了一个“真正想处理”的 View 那里,就会调用到这个 View 的 onTouchEvent 方法。
    • onTouchEvent 里,如果这个 View 觉得“嗯,这个事件是我的菜,我能处理,并且我还需要后续的事件(比如 ACTION_MOVEACTION_UP)”,那它就应该返回 true。返回 true 就等于告诉系统:“我消费了这个事件,接下来的事件请继续发给我。”
    • 如果返回 false,就表示“我不处理这个事件”,那这个事件就会往回冒泡,传给它的父 View 的 onTouchEvent 去处理。
  3. requestDisallowInterceptTouchEvent(boolean disallow) 方法(主要由子 View 调用):

    • 这个是子 View 的一个“反制”手段。有时候,父 View 可能比较“霸道”,或者判断条件比较模糊,老是想抢事件。但子 View 一旦开始处理某个手势(比如它是一个左右滑动的 View,并且用户确实开始左右滑了),它就特别不希望父 View 再来捣乱把它后续的事件给截走。
    • 这时候,子 View 就可以调用 getParent().requestDisallowInterceptTouchEvent(true)。这个方法一调用,就是在跟它的父 View 说:“爹,你接下来别再用 onInterceptTouchEvent 拦截我的事件了哈,这个手势我接管了!”
    • 通常情况下,有良好教养的父 View (比如安卓 SDK 里的那些 ViewGroup) 在收到这个请求后,在其后续的 onInterceptTouchEvent 调用中,只要 disallow 标志位还是 true,并且不是 ACTION_DOWN 事件,它就不会再拦截事件了,会把事件继续透传给这个子 View,直到整个手势结束(比如手指抬起,或者子 View 又调用了 requestDisallowInterceptTouchEvent(false) 把权限还给父 View)。

所以,怎么避免冲突呢?常见的思路有两种:

  • 外部拦截法:主要靠父 View 在 onInterceptTouchEvent() 里做精密的判断。比如,在 ACTION_DOWN 的时候先返回 false(不拦截),让子 View 有机会处理。然后在 ACTION_MOVE 的时候,根据滑动的距离、角度、速度等综合判断,这个手势到底是想触发父 View 的行为(比如上下滑)还是子 View 的行为(比如左右滑)。如果判断是自己的,就返回 true 把事件拦截下来。
  • 内部拦截法:这种方式下,父 View 在 onInterceptTouchEvent() 里通常比较“佛系”,可能除了 ACTION_DOWN 之外都返回 false(或者只在某些特定情况下才拦截)。主要靠子 View 在自己的 onTouchEvent() 里判断。如果子 View 觉得这个手势是自己的,并且不想被父 View 打断,它就主动调用 getParent().requestDisallowInterceptTouchEvent(true) 来“请求”父 View 不要拦截。

具体用哪种,要看具体场景和需求。但核心思想就是,父子 View 之间要有一个明确的“协商”和“责任划分”机制,不能各干各的,不然就很容易打架。一般我们会在 ACTION_MOVE 事件里,根据用户滑动的方向和幅度来做这个“仲裁”的判断。


面试官​:自定义 View 的性能优化有什么经验?

​:
性能优化的核心是“少干活,干巧活”。比如:

  1. 减少过度绘制​:用 canvas.clipRect() 限制绘制区域。之前做股票 K 线图,只画可视区域的数据,帧率直接翻倍。
  2. 避免主线程耗时操作​:在 onDraw() 里解析数据是大忌。后来我把数据预处理放到子线程,主线程只拿结果绘制。
  3. 硬件加速​:对静态部分用 setLayerType(LAYER_TYPE_HARDWARE, null) 缓存为纹理,但动态内容要切回软件层,否则反而更卡。

有个实战技巧:用 Android Studio 的 ​Layout Inspector​ 和 ​GPU 渲染分析​ 工具,能直接看到哪部分绘制耗时最长。比如之前发现一个圆角背景的过度绘制,改成 canvas.drawRoundRect() 替代多层叠加,性能提升明显。

源码启发
RecyclerView 的源码发现,它对不可见 Item 会回收复用。受此启发,我做图表组件时也加了缓存池,复用 PathPaint 对象,减少内存抖动。


面试官​:如果让你设计一个支持动画的自定义 View,需要注意什么?

​:
动画 View 的关键是“流畅”和“可控”。比如做过一个下拉刷新控件,下拉时箭头旋转,松手后加载动画:

  1. 使用 ValueAnimator 而不是死循环​:通过插值器(如 LinearInterpolator)控制动画进度,在 onAnimationUpdate() 里调用 invalidate()
  2. 及时释放资源​:在 onDetachedFromWindow() 里取消动画,避免内存泄漏。
  3. 硬件加速​:对复杂动画启用 LAYER_TYPE_HARDWARE,但要注意 Android 4.0 以下兼容性问题。

踩坑案例
第一次做加载动画时,没考虑屏幕旋转,横竖屏切换后动画错乱。后来在 onSaveInstanceState() 里保存动画进度,恢复时从进度继续执行,才解决这个问题。


设计模式在自定义VIew的应用 

1.模板方法模式:View 生命周期的骨架控制

面试官问​:能举个模板方法模式在自定义 View 中的例子吗?

回答​:
模板方法模式的核心是定义算法骨架,子类实现具体步骤。在 Android 的自定义 View 中,View 类通过 onMeasure()onLayout()onDraw() 这三个方法,构建了绘制流程的“模板”。

举个例子,比如我要实现一个圆形 View:

  1. 继承 View 类,重写 onMeasure() 方法确定 View 的大小。
  2. 在 onMeasure() 中根据 MeasureSpec 处理父容器的约束条件,调用 setMeasuredDimension() 设置最终尺寸。
  3. 重写 onDraw(),用 Canvas 绘制一个圆形,圆心在 View 中心,半径为宽高较小值的一半。

源码体现​:

  • View 的 draw() 方法内部调用 onDraw(),但 View 的默认 onDraw() 是空实现,这就是模板方法模式的典型应用——父类定义流程(draw()),子类实现细节(onDraw())。
  • 如果子类不重写 onMeasure() 且未调用 setMeasuredDimension(),直接抛 IllegalStateException,这正是模板方法对流程的强约束。

2. 策略模式:动态切换绘制效果

面试官问​:如何让一个 View 支持多种绘制效果,比如圆形和矩形?

回答​:
这时候可以用策略模式,把绘制逻辑抽象成接口,不同效果实现不同策略。比如:

  1. 定义 DrawingStrategy 接口,包含 draw(Canvas) 方法。
  2. 实现两种策略:CircleStrategy(画圆)和 RectangleStrategy(画矩形)。
  3. 在自定义 View 中持有 DrawingStrategy 的引用,通过 setStrategy() 方法动态切换策略,调用 invalidate() 触发重绘。

代码示例​:

// 自定义 View 的 onDraw() 中调用策略对象
@Override
protected void onDraw(Canvas canvas) {if (strategy != null) {strategy.draw(canvas, getWidth(), getHeight());}
}

优势​:

  • 解耦​:绘制逻辑与 View 主体分离,新增效果只需新增策略类。
  • 灵活​:运行时动态切换策略,比如根据用户选择切换形状。

3. 观察者模式:状态变化的监听与通知

面试官问​:如果 View 的状态变化需要通知外部,如何设计?

回答​:
可以用观察者模式,比如自定义进度条需要监听进度变化:

  1. 定义接口 OnProgressChangeListener,包含 onProgressChanged(int) 方法。
  2. 在 View 中维护一个监听器列表(List<OnProgressChangeListener>)。
  3. 当进度变化时(如 setProgress() 被调用),遍历列表通知所有监听器。

代码示例​:

// 在 View 中定义添加/移除监听器的方法
public void addProgressListener(OnProgressChangeListener listener) {listeners.add(listener);
}// 进度更新时触发通知
private void notifyProgressChanged() {for (OnProgressChangeListener listener : listeners) {listener.onProgressChanged(progress);}
}

实际应用​:

  • 类似 RecyclerView.addOnScrollListener(),Android 系统广泛使用观察者模式处理事件回调。

4. 组合模式:ViewGroup 的树形结构

面试官问​:ViewGroup 如何管理子 View?这用到了什么模式?

回答​:
组合模式是 ViewGroup 的核心设计思想。ViewGroup 本身是一个 View,同时可以包含多个子 View 或子 ViewGroup,形成树形结构。

关键行为​:

  1. 测量子 View​:在 onMeasure() 中遍历所有子 View,调用 child.measure()
  2. 布局子 View​:在 onLayout() 中确定每个子 View 的位置(如垂直排列时累加高度)。
  3. 绘制子 View​:ViewGroup 默认不重绘自身,但会调用 dispatchDraw() 绘制子 View。

源码体现​:

  • ViewGroup 继承自 View,但重写了 dispatchDraw() 方法,遍历子 View 调用 child.draw(canvas)
  • 用户操作 ViewGroup 时(如设置背景),无需关心内部子 View 的细节,这正是组合模式的“整体与部分一致对待”思想。

​5. requestLayout()的坑与优化

面试官:“为什么在onCreate()中调用requestLayout()无效?”

回答​:
原因​:onCreate()View还未附加到窗口(mAttachInfo == null),布局请求会被忽略。
解决方案​:用post()延迟到View附加后执行:

view.post(() -> view.requestLayout()); // 等View挂载到窗口再触发布局  

性能优化技巧​:

  • 避免冗余调用​:在onLayout()onMeasure()中通过isLayoutRequested()判断是否需要重新布局。
  • 局部刷新​:优先用invalidate(Rect)代替全量重绘,减少性能开销。

Android自定义View全流程深度剖析

面试官​:

“你在项目中做过复杂自定义View对吧?说说整个绘制流程的源码级理解,比如onMeasure()onDraw()是怎么串联起来的。”

候选人​:

“绘制流程就像造房子,得先打地基再盖屋顶。系统通过ViewRootImpl这个‘总工程师’来调度整个过程:

  1. 测量阶段​(量房):

    • 当调用requestLayout()时,ViewRootImpl会启动performTraversals(),先调用performMeasure()
    • 父容器(比如LinearLayout)通过measureChildWithMargins()给子View传递MeasureSpec(包含宽高约束),就像给装修队图纸。
    • 子View在onMeasure()里确定自己的尺寸,必须调用setMeasuredDimension()交卷,否则系统会抛异常——这就像施工队必须签字确认图纸。
  2. 布局阶段​(放样):

    • performLayout()触发后,父容器在onLayout()里调用每个子View的layout(l, t, r, b)方法,确定它们的位置坐标。
    • 这里有个优化技巧:View.layout()内部会先检查位置是否变化,避免无意义的重绘。就像装修师傅发现墙面没动,直接跳过刷漆。
  3. 绘制阶段​(施工):

    • performDraw()最终调用View.draw(),这里藏着四个关键步骤:
      • 画背景(drawBackground())→ 相当于刷墙底漆
      • 画内容(onDraw())→ 手绘壁画,开发者在这里发挥创意
      • 画子View(dispatchDraw())→ 给每个房间装家具
      • 画前景(onDrawForeground())→ 最后挂装饰画
    • 系统用DisplayList记录绘制指令,通过RenderThread异步渲染,避免阻塞主线程。”

面试官​:

“提到MeasureSpec,能具体说说它的三种模式吗?实际项目中怎么处理?”

候选人​:

MeasureSpec就像装修预算书,分三种情况:

  • EXACTLY​(精确尺寸):父容器给了确定数值,比如layout_width="100dp",这时候直接按数值施工。
  • AT MOST​(最大限制):父容器给上限,比如layout_width="wrap_content"但父容器剩余空间有限。这时候要量力而行,比如文本不能超过父容器宽度。
  • UNSPECIFIED​(自由发挥):少见但重要,比如ScrollView的子View高度可以无限延伸。

实战案例:我们做过一个流式布局标签组件,处理AT MOST时需要动态计算每行能放几个标签。算法类似摆俄罗斯方块——遍历子View,累加宽度直到超出剩余空间就换行。”


面试官​:

“如果自定义View需要同时处理双击和长按,怎么避免手势冲突?”

候选人​:

“这就像同时要听清两个人的指令,得设置优先级和状态机。我们的解决方案:

  1. 长按检测​:

    • onTouchEvent()ACTION_DOWN里启动postDelayed(),延迟500ms触发长按回调。
    • 如果在这期间发生移动(ACTION_MOVE超过触摸容差)或抬起(ACTION_UP),立即移除延迟任务。
  2. 双击检测​:

    • 记录两次ACTION_DOWN的时间差,若小于300ms且坐标在阈值内视为双击。
    • 检测到第一次点击时启动定时器,超时未收到第二次点击则触发单击事件。
  3. 冲突仲裁​:

    • 当长按先触发时,标记isLongPressHandled=true,后续抬起事件不再触发单击。
    • 使用GestureDetector简化代码,但需要重写onDoubleTap()onLongPress()方法。

去年做画图APP时,这个逻辑让橡皮擦工具(长按激活)和撤销功能(双击)完美共存。”


面试官​:

invalidate()postInvalidate()在源码层面有什么区别?为什么子线程要用后者?”

候选人​:

“这两个方法就像快递派件:

  • ​**invalidate()**​ 是主线程专送——直接在当前线程标记脏区域,要求立即重绘。如果子线程调用,会触发CalledFromWrongThreadException,就像让外卖小哥进办公楼没工牌被拦下。
  • ​**postInvalidate()**​ 是跨线程中转——内部通过Handler把重绘请求抛到主线程的MessageQueue。源码里能看到它调用ViewRootImpl.dispatchInvalidateOnAnimation(),确保线程安全。

源码级区别:

// View.java
public void postInvalidate() {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this); // 跨线程转发}
}

我们直播礼物动画模块就大量使用postInvalidate(),在渲染线程计算粒子位置,主线程只负责绘制。”


面试官​:

“自定义View导致过度绘制严重,有哪些优化手段?”

候选人​:

“过度绘制就像反复刷墙浪费油漆,我们有四把手术刀:

  1. 裁剪画布​:

    • onDraw()里先用canvas.clipRect()划定绘制区域,避免绘制不可见部分。
    • 比如列表项只绘制可见区域,滚动时动态调整clipRect的边界。
  2. 分层绘制​:

    • 对静态背景使用setLayerType(LAYER_TYPE_HARDWARE, null),生成纹理缓存。
    • 动态内容用软件绘制,减少GPU指令提交次数。
  3. 合并绘制​:

    • 将多个drawText()调用合并为一次,利用StaticLayout预生成文本布局。
    • 使用drawTextRun()替代多个drawText(),减少JNI调用开销。
  4. 工具检测​:

    • 开启开发者选项中的‘显示过度渲染’,红色区域需要优先优化。
    • 使用Tracer for OpenGL跟踪GPU绘制命令,定位重复绘制区域。

最近优化股票K线图组件,通过canvas.clipRect()把绘制区域限制在可视范围,帧率从45FPS提升到58FPS。”


面试官​:能详细说说 Android 中 View 的绘制源码调用顺序吗?比如从 ViewRootImplonDraw() 的完整流程。

​:
这个问题其实可以比喻成“一场精心策划的演出”,ViewRootImpl 是总导演,View 是演员,而 Choreographer 是舞台调度。整个流程大致分三步:​量舞台(Measure)→ 摆道具(Layout)→ 演出(Draw)​,但背后源码的协作非常精密。


1. 流程起点:ViewRootImpl 的 performTraversals()​

当你在代码里调用 requestLayout() 或者界面需要更新时,ViewRootImplperformTraversals() 方法会被触发。这个方法是整个绘制流程的“总开关”,它根据当前状态(比如是否需要重新测量、布局或绘制)决定执行哪些步骤。

源码关键逻辑​:

// ViewRootImpl.java
private void performTraversals() {// 1. 检查是否需要重新测量if (mLayoutRequested) {performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}// 2. 检查是否需要重新布局if (layoutRequested) {performLayout(lp, desiredWindowWidth, desiredWindowHeight);}// 3. 检查是否需要重新绘制if (!mDirty.isEmpty() || mIsAnimating) {performDraw();}
}

2. Measure 阶段:确定 View 的尺寸

调用链​:
ViewRootImpl.performMeasure()DecorView.measure()View.measure()View.onMeasure()

源码细节​:

  • 父容器传递约束​:父容器(如 LinearLayout)通过 MeasureSpec 告诉子 View 可用的空间和模式(EXACTLY/AT_MOST/UNSPECIFIED)。
  • 子 View 计算尺寸​:子 View 在 onMeasure() 中根据父容器的约束,调用 setMeasuredDimension() 确定自己的宽高。

示例​:
假设一个 TextViewonMeasure() 需要计算文本占用的空间:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 计算文本宽度(伪代码)int textWidth = calculateTextWidth(mText);int textHeight = calculateTextHeight(mText);// 处理父容器的约束int width = resolveSize(textWidth, widthMeasureSpec);int height = resolveSize(textHeight, heightMeasureSpec);setMeasuredDimension(width, height); // 必须调用!
}

3. Layout 阶段:确定 View 的位置

调用链​:
ViewRootImpl.performLayout()DecorView.layout()View.layout()View.onLayout()

源码细节​:

  • 递归布局子 View​:父容器(如 RelativeLayout)在 onLayout() 中遍历所有子 View,调用每个子 View 的 layout(l, t, r, b) 方法,传递左上右下坐标。
  • 位置是否变化​:View.layout() 内部会检查新旧坐标是否一致,避免无意义的重复布局。

示例​:
LinearLayout 垂直排列子 View 的核心逻辑:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int childTop = mPaddingTop;for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);// 计算子 View 的位置child.layout(l, childTop, l + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());childTop += child.getMeasuredHeight() + mDividerHeight; // 累加高度}
}

4. Draw 阶段:绘制内容到屏幕

调用链​:
ViewRootImpl.performDraw()DecorView.draw()View.draw()View.onDraw()

源码细节​:

  • 绘制顺序​:View.draw() 方法内部按顺序执行四个步骤:
    1. 画背景​:drawBackground(canvas)
    2. 画内容​:onDraw(canvas)(开发者自定义)
    3. 画子 View​:dispatchDraw(canvas)ViewGroup 实现)
    4. 画前景​:onDrawForeground(canvas)(滚动条、边缘效果等)

示例​:
自定义圆形按钮的 onDraw()

@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);// 画圆形背景canvas.drawCircle(getWidth()/2, getHeight()/2, mRadius, mPaint);// 画文本canvas.drawText(mText, getWidth()/2, getHeight()/2, mTextPaint);
}

5. 关键角色:Choreographer 与 VSYNC
  • Choreographer​:负责协调绘制时序,监听 VSYNC 信号(屏幕刷新信号,16ms 一次)。
  • VSYNC 的作用​:确保 performTraversals() 的执行与屏幕刷新同步,避免撕裂(tearing)现象。

源码中的协作​:

// ViewRootImpl 中安排绘制任务
void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;// 通过 Choreographer 在下一个 VSYNC 信号到来时执行mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}
}

6. 性能优化关键点
  • 减少 Measure 次数​:避免在 onMeasure() 中频繁修改布局参数。
  • 局部重绘​:用 invalidate(left, top, right, bottom) 替代全局刷新。
  • 硬件加速​:对复杂图形使用 Canvas 的硬件加速(setLayerType(LAYER_TYPE_HARDWARE, null))。

案例​:
在实现一个滚动时间选择器时,发现频繁调用 requestLayout() 导致卡顿。通过缓存子 View 的测量结果,只在滚动停止时触发重新布局,帧率从 30 提升到 60。


7. 高频面试题:为什么 requestLayout() 后不立即绘制?​

答案​:
因为 requestLayout() 只是标记需要重新测量和布局,真正的绘制需要等待下一个 VSYNC 信号。Choreographer 会确保在屏幕刷新周期内统一处理所有 UI 更新请求,避免频繁无意义的绘制。


基础知识讲解 :

Android 中 View 的绘制源码调用顺序详解

1. 整体流程概述

Android 中 View 的绘制流程由 ​**ViewRootImpl​ 驱动,通过 ​performTraversals()​ 方法协调 ​测量(Measure)​**、布局(Layout)​​ 和 ​绘制(Draw)​​ 三个阶段。以下是完整的源码调用顺序:

2. 流程起点:ViewRootImpl.performTraversals()

View 首次附加到窗口或需要更新时,ViewRootImplperformTraversals() 方法被触发。
核心逻辑​:

  • 根据标志位 mLayoutRequestedmDirty 决定是否执行测量、布局或绘制。
  • 通过 Choreographer 同步 VSync 信号,确保绘制在屏幕刷新周期内完成。
// ViewRootImpl.java
private void performTraversals() {// 1. 测量(Measure)if (mLayoutRequested) {performMeasure(...);}// 2. 布局(Layout)if (layoutRequested) {performLayout(...);}// 3. 绘制(Draw)if (!mDirty.isEmpty()) {performDraw(...);}
}

3. 测量阶段(Measure)​

测量阶段确定每个 View 的尺寸,从根 DecorView 开始递归调用子 View
关键方法调用顺序​:

  1. ViewRootImpl.performMeasure()
    调用根 View(即 DecorView)的 measure() 方法。
  2. View.measure(int, int)
    内部调用 onMeasure(),并强制要求子类必须调用 setMeasuredDimension()
  3. View.onMeasure(int, int)
    开发者重写此方法计算 View 的宽高。
  4. View.setMeasuredDimension(int, int)
    保存测量结果,否则抛出 IllegalStateException
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {onMeasure(widthMeasureSpec, heightMeasureSpec);if (!setMeasuredDimension) {throw new IllegalStateException("必须调用 setMeasuredDimension()");}
}// 自定义 View 示例
@Override
protected void onMeasure(int widthSpec, int heightSpec) {int width = resolveSize(widthSpec, desiredWidth);int height = resolveSize(heightSpec, desiredHeight);setMeasuredDimension(width, height);
}

4. 布局阶段(Layout)​

布局阶段确定每个 View 的位置,从根 View 开始递归调用子 View
关键方法调用顺序​:

  1. ​**ViewRootImpl.performLayout()**​
    调用根 Viewlayout() 方法。
  2. ​**View.layout(int, int, int, int)**​
    检查布局是否变化,调用 onLayout()
  3. ​**View.onLayout(boolean, int, int, int, int)**​
    • 普通 View 默认无操作。
    • ViewGroup 必须重写此方法布局子 View
// ViewGroup.java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);child.layout(childLeft, childTop, childRight, childBottom);}
}

5. 绘制阶段(Draw)​

绘制阶段将 View 内容渲染到屏幕,从根 View 开始递归调用子 View
关键方法调用顺序​:

  1. ViewRootImpl.performDraw()
    调用根 Viewdraw() 方法。
  2. ​**View.draw(Canvas)**​
    按顺序执行以下绘制步骤:
    • 绘制背景​:drawBackground(Canvas)
    • 绘制内容​:onDraw(Canvas)(开发者重写)
    • ​**绘制子 View**​:dispatchDraw(Canvas)ViewGroup 实现)
    • 绘制前景​:onDrawForeground(Canvas)
  3. View.onDraw(Canvas)
    开发者在此实现自定义绘制逻辑(如绘制图形、文字等)。
// View.java
public void draw(Canvas canvas) {drawBackground(canvas);       // 步骤1:背景onDraw(canvas);               // 步骤2:内容dispatchDraw(canvas);         // 步骤3:子 ViewonDrawForeground(canvas);     // 步骤4:前景
}// 自定义 View 示例
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawCircle(centerX, centerY, radius, paint);
}

6. 递归流程(View 树的遍历)​
  1. View(DecorView)​
    performTraversals()measure()layout()draw().
  2. ​子 ViewGroup
    measure()onMeasure()layout()onLayout()draw()dispatchDraw().
  3. ​叶子 View
    measure()onMeasure()layout()onLayout()draw()onDraw().

7. 性能优化关键点
  1. 减少测量和布局次数
    • 避免在 onMeasure()onLayout() 中频繁修改布局参数。
    • 使用 View.isLayoutRequested() 检查是否需要重新布局。
  2. 局部重绘
    • 调用 invalidate(left, top, right, bottom) 而非全局刷新。
  3. 避免绘制阻塞
    • 不在 onDraw() 中创建对象或执行耗时操作。
    • 使用硬件加速(setLayerType(LAYER_TYPE_HARDWARE, null))。

相关文章:

  • C51单片机学习笔记——矩阵按键
  • #RabbitMQ# 消息队列入门
  • QNAP NEXTCLOUD 域名访问
  • JVM 的垃圾回收器
  • 基于aspnet,微信小程序,mysql数据库,在线微信小程序汽车故障预约系统
  • 使用Arduino UNO复活电脑的风扇
  • 【第三十七周】SigLip:Sigmoid Loss for Language Image Pre-Training
  • 汽车软件刷写 APP SBL PBL概念
  • RabbitMQ 断网自动重连失效
  • Newtonsoft Json序列化数据不序列化默认数据
  • Python基于Django的主观题自动阅卷系统【附源码、文档说明】
  • 699SJBH库存系统V2
  • TIGER - 一个轻量高效的语音分离模型,支持人声伴奏分离、音频说话人分离等 支持50系显卡 本地一键整合包下载
  • AI练习:指纹
  • GO语言基础4 Errors 报错
  • 线程池优雅关闭的哲学
  • 动态库加载的底层原理
  • 10G/25G PCS only mode for CoaXPress Over Fiber
  • 基于ICEEMDAN-SSA-BP的混合预测模型的完整实现过程
  • 【排序算法】冒泡排序详解--附详细流程代码
  • 浙江网站建设cms/百度网页版登录首页
  • 网站建设素材网页/百度爱采购怎样入驻
  • 建设维护网站运营方案/如何免费做视频二维码永久
  • 凡科网做网站/百度人工客服在线咨询电话
  • 团购的网站扣佣金分录怎么做/磁力吧ciliba
  • 建小说网站需要多少钱/怎样进行seo推广