Android动画小补充
Android动画小补充
文章目录
- Android动画小补充
- 背景:
- 一、动画实现全流程
- 1. 初始化阶段(ProtectBurnView2 主导)
- 2. 动画启动阶段(Anim 基类 + EnterAnimLayout 协作)
- 3. 动画运行阶段(EnterAnimLayout + AnimLadder 核心)
- 4. 交互停止阶段(事件处理 + 跨组件通信)
- 5. 资源清理阶段(生命周期管理)
- 二、核心技术点解析
- 1. 自定义 View 绘制机制
- 2. 动画状态管理
- 3. 事件分发与拦截
- 4. 跨组件通信
- 5. 性能优化
- 6. 内存管理
- 三、关键机制详解
- 1. 动画下方内容可见性保障
- 2. `invalidate()` 绘制循环原理
- 四、QA
- invalidate小总结
- 一、`invalidate()` 的核心作用
- 二、绘制循环的触发与执行流程
- 1. 初始触发:启动动画时首次调用 `invalidate()`
- 2. 系统处理:从 `invalidate()` 到 `dispatchDraw()` 的调用链
- 3. 循环核心:在 `dispatchDraw()` 中再次调用 `invalidate()`
- 4. 动画帧更新:每一次循环都是一帧
- 三、循环的停止条件
- 四、与代码结合的关键细节
- 总结
背景:
之前做了一个润屏效果的小动画,今天总结一下里面的知识点,效果是随机生成水波纹扩散。
一、动画实现全流程
整个动画流程基于 ProtectBurnView2
、EnterAnimLayout
、AnimLadder
、Anim
四个类协作,分为五个阶段:
1. 初始化阶段(ProtectBurnView2 主导)
- 创建子视图
EnterAnimLayout
并设置透明背景、充满父容器布局参数; - 注册交互监听器(
OnUserInteractionListener
)实现跨组件通信; - 预加载图片资源(预留扩展);
- 通过
Handler
延迟 200ms 发送启动消息,避免布局未完成导致的尺寸异常。
2. 动画启动阶段(Anim 基类 + EnterAnimLayout 协作)
ProtectBurnView2
接收启动消息后,创建AnimLadder
(具体动画实现)并关联到EnterAnimLayout
;- 记录动画开始时间,标记
mIsAnimaionRun = true
开启动画状态; - 调用
invalidate()
触发EnterAnimLayout
的dispatchDraw
启动绘制循环。
3. 动画运行阶段(EnterAnimLayout + AnimLadder 核心)
EnterAnimLayout
重写dispatchDraw
作为调度中心:若动画运行,则调用AnimLadder.handleCanvas
绘制当前帧,再通过invalidate()
触发下一次重绘,形成循环;AnimLadder
实现具体绘制逻辑:动态生成波纹圆、清理已完成圆、计算每帧半径 / 透明度,绘制带模糊效果的波纹扩散效果。
4. 交互停止阶段(事件处理 + 跨组件通信)
- 用户交互(触摸 / 悬停)被
ProtectBurnView2
或EnterAnimLayout
捕获; EnterAnimLayout
调用handleUserInteraction
停止动画(mIsAnimaionRun = false
),并通过监听器通知ProtectBurnView2
;ProtectBurnView2
清理资源并移除整个视图。
5. 资源清理阶段(生命周期管理)
- 视图从窗口移除时(
onDetachedFromWindow
),清理AnimLadder
圆形列表、回收画笔和Bitmap
资源,避免内存泄漏。
二、核心技术点解析
1. 自定义 View 绘制机制
dispatchDraw
与onDraw
的区别:EnterAnimLayout
作为ViewGroup
重写dispatchDraw
(而非onDraw
),因dispatchDraw
负责子视图绘制调度,可在子视图绘制前处理动画画布,确保动画覆盖子视图;onDraw
用于 View 自身绘制,ViewGroup
默认不触发(除非设背景)。- 绘制循环触发:通过
invalidate()
在主线程触发重绘,dispatchDraw
中通过mIsAnimaionRun
控制循环启停。
2. 动画状态管理
- 核心标志位:
mIsAnimaionRun
(EnterAnimLayout
中)控制动画启动 / 停止; - 模板方法模式:
Anim
基类封装通用逻辑(启动 / 停止),子类(如AnimLadder
)实现具体绘制(handleCanvas
),保证接口一致性与个性化扩展。
3. 事件分发与拦截
- 优先级原则:
dispatchTouchEvent
(分发)>onInterceptTouchEvent
(拦截)>onTouchEvent
(处理); - 交互保障:重写事件方法优先捕获用户操作,
onInterceptTouchEvent
返回true
阻止事件传递到子视图,确保动画及时停止。
4. 跨组件通信
- 接口回调:通过
OnUserInteractionListener
实现EnterAnimLayout
到ProtectBurnView2
的通知,解耦父子视图; Handler
作用:主线程内延迟任务(如延迟启动)和线程间通信(确保 UI 操作在主线程)。
5. 性能优化
- 资源复用:复用画笔避免频繁创建对象;
- 过度绘制控制:限制活跃圆形数量(
MAX_ACTIVE_CIRCLES = 10
); - 帧率平衡:
TARGET_FRAME_TIME = 33ms
(约 30 帧 / 秒); - 渲染兼容:禁用硬件加速(
LAYER_TYPE_SOFTWARE
)避免模糊效果异常。
6. 内存管理
- 生命周期绑定:
onDetachedFromWindow
中回收Bitmap
(recycle()
)、清理画笔和圆形列表; - 避免泄漏:
Handler
及时移除回调,AnimLadder.cleanup()
清空列表释放引用。
三、关键机制详解
1. 动画下方内容可见性保障
- 透明容器:
ProtectBurnView2
和EnterAnimLayout
均设置透明背景(Color.TRANSPARENT
),避免遮挡下层内容; - 半透明元素:波纹圆通过动态计算透明度(
alpha
)绘制,确保仅视觉叠加不遮挡下层交互。
2. invalidate()
绘制循环原理
- 触发逻辑:
invalidate()
标记视图为 “脏区域”,等待下一次 VSync 信号触发重绘; - 循环形成:
EnterAnimLayout.dispatchDraw
中,动画运行时(mIsAnimaionRun = true
)绘制当前帧后再次调用invalidate()
,形成 “绘制→触发下一次绘制” 的循环; - 帧更新:每帧通过当前时间与开始时间计算进度,更新波纹圆半径、透明度,实现连续动画效果;
- 停止条件:
mIsAnimaionRun = false
或视图销毁时,停止调用invalidate()
终止循环。
四、QA
问题 | 核心答案要点 |
---|---|
为什么 EnterAnimLayout 重写 dispatchDraw ? | ViewGroup 中 dispatchDraw 负责子视图绘制调度,可在子视图绘制前处理动画,onDraw 默认不触发。 |
动画循环如何实现? | 通过 dispatchDraw 中 invalidate() 反复触发重绘,mIsAnimaionRun 控制循环启停;invalidate() 用于主线程更高效。 |
Anim 抽象类的设计目的? | 模板方法模式:封装通用逻辑(启动 / 停止),抽象 handleCanvas 强制子类实现具体绘制,保证一致性与扩展性。 |
如何避免内存泄漏? | onDetachedFromWindow 回收 Bitmap 和画笔;AnimLadder.cleanup() 清空列表;Handler 及时移除回调。 |
为什么限制最大活跃圆形数量? | 避免过度绘制(Overdraw),减少 GPU 负担,平衡视觉效果与性能(经验值 10 个)。 |
如何扩展新动画类型? | 新建类继承 Anim 实现 handleCanvas ,替换 AnimLadder 实例即可,符合开闭原则。 |
延迟 200ms 启动动画的原因? | 确保视图完成测量布局,获取正确宽高,避免波纹圆位置计算错误。 |
为什么禁用硬件加速? | 部分设备对 BlurMaskFilter (模糊效果)支持不完善,软件渲染可避免闪烁或异常。 |
invalidate小总结
在 Android 视图系统中,invalidate()
是触发视图重绘的核心方法,而 “绘制循环” 则是通过反复调用 invalidate()
持续触发重绘,从而实现动画效果的机制。结合你提供的代码(尤其是 EnterAnimLayout
和 AnimLadder
),可以从 “触发→处理→循环” 三个层面详细解释:
一、invalidate()
的核心作用
invalidate()
是 View 类的方法,其本质是向系统发送 “视图需要重绘” 的请求。当调用 invalidate()
后,系统会将该视图标记为 “脏区域”(需要更新的区域),并在合适的时机(通常是下一次屏幕刷新信号,即 VSync 信号)触发视图的绘制流程。
- 核心目标:更新视图的视觉表现(如位置、颜色、形状变化)。
- 注意:
invalidate()
必须在主线程调用(子线程需用postInvalidate()
),因为 UI 操作只能在主线程执行。
二、绘制循环的触发与执行流程
在你的动画代码中,绘制循环的核心逻辑在 EnterAnimLayout.dispatchDraw()
中实现,完整流程如下:
1. 初始触发:启动动画时首次调用 invalidate()
当动画启动(Anim.startAnimation()
)时,会调用 view.invalidate()
,这是循环的起点:
// Anim.startAnimation() 中
view.setmIsAnimaionRun(true);
view.invalidate(); // 首次触发重绘,启动循环
2. 系统处理:从 invalidate()
到 dispatchDraw()
的调用链
invalidate()
触发后,系统会经历以下步骤(简化版):
- 标记视图为 “脏区域”,并合并父视图的脏区域(避免重复绘制)。
- 等待下一次 VSync 信号(由系统的显示子系统发送,通常 60 次 / 秒,即 16ms 一次)。
- 触发视图树的绘制流程:从根视图开始,依次调用
measure()
(测量)→layout()
(布局)→draw()
(绘制)。 - 对于
EnterAnimLayout
(ViewGroup),最终会调用dispatchDraw()
方法(负责子视图绘制的调度)。
3. 循环核心:在 dispatchDraw()
中再次调用 invalidate()
EnterAnimLayout
重写了 dispatchDraw()
,在动画运行时(mIsAnimaionRun = true
),会在绘制完成后再次调用 invalidate()
,形成循环:
// EnterAnimLayout.dispatchDraw() 中
if (mIsAnimaionRun && anim != null) {// 1. 绘制当前帧动画(波纹圆)anim.handleCanvas(canvas, rate); super.dispatchDraw(canvas); // 绘制子视图// 2. 若动画仍在运行,调用invalidate()触发下一次重绘if (mIsAnimaionRun) {invalidate(); // 关键:启动下一次循环}
}
- 为什么能形成循环:每次
dispatchDraw()
执行完当前帧绘制后,若动画未停止(mIsAnimaionRun = true
),就通过invalidate()
触发下一次dispatchDraw()
,如此反复,实现 “绘制→触发下一次绘制” 的循环。
4. 动画帧更新:每一次循环都是一帧
在循环中,AnimLadder.handleCanvas()
会根据当前时间计算动画进度(如波纹圆的半径、透明度),并绘制新的帧:
// AnimLadder.handleCanvas() 中
long currentTime = System.currentTimeMillis();
// 计算每个波纹圆的当前进度(半径、透明度)
for (RippleCircle circle : rippleCircles) {drawExpandingCircle(canvas, circle, currentTime); // 绘制当前帧的波纹圆
}
每一次循环(dispatchDraw()
调用)对应动画的一帧,通过不断更新绘制内容(如波纹圆扩大),视觉上就形成了连续的动画。
三、循环的停止条件
绘制循环不会无限执行,当满足以下条件时会终止:
-
动画停止标记:设置
mIsAnimaionRun = false
(如用户交互时),此时dispatchDraw()
中不再调用invalidate()
,循环终止:// EnterAnimLayout.handleUserInteraction() 中 mIsAnimaionRun = false; // 停止循环
-
视图销毁:当视图从窗口移除(
onDetachedFromWindow()
),系统会停止处理该视图的invalidate()
请求,循环自然终止。
四、与代码结合的关键细节
-
帧率控制:你的代码中
AnimLadder
通过TARGET_FRAME_TIME = 33ms
限制循环频率(约 30 帧 / 秒),避免过度绘制:// AnimLadder.handleCanvas() 中 if (currentTime - lastFrameTime < TARGET_FRAME_TIME) {return; // 未到目标帧间隔,不更新绘制 }
-
动画连贯性:通过
startTime
和当前时间计算进度,确保每次循环的动画状态连续(如波纹圆的扩散不会因帧率波动而跳跃)。
总结
invalidate()
绘制循环的核心是:通过反复调用 invalidate()
触发 dispatchDraw()
重绘,每一次重绘更新动画帧内容,最终形成视觉上的连续动画。在你的代码中,这一机制由 EnterAnimLayout
主导调度,AnimLadder
负责具体帧绘制,通过 mIsAnimaionRun
控制循环启停,既实现了波纹扩散的动画效果,又保证了性能可控。