Android 动画深入分析

动画分类
Android动画可以分3种:View动画,帧动画和属性动画;属性动画为API11的新特性,在低版本是无法直接使用属性动画的,但可以用nineoldAndroids来实现(但是本质还是view动画)。

View动画

  • View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation(平移动画)、ScaleAnimation(缩放动画)、RotateAnimation(旋转动画)和AlphaAnimation(透明度动画),他们即可以用代码来动态创建也可以用XML来定义,推荐使用可读性更好的XML来定义。
  • 标签表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且他的内部也可以嵌套其他动画集合。

android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
android:shareInterpolator表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。

  • Animation通过setAnimationListener方法可以给View动画添加过程监听。
  • 自定义View动画只需要继承Animation这个抽象类,并重写initializeapplyTransformation方法,在initialize方法中做一些初始化工作,在 applyTransformation中进行相应的矩形变换,很多时候需要采用Camera来简化矩形变换过程。

view动画应用场景

  • 在ViewGroup中控制子元素的出场效果
  • 在Activity中实现不同的Activity之间的切换效果
LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,当他的子元素出场的时候都会具有这种动画,ListView、RecyclerView上用的多,LayoutAnimation也是一个View动画。

代码实现:




//--- animationOrder 表示子元素的动画的顺序,有三种选项:
//normal(顺序显示)、reverse(逆序显示)和random(随机显示)。
#anim_item.xml


 
 

第一种,在布局中引用LayoutAnimation


第二种,代码中使用

Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listview.setLayoutAnimation(controller);
Activity的切换效果

主要用到overridePendingTransition(int enterAnim,int exitAnim) 这个方法,这个方法必须在startActivity(Intent)或者finish()之后调用才能生效

  • enterAnim——Activity被打开时,所需动画资源的id
  • exitAnim——Activity被暂停时,所需动画资源的id

当启动一个Activity时

            Intent intent = new Intent(this, TestActivity.class);
            startActivity(intent);
            overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);

当Activity退出时

    @Override
    public void finish() {
        super.finish();
        overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
    }

帧动画

从字面上理解就是一帧挨着一帧的播放图片,类似于播放电影的效果。不同于View动画,Android系统提供了一个类AnimationDrawable来实现帧动画。
代码实现




    
    
    


然后将上述的XML作为view的背景并通过drawable来播放就可以了

 iv_icon.setBackgroundResource(R.drawable.animation_test2);
        AnimationDrawable ad = (AnimationDrawable) iv_icon.getBackground();
        ad.start();

属性动画

  • 属性动画可以对任意对象的属性进行动画,不仅是View
  • 动画默认时间间隔300ms,帧率10ms/帧
  • 其达到的效果是:在一个时间间隔内按一定帧率将对象从一个属性值到另一个属性值改变

比较常用的几个动画类(Java实现方法)是:

ValueAnimatior、ObjectAnimatiorAnimatiorSet

其中ObjectAnimatior继承自ValueAnimatiorAnimatiorSet是动画集合,可以定义一组动画。

基本用法

改变一个对象的translationY属性,让其沿着Y轴向上平移一个时间,该动画在默认的时间完成,好,我们来看下怎么去用

ObjectAnimator.ofFloat(iv_icon, "translationY", -iv_icon.getHeight()).start();

改变一个对象的背景颜色值,典型的就是改变view的背景,下面的动画是让view的背景从0xffff8080到0xff8080ff,动画会无限循环和反转

ValueAnimator valueAnimator =
        ObjectAnimator.ofInt(ll_content, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
valueAnimator.setDuration(3000);
valueAnimator.setEvaluator(new ArgbEvaluator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.start();

动画集合,5s内对view的旋转,平移,缩放和透明度进行改变

AnimatorSet set = new AnimatorSet();
set.playTogether(
        ObjectAnimator.ofFloat(iv_icon, "rotationX", 0, 360),
        ObjectAnimator.ofFloat(iv_icon, "rotationY", 0, 180),
        ObjectAnimator.ofFloat(iv_icon, "rotation", 0, -90),
        ObjectAnimator.ofFloat(iv_icon, "trabslationX", 0, 90),
        ObjectAnimator.ofFloat(iv_icon, "trabslationY", 0, 90),
        ObjectAnimator.ofFloat(iv_icon, "scaleX", 0, 1.5f),
        ObjectAnimator.ofFloat(iv_icon, "scaleY", 0, 0.5f),
        ObjectAnimator.ofFloat(iv_icon, "alpha", 0, 2.5f, 1)
);
set.setDuration(3000).start();

属性动画还可以用XML来表示的,在res/animator目录下




    

    

  • android:propertyName:表示属性动画作用对象的属性的名称
  • android:duration:表示动画的时长
  • android:valueFrom:表示属性的起始值
  • android:valueTo:表示属性的结束值
  • android:startOffset:表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正的播放
  • android:repeatCount:表示动画的重复次数
  • android:repeatMode:表示动画的重复模式
  • android:valueType:表示propertyName有两个属性有int和float两个可选项,分别表示属性的类型,和浮点型,另外,如果所制定的是颜色类型,那么就不需要指定propertyName,系统会自动对颜色类型进行处理

具体代码实现:




    

    


代码中使用:

AnimatorSet sets = (AnimatorSet) AnimatorInflater.loadAnimator(MainActivity.this, R.animator.property_animator);
sets.setTarget(iv_icon);
sets.start();

在实际开发当中我还是建议使用代码来实现属性动画,这是因为用代码会比较简单,比如一个view需要从左边移动到右边,但是如果但是XML,是不知道屏幕的宽高的。

插值器和估值器

TimeInterpolator中文翻译是时间插值器的意思,他的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预设的是LinearInterpolator(线性加速器,匀速加速器),加速和减速插值器,TypeEvaluator的中文翻译是类型估值算法,也叫估值器,他的作用是根据当前属性变化的百分比来计算变化后的属性值,系统也预设了针对整型属性,浮点型,和color颜色值,属性动画中的插值器和估值器很重要,它们是实现非匀速动画的重要手段。

如图,他表示的是一个匀速动画,采用了线性插值器和整形估值算法,在40ms内,x属性的值从0-40的变换:


20170422184834369.png

由于动画的默认刷新率为10ms/帧,所有该动画将分5帧进行,我们来考虑一下第三帧,当时间为20ms的时候,百分比为0.5,意味着时间过去了一半,那x改变了多少?其实x也是0.5,为什么因为他使用了线性的插值器也就是匀速动画。

查看线性插值器的代码:

@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

很显然,线性插值器的返回值和输入值是一样的,因为都是0.5,这意味着x的改变值是0.5,这个时候插值器的工作就完成了。具体x变成了什么值是需要估值算法来确定的,查看整型估值算法的源码:

public class IntEvaluator implements TypeEvaluator {

    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

上述的算法很简单,evaluate的三个参数分别表示估算的小数,开始值和结束值,对于我们的例子而言分别是0.5 0 40 ,根据上述算法,整型估算器返回给我们的结果是20,这就是(x= 20 t = 20ms)的由来。

属性动画的监听器

属性动画监听器用于监听动画的播放过程,主要有两个接口:AnimatorUpdateListenerAnimatorListener

AnimatorListener
public static interface AnimatorListener {
    void onAnimationStart(Animator animation); //动画开始
    void onAnimationEnd(Animator animation); //动画结束
    void onAnimationCancel(Animator animation); //动画取消
    void onAnimationRepeat(Animator animation); //动画重复播放
}

系统还提供了AnimatorListenerAdapter这个类,他是AnimatorListener的适配器,这样我们就可以有选择的实现上面的4个方法。

public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animator);
}

AnimatorUpdateListener比较特殊,他会监听整个动画过程,利用这个特性,我们可以做很多的事情。

属性动画工作原理

属性动画要求动画作用的对象提供该属性的get/set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画效果多次去调用set方法,每次传递的set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值,总结一下,我们对object的属性abc做动画,如果想让动画生效,要同时满足两个条件:
1.object必须要提供set方法,如果动画的时候没有传递初始值,那么我们还要提供get方法,因为系统要去取abc的属性(如果这条不满足,程序直接Crash)

2.object的set方法对abc所做的改变必须通过某种方法反应,比如带来UI的改变(如果这条不满足,动画无效果但是不会Crash)

我们从属性动画的start()方法进入查看代码:

#ValueAnimator.java
public void start() {
        start(false);
    }

private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        mSelfPulse = !mSuppressSelfPulseRequested;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
            if (mRepeatCount == INFINITE) {
                // Calculate the fraction of the current iteration.
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        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;
       // focus1 
        addAnimationCallback(0);

        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
                // focus2
                startAnimation();
            if (mSeekFraction == -1) {
                // focus3
                setCurrentPlayTime(0);
            } else {
                //focus4
                setCurrentFraction(mSeekFraction);
            }
        }
    }

直观来看,似乎更多的细节应该在focus2处:

    private void startAnimation() {
       ....
        initAnimation();
        mRunning = true;
        if (mSeekFraction >= 0) {
            mOverallFraction = mSeekFraction;
        } else {
            mOverallFraction = 0f;
        }
        if (mListeners != null) {
            notifyStartListeners();
        }
    }

这里调用了两个方法,initAnimation()notifyStartListeners()

确实只是进行一些初始化工作而已,看看另外一个:

这里也只是通知动画开始,回调 listener 的接口而已。

回到start(),查看focus1处的代码:

#ValueAnimator.start
addAnimationCallback(0);
private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        getAnimationHandler().addAnimationFrameCallback(this, 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));
        }
    }

mAnimationCallbacks 是一个 ArrayList,每一项保存的是 AnimationFrameCallback 接口的对象,看命名这是一个回调接口,那么是谁在什么时候会对它进行回调呢?根据目前仅有的信息,我们并没有办法看出来,那么可以先放着,这里只要记住第一个参数之前传进来的是 this,也就是说如果这个接口被回调时,那么 ValueAnimator 对这个接口的实现将会被回调。

接下去开始按顺序过代码了,当 mAnimationCallbacks 列表大小等于 0 时,将会调用一个方法,很明显,如果动画是第一次执行的话,那么这个列表大小应该就是 0,因为将 callback 对象添加到列表里的操作是在这个判断之后,所以这里我们可以跟进看看:

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

        final Choreographer mChoreographer = Choreographer.getInstance();

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

        @Override
        public void postCommitCallback(Runnable runnable) {           mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
        }

        @Override
        public long getFrameTime() {
            return mChoreographer.getFrameTime();
        }

        @Override
        public long getFrameDelay() {
            return Choreographer.getFrameDelay();
        }

        @Override
        public void setFrameDelay(long delay) {
            Choreographer.setFrameDelay(delay);
        }
    }

注意focus1处的代码,我们知道Choreographer是处理界面渲染的类,查看它的相关方法:

#Choreographer.postFrameCallback
public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }


#Choreographer.postFrameCallbackDelayed
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

所以内部其实是调用了 postCallbackDelayedInternal() 方法。

Choreographer 内部有几个队列,上面方法的第一个参数 CALLBACK_ANIMATION 就是用于区分这些队列的,而每个队列里可以存放 FrameCallback 对象,也可以存放 Runnable 对象。
Animation 动画原理上就是通过 ViewRootImpl 生成一个 doTraversal() 的 Runnable 对象(其实也就是遍历 View 树的工作)存放到 Choreographer 的队列里的。而这些队列里的工作,都是用于在接收到屏幕刷新信号时取出来执行的。但有一个关键点,Choreographer 要能够接收到屏幕刷新信号的事件,是需要先调用 Choreographer 的 scheduleVsyncLocked() 方法来向底层注册监听下一个屏幕刷新信号事件的。

而如果继续跟踪 postCallbackDelayedInternal() 这个方法下去的话,你会发现,它最终就是走到了 scheduleVsyncLocked() 里去。

ValueAnimator 调用了 start() 方法之后,首先会对一些变量进行初始化工作并通知动画开始了,然后 ValueAnimator 实现了 AnimationFrameCallback 接口,并通过AnimationHander 将自身 this 作为参数传到 mAnimationCallbacks 列表里缓存起来。而 AnimationHandlermAnimationCallbacks 列表大小为 0 时会通过内部类 MyFrameCallbackProvider 将一个 mFrameCallback 工作缓存到 Choreographer的待执行队列里,并向底层注册监听下一个屏幕刷新信号事件。

当屏幕刷新信号到的时候,ChoreographerdoFrame() 会去将这些待执行队列里的工作取出来执行,那么此时也就回调了 AnimationHandlermFrameCallback 工作。

那么,接下去就继续看看,当接收到屏幕刷新信号之后,mFrameCallback 又继续做了什么:

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

其实就做了两件事,一件是去处理动画的相关工作,也就是说要找到动画真正执行的地方,跟着 doAnimationFrame() 往下走应该就行了。而剩下的代码就是处理另外一件事:继续向底层注册监听下一个屏幕刷新信号。

先讲讲第二件事,我们知道,动画是一个持续的过程,也就是说,每一帧都应该处理一个动画进度,直到动画结束。既然这样,我们就需要在动画结束之前的每一个屏幕刷新信号都能够接收到,所以在每一帧里都需要再去向底层注册监听下一个屏幕刷新信号事件。所以你会发现,上面代码里参数是 this,也就是 mFrameCallback 本身,结合一下之前的那个流程,这里可以得到的信息是:

当第一个属性动画调用了 start() 时,由于 mAnimationCallbacks 列表此时大小为 0,所以直接由 addAnimationFrameCallback() 方法内部间接的向底层注册下一个屏幕刷新信号事件,然后将该动画加入到列表里。而当接收到屏幕刷新信号时,mFrameCallback 的 doFrame() 会被回调,该方法内部做了两件事,一是去处理当前帧的动画,二则是根据列表的大小是否不为 0 来决定继续向底层注册监听下一个屏幕刷新信号事件,如此反复,直至列表大小为 0。

所以,这里可以猜测一点,如果当前动画结束了,那么就需要将其从 mAnimationCallbacks 列表中移除,这点可以后面跟源码过程中来验证。

那么,下去就是跟着doAnimationFrame() 来看看,属性动画是怎么执行的:

这里概括下其实就做了两件事:

一是去循环遍历列表,取出每一个 ValueAnimator,然后判断动画是否有设置了延迟开始,或者说动画是否到时间该执行了,如果到时间执行了,那么就会去调用ValueAnimatordoAnimationFrame();

二是调用了 cleanUpList()方法,看命名就可以猜测是去清理列表,那么应该也就是处理掉已经结束的动画,因为 AnimationHandler是为所有属性动画服务的,同一时刻也许有多个动画正在进行中,那么动画的结束肯定有先后,已经结束的动画肯定要从列表中移除,这样等所有动画都结束了,列表大小变成 0 了,mFrameCallback 才可以停止向底层注册监听下一个屏幕刷新信号事件,AnimationHandler 才可以进入空闲状态,不用再每一帧都去处理动画的工作。

清理的工作梳理完,那么接下去就是继续去跟着动画的流程了,还记得我们上面提到了另一件事是遍历列表去调用每个动画 ValueAnimatordoAnimationFrame() 来处理动画逻辑么,那么我们接下去就跟进这个方法看看:

#ValueAnimator#doAnimationFrame
final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);

        if (finished) {
            endAnimation();
        }
        return finished;

1.根据当前时间计算当前帧的动画进度,所以动画的核心应该就是在 animateBaseOnTime() 这个方法里,意义就类似 Animation 动画的 getTransformation()方法;

2.判断动画是否已经结束了,结束了就去调用 endAnimation(),按照我们之前的猜测,这个方法内应该就是将当前动画从 mAniamtionCallbacks 列表里移除。

小结

1.ValueAnimator 属性动画调用了 start() 之后,会先去进行一些初始化工作,包括变量的初始化、通知动画开始事件;

2.然后通过 AnimationHandler 将其自身 this 添加到 mAnimationCallbacks 队列里,AnimationHandller 是一个单例类,为所有的属性动画服务,列表里存放着所有正在进行或准备开始的属性动画;

3.如果当前存在要运行的动画,那么 AnimationHandler 会去通过 Choreographer 向底层注册监听下一个屏幕刷新信号,当接收到信号时,它的 mFrameCallback 会开始进行工作,工作的内容包括遍历列表来分别处理每个属性动画在当前帧的行为,处理完列表中的所有动画后,如果列表还不为 0,那么它又会通过 Choreographer 再去向底层注册监听下一个屏幕刷新信号事件,如此反复,直至所有的动画都结束。

4.AnimationHandler 遍历列表处理动画是在 doAnimationFrame() 中进行,而具体每个动画的处理逻辑则是在各自,也就是 ValueAnimator 的 doAnimationFrame() 中进行,各个动画如果处理完自身的工作后发现动画已经结束了,那么会将其在列表中的引用赋值为空,AnimationHandler 最后会去将列表中所有为 null 的都移除掉,来清理资源。

5.每个动画 ValueAnimator 在处理自身的动画行为时,首先,如果当前是动画的第一帧,那么会根据是否有"跳过片头"(setCurrentPlayTime())来记录当前动画第一帧的时间 mStartTime 应该是什么。

6.第一帧的动画其实也就是记录 mStartTime 的时间以及一些变量的初始化而已,动画进度仍然是 0,所以下一帧才是动画开始的关键,但由于属性动画的处理工作是在绘制界面之前的,那么有可能因为绘制耗时,而导致 mStartTime 记录的第一帧时间与第二帧之间隔得太久,造成丢了开头的多帧,所以如果是这种情况下,会进行 mStartTime 的修正。

7.修正的具体做法则是当绘制工作完成后,此时,再根据当前时间与 mStartTime 记录的时间做比较,然后进行修正。

8.如果是在动画过程中的某一帧才出现绘制耗时现象,那么,只能表示无能为力了,丢帧是避免不了的了,想要解决就得自己去分析下为什么绘制会耗时;而如果是在第一帧是出现绘制耗时,那么,系统还是可以帮忙补救一下,修正下 mStartTime 来达到避免丢帧。

ValueAnimation.animateBasedOnTime

接着来看 animateBasedOnTime:

 boolean animateBasedOnTime(long currentTime) {
            ...
            float currentIterationFraction = getCurrentIterationFraction(
                    mOverallFraction, mReversing);
            animateValue(currentIterationFraction);
            ...
    }

在代码的末尾调用了animateValue

void animateValue(float fraction) {
      // focus1
       fraction = mInterpolator.getInterpolation(fraction);
       mCurrentFraction = fraction;
       // focus2
       int numValues = mValues.length;
       for (int i = 0; i < numValues; ++i) {
           mValues[i].calculateValue(fraction);
       }
      // focus3
       if (mUpdateListeners != null) {
           int numListeners = mUpdateListeners.size();
           for (int i = 0; i < numListeners; ++i) {
               mUpdateListeners.get(i).onAnimationUpdate(this);
           }
       }
   }

1.根据插值器来计算当前的真正的动画进度,插值器算是动画里比较重要的一个概念了,可能平时用的少,如果我们没有明确指定使用哪个插值器,那么系统通常会有一个默认的插值器。

2.根据插值器计算得到的实际动画进度值,来映射到我们需要的数值。这么说吧,就算经过了插值器计算之后,动画进度值也只是 0-1 区间内的某个值而已。而我们通常需要的并不是 0-1 的数值,比如我们希望一个 0-500 的变化,那么我们就需要自己在拿到 0-1 区间的进度值后来进行转换。第二个步骤,大体上的工作就是帮助我们处理这个工作,我们只需要告诉 ValueAnimator 我们需要 0-500 的变化,那么它在拿到进度值后会进行转换。

3.通知动画的进度回调而已了。

注意focus2处的代码calculateValue,它的作用是计算每帧动画所对应的属性的值,下面看下具体是在哪里调用属性的get/set。

在初始化的时候,如果属性的初始值没有提供,则get方法就会调用,请看PropertyValuesHolder中的setupValue,可以发现get方法是通过反射来调用的:

    private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            Object value = convertBack(mProperty.get(target));
            kf.setValue(value);
        }
        try {
            if (mGetter == null) {
                Class targetClass = target.getClass();
                setupGetter(targetClass);
                if (mGetter == null) {
                    // Already logged the error - just return to avoid NPE
                    return;
                }
            }
            Object value = convertBack(mGetter.invoke(target));
            kf.setValue(value);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }

当动画的下一帧到来的时候,PropertyValuesHolder中的setAnimatedValue方法将新的属性值设置给对象,调用其set方法,下面是源码,通过反射调用

    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());
            }
        }
    }

使用动画的注意事项

  • 使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量避免使用帧动画。

  • 使用无限循环的属性动画时,在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄露。

  • View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏(setVisibility(View.GONE)失效),这时候调用view.clearAnimation()清理View动画即可解决。

  • 不要使用px,使用px会导致不同设备上有不同的效果。

  • View动画是对View的影像做动画,View的真实位置没有变动,也就导致点击View动画后的位置触摸事件不会响应,属性动画不存在这个问题。

  • 使用动画的过程中,使用硬件加速可以提高动画的流畅度。

参考:
属性动画 ValueAnimator 运行原理全解析

《Android开发艺术探索》

你可能感兴趣的:(Android 动画深入分析)