Android View | 属性动画原理解析

1. 属性动画的使用

属性动画有 ValueAnimator 和 ObjectAnimator,其中 ObjectAnimator 继承自 ValueAnimator,属性动画的常见使用如下:

        //ValueAnimator 的使用
        ValueAnimator moveValueAnimator = ValueAnimator.ofInt(0, 800);
        moveValueAnimator.setDuration(10000);
        //添加动画计算监听
        moveValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                //调用View.layout 方法,改变 tvMove 的位置达到动画效果
                tvObject.layout(tvMove.getLeft(), curValue,
                        tvMove.getRight(), curValue + tvMove.getHeight());
            }
        });
        moveValueAnimator.start();

        //ObjectAnimator 的使用
        ObjectAnimator.ofFloat(tvObject, "X", 200).start();

上面代码中,ValueAnimator 实现了在10s内将 tvObject 位置向下移动了800的动画效果。使用 ValueAnimatior 实现了值在10s内从0~800的动画,监听值的动画进度,每调用一次 onAnimationUpdate() 方法通过调用 tvObject.layout() 传入需要变更的位置信息对 tvObject 的位置进行变更。而 ObjectAnimator 则实现了将 tvObject 移动到横坐标为200的位置。

2. 源码解析

考虑一下,调用了 start() 方法之后属性动画是如何改变属性值的呢?属性动画对每一帧又是如何绘制的呢?那么从 ObjectAnimator 的 start() 方法为入口来研究属性动画的实现原理。

  • ObjectAnimator#start()
    @Override
    public void start() {
        AnimationHandler.getInstance().autoCancelBasedOn(this);
        if (DBG) {
            Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
            for (int i = 0; i < mValues.length; ++i) {
                PropertyValuesHolder pvh = mValues[i];
                Log.d(LOG_TAG, "   Values[" + i + "]: " +
                    pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
                    pvh.mKeyframes.getValue(1));
            }
        }
        super.start();
    }

首先调用 AnimationHandlerautoCancelBasedOn() 方法停止动画,这里可以保证当我们多次调用同一个 ObjectAnimator 的 start() 方法时停止并清除掉上一次调用 start()时正在进行的动画效果,确保动画同步。如果是 debug 模式则遍历 mValues 打印出每一各属性动画的属性名、属性的开始值结束值,最后调用了父类 ValueAnimator 的 start()方法。

  • VlaueAnimtor#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) {
            if (mRepeatCount == INFINITE) {
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        ...
        addAnimationCallback(0);

        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            startAnimation();
            if (mSeekFraction == -1) {
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }
  1. 首先判断 Looper.myLooper()是否为空,为空则抛出异常,可见属性动画效果的实现是在Looper 线程(UI线程)进行的;
  2. playBackwards表示动画是否要反向播放,如果是反向播放根据动画播放重复次数是否为 INFINITE(无限循环) 设置当前的播放进度 mSeekFraction
  3. 如果没有设置延时启动动画,调用 startAnimation() 方法,startAnimation() 方法里设置了动画的相关参数并调用了 initAnimation() 方法。
  • ValueAnimator#initAnimation()
    void initAnimation() {
        if (!mInitialized) {
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].init();
            }
            mInitialized = true;
        }
    }

initAnimation() 方法很简单,遍历 mValues 并调用 PropertyValuesHolder 的 init() 方法。注意:initAnimation() 方法会在处理动画第一帧时调用,接下来我们进入到 PropertyValuesHolder 的 init() 方法去看看具体做了什么。

  • PropertyValuesHolder#init()
    void init() {
        if (mEvaluator == null) {
            // We already handle int and float automatically, but not their Object
            // equivalents
            mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                    (mValueType == Float.class) ? sFloatEvaluator :
                    null;
        }
        if (mEvaluator != null) {
            // KeyframeSet knows how to evaluate the common types - only give it a custom
            // evaluator if one has been set on this class
            mKeyframes.setEvaluator(mEvaluator);
        }
    }

PropertyValuesHolder 的 init() 中方法中为属性设置了 TypeEvaluator,TypeEvaluator 用于计算属性在每一帧的属性值。走到这一步我们发现从调用 ObjectAnimator 的 start() 方法开始,都是在做属性动画的初始化工作,包括设置属性动画的属性类型、播放进度等。
那么我们如何去设置属性的值呢?回到 VlaueAnimator#start() 方法中,调用了 initAnimation() 方法后,接着判断了mSeekFraction 值是否为-1,其默认值为-1,如果是第一次调用则会调用 setCurrentPlayTime(),否则调用 setCurrentFraction(),源码中 setCurrentPlayTime() 同样也调用了 setCurrentFraction() 方法,setCurrentFraction() 方法主要计算了当前的属性进度最后调用了 animateValue() 方法,我们来 animateValue() 的代码:

  • VlaueAnimator#setCurrentFraction()
    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);
            }
        }
    }

好像也没做什么,只是根据当前属性变化的进度计算了一下对应的属性值,再回到 VlaueAnimator#start() 方法中,遗漏了一个方法的调用 addAnimationCallback(0);

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

getAnimationHandler()返回了 AnimationHandler 的单例并调用了 addAnimationFrameCallback() 方法。

  • AnimationHandler#addAnimationFrameCallback()
    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.size() 肯定是0,这里调用了 getProvider().postFrameCallback(mFrameCallback),getProvider()返回的是 AnimationFrameCallbackProvider ,AnimationFrameCallbackProvider是一个接口,它的实现类 MyFrameCallbackProvider 中实现了 postFrameCallback() 方法:

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

进到 Choreographer 的 postFrameCallback()看看:

  • Choreographer#postFrameCallback()
    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].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);
            }
        }
    }

基本从 addAnimationCallback() 到 postFrameCallbackDelayed() 我们就大概知道,start() 中调用 addAnimationCallback() 方法是实现屏幕刷新的,Choreographer 中的 FrameHandler(是一个Handler类,用于接收处理 callback 的消息)会调用 doFrame() 处理每一帧刷新和绘制,具体屏幕刷新机制可以参考这里 。回到 addAnimationFrameCallback() ,调用 getProvider().postFrameCallback(mFrameCallback) 时传入的 mFrameCallback 是什么呢?

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

从上面代码中可以明显看到,mFrameCallback 其实就是一个 Choreographer.FrameCallback 的接口对象,Choreographer 中接收到需要刷新屏幕的通知时就会调用该接口方法 doFrame(),mFrameCallback 实现了其中的方法 doFrame(),方法中先是调用 doAnimationFrame() 方法,接着判断 mAnimationCallbacks 是否为空,即是否还有 AnimationFrameCallback 动画帧的回调,如果有则继续调用 Choreographer的postFrameCallback() 循环上述步骤,直到所有动画帧执行完毕。这里应该很明显了 doAnimationFrame() 方法就是去处理每一帧动画的回调的,我们来看看它的实现:

  • AnimationHandler#doAnimationFrame()
    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();
    }

doAnimationFrame() 中去遍历我们的回调集合 mAnimationCallbacks,接着调用了接口 AnimationFrameCallback 的方法 doAnimationFrame() ,还调用了 commitAnimationFrame() 方法,该方法主要用于处理由于遍历耗时而导致动画开始时的第一帧未绘制的问题,最后调用 cleanUpList() 清空动画帧回调集合。从方法命名也能看出 doAnimationFrame() 是在处理每一帧的动画的,具体来看 doAnimationFrame() 的实现,该方法在哪里实现的呢?我们要去找在哪里传入了 AnimationFrameCallback 对象。

回到最开始 VlaueAnimator 的 start() 方法中,有这么一行代码 addAnimationCallback(),传入了 this,这里的 this 就是我们要找的 AnimationHandler.AnimationFrameCallback,这说明 ValueAnimtor 实现了该接口的方法 doAnimationFrame(),来看具体实现:

    public final boolean doAnimationFrame(long frameTime) {
        ...
        // Handle pause/resume
        if (mPaused) {
            ...
            removeAnimationCallback();
        } else if (mResumed) {
            ...
        }

        if (!mRunning) {
            if (mStartTime > frameTime && mSeekFraction == -1) {
                return false;
            } else {
                mRunning = true;
                startAnimation();
            }        
        }
        ..
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);
        if (finished) {
            endAnimation();
        }
        return finished;
    }

1)如果是停止状态,调用 removeAnimationCallback(),将当前 AnimationFrameCallback 从 mAnimationCallbacks 中移除,移除操作的实现是将其置空,这点可以从 AnimationHandler#removeCallback() 中 mAnimationCallbacks.set(id, null) 看出;
2)如果是第一次绘制调用 startAnimation() 开启动画;
3)如果已经完成了当前设置的属性动画,调用 endAnimation() 重置相关参数并清空所有的 AnimationFrameCallback。

整个流程下来,还有个疑问我们是怎么给属性设置值的?,具体来看 PropertyValuesHolder 的 setupValue() 方法,该方法中调用 keyFrame.setValue() 用于设置关键帧对应的属性值,这个属性值怎么来的呢,我们来看代码:

  • PropertyVlauesHolder#setupValue()
    private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            Object value = convertBack(mProperty.get(target));
            kf.setValue(value);
        } else {
            ...
                if (mGetter == null) {
                    Class targetClass = target.getClass();
                    setupGetter(targetClass);
                    if (mGetter == null) {
                        return;
                    }
                }
                Object value = convertBack(mGetter.invoke(target));
                kf.setValue(value);
            ...
        }
    }

当前属性值是通过属性的 get() 方法拿到的,而 get() 方法则是根据属性名通过反射去调用的,调用结果就是返回当前的属性值。那么 set() 方法呢?我们从 setAnimatedValue() 方法中了解到,和 get() 方法一样,都是通过反射调用的。

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

其中 getAnimatedValue() 方法返回当前计算的属性值,通过反射调用set() 方法设置属性当前的属性值。

  • 总结:

1)属性动画运行在 Looper 线程(即 UI 线程);
2)属性动画的 set() 和 get() 方法都是根据属性名反射调用的;
3)属性动画的属性必须要有 set() 和 get() 方法,set() 方法用于动画每一帧的绘制,而 get() 方法用于返回当前属性值,如果我们再设置属性动画时没有指定属性的初始值,系统就会调用 get() 方法去获取,所以 get() 方法也是必须的。
4)属性动画的实现就是不断调用属性的 set() 方法去绘制每一帧,直到达到属性的结束值,绘制停止动画也就停止了。

回答开头提到的两个问题:
  1. 属性动画是如何改变属性值的?
    通过 TypeEvaluator 计算的来的当前的属性值,通过反射调用属性的 set() 方法设置属性值。
  2. 属性动画是如何绘制每一帧的?
    AnimationHandler 用于管理动画,其中集合 mAnimationCallbacks 是一个存储 AnimationFrameCallback 的集合,AnimationHandler 接收到屏幕刷新的通知时(通过Choreographer.FrameCallback接收)会依次执行每一个AnimationFrameCallback 的 doAnimationFrame() 方法,在 doAnimationFrame() 实现了每一帧动画的开始、绘制、结束整个流程。
疑问:动画是在 UI线程运行的,动画是需要耗时的,是否和不能在 UI线程执行耗时操作相矛盾?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349