Android动画与视图绘制流程的关系

Android动画主要分为三种:帧动画、View动画(补间动画)、属性动画。每种动画的实现原理和它们与视图绘制流程(测量、布局和绘制)之间的关系如下:

1. 帧动画(Frame Animation)

帧动画通过顺序播放一组预先定义好的图片实现动画效果,类似于播放视频。

1.1 实现步骤:

  1. res/drawable目录下定义一个XML文件,根节点为<animation-list>,包含多个<item>,每个<item>定义一帧图片及其持续时间。
  2. 使用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 实现步骤

  1. res/anim目录下定义XML文件,描述动画效果。
  2. 使用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 实现步骤

  1. 创建ValueAnimatorObjectAnimator对象,定义动画属性和持续时间。
  2. 设置动画的监听器,以便在动画过程中更新属性值。
  3. 启动动画。
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() 方法的实现步骤如下:

  1. 标记视图为“脏”状态

    • 调用 invalidate() 后,视图会被标记为“脏”状态,表示需要重绘。
  2. 请求父视图重绘

    • invalidate() 会通知视图的父视图,它的某个子视图需要重绘。如果父视图本身也需要重绘,那么父视图也会被标记为“脏”状态。
  3. 将重绘请求发送到消息队列

    • 最终,invalidate() 会将重绘请求发送到主线程的消息队列,确保在下一个渲染周期处理重绘请求。

以下是一个简单的 invalidate() 方法调用示例:

public void setAlpha(float alpha) {
    if (this.alpha != alpha) {
        this.alpha = alpha;
        invalidate(); // 标记视图需要重绘
    }
}

在这个示例中,当视图的透明度发生变化时,会调用 invalidate() 方法,触发视图重绘。

4.2 视图绘制流程

视图的绘制流程主要包括三个阶段:测量(measure)、布局(layout)和绘制(draw)。invalidate() 方法主要影响绘制阶段,但也可能间接影响测量和布局阶段。具体流程如下:

  1. 测量阶段(measure)

    • measure() 方法确定视图的尺寸。调用 requestLayout() 会触发测量阶段。
    • invalidate() 方法不会直接触发测量阶段,但如果视图的某些属性(如尺寸)改变,可能会间接触发测量阶段。
  2. 布局阶段(layout)

    • layout() 方法确定视图在其父视图中的位置。调用 requestLayout() 会触发布局阶段。
    • invalidate() 方法不会直接触发布局阶段,但如果视图的位置或尺寸改变,可能会间接触发布局阶段。
  3. 绘制阶段(draw)

    • draw() 方法负责将视图绘制到屏幕上。调用 invalidate() 会直接触发绘制阶段。
    • invalidate() 会触发 draw() 方法,从而调用视图的 onDraw() 方法。

4.3 invalidate() 方法与绘制流程的关系

当调用 invalidate() 方法时,系统会在下一个渲染周期内处理重绘请求。具体步骤如下:

  1. 调用 invalidate()

    • 视图被标记为“脏”状态,并请求重绘。
  2. 将重绘请求添加到消息队列

    • invalidate() 方法会将重绘请求添加到主线程的消息队列中,确保在下一个渲染周期处理。
  3. 处理重绘请求

    • 在下一个渲染周期,主线程的消息循环会处理重绘请求,调用 ViewRootImplperformTraversals() 方法。
  4. 执行 performTraversals()

    • performTraversals() 方法会依次执行测量(performMeasure())、布局(performLayout())和绘制(performDraw())操作。
    • 对于仅需要重绘的情况,通常只会执行 performDraw() 阶段。
  5. 调用 draw() 方法

    • performDraw() 方法会调用视图的 draw() 方法。
    • draw() 方法内部会调用 onDraw() 方法,执行实际的绘制操作。

5 主线程(也称为 UI 线程)的消息队列

在 Android 中,主线程(也称为 UI 线程)的消息队列负责处理一系列与用户界面和应用逻辑相关的任务。消息队列由 LooperHandler 机制来管理,通过消息(Message)和可运行的任务(Runnable)来调度和执行任务。

5.1 主线程消息队列中常见的请求类型:

1. UI 更新请求

这些请求主要用于更新用户界面元素,包括重绘视图、调整布局等。

  • 重绘请求:通过调用 invalidate()postInvalidate() 方法触发视图重绘,会将重绘请求添加到消息队列中。
  • 布局请求:调用 requestLayout() 方法触发视图的重新测量和布局。

2. 输入事件

这些事件是由用户的交互引发的,包括触摸事件、键盘事件等。

  • 触摸事件:如点击、滑动等,通过 MotionEvent 处理。
  • 键盘事件:如按键按下和释放,通过 KeyEvent 处理。

3. 定时任务

通过 Handler 发送延迟消息或定时执行的任务。

  • 延迟消息:使用 HandlersendMessageDelayed() 方法发送的消息。
  • 定时任务:使用 postDelayed() 方法调度的 Runnable

4. 动画请求

这些请求用于执行属性动画、视图动画等。

  • 属性动画:如 ObjectAnimatorValueAnimator,会定期更新属性值并重绘视图。
  • 视图动画:如平移、缩放、旋转等补间动画,通过 Animation 类实现。

5. 布局变化

这些请求用于处理视图层次结构的变化。

  • 视图添加/移除:如 addView()removeView() 方法引发的布局更新。
  • 布局参数变化:如 setLayoutParams() 方法引发的重新布局。

6. 系统事件

包括与应用生命周期和系统资源相关的事件。

  • Activity 生命周期事件:如 onCreate()onResume()onPause() 等。
  • 系统广播:如网络状态变化、电量低等广播事件。

7. 后台任务结果

异步任务完成后,将结果发送回主线程更新 UI。

  • AsyncTask:通过 onPostExecute() 将结果传递回主线程。
  • 线程池:通过 Handler 将结果传递回主线程。

5.2 视图绘制流程与消息队列的关系

当我们调用 invalidate()requestLayout() 等方法时,这些请求会被加入到主线程的消息队列中,等待处理。消息队列会按顺序处理这些请求,确保视图在正确的时间被重新测量、布局和绘制。以下是这些方法与消息队列的具体关系:

  1. invalidate()

    • invalidate() 标记视图为需要重绘,并将重绘请求添加到消息队列中。
    • 消息队列在下一个渲染周期处理重绘请求,调用 ViewRootImpl.performTraversals() 执行绘制阶段。
  2. 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);
    }
}

完整的绘制流程

  1. 触发重绘请求:调用 invalidate() 方法,标记视图为需要重绘,并将重绘请求添加到消息队列中。
  2. 调度下一帧Choreographer 调用 postFrameCallback() 安排在下一帧调用 doFrame() 方法。
  3. 处理绘制回调:在下一帧到来时,Choreographer 调用 doFrame() 方法,执行所有的绘制回调。
  4. 执行绘制操作ViewRootImpldoTraversal() 方法依次执行测量(measure)、布局(layout)和绘制(draw)操作,最终调用视图的 onDraw() 方法进行绘制。
  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值