您现在的位置是:首页 > 正文

Android 属性动画原理解析

2024-02-01 03:20:39阅读 2

目录

属性动画简介

什么是属性动画

属性动画的基本模型

android 属性动画使用示例

ValueAnimator

ObjectAnimator

ViewPropertyAnimator

属性动画的执行流程

动画循环

代码实现

帧刷新机制

Android Display系统的工作方式

早期帧刷新机制

优化:Project Butter

代码实现

根据动画已播放时长计算属性值

线性动画示例

非线性动画示例

时间插值器

类型估值器

代码实现

设置属性值

代码实现

界面绘制

Android 绘制模型

总结

Android属性动画实现原理

兼容性问题

硬件加速

内存泄漏

参考文档

Choreographer相关

属性动画

硬件加速


属性动画简介

什么是属性动画

顾名思义,属性动画就是通过改变对象的属性做动画

想象一个自由落体运动,在下落过程中,从最高点落到最低点,物体的位置不断变化,物体其他属性没有发生变化。我们要模拟自由落体运动,就可以通过改变物体的位置来实现这个动画效果。在这个例子中,位置就是物体的一个属性,动画效果被抽象成位置属性的连续变化,属性控制了画面的显示效果。通过这种方式制作的动画就称为属性动画

属性动画的基本模型

一个属性动画通常由以下基本元素构成:

  • 动画对象
    • 动画操作的对象,通过改变其属性值使得该对象的显示效果发生改变
  • 动画属性
    • 随着的动画播放,值连续改变的属性
  • 动画时长
    • 动画持续的时间
  • 动画刷新频率
    • 每秒画面刷新的次数
  • 动画时间到属性值的映射关系
    • 指定如何根据动画的当前进度来计算属性的值
    • 可以分为线性映射关系和非线性映射关系,从而将动画分为线性动画和非线性动画
  • 动画绘制
    • 根据新的属性值绘制新画面的过程

android 属性动画使用示例

Android属性动画是在Android 3.0 之后出现的,属性动画系统是一个功能强大的框架,可用于为几乎任何对象添加动画效果。属性动画会在指定时长内更改属性(对象中的字段)的值

Android 属性动画支持定义动画的以下特性:

  • 时长
  • 时间插值函数
  • 重复计数和行为
    • 指定是否在某个时长结束后重复播放动画以及重复播放动画多少次;还可以指定是否要反向播放动画。如果将其设置为反向播放,则会先播放动画,然后反向播放动画,直到达到重复次数
  • AnimatorSet
    • 将动画分成多个逻辑集,它们可以一起播放、按顺序播放或者在指定的延迟时间后播放

要为对象属性添加动画效果,需要指定要添加动画效果的对象属性,例如对象在屏幕上的位置、动画效果持续多长时间以及要在哪些值之间添加动画效果

Android提供了一组属性动画API,可以方便的创建属性动画。属性动画的使用方法按照封装程度从低到高(自由度从高到低)依次为ValueAnimator、ObjectAnimator、ViewPropertyAnimator三种,其中ValueAnimator是核心,ObjectAnimator继承于ValueAnimator,ViewPropertyAnimator 内部采用ValueAnimator 实现动画

ValueAnimator

ValueAnimator是一个数值发生器,它并不会直接改变属性的值,而是用来产生随动画进度变化的数值,间接控制动画的实现过程。我们需要做的就是监听这些值的改变,改变View的属性,进而产生动画效果

用法

ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setTarget(mTextView); // 这行代码没用,ValueAnimator的该方法是空实现
animator.setDuration(4000);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mTextView.setAlpha(animation.getAnimatedFraction());
    }
});

animator.start();

ObjectAnimator

ObjectAnimator提供了更简便的属性设置方式。在ValueAnimator的基础之上,其内部方法通过反射方式调用对象某个属性的set方法。因此这个对象的属性需要具有set/get方法

用法

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mTextView, "alpha", 0, 1);
objectAnimator.setDuration(4000);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.start();

如果需要对控件的多个属性执行动画,有两种方式可以实现:

1、使用PropertyValuesHolder实现

PropertyValuesHolder pvh1 = PropertyValuesHolder.ofFloat("alpha", 0, 1);
PropertyValuesHolder pvh2 = PropertyValuesHolder.ofFloat("scaleX", 0, 1);
PropertyValuesHolder pvh3 = PropertyValuesHolder.ofFloat("scaleY", 0, 1);
ObjectAnimator.ofPropertyValuesHolder(mTextView, pvh1, pvh2, pvh3)
    .setDuration(4000)
    .start();

2、使用AnimatiorSet实现。相对于第一种方法,AnimatorSet可以对播放顺序进行更精准的控制。该类通过playTogether、playSequentially、play().with()、play().before()、play().after()等方法支持协调多个动画的播放顺序,可以实现连续动画、多属性复杂动画等。

ObjectAnimator o1 = ObjectAnimator.ofFloat(mTextView, "alpha", 0, 1);
ObjectAnimator o2 = ObjectAnimator.ofFloat(mTextView, "scaleX", 0, 1);
ObjectAnimator o3 = ObjectAnimator.ofFloat(mTextView, "scaleY", 0, 1);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(4000);
animatorSet.playTogether(o1, o2, o3);
animatorSet.start();

ViewPropertyAnimator

为了更方便地对View使用属性动画,Google 很贴心地为View增加了animate方法直接创建属性动画,开发者可以更方便、更优雅地实Vview的属性动画,此时返回的对象是ViewPropertyAnimater的实例。

ViewPropertyAnimator 内部使用单个 ValueAnimator 对象为 View 的多个属性并行添加动画效果。它的行为方式与 ObjectAnimator 非常相似,它会修改视图属性的实际值,但在同时为多个属性添加动画效果时,更为高效。使用 ViewPropertyAnimator 的代码也更简洁,也更易读

mTextView.animate()
    .alpha(1)
    .scaleX(2f)
    .scaleY(2f)
    .setDuration(4000)
    .start();

属性动画的执行流程

我们了解了属性动画的基本组成与用法,接下来看看属性动画是如何工作的。简化的属性动画的执行过程可以借助以下伪代码描述:

long startTime = System.currentTimeMillis();
int propStartVal = 0, propEndVal = 100;
int propVal = propStartVal;
long dt = 0, duration = 1000;

while(dt < duration){ // --1、动画循环
    Thread.sleep(20); // --2、等待下一帧刷新
    dt = System.currentTimeMillis() - starttime;// --3、根据动画进度计算属性值
    float fraction = dt / duration;
    float currVal = fraction * (propEndVal - propStartVal);
    propVal = currVal; // --4、设置属性值
    draw(); // --5、执行绘制
}

属性动画的执行流程可以分为动画循环、等待帧刷新、计算属性值、更新属性值和绘制画面五个部分,下面将对每一部分进行详细说明

动画循环

在动画周期中,动画一直进行,画面需要不断地刷新。在Android系统中,一切事件都由消息机制驱动,UI线程如果在5秒内不能响应用户交互,就会发生崩溃。所以动画循环不能一直运行阻塞住UI线程,需要遵循消息机制实现重绘循环。我们需要在每一次帧刷新消息到来时,更新属性并重绘View。因此动画处理程序要在动画开始时,注册帧刷新的回调;在动画结束时,解除注册的帧刷新回调

代码实现

注册帧刷新回调

// ValueAnimator.class
private void start(boolean playBackwards) {
    /***部分代码省略***/
    addAnimationCallback(0);
    /***部分代码省略***/
}

private void addAnimationCallback(long delay) {
    if (!mSelfPulse) {
        return;
    }
    getAnimationHandler().addAnimationFrameCallback(this, delay);

}

public AnimationHandler getAnimationHandler() {
    return AnimationHandler.getInstance();
}

AnimationHandler是一个负责管理应用程序注册的动画帧回调接口的类, 应用程序通过该类注册和解注册帧刷新回调,该类维护一个回调队列,当动画帧刷新事件到来时,统一调用队列中的回调接口来通知应用程序,该类实例是一个ThreadLocal类型的对象,一个线程仅有一份

调用其addAnimationFrameCallback 方法会将该回调接口添加到其内部维护的回调队列 mAnimationCallbacks中

public class AnimationHandler {
    public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
    public static AnimationHandler getInstance() {
        if (sAnimatorHandler.get() == null) {
            sAnimatorHandler.set(new AnimationHandler());
        }
        return sAnimatorHandler.get();
    }

    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

    public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        if (mAnimationCallbacks.size() == 0) {
            // mFrameCallback:FrameCallback
            getProvider().postFrameCallback(mFrameCallback);
        }

        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);
        }

        if (delay > 0) {
            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
        }
    }

    private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {    
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }
}

AnimationFrameCallbackProvider是一个跟动画协调对象Choreographer交互的一个接口

MyFrameCallbackProvider 内部持有一个 Choreographer 对象,该对象也是线程局部变量,即线程唯一的。MyFrameCallbackProvider的postFrameCallback方法内部调用了Choreographer 对象 的postFrameCallback方法添加FrameCallback类型的接口回调,该回调接口只有一个doFrame 方法,即在下一帧刷新时会回调该方法进行处理

private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
    final Choreographer mChoreographer = Choreographer.getInstance();

    @Override
    public void postFrameCallback(Choreographer.FrameCallback callback) {
        mChoreographer.postFrameCallback(callback);
    }
}

public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
}

解注册帧刷新回调

// ValueAnimator.class
private void endAnimation() {
    if (mAnimationEndRequested) {
        return;
    }
    removeAnimationCallback();   
    // ...
}

private void removeAnimationCallback() {
    /​/ ...
    getAnimationHandler().removeCallback(this);
}

帧刷新机制

当下一帧刷新时,动画刷新属性值,然后重新绘制页面。那属性动画值是在什么时候刷新的呢?我们先了解一下Android 帧刷新的机制

Android Display系统的工作方式

手机屏幕是由许多微小的像素点组成的。通过为每个像素点设置不同的颜色,屏幕可以呈现丰富多彩的图像

屏幕展示的颜色数据

  • 在GPU中有一块缓冲区叫做 Frame Buffer ,这个帧缓冲区可以认为是存储像素颜色值的二维数组
  • 数组中的每一个值对应着手机屏幕的像素点需要显示的颜色
  • 应用程序在绘制后将帧缓冲区的数据进行了修改,只要将修改后的数据刷新到屏幕上就可以显示绘制的图像了
  • 至于刷新工作,Android系统会定期将 Frame Buffer刷新到显示屏上。目前主流的刷新频率为60次/秒,折算出来就是16ms刷新一次

GPU的Frame Buffer中的数据

  • GPU 除了Frame Buffer用以交给手机屏幕进行绘制外,还有一个缓冲区 Back Buffer,用以交给应用程序往里面填充数据
  • 系统会定期交换 Back Buffer 和 Frame Buffer ,也就是将Back Buffer中的数据交给屏幕进行显示,同时让原先的Frame Buffer 变成 Back Buffer ,让程序可以绘制下一帧

早期帧刷新机制

早期Android系统的帧刷新是通过发送定时消息实现,默认是每10毫秒刷新一次。然而通过定时消息来刷新帧会导致UI流畅性差: 

分析上图可知

  • 1.时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者互不干扰,一切正常。
  • 2.时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU 却并未及时开始绘制第2帧数据(注意前面的空白区),而是在本周期过了一段时间时,CPU/GPU才去处理第2帧数据。
  • 3.时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU没有在合适的时间开始第2帧的绘制,导致第二帧没有绘制完成。故Display只能继续显示第一帧的数据,结果使得第1 帧多画了一次(对应时间段上标注了一个掉帧)。
  • 4.通过上述分析可知,此处发生掉帧的关键问题在于CPU的绘制节奏与屏幕刷新的节奏存在相位差。当CPU绘制完成一帧后,即使每一帧的绘制时间都小于屏幕的刷新间隔,也无法保证每一帧的图像都会被刷新到屏幕上。

优化:Project Butter

为了解决掉帧问题,Android4.1 启动了Project Butter来解决这一系列问题

Project Butter对Android Display系统进行了重构,引入了三个核心元素,即VSYNC、Triple Buffer和Choreographer。其中, VSYNC是理解Project Buffer的核心。VSYNC是Vertical Synchronization(垂直同步)的缩写,是一种在PC上已经很早就广泛使用的技术。 可简单的把它认为是一种定时中断。该中断触发的时机与交换Buffer的时机相同,当中断发生时,可以通知应用程序绘制下一帧。下图表示引入了VSYNC机制后的绘制流程:

 

每当应用程序收到VSYNC中断,CPU就开始处理下一帧的数据,整个过程非常完美。只要每一帧的绘制时间不超过帧间隔(16ms),就不会发生丢帧的现象。

不过,仔细琢磨图2却会发现一个新问题:图2中,CPU和GPU处理数据的速度似乎都能在16ms内完成,而且还有时间空余,也就是说,CPU/GPU的FPS(帧率,Frames Per Second)要高于Display的FPS。确实如此。由于CPU/GPU只在收到VSYNC时才开始数据处理,故它们的FPS被拉低到与Display的FPS相同。但这种处理并没有什么问题,因为Android设备的屏幕刷新率一般是60,其对应的显示效果非常平滑,高于这一频率的帧实际上不会展示到屏幕上。另一方面,假设CPU/GPU的FPS小于屏幕刷新的FPS,会是什么情况呢?请看下图:

 

分析这段绘制流程,我们可以发现:

1.在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。

2.同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer当前展示在屏幕上,B Buffer被GPU在使用,此时没有可以用于绘制的Buffer。注意,由于使用了VSYNC机制,CPU只会在VSYNC信号到来时检查Buffer的可用状态,导致CPU资源可能被浪费。

三级缓存

为什么CPU不能在第二个16ms处开始绘制工作呢?原因就是只有两个Buffer。如果有第三个Buffer的存在,CPU就能直接使用它, 而不至于空闲。出于这一思路就引出了Triple Buffer,即三级缓存。优化后的绘制过程如下图所示:

 

分析该绘制过程,在第二个16ms时间段,CPU使用C Buffer绘制,避免空等。虽然还是会多显示A帧一次,但后续可以实现流畅刷新。这种类似流水线的工作方式降低了流畅动画的速度要求,只需CPU和GPU单独处理时间不超过一帧间隔即可(即CPU处理时间不超过16ms,GPU处理时间不超过16ms,二者相加总时间可以超过16ms)。

让我们回顾一下Project Buffer的关键点:

  • VSYNC:需要VSYNC定时中断来保证显示系统定时刷新。
  • Triple Buffer:当双Buffer成为瓶颈时,系统可分配第三块Buffer充分利用CPU、GPU资源。

为了实现将绘制工作都统一到VSYNC时间点上,需要一个组织者来协调绘制过程,这就是Choreographer的作用。在它的统一指挥下,应用的绘制工作将变得井井有条

代码实现

Choreographer

中文翻译是编舞者,用于协调input、animation和drawing的时机

Choreographer通过接收显示系统的时间脉冲(即VSYNC信号), 来触发下一帧的渲染工作

通常App层通过高层抽象的API调用和Choreographer交互, 如ValueAnimator#start / View#postOnAnimation等

Choreographer 工作过程分为 2 部分来分析:监听VSYNC和接收到VSYNC后的处理流程

请求VSYNC

AnimationHandler通过MyFrameCallbackProvider的postFrameCallback 方法向Choreographer添加一个帧回调,即调用了Choreographer 的postFrameCallback方法

private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
    final Choreographer mChoreographer = Choreographer.getInstance();

    @Override
    public void postFrameCallback(Choreographer.FrameCallback callback) {
        mChoreographer.postFrameCallback(callback);
    }
    // 省略一些代码
}

那我们就来看看Choreographer 对象的postFrameCallback方法做了什么

该方法内部调用了postFrameCallbackDelayed(callback,0)方法,其内部又调用了postCallbackDelayedInternal 方法,并加上了一个callback 类型参数 CALLBACK_ANIMATION,指明这是一个动画帧回调监听,token参数是一个常量,应用程序通过该方法添加的回调都使用这个token,后面在新一帧到来后通知这些callback后,会将应用程序添加的callback移除,所以这里需要用token标记这些callback

// Choreographer.class
private final CallbackQueue[] mCallbackQueues;

public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);

}

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    // ......
    postCallbackDelayedInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN, delayMillis);

}

private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
   
        // 把任务都放在 mCallbackQueues[callbackType] 队列中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

addCallbackLocked就是向对应回调类型CALLBACK_ANIMATION的消息队列中按照时间顺序添加一个元素

// CallbackQueue.class
@UnsupportedAppUsage
public void addCallbackLocked(long dueTime, Object action, Object token) {
    CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
    CallbackRecord entry = mHead;
    if (entry == null) {
        mHead = callback;
        return;
    }

    if (dueTime < entry.dueTime) {
        callback.next = entry;
        mHead = callback;
        return;
    }

    while (entry.next != null) {
        if (dueTime < entry.next.dueTime) {
            callback.next = entry.next;
            break;
        }
        entry = entry.next;
    }
    entry.next = callback;
}

private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
    CallbackRecord callback = mCallbackPool;
    if (callback == null) {
        callback = new CallbackRecord();
    } else {
        mCallbackPool = callback.next;
        callback.next = null;
    }

    callback.dueTime = dueTime;
    callback.action = action;
    callback.token = token;
    return callback;
}

添加到回调队列后,Choreographer 请求显示系统接收下一次的VSYNC信号:

// Choreographer.class

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) { // 是否请求过执行帧 flag,没有就发送请求
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (DEBUG_FRAMES) {
                   Log.d(TAG, "Scheduling next frame on VSYNC.");
            }
            // 采用了VSYNC机制,且在主线程,则立即执行 scheduleVSYNCLocked(),
            if (isRunningOnLooperThreadLocked()) {
                scheduleVSYNCLocked();
            } else {
                // 如果不是主线程,则通过 mHandler 发消息给主线程,最终也是执行scheduleVSYNCLocked()
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
       } else {
            // 没有采用VSYNC机制,就自己在一段时间后发送帧刷新消息
            final long nextFrameTime = Math.max(
            mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
            }

            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

该方法的功能:

  1. 采用了VSYNC机制
    1. 当运行在Looper线程,则立刻调度scheduleVSYNCLocked();
    2. 当运行在其他线程,则通过发送一个消息到Looper线程,然后再执行scheduleVSYNCLocked()
  1. 没有采用VSYNC机制
    1. 向消息队列发送一条异步定时消息
// Choreographer.class
private final FrameDisplayEventReceiver mDisplayEventReceiver; // 显示器事件接收器

@UnsupportedAppUsage
private void scheduleVSYNCLocked() {
    mDisplayEventReceiver.scheduleVSYNC();
}

Choreographer的scheduleVSYNCLocked 方法内部调用了 mDisplayEventReceiver 的scheduleVSYNC 方法,mDisplayEventReceiver 是显示屏事件接收器,接收显示器事件如VSYNC事件。该对象是在Choreographer的构造方法中初始化的,只有在开启VSYNC机制时才会被初始化

private Choreographer(Looper looper, int VSYNCSource) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper, VSYNCSource) : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
    // b/68769804: For low FPS experiments.
    setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

FrameDisplayEventReceiver 类继承自DisplayEventReceiver类,且实现了Runnable接口。该对象在创建时就会将自己初始化以显示屏事件,具体实现逻辑是由native代码实现的

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    private boolean mHavePendingVSYNC;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int VSYNCSource) {
        super(looper, VSYNCSource);
    }
}

FrameDisplayEventReceiver初始化

// FrameDisplayEventReceiver.class
private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
MessageQueue messageQueue, int VSYNCSource);

public DisplayEventReceiver(Looper looper, int VSYNCSource) {
    if (looper == null) {
        throw new IllegalArgumentException("looper must not be null");
    }

    mMessageQueue = looper.getQueue();
    mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue, VSYNCSource);
    mCloseGuard.open("dispose");
}

回到我们之前说的Choreographer类的scheduleVSYNCLocked方法调用了 mDisplayEventReceiver的scheduleVSYNC方法,看看这个方法里面做了什么

// Choreographer.class

@UnsupportedAppUsage
private void scheduleVSYNCLocked() {
    mDisplayEventReceiver.scheduleVSYNC();
}

// FrameDisplayEventReceiver.class
/**
* 计划在下一个显示帧开始时发送单个垂直同步脉冲
*/
@UnsupportedAppUsage
public void scheduleVSYNC() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed.");
    } else {
        nativeScheduleVSYNC(mReceiverPtr); // native方法
    }
}

具体的接收VSYNC的请求由native代码处理,我们就不继续看下去了。接收VSYNC的请求到此结束,我们接下来看Choreographer 如何接收VSYNC

收到 VSYNC

当显示系统下一次发生VSYNC 信号脉冲时,会回调FrameDisplayEventReceiver类的dispatchVsync方法

// Called from native code.
@SuppressWarnings("unused")
@UnsupportedAppUsage
private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {
    onVsync(timestampNanos, physicalDisplayId, frame);
}

该方法调用了onVsync方法,该方法是空实现,需要子类实现

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        // ......
        // 向handler post一条VSYNC事件消息
        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVSYNC = false;
        doFrame(mTimestampNanos, mFrame); // Choreographer类的方法
    }
}

onVsync方法向handler post了一条VSYNC事件消息,当handler处理该消息时,会调用该类的run方法,进而调用Choreographer的doFrame方法,执行下一帧的渲染流程

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }

        // ......
        try {
            /​/ ......
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            //标记动画开始时间
            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            // ......
        }
        // ......
    }
}

doFrame方法会依次执行输入、动画、遍历和提交事件的回调,就是调用doCallbacks方法,传不同的callback类型

frameTimeNanos是底层VSYNC信号到达的时间戳

  1. 每调用一次scheduleFrameLocked(),则mFrameScheduled=true,能执行一次doFrame()操作,执行完doFrame()并设置mFrameScheduled=false;
  2. 最终有4个回调类别,如下所示:
    • INPUT:输入事件 ( 按键、touch事件、轨迹球等 )
    • ANIMATION:动画
    • TRAVERSAL:窗口刷新,执行measure/layout/draw操作
    • COMMIT:提交帧
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    // ......
    try {
        // 从队列取出执行时间在当前时间之前的所有 CallbackRecord,callbacks 是一个链表,然后遍历 callbacks 执行 run 方法
        final long now = System.nanoTime();
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;
        // ......
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            // ......
            c.run(frameTimeNanos);
        }
    } finally {
        // 回收callbacks,加入对象池mCallbackPool
        synchronized (mLock) {
        mCallbacksRunning = false;
        do {
            final CallbackRecord next = callbacks.next;
            recycleCallbackLocked(callbacks); // 回收回调过的callback对象
            callbacks = next;
        } while (callbacks != null);
    }
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

该方法主要功能:

  • 从队列头mHead查找CallbackRecord对象,当队列头部的callbacks对象为空或者执行时间还没到达,则直接返回;
  • 开始执行相应回调的run()方法;
  • 回收callbacks,加入对象池mCallbackPool,就是说callback一旦执行完成,则会被回收

每个CallbackRecord关联一个应用程序注册的回调,在run方法里会调用FrameCallback对象的doFrame方法

private static final class CallbackRecord {
    public CallbackRecord next;
    public long dueTime;
    public Object action; // Runnable or FrameCallback
    public Object token;

    @UnsupportedAppUsage
    public void run(long frameTimeNanos) {
        if (token == FRAME_CALLBACK_TOKEN) {
            // 是应用程序注册的回调,就执行该 doFrame 方法
            ((FrameCallback)action).doFrame(frameTimeNanos);
        } else {
            ((Runnable)action).run();
        }
    }
}

private void recycleCallbackLocked(CallbackRecord callback) {
    callback.action = null;
    callback.token = null;
    callback.next = mCallbackPool;
    mCallbackPool = callback;
}

这里的回调方法run()有两种执行情况:

  • 当token的数据类型为FRAME_CALLBACK_TOKEN,则执行该对象的doFrame()方法;
  • 当token为其他类型,则执行该对象的run()方法

recycleCallbackLocked方法将其关联的action对象和token都置为空,且将自己放入缓存池中了,也就是说,应用注册的VSYNC回调只会被调用一次,回调完如果还想要接收下一次的VSYNC事件,需要再次注册

回到我们说到的在run方法里会调用FrameCallback对象的doFrame方法,对应到我们之前AnimationHandler的成员变量 mFrameCallback,他的doFrame方法内部调用了doAnimationFrame

public class AnimationHandler {
    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {

        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                // 如果应用程序向他注册的回调列表不空,说明还需要监听下一次的VSYNC信号,因此还需要再次向Choreographer注册回调
                getProvider().postFrameCallback(this);
            }
        }
    };

    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i++) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }

            if (isCallbackDue(callback, currentTime)) {
                callback.doAnimationFrame(frameTime);
                if (mCommitCallbacks.contains(callback)) {
                    getProvider().postCommitCallback(new Runnable() {
                        @Override
                        public void run() {
                            commitAnimationFrame(callback, getProvider().getFrameTime());
                        }
                    });
                }
            }
        }
        cleanUpList();
    }
}

mFrameCallback 在帧刷新回调方法中,调用了doAnimationFrame 方法,还记得之前在动画开始的时候,valueAnimator对象 在AnimationHandler 注册了自己,监听帧刷新事件吗?就是这句代码

getAnimationHandler().addAnimationFrameCallback(this, delay),doAnimationFrame 方法中,会回调 AnimationFrameCallback#doAnimationFrame 方法,我们回过头来看看,valueAnimator 对象的

doAnimationFrame 方法做了什么

public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
    // 处理一个动画帧
    public final boolean doAnimationFrame(long frameTime) {
        // 省略一些代码
        // 处理暂停/恢复的情况,
        // 省略。。。
        final long currentTime = Math.max(frameTime, mStartTime);
        // 根据当前流逝的时间,更新动画属性,并根据返回值判断动画是否完成
        boolean finished = animateBasedOnTime(currentTime);
        if (finished) {
            // 动画完成就结束动画
            endAnimation();
        }
        return finished;
    }
}

ValueAnimator#animateBasedOnTime方法

boolean animateBasedOnTime(long currentTime) {
    boolean done = false;
    if (mRunning) {
        final long scaledDuration = getScaledDuration();
        final float fraction = scaledDuration > 0 ?
        (float)(currentTime - mStartTime) / scaledDuration : 1f;
        final float lastFraction = mOverallFraction;
        final boolean newIteration = (int) fraction > (int) lastFraction;
        final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) && (mRepeatCount != INFINITE); // 最后一轮动画是否结束
        if (scaledDuration == 0) {
            // 动画时长为0,直接跳到结束动画阶段
            done = true;
        } else if (newIteration && !lastIterationFinished) {
            // 动画重复播放回调
            if (mListeners != null) {
                int numListeners = mListeners.size();
                for (int i = 0; i < numListeners; ++i) {
                    mListeners.get(i).onAnimationRepeat(this);
                }
            }
        } else if (lastIterationFinished) {
            done = true;
        }

        mOverallFraction = clampFraction(fraction);
        float currentIterationFraction = getCurrentIterationFraction(
        mOverallFraction, mReversing);
        animateValue(currentIterationFraction); // 更新属性值
    }
    return done;
}

根据动画已播放时长计算属性值

世界上的动画效果有千千万,有模拟现实世界物体运动规律的动画,也有违反现实世界物体运动规律的动画,不同的动画效果对应的是动画已播放时长与属性值的不同映射关系,即属性值是动画进度的函数,动画进度是自变量,属性值是因变量

根据映射关系的特性,可以将动画分为线性动画和非线性动画,线性动画是指随着时间推移,动画执行的速率不变,属性值按照恒定的速度进行变化;非线性动画则是指随着时间推移,动画执行速率变化,属性值改变的速率也会改变,从而导致属性值的改变趋势是一条曲线,如下所示:

线性动画示例

下图描绘了一个假设的对象,该对象的 x 属性添加了动画效果。动画时长设置为 40 毫秒,要移动的距离为 40 像素。该对象每隔 10 毫秒会水平移动 10 像素。在 40 毫秒时,动画停止,同时对象在水平位置 40 处停止。这是使用线性插值(表示对象以恒定速度移动)的动画示例

 

非线性动画示例

下图展示了一个假设的对象,它在动画开始时加速,在动画结束前减速。该对象仍在 40 毫秒内移动了 40 像素,但这种移动是非线性的。开始时,此动画加速移动到中间点,然后从中间点减速移动,直至动画结束。如图所示,动画在开头和结尾移动的距离小于在中间移动的距离

 

为了更灵活地支持丰富的动画效果,Android引入动画执行进度(即当前属性值改变的百分比)变量,将动画已播放时长百分比与属性值的映射关系分为两段,第一段是动画已播放时长百分比到属性变化百分比的映射,这种映射关系称为时间插值器;第二段是属性变化百分比到属性值的映射,这种映射关系称为类型估值器。Q:为什么需要类型估值器呢?

时间插值器

时间插值器的作用是改变动画的执行速率,根据时间流逝的百分比计算出当前属性值改变的百分比。一般情况我们不需要自己实现插值器,Android ​系统为我们提供了9种插值器。使用插值器后会让动画执行的效果更酷炫,如果想自定义插值器也不难,可以查看已经实现插值器源码做参考

9种插值器

  1. LinearInterpolator:线性的插值器。从开始到结束匀速运动
  2. AccelerateInterpolator:一直加速
  3. AccelerateDecelerateInterpolator:先加速后减速
  4. AnticipateInterpolator: 开始的时候先向后甩一点然后向前,就好比人扔东西会先向后甩一下,这样才能抛的远
  5. AnticipateOvershootInterpolator:和上一种插值器相似,先向后抛然后向前,到达终点后会有回弹一下效果,好比我们将球抛到墙上,然后反弹回来
  6. BounceInterpolator:动画结束的时候弹起,类似皮球落地,会弹几下才停止
  7. CycleInterpolator:动画循环播放特定的次数回到原点,速率改变沿着正弦曲线
  8. DecelerateInterpolator:减速的插值器,刚开始速度快,然后越来越慢直到停止
  9. OvershootInterpolator:向前超过设定值一点然后返回

类型估值器

类型估值器的作用是根据当前属性改变的百分比来计算改变后的属性值,属性的类型除了基本数值类型之外,还有Point、Rect、数组和存储argb颜色的整数等不适用于数值插值实现的类型。因此Android提供这样一个接口,允许开发者自由实现如何根据当前属性变化的百分比计算出自定义属性的值

public interface TypeEvaluator<T> {
    public T evaluate(float fraction, T startValue, T endValue);
}

代码实现

ValueAnimator.animateValue

public class ValueAnimator extends Animator {
    /***部分代码省略***/
    void animateValue(float fraction) {
        // 计算当前属性改变的百分比
        fraction = mInterpolator.getInterpolation(fraction);
        mCurrentFraction = fraction;
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
           // 计算最新属性值
            mValues[i].calculateValue(fraction);
        }
        if (mUpdateListeners != null) {
            int numListeners = mUpdateListeners.size();
            for (int i = 0; i < numListeners; ++i) {
                //属性值改变的回调
                mUpdateListeners.get(i).onAnimationUpdate(this);
            }
        }
    }
}

PropertyValuesHolder

PropertyValuesHolder这个类的意义就是保存动画过程中所需要操作的属性和对应的值。在封装成PropertyValuesHolder实例以后,后期的对属性值的操作也是在PropertyValuesHolder进行

public class PropertyValuesHolder implements Cloneable {
    /***部分代码省略***/
    public static <V> PropertyValuesHolder ofObject(Property property, TypeEvaluator<V> evaluator, V... values) {
        PropertyValuesHolder pvh = new PropertyValuesHolder(property);
        pvh.setObjectValues(values);
        pvh.setEvaluator(evaluator);
        return pvh;
    }

    public void setObjectValues(Object... values) {
        mValueType = values[0].getClass();
        // mKeyframes:KeyframeSet
        mKeyframes = KeyframeSet.ofObject(values);
        if (mEvaluator != null) {
            mKeyframes.setEvaluator(mEvaluator);
        }
    }

    public void setEvaluator(TypeEvaluator evaluator) {
        mEvaluator = evaluator;
        mKeyframes.setEvaluator(evaluator);
    }

    
    void calculateValue(float fraction) {
        Object value = mKeyframes.getValue(fraction);
        mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
    }
}

KeyFrameSet#getValue 方法

public Object getValue(float fraction) {
    // Special-case optimization for the common case of only two keyframes
    if (mNumKeyframes == 2) {
        if (mInterpolator != null) {
            fraction = mInterpolator.getInterpolation(fraction);
        }
        return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),, mLastKeyframe.getValue());
    }
    // ......处理多于2个 KeyFrame的情况
}

设置属性值

我们计算出下一帧的属性值之后,需要赋值给对应的属性。那Android 中如何更新属性值的呢?就是通过反射来获取属性的 get 和 set 方法,再调用set方法为属性赋值,从而实现属性值的更新。所以,这就需要我们的View (如自定义 View 中)动画属性具有 set 和 get 方法

代码实现

public class PropertyValuesHolder implements Cloneable {
    /**
    * 属性名称
    */
    String mPropertyName;

    /**
    * 属性的set方法
    */
    Method mSetter = null;

    /**
    * 属性的get方法
    */
    private Method mGetter = null;

    /**
    * 类型估值器
    */
    private TypeEvaluator mEvaluator;

    /**
    * 当前最新的属性值
    */
    private Object mAnimatedValue;
}

动画初始化时,会初始化属性的set方法和get方法

// ObjectAnimator.class
void initAnimation() {
    if (!mInitialized) {
        // mValueType may change due to setter/getter setup; do this before calling super.init(),
        // which uses mValueType to set up the default type evaluator.
        final Object target = getTarget();
        if (target != null) {
            final int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].setupSetterAndGetter(target);
            }
        }
        super.initAnimation();
    }
}


// FloatPropertyValueHolder.class
void setupSetterAndGetter(Object target) {
    mKeyframes.invalidateCache();
    if (mProperty != null) {
        /***部分代码省略***/
    }
    if (mProperty == null) {
        Class targetClass = target.getClass();
        if (mSetter == null) {
            //初始化mSetter
            setupSetter(targetClass);
        }

        List<Keyframe> keyframes = mKeyframes.getKeyframes();
        int keyframeCount = keyframes == null ? 0 : keyframes.size();
        for (int i = 0; i < keyframeCount; i++) {
            Keyframe kf = keyframes.get(i);
            if (!kf.hasValue() || kf.valueWasSetOnStart()) {
                if (mGetter == null) {
                    setupGetter(targetClass);
                    if (mGetter == null) {
                        // Already logged the error - just return to avoid NPE
                        return;
                    }
                }
                // ......
            }
        }
    }
}

ObjectAnimator.animateValue

public final class ObjectAnimator extends ValueAnimator {
    /***部分代码省略***/
    @Override
    void animateValue(float fraction) {
        final Object target = getTarget();
        if (mTarget != null && target == null) {
            // We lost the target reference, cancel and clean up.
            cancel();
            return;
        }
        // ValueAnimatoranimateValue方法
        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            //设置target的属性值,进行View的移动,产生动画
            mValues[i].setAnimatedValue(target);
        }
    }
}

ObjectAnimator#setAnimatedValue方法

void setAnimatedValue(Object target) {
    if (mProperty != null) {
        mProperty.set(target, getAnimatedValue());
    }
    if (mSetter != null) {
        try {
            mTmpValueArray[0] = getAnimatedValue();
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }
}

Object getAnimatedValue() {
    return mAnimatedValue;
}

界面绘制

当属性值更新后,需要执行绘制操作,得到新的帧数据。Android的View绘制与动画过程中的属性计算一样,由Choreographer对象统一调度,当doFrame方法调度动画更新完属性后,再调用绘制帧回调接口

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        // ......
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        // ......
    }
}

ViewRootImpl类注册了帧刷新的绘制回调接口,因此Choreographer 类的绘制处理方法会调用 TraversalRunnable类的run方法,开始View的三大绘制流程

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        performTraversals();
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

Android 绘制模型

Android 有两种绘制模型:基于软件的绘制模型和硬件加速绘制模型

页面渲染背景知识

  • 页面渲染时,被绘制的元素最终要转换成矩阵像素点(例如Bitmap),才能被显示器显示
  • 页面由各种基本元素组成,例如圆形、圆角矩形、线段、文字、矢量图(常用贝塞尔曲线组成)、Bitmap等
  • 元素绘制时尤其是动画绘制过程中,经常涉及插值、缩放、旋转、透明度变化、动画过渡等运算
  • 绘制过程经常需要进行逻辑较简单、但数据量庞大的浮点运算

基于软件的绘制模型

软件绘制时,界面的渲染工作都会由主线程完成,主线程在处理完input、animation、measure、layout后,开始执行draw时,直接通过调用 skia库实现drawLine、drawText等操作;每当应用需要更新界面时,都需要计算重新绘制的屏幕区域,然后进行重绘

缺点:

每次绘制时都需要处理大量数据。例如,如对某个按钮调用 invalidate() ,Android 系统会重新绘制该View;此时CPU需要重新计算该View每一个像素的值,这个计算通常会花费较多的时间。CPU 相对更擅长处理逻辑,应当由更擅长批量计算的GPU承担界面的绘制工作

硬件加速绘制模型

硬件加速,实际上应该叫 GPU 加速,软件绘制与硬件加速的区别主要是图形的绘制究竟是 GPU 来处理还是 CPU;如果是 GPU,就认为是硬件加速绘制,反之,则是软件绘制

硬件加速的主要原理,就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成

从 Android 3.0(API 级别 11)开始,Android 2D 渲染管道支持硬件加速,也就是说,在 View 的画布上执行的绘制操作都会使用 GPU;从API 14 开始,硬件加速默认处于启用状态

在硬件加速模型中,app会存在主线程和渲染线程,界面的最终绘制工作是由渲染线程驱动GPU完成

支持硬件加速后,Android View 源码新增了RenderNode 数据结构,View被抽象成RenderNode 节点,View的绘制内容被抽象成一系列绘制操作,称为显示列表(Display list),一个View的RenderNode 中包含了自身的显示列表及子View的显示列表的信息

当我们使用invalidate() 和 draw() 请求屏幕更新和渲染View时,view 会将这些绘制命令记录在显示列表中,且只记录和更新被invalidate()标记的View的显示列表,然后由GPU绘制显示列表。如果view没有invalidate,就无需再次执行View的onDraw方法

启用硬件加速后,属性动画( alpha \ translation \ scale \ rotation \ )过程中只更新 View 的 属性,无需更新显示列表;display list主要由渲染线程负责绘制,减少了主线程的工作,提高了主线程的响应速度

目前在硬件加速模式下不需要重新生成display list的动画属性有:

  • alpha
  • x、y
  • translationX、translationY
  • scaleX、scaleY
  • rotation、rotationX、rotationY
  • pivotX、pivotY

DisplayList:

DisplayList是一个基本绘制元素,包含元素原始属性(位置、尺寸、角度、透明度等),对应Canvas的drawXxx()方法(如下图)

 

总结

Android属性动画实现原理

  • Android属性动画通过监听帧刷新事件,在下一帧开始绘制前更新属性值
  • Android 帧刷新统一由Choreographer 指挥,在每一帧到来时,依次处理input、animation、traversal和commit事件,属性动画在animation 阶段进行属性值更新
  • Android属性更新分为两阶段,第一阶段:将动画已播放时长百分比通过时间插值器转换为动画完成百分比(即属性变化的百分比);第二阶段:将属性变化的百分比通过类型估值器映射为特定的属性值
  • android界面绘制由两种模型:软件绘制和硬件加速绘制,软件绘制时,主线程负责执行绘制工作;硬件加速时,主线程将要绘制的内容交给渲染线程,由渲染线程执行绘制的工作。针对特定属性动画时,开启硬件加速可以提升动画流畅性

兼容性问题

  • 属性动画支持Android3.0及以上版本,在3.0以下的系统上有兼容性问题

硬件加速

  • 使用属性动画时,开启硬件加速,可以使动画更流畅(alpha、scale、translate、rotation等效果更突出)

内存泄漏

  • 属性动画开始时,向AnimationHandler注册自身作为帧刷新回调接口,结束时才会解除注册,如果view销毁时,动画没有结束,AnimationHandler会一直持有该对象,造成内存泄漏问题

参考文档

Choreographer相关

https://cloud.tencent.com/developer/article/1176313

https://cloud.tencent.com/developer/article/1634784

https://blog.csdn.net/innost/article/details/8272867

属性动画

https://juejin.cn/post/6844903870863278094

https://developer.android.com/guide/topics/graphics/prop-animation?hl=zh-cn

https://cloud.tencent.com/developer/article/1634796

https://blog.csdn.net/carson_ho/article/details/79860980

硬件加速

https://tech.meituan.com/2017/01/19/hardware-accelerate.html

https://developer.android.com/guide/topics/graphics/hardware-accel?hl=zh-cn

https://www.jianshu.com/p/c100d42eb42f

https://zhuanlan.zhihu.com/p/75458539

https://juejin.cn/post/6844903517803511822

网站文章

  • 阿里云在线温湿度-小熊派qpython(综合展示)

    阿里云在线温湿度-小熊派qpython(综合展示)

    需要用到的东西:小熊派的ec100y开发板;i2c的温湿度传感器(我这里用的sht31,其他的也可以,自行修改代码);阿里云账号;接线:用到3.3v,GND,i2c的SCL和SDA阿里云显示展示:ap...

    2024-02-01 03:20:31
  • 模拟post发送,收到的数据乱码 且已设置utf-8标准编码

    模拟post发送,收到的数据乱码 且已设置utf-8标准编码

    模拟Post发送乱码

    2024-02-01 03:19:59
  • Spring知识篇2:Spring Frame框架的基本知识

    Spring知识篇2:Spring Frame框架的基本知识

    本文主要介绍Spring Frame需要掌握的知识

    2024-02-01 03:19:53
  • 微服务之间调用(consul)

    1.首先确定是不是在同一注册中心。 2.确实微服务名唯一,不然可能会调到其他微服务中,导致错误。 3.引入二个包 RestTemplate 和 DiscoveryClient 。 4.通过获取注册提供...

    2024-02-01 03:19:47
  • 【UnityShader】光线追踪体积光

    【UnityShader】光线追踪体积光

    最近尝试实现了一下光线追踪体积光,效果如下: 光线追踪(Ray tracing)是三维计算机图形学中的特殊渲染算法,跟踪从眼睛发出的光线而不是光源发出的光线,通过这样一项技术生成编排好的场景的数学模型显现出来。(摘自维基百科) 实现步骤: 一、在灯光区域生成体积光的载体mesh,即我们的体积光实际上是渲染在mesh上的,因此光线追踪的起点是每个顶点的位

    2024-02-01 03:19:39
  • MongoDB图形化工具下载和使用

    MongoDB图形化工具下载和使用

    MongoDB图形化工具下载和使用。

    2024-02-01 03:19:09
  • RabbitMQ:RabbitMQ + Spring配置文件rabbit标签

    RabbitMQ:RabbitMQ + Spring配置文件rabbit标签1.消费者配置文件和启动类: 【Consumer.xml】:&lt;?xml version="1.0" encoding="UTF-8"?&gt;&lt;beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi...

    2024-02-01 03:19:04
  • 3.1 TensorFlow计算模型 --- 计算图

      TensorFlow程序一般可以分为两个阶段。在第一个阶段需要定义计算图中的所有的计算。第二个阶段为执行计算。   在TensorFlow程序中,系统会自动维护一个默认的计算图,通过tf.get_default_graph函数可以获取当前默认的计算图。   除去使用默认的计算图,TensorFlow支持通过tf.Graph函数生成新的计算图。不同计算图上的张量和运算都不...

    2024-02-01 03:18:47
  • 阿里云数据库白名单导致的数据库链接不上问题

    百度搜索 IP 获取外网IP。修改白名单

    2024-02-01 03:18:17
  • MyEclipse设置代码提示功能 热门推荐

    MyEclipse设置代码提示功能 热门推荐

    写代码的时候为什么能给我们提示?你知道提示分为手动提示和自动提示么?今天我们就以MyEclipse为例讲一下MyEclipse手动提示和自动提示如何设置。在开始这篇博客之前,我们先来说一下代码的手动提...

    2024-02-01 03:18:11