Android动画浅析

Posted by Ann on December 30, 2021

1 前言

日常处理UI相关的开发过程中,我们不免要与动画打交道。通过动画,我们实现了酷炫的效果、高级的交互体验,完成了web网页不可替代的顺滑体验。Android基础库为我们提供了多种动画的实现方式,支持的功能也愈来愈强大。

  • 作用范围上,动画不只作用在单个View上,也支持作用在ViewGroup、RecyclerView、ViewPager、Fragment、甚至Activity之间完成动画。
  • 动画效果上,除了帧动画,以及基础的补间动画;属性动画的出现支持我们更加灵活的配置、处理;FlingAnimation、SpringAnimation的出现支持动画符合物理特性,更加自然生动。

2 总览

3 视图动画

视图动画又分为帧动画(Frame Animation)和补间动画(Tween Animation),帧动画可以去加载一系列可绘制的Drawable逐帧实现动画,补间动画可以执行一系列简单的变化,这些变化包括旋转、位移、缩放等等,他们都做用在单个view上。

3.1 帧动画

3.1.1 原理

帧动画的原理就是一组图片按照指定顺序、指定时间顺序展现的播放的图片。

3.1.2 实现方式

关键类:AnimationDrawable

实现方式

  • 【Step 1 】 在 res/anim的文件夹里创建动画效果xml文件
<?xml version="1.0" encoding="utf-8"?>
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true" // 设置是否只播放一次,默认为false
    >
    <item android:drawable="@drawable/a0" android:duration="100"/>
    <item android:drawable="@drawable/a1" android:duration="100"/>
</animation-list>
  • 【Step 2 】在指定位置中设置动画执行
void startFrameAnimation() {
    // 执行动画view
    View targetView = findViewById(R.id.tag_desc);
    // 获取动画对象
    AnimationDrawable animationDrawable = (AnimationDrawable) (targetView).getDrawable();
    // 启动动画
    animationDrawable.start();
}

3.1.3 关键代码

private void setFrame(int frame, boolean unschedule, boolean animate) {
    // 判断当前帧是否在
    if (frame >= mAnimationState.getChildCount()) {
        return;
    }
    // 选取该帧图片
    selectDrawable(frame);
    if (unschedule || animate) {
        unscheduleSelf(this);
    }
    if (animate) {
        // 定时绘制该帧图片
        scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
    }
}

3.1.4 优势&弊端

优点:简单方便,通过帧动画可以实现一些特殊的动画效果

弊端:图片资源过多,容易引起OOM

3.2 补间动画

补间动画的四大子类包括:

  • translate (平移动画) — TranslateAnimation
  • scale (缩放动画) — ScaleAnimation
  • rotate (旋转动画) — RotateAnimation
  • alpha ( 透明度动画) — AlphaAnimation

3.2.1 原理

通过确定开始的视图样式 & 结束的视图样式、中间动画变化过程由系统补全来确定一个动画。

3.2.2 使用

初始化

支持xml设置和java动态设置,实现方式略。

组合动画

  • 支持组合动画,AnimationSet继承于Animation,可以看做基本补间动画的容器集合。
  • 支持设置多种基本补间动画,补间动画间的插值器可相互独立也可复用。
  • AnimationSet的初始化、执行动画逻辑同普通Animation,不支持串行执行。

插值器

通过设置插值器的方式,可以是动画按照预期速率执行,除了系统插值器,还可以自定义插值器。

自定义插值器需要重写getInterpolation方法,以AccelerateInterpolator 插值器为例:

public class AccelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
    private final float mFactor;
    private final double mDoubleFactor;
 
    public AccelerateInterpolator() {
        mFactor = 1.0f;
        mDoubleFactor = 2.0;
    }
 
       public AccelerateInterpolator(float factor) {
        mFactor = factor;
        mDoubleFactor = 2 * mFactor;
    }
     
    public float getInterpolation(float input) {
        if (mFactor == 1.0f) {
            return input * input;
        } else {
            return (float)Math.pow(input, mDoubleFactor);
        }
    }
 }

getInterpolation()方法中,input传入是均匀等分的float字段,通过实现x^2的函数,达到了逐渐加速的效果。

Input=0.1 返回0.01,input=0.9 返回0.81,input=1 返回1

参考文档

🚗🚗

3.2.3 关键代码

整体流程

  • start逻辑
/**
 * Start the specified animation now.
 *
 * @param animation the animation to start now
 */
public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}
  • view绘制逻辑
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    final Animation a = getAnimation();
    if (a != null) {
        // 执行动画的核心逻辑
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        // 是否需要改变边界martix,除alpha动画外均需要
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        // 返回值非空
        transformToApply = parent.getChildTransformation();
    } else {
        // ...省略
    }
    // more为false 通知父view动画结束,修改flag值
    if (a != null && !more) {
        if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
            onSetAlpha(255);
        }
        parent.finishAnimatingView(this, a);
    }
    // more为true 动画状态中
    if (more && hardwareAcceleratedCanvas) {
        // 含有alpha动画需要通知子view更新动画
        if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
            invalidate(true);
        }
        // 否则子view无需重建canvas
    }
    if (transformToApply{
        // 进行一些绘制操作
    }
}
}
 
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
                                     Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
    // 若未初始化先初始化
    if (!initialized) {
        //Animation初始化
    }
    // 动画内核心执行逻辑
    boolean more = a.getTransformation(drawingTime, t, 1f);
    ...
    if (more) {//成功
        // 不管是否需要改变边界值,都通知父view更新
        if (!a.willChangeBounds()) {//动画不会改变View的边界,如Alpha动画
           parent.invalidate(mLeft, mTop, mRight, mBottom);
        } else {//动画会改变View的边界,如Scale动画
            parent.invalidate(left, top, left + (int) (region.width() + .5f),
                    top + (int) (region.height() + .5f));
        }
    }
    return more;
}
  • Animation相关逻辑
public boolean getTransformation(long currentTime, Transformation outTransformation) {
    if (duration != 0) { //经历的时间占总耗时的比例
        normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /(float) duration;
    } else {
        normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
    }
    //比例大于等于1说明动画结束
    final boolean expired = normalizedTime >= 1.0f;
    // More标识动画是否结束
    mMore = !expired;
    ...
    // todo mFillBefore如何保证动画结束不改变view布局 没找到具体逻辑,只找到了废弃的逻辑
    if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
        // 通过插值器获取当前时间点的相对比值
        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
        // 执行动画核心逻辑,不同animation需重写此方法。
        applyTransformation(interpolatedTime, outTransformation);
    }
    //动画结束
    if (expired) {
        if (mRepeatCount == mRepeated) {
           // 通知动画结束
        } else {
            //判断repeat,reverse等逻辑
        }
    }
    ...
    return mMore;
}

参考文档

总结

补间动画的核心本质是在一定的持续时间内,不断改变视图Matrix变换,并且不断刷新的过程。

在动画过程中,第一帧刷新父视图和动画视图。

父视图每一帧都会Canvas重建。

在动画启动时,第一帧动画视图Canvas重建,后续若包含alpha动画,动画视图也会重复invalidate,否则不会再Canvas重建。

动画视图的兄弟视图不会Canvas重建,不会触发onDraw方法。

问题解疑

  • 调用startAnimation会立即执行动画嘛?
    • 不会,会先更新设置值,通过调用父view的invalidate,通知到跟布局遍历view tree进行绘制,触发动画逻辑。
  • Animation重复调用startAnimation会发生什么?
    • 可以看到每次startAnimation都会重新设置flag值及动画的标记信息,重复调用的表现是:
      • 若重复调用的时间 < 动画执行时长,表现可能为旧动画被新动画覆盖,或是不会重复执行动画,仅会在最后一次调用startAnimation后执行。
      • 若重复调用的时间 > 动画执行时长,表现是连续多次执行动画。
  • Animation创建销毁是否会有内存泄露?
    • 不会,view中持有Animation,在view的onDetachedFromWindow方法会取消掉动画。

优势 & 弊端

优势:操作简单,可实现动画效果丰富,支持叠加动效,动画与view绑定,不会产生泄露隐患。

弊端:仅与View绑定,无法针对其他对象做动画。无法做更高级的动画,如变色、按照贝塞尔曲线移动等。

3.3 布局动画

布局动画是在viewgroup层级上的动画,通过设置布局动画可以给容器中的控件统一设置进入、进出动画等。

  • 布局动画主要分为:
    • layoutAnimation
    • gridLayoutAnimation
    • animateLayoutChanges
    • LayoutTransition

3.3.1 原理

以layoutAnimation为例,其原理类似于补间动画,其设置、调用方法在viewgroup中,通过设置标记值在onDraw中遍历viewTree进行动画变换。

  • 区别
    • layoutAnimation:作用于viewgroup,动画只能使用补间动画
    • gridLayoutAnimation:给网格布局使用的,动画只能使用补间动画
    • animateLayoutChanges:这是在 ViewGroup 创建显示之后,内容改变时对于新的内容做动画,和 layoutAnimation 是配合使用的。
    • LayoutTransition:作用范围 = layoutAnimation + animateLayoutChanges。
  • 系统中支持以下5种状态变化:
    • 1、APPEARING:容器中出现一个视图。
    • 2、DISAPPEARING:容器中消失一个视图。
    • 3、CHANGING:布局改变导致某个视图随之改变,例如调整大小,但不包括添加或者移除视图。
    • 4、CHANGE_APPEARING:其他视图的出现导致某个视图改变。
    • 5、CHANGE_DISAPPEARING:其他视图的消失导致某个视图改变。 动画只能使用属性动画

3.3.2 使用

以LayoutAnimation为例:初始化,支持xml设置 & java动态设置

参考资料

4 属性动画

由于补间动画一些限制问题(动画仅可作用于View,无法自定义动画,无法真实的改变view的位置…),Android在3.0版本当中引入属性动画这个功能,它不再是针对于View来设计的了,也不限定于只能实现移动、缩放、旋转和淡入淡出这几种动画操作,同时也不再只是一种视觉上的动画效果了。

由Animator可以看出,ValueAnimator是属性动画的关键类,下面将以ValueAnimator为例进行介绍。

4.1 原理

在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现任意对象在该属性上的动画效果。

4.2 使用

支持xml和JAVA动态设置的方式。实际开发中,建议使用Java代码实现属性动画:因为很多时候属性的起始值是无法提前确定的(无法使用XML设置),需要在Java代码里动态获取。

开发者文档

4.2.1 初始化

ValueAnimtor重要初始化方法

  • ValueAnimator.ofInt(int… values)
  • ValueAnimator.ofFloat(float… values)
  • ValueAnimator.ofArgb(int… values)
  • ValueAnimator.ofPropertyValuesHolder(PropertyValuesHolder… values)
  • ValueAnimator.ofObject(TypeEvaluator evaluator, Object… values)

区别在于初始值~结束值可以以int型/float型/… 过渡到结束值,分别对应了不用的估值器。这里见到了一些新的名词,下一小节进行介绍。

ValueAnimtor也可通过xml进行设置,通过AnimatorInflater.loadAnimator()方法进行载入。

4.2.2 估值器

什么是估值器TypeEvaluator?

估值器类似于差值器,是辅佐动画执行的重要类。他的作用是设置初始值→ 结束值之间过渡的具体逻辑。

估值器进可作用于属性动画。

  • 估值器 VS 插值器
    • 插值器(Interpolator)决定 值 的变化模式(匀速、加速blabla)
    • 估值器(TypeEvaluator)决定 值 的具体变化数值

用法

同样的,我们可以通过重写evaluate方法自定义估值器实现特殊功能。Android也为我们实现了几种常用的估值方法,下面以ArgbEvaluator和FloatEvaluator为例。

  • ArgbEvaluator
public Object evaluate(float fraction, Object startValue, Object endValue) {
    int startInt = (Integer) startValue;
    float startA = ((startInt >> 24) & 0xff) / 255.0f;
    float startR = ((startInt >> 16) & 0xff) / 255.0f;
    float startG = ((startInt >>  8) & 0xff) / 255.0f;
    float startB = ( startInt        & 0xff) / 255.0f;
 
    int endInt = (Integer) endValue;
    float endA = ((endInt >> 24) & 0xff) / 255.0f;
    float endR = ((endInt >> 16) & 0xff) / 255.0f;
    float endG = ((endInt >>  8) & 0xff) / 255.0f;
    float endB = ( endInt        & 0xff) / 255.0f;
 
    // 默认线性变换,计算中间色值
    float a = startA + fraction * (endA - startA);
    float r = startR + fraction * (endR - startR);
    float g = startG + fraction * (endG - startG);
    float b = startB + fraction * (endB - startB);
 
    return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
}
  • FloatEvaluator
public Float evaluate(float fraction, Number startValue, Number endValue) {
    float startFloat = startValue.floatValue();
    // 返回对应的float中间值
    return startFloat + fraction * (endValue.floatValue() - startFloat);
}

4.2.3 Keyframe

  • 什么是Keyframe?
    • 顾名思义,他代表了关键帧。 前面讲到我们可以通过设置插值器或插值器修改动画速率,但这些都需要有一定的数学基础来设计速率曲线。为拯救广大学渣,keyframe诞生了!
  • 使用
    • 从构造函数可以看出,接受两个传参(float fraction,T value),即置顶关键帧的value值,系统通过多组keyframe来计算速率。

4.2.4 PropertyValuesHolder

  • 什么是PropertyValuesHolder?

它其中保存了动画过程中所需要操作的属性和对应的值。

开发者文档:https://developer.android.com/reference/android/animation/PropertyValuesHolder.html

  • 初始化

以ofFloat为例,他支持传入动画属性对象or属性名称。

4.3 关键代码

我们还是从调用启动动画animator.start()开始看起

private void start(boolean playBackwards) {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }
    mReversing = playBackwards;
    mSelfPulse = !mSuppressSelfPulseRequested;
    // 如果需要反向执行动画,反向修改当前进度值
    if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
        // ..
    }
    // 修改标记值
    mStarted = true;
    mPaused = false;
    mRunning = false;
    mAnimationEndRequested = false;
    // Resets mLastFrameTime when start() is called, so that if the animation was running,
    // calling start() would put the animation in the
    // started-but-not-yet-reached-the-first-frame phase.
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;
 
 
    // 关键方法
    addAnimationCallback(0);
 
    if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
        // 若无需延迟,直接执行动画,这里做了些初始化和通知回调的逻辑
        startAnimation();
        // 更新fraction
        setCurrentFraction(mSeekFraction);
    }
}

start方法中主要是进行一些变量的初始化,及回调的通知(startAnimation()),跟进addAnimationCallback方法,可以近一步看到执行动画逻辑。

private void addAnimationCallback(long delay) {
    if (!mSelfPulse) {
        return;
    }
    // AnimationHandler是一个单例
    getAnimationHandler().addAnimationFrameCallback(this, delay);
}
 
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        // 执行本帧动画
        doAnimationFrame(getProvider().getFrameTime());
        // 持续执行下一帧动画
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};
/**
 * Register to get a callback on the next frame after the delay.
 */
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    // 若第一次执行动画,或之前动画已执行完
    if (mAnimationCallbacks.size() == 0) {
        getProvider().postFrameCallback(mFrameCallback);
    }
    if (!mAnimationCallbacks.contains(callback)) {
        mAnimationCallbacks.add(callback);
    }
    if (delay > 0) {
        mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
}

动画执行的逻辑比较明朗了,那么我们就跟进doAnimationFrame()方法看下属性动画每一帧是怎么执行的。

private void doAnimationFrame(long frameTime) {
    long currentTime = SystemClock.uptimeMillis();
    final int size = mAnimationCallbacks.size();
    // 遍历动画队列(mAnimationCallbacks)
    for (int i = 0; i < size; i++) {
        final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
        if (callback == null) {
            continue;
        }
        // 判断当前时间是否到达(需要延迟返回false)
        if (isCallbackDue(callback, currentTime)) {
            // 这里调用了animator的doAnimationFrame方法
            callback.doAnimationFrame(frameTime);
            if (mCommitCallbacks.contains(callback)) {
                getProvider().postCommitCallback(new Runnable() {
                    @Override
                    public void run() {
                        commitAnimationFrame(callback, getProvider().getFrameTime());
                    }
                });
            }
        }
    }
    // 清理动画队列mAnimationCallbacks
    cleanUpList();
}
 
 
/**
 * Processes a frame of the animation, adjusting the start time if needed.
 *
 * @param frameTime The frame time.
 * @return true if the animation has ended.
 * @hide
 */
public final boolean doAnimationFrame(long frameTime) {
    // 处理 pause/resume
    if (mPaused) {
        // ...
        return false
    } else if (mResumed) {
        // ...
    }
    // 允许动画已被触发过
    if (mLastFrameTime < 0) {
        if (mSeekFraction >= 0) {
            long seekTime = (long) (getScaledDuration() * mSeekFraction);
            mStartTime = frameTime - seekTime;
            mSeekFraction = -1;
        }
        mStartTimeCommitted = false; // allow start time to be compensated for jank
    }
    // 重要代码
    boolean finished = animateBasedOnTime(currentTime);
    // 若动画结束,进行一些清理工作
    if (finished) {
        endAnimation();
    }
    return finished;
}
boolean animateBasedOnTime(long currentTime) {
    // 是否完成动画
    boolean done = false;
    if (mRunning) {
        // 是否全部循环执行完
        final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
                (mRepeatCount != INFINITE);
        // 判断是否已结束
        if (scaledDuration == 0) {
            // 0 duration animator, ignore the repeat count and skip to the end
            done = true;
        } else if (newIteration && !lastIterationFinished) {
            // Time to repeat
            // ...
        } else if (lastIterationFinished) {
            done = true;
        }
        // 获取当前进度值
        float currentIterationFraction = getCurrentIterationFraction(
                mOverallFraction, mReversing);
        // 关键方法
        animateValue(currentIterationFraction);
    }
    return done;
}
 
 
void animateValue(float fraction) {
    // 根据插值器重新获取进度值
    fraction = mInterpolator.getInterpolation(fraction);
    mCurrentFraction = fraction;
    // 通过PropertyValuesHolder计算当前进度下的值
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction);
    }
    // 通知update回调,返回最新进度
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}

总结

属性动画不是与View强绑定的,它作用于任意对象。

通过AnimationHandler维持当前项目内全部待执行的属性动画,依赖Choreographer.FrameCallback回调在每次刷新屏幕时处理动画逻辑。

动画处理逻辑是:

遍历handler中全部animator,若当前帧需要执行动画,更新对应的PropertyValuesHolder。

参考文档

4.4 其他animator

4.4.1 ObjectAnimator

ObjectAnimator可以直接的将动画作用于对象。

相比较于ValueAnimator往往需要外部设置updateListener去手动的设置对象值,ObjectAnimator支持设置目标对象,并自动的更新对象相关值。可以看做ValueAnimator的升级版。

初始化

public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    anim.setIntValues(values);
    return anim;
}
 
 
private ObjectAnimator(Object target, String propertyName) {
    setTarget(target);
    setPropertyName(propertyName);
}

执行动画关键代码

void animateValue(float fraction) {
    final Object target = getTarget();
    // 获取目标对象,对不符合预期的情况进行取消
    if (mTarget != null && target == null) {
        // We lost the target reference, cancel and clean up. Note: we allow null target if the
        /// target has never been set.
        cancel();
        return;
    }
 
    super.animateValue(fraction);
    int numValues = mValues.length;
    // 自动修改对象值
    for (int i = 0; i < numValues; ++i) {
        mValues[i].setAnimatedValue(target);
    }
}

开发者文档

4.4.2 TimeAnimator

这个Animator主要就提供了一个setTimeListner的方法。它没有时间,插值或是对象值设定。回调监听器为每一帧动画接受信息,包括总运行时间和从前一帧到现在的运行时间.

4.4.3 LottieAnimator

服务于lottie的属性动画,目前作用在lottie渲染动画类LottieDrawable中,进行lottie动画的一些设置。

4.4.4 AnimatorSet

属性动画的动画集合,相比较AnimationSet,对动画的设置更加灵活。

  • 支持序列化执行动画

  • 支持同时执行动画 animatorSet.playTogether(objectAnimator,objectAnimator1);

  • 支持各动画独立设置属性,但对于animatorSet设置的属性(duration、target、插值器等)优先级更高,会覆盖带个动画相关属性的设置。

开发者文档

4.5 优势&弊端

优势:功能强大,支持更多种、丰富的动效,支持对非view对象进行操作。

弊端:操作时需要更加关注初始化&销毁逻辑,易引发内存泄露。

5 几点建议

5.1 性能优化

整体思路:

对于复杂动画需要叠加多个Animator,减小Animator量级,进而减少AnimationHandler中持有的动画队列的量级,减少动画更新过程中整体的迭代次数。

针对同一个对象,进行复杂动画时,善用PropertyValuesHolder 和 Keyframe or 插值器或ViewPropertyAnimator。 示例1:同时为一个对象设置多种属性动画,通过PropertyValuesHolder设置方式,可以使叠加动画统一成一个Animator。

//传统写法
ObjectAnimator tX = ObjectAnimator.ofFloat(textView, "translationX", currentX, -300, currentX);
ObjectAnimator tY = ObjectAnimator.ofFloat(textView, "translationY", currentY, 200, currentY);
AnimatorSet set = new AnimatorSet();
set.setDuration(1000).playTogether(tX, tY);
set.start();
  
//一个view同时发生多种效果时,建议使用PropertyValuesHolder,这样ObjectAnimator只有一个对象
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", currentX, 500, currentX);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", currentY, 400, currentY);
ObjectAnimator.ofPropertyValuesHolder(textView, pvhX, pvhY).setDuration(1000).start();

示例2:为一个对象设置同一属性动画的变速操作,建议使用Keyframe or 插值器整合。

//传统写法
ObjectAnimator transYFirstAnim = ObjectAnimator.ofFloat(textView, "translationY", 0, 100);
ObjectAnimator transYSecondAnim = ObjectAnimator.ofFloat(textView, "translationY", 100, 0);
AnimatorSet animSet = new AnimatorSet();
animSet.playSequentially(transYFirstAnim, transYSecondAnim);
  
 //一个view的单个属性先后发生一系列变化时,建议使用Keyframe达到效果。从词义上来理解Keyframe是关键帧
Keyframe k0 = Keyframe.ofFloat(0f, 0); //第一个参数为“何时”,第二个参数为“何地”
Keyframe k1 = Keyframe.ofFloat(0.5f, 100);
Keyframe k2 = Keyframe.ofFloat(1f, 0);
PropertyValuesHolder p = PropertyValuesHolder.ofKeyframe("translationY", k0, k1, k2);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, p);
objectAnimator.start();

针对不同对象进行复杂动画,建议使用ValueAnition进行动画速率控制,外部设置updateListener对不同对象进行统一操作。

进度回调建议使用AnimatorListenerAdapter代替AnimatorListener 由于AnimatorListener是接口,所以实现它得实现它所有的方法,而我们有时只用到它的个别回调(如:onAnimationStart),使用它会导致代码看起来非常冗杂。

而AnimatorListenerAdapter是默认实现了AnimatorListener的一个抽象类,你可以按需要重写其中的方法,代码会优雅一点。

示例3:ViewPropertyAnimator的使用

5.2 StackOverflow

我们已知属性动画时由一个全局的单例中持有的,只有动画结束或者被cancel掉,才会从队列中移除。

如果我们将动画设置成了极易循环播放,引发内存泄露。

建议: Activity/Fragment生命周期结束时,如onDestroy方法中,及时的cancel掉并置空属性动画。 View内部动画,在onDetachedFromWindow,及时的cancel掉并置空属性动画。 若View内部动画较简单,建议使用补间动画。

5.3 OOM

帧动画中,图片资源过大易引起OOM。由于加在动画xml时系统会按照定义顺序把所有的图片都读取到内存中,易导致 OOM 的发生。

思路: 减少图片资源大小 建立异步线程逐个加载图片,并通过handler发送到主线程中,减少同时读取的情况 用属性动画 or lottie等动画代替帧动画

5.4 补充建议

  • 尽量不要在刷新时做耗时操作,必须准备数据,创建图片,图片变换等,数据和图片都应该在之前就加载到内存中,图片变换用canvas的变换来实现。
  • 同一个界面中多个动画重叠出现时,尽量将动画的刷新过程统一进行刷新,避免频繁的invalidate,尤其是多个动画有时序上的关系时更应该统一。
  • 尽量使用带有参数的invalidate来刷新,这样可以减少很多运算量。
  • 开启硬件加速,硬件加速由于采用了显示列表的概念,所以刷新过程也有很大的优化,但是会增加额外的8M内存占用。