Android动画主要分为三种:帧动画、View动画(补间动画)、属性动画。每种动画的实现原理和它们与视图绘制流程(测量、布局和绘制)之间的关系如下:
1. 帧动画(Frame Animation)
帧动画通过顺序播放一组预先定义好的图片实现动画效果,类似于播放视频。
1.1 实现步骤:
- 在
res/drawable
目录下定义一个XML文件,根节点为<animation-list>
,包含多个<item>
,每个<item>
定义一帧图片及其持续时间。 - 使用
AnimationDrawable
类播放定义好的Drawable中的图片,形成动画效果。
<!-- res/drawable/frame_animation.xml -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"android:oneshot="false"><item android:drawable="@drawable/image01" android:duration="500"/><item android:drawable="@drawable/image02" android:duration="500"/><item android:drawable="@drawable/image03" android:duration="500"/>
</animation-list>Button button = findViewById(R.id.bt_001);
button.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable) button.getBackground();
animationDrawable.start();
1.2 与视图绘制流程的关系:
- 测量(measure):帧动画不会影响视图的测量过程。视图的大小在动画开始前已经确定。
- 布局(layout):帧动画不会影响视图的布局过程。视图的位置在动画开始前已经确定。
- 绘制(draw):帧动画通过切换不同的Drawable来实现。每一帧都会重新绘制视图,因此会调用
invalidate()
方法来触发视图重绘。
2 View动画(补间动画)
View动画通过对视图进行平移、缩放、旋转和透明度变化来实现动画效果,但并不真正改变视图的属性,只是改变了视图在屏幕上的显示效果。
2.1 实现步骤
- 在
res/anim
目录下定义XML文件,描述动画效果。 - 使用
AnimationUtils.loadAnimation
加载动画资源,并通过startAnimation
应用到视图。
<!-- res/anim/translate_animation.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"android:fromXDelta="100"android:toXDelta="0"android:duration="1000"/>Animation animation = AnimationUtils.loadAnimation(context, R.anim.translate_animation);
view.startAnimation(animation);
2.2 与视图绘制流程的关系
- 测量(measure):View动画不会影响视图的测量过程。视图的大小不变。
- 布局(layout):View动画不会影响视图的布局过程。视图的位置不变。
- 绘制(draw):View动画通过变换矩阵(Transformation Matrix)在绘制过程中应用动画效果。例如,平移动画在绘制时通过变换矩阵改变视图在屏幕上的位置,但视图的实际坐标没有改变。
3 属性动画(Property Animation)
属性动画通过改变对象的属性值来实现动画效果,能够对任何对象的任何属性进行动画操作,不仅限于视图的属性。
3.1 实现步骤
- 创建
ValueAnimator
或ObjectAnimator
对象,定义动画属性和持续时间。 - 设置动画的监听器,以便在动画过程中更新属性值。
- 启动动画。
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
animator.setDuration(1000);
animator.start();
3.2 与视图绘制流程的关系:
- 测量(measure):属性动画可以影响视图的测量过程。如果动画改变了视图的尺寸(如缩放动画),则可能触发重新测量。
- 布局(layout):属性动画可以影响视图的布局过程。如果动画改变了视图的位置(如移动动画),则可能触发重新布局。
- 绘制(draw):属性动画直接改变视图的属性(如透明度、位置等),并通过
invalidate()
方法触发视图重绘,从而在onDraw()
方法中反映动画效果。
4 请求重绘视图invalidate()
在 Android 中用于请求重绘视图。调用 invalidate()
后,视图会被标记为“需要重绘”,这会触发视图的绘制流程。以下是 invalidate()
方法的详细解释及其与视图绘制流程的关系。
4.1 invalidate()
方法的工作原理
invalidate()
方法的主要作用是标记视图为需要重绘,并请求系统在下一个渲染周期进行重绘。具体来说,invalidate()
方法的实现步骤如下:
-
标记视图为“脏”状态:
- 调用
invalidate()
后,视图会被标记为“脏”状态,表示需要重绘。
- 调用
-
请求父视图重绘:
invalidate()
会通知视图的父视图,它的某个子视图需要重绘。如果父视图本身也需要重绘,那么父视图也会被标记为“脏”状态。
-
将重绘请求发送到消息队列:
- 最终,
invalidate()
会将重绘请求发送到主线程的消息队列,确保在下一个渲染周期处理重绘请求。
- 最终,
以下是一个简单的 invalidate()
方法调用示例:
public void setAlpha(float alpha) {if (this.alpha != alpha) {this.alpha = alpha;invalidate(); // 标记视图需要重绘}
}
在这个示例中,当视图的透明度发生变化时,会调用 invalidate()
方法,触发视图重绘。
4.2 视图绘制流程
视图的绘制流程主要包括三个阶段:测量(measure)、布局(layout)和绘制(draw)。invalidate()
方法主要影响绘制阶段,但也可能间接影响测量和布局阶段。具体流程如下:
-
测量阶段(measure):
measure()
方法确定视图的尺寸。调用requestLayout()
会触发测量阶段。invalidate()
方法不会直接触发测量阶段,但如果视图的某些属性(如尺寸)改变,可能会间接触发测量阶段。
-
布局阶段(layout):
layout()
方法确定视图在其父视图中的位置。调用requestLayout()
会触发布局阶段。invalidate()
方法不会直接触发布局阶段,但如果视图的位置或尺寸改变,可能会间接触发布局阶段。
-
绘制阶段(draw):
draw()
方法负责将视图绘制到屏幕上。调用invalidate()
会直接触发绘制阶段。invalidate()
会触发draw()
方法,从而调用视图的onDraw()
方法。
4.3 invalidate()
方法与绘制流程的关系
当调用 invalidate()
方法时,系统会在下一个渲染周期内处理重绘请求。具体步骤如下:
-
调用
invalidate()
:- 视图被标记为“脏”状态,并请求重绘。
-
将重绘请求添加到消息队列:
invalidate()
方法会将重绘请求添加到主线程的消息队列中,确保在下一个渲染周期处理。
-
处理重绘请求:
- 在下一个渲染周期,主线程的消息循环会处理重绘请求,调用
ViewRootImpl
的performTraversals()
方法。
- 在下一个渲染周期,主线程的消息循环会处理重绘请求,调用
-
执行
performTraversals()
:performTraversals()
方法会依次执行测量(performMeasure()
)、布局(performLayout()
)和绘制(performDraw()
)操作。- 对于仅需要重绘的情况,通常只会执行
performDraw()
阶段。
-
调用
draw()
方法:performDraw()
方法会调用视图的draw()
方法。draw()
方法内部会调用onDraw()
方法,执行实际的绘制操作。
5 主线程(也称为 UI 线程)的消息队列
在 Android 中,主线程(也称为 UI 线程)的消息队列负责处理一系列与用户界面和应用逻辑相关的任务。消息队列由 Looper
和 Handler
机制来管理,通过消息(Message)和可运行的任务(Runnable)来调度和执行任务。
5.1 主线程消息队列中常见的请求类型:
1. UI 更新请求
这些请求主要用于更新用户界面元素,包括重绘视图、调整布局等。
- 重绘请求:通过调用
invalidate()
、postInvalidate()
方法触发视图重绘,会将重绘请求添加到消息队列中。 - 布局请求:调用
requestLayout()
方法触发视图的重新测量和布局。
2. 输入事件
这些事件是由用户的交互引发的,包括触摸事件、键盘事件等。
- 触摸事件:如点击、滑动等,通过
MotionEvent
处理。 - 键盘事件:如按键按下和释放,通过
KeyEvent
处理。
3. 定时任务
通过 Handler
发送延迟消息或定时执行的任务。
- 延迟消息:使用
Handler
的sendMessageDelayed()
方法发送的消息。 - 定时任务:使用
postDelayed()
方法调度的Runnable
。
4. 动画请求
这些请求用于执行属性动画、视图动画等。
- 属性动画:如
ObjectAnimator
和ValueAnimator
,会定期更新属性值并重绘视图。 - 视图动画:如平移、缩放、旋转等补间动画,通过
Animation
类实现。
5. 布局变化
这些请求用于处理视图层次结构的变化。
- 视图添加/移除:如
addView()
和removeView()
方法引发的布局更新。 - 布局参数变化:如
setLayoutParams()
方法引发的重新布局。
6. 系统事件
包括与应用生命周期和系统资源相关的事件。
- Activity 生命周期事件:如
onCreate()
、onResume()
、onPause()
等。 - 系统广播:如网络状态变化、电量低等广播事件。
7. 后台任务结果
异步任务完成后,将结果发送回主线程更新 UI。
- AsyncTask:通过
onPostExecute()
将结果传递回主线程。 - 线程池:通过
Handler
将结果传递回主线程。
5.2 视图绘制流程与消息队列的关系
当我们调用 invalidate()
、requestLayout()
等方法时,这些请求会被加入到主线程的消息队列中,等待处理。消息队列会按顺序处理这些请求,确保视图在正确的时间被重新测量、布局和绘制。以下是这些方法与消息队列的具体关系:
-
invalidate()
invalidate()
标记视图为需要重绘,并将重绘请求添加到消息队列中。- 消息队列在下一个渲染周期处理重绘请求,调用
ViewRootImpl.performTraversals()
执行绘制阶段。
-
requestLayout()
requestLayout()
标记视图为需要重新测量和布局,并将布局请求添加到消息队列中。- 消息队列在下一个渲染周期处理布局请求,调用
ViewRootImpl.performTraversals()
执行测量和布局阶段。
6 渲染周期
在 Android 中,“下一个渲染周期”是指系统在处理主线程消息队列中的绘制请求时,下一个执行绘制操作的时间点
6.1 渲染周期的概念
Android 的渲染周期基于帧率(Frame Rate)进行调度。典型的渲染帧率是 60 帧每秒(FPS),也就是说,每帧的时间间隔约为 16.67 毫秒(1000 毫秒 / 60 帧)。在每一帧中,系统会执行一系列操作,包括处理输入事件、更新动画、测量和布局视图、绘制视图等。
6.2 主线程消息循环
主线程(UI 线程)通过消息循环(Looper 和 Handler)来管理和处理各种任务。消息循环会不断从消息队列中取出消息并处理这些消息。
在 Android 的消息队列中,绘制请求(如 invalidate()
)会被添加到消息队列,并在合适的时间点进行处理。为了确保流畅的 UI 渲染,Android 会尽量在每一帧的时间间隔内完成所有的绘制请求。
6.2 Choreographer
类
Choreographer
是 Android 系统中的一个关键类,它用于协调 UI 线程的渲染工作。Choreographer
会根据系统的刷新频率来调度回调,确保在正确的时间点进行绘制操作。每当需要进行绘制时,Choreographer
会在下一帧到来之前安排一次绘制回调。
当调用 invalidate()
方法时,系统会将重绘请求添加到消息队列中。然后,Choreographer
会在下一个渲染周期到来时调用 doFrame()
方法,执行绘制操作。以下是一个简化的示例:
// View 的 invalidate 方法
public void invalidate() {if (mParent != null && mAttachInfo != null) {mParent.invalidateChild(this, null);}
}// ViewParent 的 invalidateChild 方法
public void invalidateChild(View child, Rect dirty) {ViewRootImpl viewRoot = getViewRootImpl();if (viewRoot != null) {viewRoot.invalidate();}
}// ViewRootImpl 的 invalidate 方法
public void invalidate() {if (!mWillDrawSoon) {mWillDrawSoon = true;Choreographer.getInstance().postFrameCallback(mTraversalRunnable);}
}// Choreographer 的 postFrameCallback 方法
public void postFrameCallback(FrameCallback callback) {mCallbackQueue.add(callback);scheduleFrameLocked();
}// 调度下一帧
private void scheduleFrameLocked() {if (!mFrameScheduled) {mFrameScheduled = true;// 通过 Handler 安排在下一帧调用 doFrame 方法Message msg = Message.obtain(mHandler, mFrameHandlerCallback);mHandler.sendMessageAtTime(msg, nextFrameTime);}
}
完整的绘制流程
- 触发重绘请求:调用
invalidate()
方法,标记视图为需要重绘,并将重绘请求添加到消息队列中。 - 调度下一帧:
Choreographer
调用postFrameCallback()
安排在下一帧调用doFrame()
方法。 - 处理绘制回调:在下一帧到来时,
Choreographer
调用doFrame()
方法,执行所有的绘制回调。 - 执行绘制操作:
ViewRootImpl
的doTraversal()
方法依次执行测量(measure)、布局(layout)和绘制(draw)操作,最终调用视图的onDraw()
方法进行绘制。