Android UI-屬性動畫(二)

概述

上一篇講到了屬性動畫,主要講到了用法和大概的思路。但是沒有講到具體是如何實現動畫的。這里我們分析下View動畫和屬性動畫的源碼,下一篇分析下動畫中非常重要的插值器和估值器。最后總結下如何自己寫一個動畫效果。

View動畫的源碼及實現

我們知道view.startAnimation(anim);是動畫開始的入口。這段代碼并沒有繪制的代碼,那么是如何實現動畫的呢。其實關鍵就在invalidate的時候。在這個方法中會重新繪制了畫面,隨帶著計算了動畫控件下一幀出現的位置。

    public void startAnimation(Animation animation) {
        // 在animation中設置時間為START_ON_FIRST_FRAME,這表示在刷新第一幀的時候開始動畫
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        // 重置參數,如果鎖屏的話設置開始時間為當前,不然會在亮屏后會開始動畫
        setAnimation(animation);
        invalidateParentCaches();
        // 重繪開始,刷新幀
        invalidate(true);
    }

我們來看下這個方法調用的周期圖,可以看到主要是重新調用了draw的相關方法。那么我們就找一找哪里調用了animation


image.png

最后在ondraw方法中發現了如下幾行代碼,這說明在draw方法中拿到了動畫的對象,然后去執行一些操作。主要的代碼在applyLegacyAnimation()中,點進去看下。

        // 獲得設置的Animation
        final Animation a = getAnimation();
        if (a != null) {
            // 動畫的主要方法
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        }

applyLegacyAnimation中代碼比較復雜,如果動畫沒有初始化,那么先初始化。a.getTransformation(drawingTime, invalidationTransform, 1f)調用了Animation中的代碼。其實上面還有一個疑問,就是draw方法中transformToApply = parent.getChildTransformation();中如何拿到的Transformation。從這里就可以找到答案,final Transformation t = parent.getChildTransformation();這個方法獲取了父控件的Transformation對象,如果沒有,父控件會創建一個。

 Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

下面代碼在Animaiton中,主要是計算插值后的時間進入,然后進行變換操作。首先,判斷是否是第一次,如果是第一次設置開始時間為當前時間。然后獲取duration等的一些參數。根據當前時間,開始時間和動畫時長就可以計算出當前的時間進度。很重要的一點,就是會拿到插值器Interpolator去計算插值后的進度。最后調用applyTransformation方法。

    /**
     * Gets the transformation to apply at a specified point in time. Implementations of this
     * method should always replace the specified Transformation or document they are doing
     * otherwise.
     *
     * @param currentTime Where we are in the animation. This is wall clock time.
     * @param outTransformation A transformation object that is provided by the
     *        caller and will be filled in by the animation.
     * @return True if the animation is still running
     */
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        // 在startAnimation的時候我們設置了Animation.START_ON_FIRST_FRAME,值為-1,第一次調用設置為當前時間
        if (mStartTime == -1) {
            mStartTime = currentTime;
        }
        // 獲取延時
        final long startOffset = getStartOffset();
        // 獲取執行時長
        final long duration = mDuration;
        float normalizedTime;
        // 獲取當前時間時,已經經歷的動畫時長占的百分比
        if (duration != 0) {
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }
        // 判斷是否已經完成了
        final boolean expired = normalizedTime >= 1.0f;
        // 是否超時,沒有的話設置有更多的標志
        mMore = !expired;
        // 當我們設置不保持在動畫開始之前的話,那么會設置到時間1時刻的狀態。
        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
        // 開始動畫
        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            if (!mStarted) {
                // 回調listener中的開始方法
                fireAnimationStart();
                mStarted = true;
                if (USE_CLOSEGUARD) {
                    guard.open("cancel or detach or getTransformation");
                }
            }

            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

            if (mCycleFlip) {
                normalizedTime = 1.0f - normalizedTime;
            }
            // 從插值其中拿到插值后的時間進度
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            // 執行變換操作
            applyTransformation(interpolatedTime, outTransformation);
        }
        // 動畫結束后的判斷
        if (expired) {
            if (mRepeatCount == mRepeated) {
                if (!mEnded) {
                    mEnded = true;
                    guard.close();
                    fireAnimationEnd();
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                fireAnimationRepeat();
            }
        }

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }

        return mMore;
    }

從上面的代碼可以看出,最后執行的是applyTransformation方法,我們進去看一下,發現是抽象類。由于Animation是一個抽象類,所以我們看下TranslateAnimation類里面的實現。這里邏輯就非常清晰了,這個方法中只是將outTransformation 做了一個平移的變換。
據此猜測,View動畫效果的實現主要是靠Matrix矩陣來實現的,所以這種動畫沒法改變點擊區域的情況的原因就十分明朗了。

@Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float dx = mFromXDelta;
        float dy = mFromYDelta;
        if (mFromXDelta != mToXDelta) {
            dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
        }
        if (mFromYDelta != mToYDelta) {
            dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
        }
        t.getMatrix().setTranslate(dx, dy);
    }

執行完這個方法以后,就應該返回View里面的applyLegacyAnimation方法了,返回值是是否還有未執行完的動畫。如果有的話會執行下面這段代碼。主要的是 a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,invalidationTransform);這一行代碼,主要是獲得控件的重新繪制所在的位置。

        if (more) {
            if (!a.willChangeBounds()) {
                if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                        ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                    parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
                } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                    // The child need to draw an animation, potentially offscreen, so
                    // make sure we do not cancel invalidate requests
                    parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    parent.invalidate(mLeft, mTop, mRight, mBottom);
                }
            } else {
                if (parent.mInvalidateRegion == null) {
                    parent.mInvalidateRegion = new RectF();
                }
                final RectF region = parent.mInvalidateRegion;
                a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                        invalidationTransform);

                // The child need to draw an animation, potentially offscreen, so
                // make sure we do not cancel invalidate requests
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                final int left = mLeft + (int) region.left;
                final int top = mTop + (int) region.top;
                parent.invalidate(left, top, left + (int) (region.width() + .5f),
                        top + (int) (region.height() + .5f));
            }
        }
        return more;

進入getInvalidateRegion方法,主要是去根據Transformation 中的參數,去重新拿到繪制區域。保存Transformation。

 public void getInvalidateRegion(int left, int top, int right, int bottom,
            RectF invalidate, Transformation transformation) {

        final RectF tempRegion = mRegion;
        final RectF previousRegion = mPreviousRegion;

        invalidate.set(left, top, right, bottom);
        transformation.getMatrix().mapRect(invalidate);
        // Enlarge the invalidate region to account for rounding errors
        invalidate.inset(-1.0f, -1.0f);
        tempRegion.set(invalidate);
        invalidate.union(previousRegion);

        previousRegion.set(tempRegion);

        final Transformation tempTransformation = mTransformation;
        final Transformation previousTransformation = mPreviousTransformation;

        tempTransformation.set(transformation);
        transformation.set(previousTransformation);
        previousTransformation.set(tempTransformation);
    }

然后就會退出到draw方法中,接下去執行中,會拿到一個Transformation類型的transformToApply的對象,這個對象保存著我們最終要執行的動畫。然后ondraw方法中會調用如下代碼,可以看到,最后通過canvas對象執行了一系列的動畫操作。

        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;

                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }

                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }

                if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }

到這里View動畫的過程大致分析完了。最主要的過程是通過每次invalidate來獲取時間,然后通過Animation計算出動畫的執行到了哪里,相應的設置Transformation中Matrix的變換參數。然后在Animation中也保存一些繪制區域和Transformation等參數。最后回到draw函數中,看是否有變換且需要去執行(transformToApply ),有的話執行平移、縮放和透明度操作。

屬性動畫

前面已經介紹了View動畫,但是這和屬性動畫的實現方式非常的不同。接下去我們就看下屬性動畫的源碼及實現。
我們從ObjectAnimator的主入口進去,這段代碼還是比較長的,但是邏輯比較簡單,首先看是否有執行中的,等待的和延遲的動畫,若有則全部取消。然后調用父類的start方法。

@Override
    public void start() {
        // See if any of the current active/pending animators need to be canceled
        AnimationHandler handler = sAnimationHandler.get();
        if (handler != null) {
            int numAnims = handler.mAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mPendingAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mDelayedAnims.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
        }
        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();
    }

進入父類的start方法,首先這個方法需要一個looper,如果沒有looper則動畫無法執行。然后是一些參數的初始化。最后會調用animationHandler.start();

private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        mPlayingBackwards = playBackwards;
        if (playBackwards && mSeekFraction != -1) {
            if (mSeekFraction == 0 && mCurrentIteration == 0) {
                // special case: reversing from seek-to-0 should act as if not seeked at all
                mSeekFraction = 0;
            } else if (mRepeatCount == INFINITE) {
                mSeekFraction = 1 - (mSeekFraction % 1);
            } else {
                mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction);
            }
            mCurrentIteration = (int) mSeekFraction;
            mSeekFraction = mSeekFraction % 1;
        }
        if (mCurrentIteration > 0 && mRepeatMode == REVERSE &&
                (mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
            // if we were seeked to some other iteration in a reversing animator,
            // figure out the correct direction to start playing based on the iteration
            if (playBackwards) {
                mPlayingBackwards = (mCurrentIteration % 2) == 0;
            } else {
                mPlayingBackwards = (mCurrentIteration % 2) != 0;
            }
        }
        int prevPlayingState = mPlayingState;
        mPlayingState = STOPPED;
        mStarted = true;
        mStartedDelay = false;
        mPaused = false;
        updateScaledDuration(); // in case the scale factor has changed since creation time
        AnimationHandler animationHandler = getOrCreateAnimationHandler();
        animationHandler.mPendingAnimations.add(this);
        if (mStartDelay == 0) {
            // This sets the initial value of the animation, prior to actually starting it running
            if (prevPlayingState != SEEKED) {
                setCurrentPlayTime(0);
            }
            mPlayingState = STOPPED;
            mRunning = true;
            notifyStartListeners();
        }
        animationHandler.start();
    }

AnimationHandler animationHandler = getOrCreateAnimationHandler();這里有一個Handler對象,我們先來看下這個Handler是什么,在這個內部靜態類的上方官方給了一段解釋

  • This custom, static handler handles the timing pulse that is shared by
  • all active animations. This approach ensures that the setting of animation
  • values will happen on the UI thread and that all animations will share
  • the same times for calculating their values, which makes synchronizing
  • animations possible.
  • The handler uses the Choreographer for executing periodic callbacks.

我想這段話的意思很明白,就是給了一個統一的心跳一樣的handler。
start方法最后會定時調用Runnable方法,我們發現最終執行的是doAnimationFrame方法。

final Runnable mAnimate = new Runnable() {
            @Override
            public void run() {
                mAnimationScheduled = false;
                doAnimationFrame(mChoreographer.getFrameTime());
            }
        };

省略了一些內容,最后發現是調用的doAnimationFrame

        void doAnimationFrame(long frameTime) {
            ...
            // Now process all active animations. The return value from animationFrame()
            // tells the handler whether it should now be ended
            int numAnims = mAnimations.size();
            for (int i = 0; i < numAnims; ++i) {
                mTmpAnimations.add(mAnimations.get(i));
            }
            for (int i = 0; i < numAnims; ++i) {
                ValueAnimator anim = mTmpAnimations.get(i);
                if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
                    mEndingAnims.add(anim);
                }
            }
           ...

調用了commitAnimationFrame,這個方法最后調用了animationFrame。可以發現,正是animationFrame這個方法中計算了時間的fraction。這個值對任何動畫來說都是相同的,代表著固定的時間流逝。然后調用了animateValue。

 boolean animationFrame(long currentTime) {
        boolean done = false;
        switch (mPlayingState) {
        case RUNNING:
        case SEEKED:
            float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
            if (mDuration == 0 && mRepeatCount != INFINITE) {
                // Skip to the end
                mCurrentIteration = mRepeatCount;
                if (!mReversing) {
                    mPlayingBackwards = false;
                }
            }
            if (fraction >= 1f) {
                if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
                    // Time to repeat
                    if (mListeners != null) {
                        int numListeners = mListeners.size();
                        for (int i = 0; i < numListeners; ++i) {
                            mListeners.get(i).onAnimationRepeat(this);
                        }
                    }
                    if (mRepeatMode == REVERSE) {
                        mPlayingBackwards = !mPlayingBackwards;
                    }
                    mCurrentIteration += (int) fraction;
                    fraction = fraction % 1f;
                    mStartTime += mDuration;
                    // Note: We do not need to update the value of mStartTimeCommitted here
                    // since we just added a duration offset.
                } else {
                    done = true;
                    fraction = Math.min(fraction, 1.0f);
                }
            }
            if (mPlayingBackwards) {
                fraction = 1f - fraction;
            }
            animateValue(fraction);
            break;
        }

        return done;
    }

這個方法比較短,但是非常重要,正是這里通過mInterpolator計算出插值。

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

然后調用了 mUpdateListeners.get(i).onAnimationUpdate(this);通過mValues[i].calculateValue(fraction);調用holder對象的方法,通過onAnimationUpdate來告訴監聽者已經刷新了。

ObjectAnimator重寫了animateValue這個方法

  @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;
        }

        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].setAnimatedValue(target);
        }
    }

調用PropertyValuesHolder的setAnimatedValue方法,最終是調用 mSetter.invoke(target, mTmpValueArray);

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

總結

這篇介紹了View動畫和屬性動畫的源碼,下篇具體介紹下如何寫一個自定義的動畫。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容