Android動畫雜七雜八

lvzishen發表於2019-04-18

目錄介紹

  • 1.動畫的分類
  • 2.補間動畫為何不能真正改變View的位置?而屬性動畫為何可以?屬性動畫是如何改變View的屬性?
  • 3.屬性動畫插值器和估值器的作用?插值器和估值器分別是如何更改動畫的?
  • 4.為什麼屬性動畫最後會改變View的點選事件位置而View動畫不會?
  • 5.屬性動畫記憶體洩漏的原因
  • 6.具體用法

1.動畫的分類

  1. 屬性動畫: 對該類物件進行動畫操作,真正改變了物件的屬性。(ObjectAnimator,ValueAnimator)
  2. 幀動畫:由一幀一幀的圖片構建起來的動畫效果,幀動畫需要注意圖片過大會發生OOM。
  3. 補間動畫(View動畫) :對View進行平移、縮放、旋轉和透明度變化的動畫,不能真正的改變view的位置,限制比較大且種類只有四種 AlphaAnimation(透明度動畫)、RotateAnimation(旋轉動畫)、ScaleAnimation(縮放動畫)、TranslateAnimation(平移動畫)且只能作用於View上。 (應用如Activity切換動畫)

2.補間動畫為何不能真正改變View的位置?而屬性動畫為何可以?屬性動畫是如何改變View的屬性?

  • View動畫改變的只是View的畫布,而沒有改變View的點選響應區域;而屬性動畫會通過反射技術來獲取和執行屬性的get、set方法,從而改變了物件位置的屬性值
  • Animation產生的動畫資料實際並不是應用在View本身的,而是應用在RenderNode或者Canvas上的(通過畫布的移動實現動畫),這就是為什麼Animation不會改變View的屬性的根本所在。我們可以理解為Animation只是操作的View畫布而並不是改變View的位置(mLeft,mRight,mTop,mBottom)。

在View的draw()方法中:

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();
} else {
    if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
        // No longer animating: clear out old animation matrix
        mRenderNode.setAnimationMatrix(null);
        mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
    }
    if (!drawingWithRenderNode
            && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
        final Transformation t = parent.getChildTransformation();
        final boolean hasTransform = parent.getChildStaticTransformation(this, t);
        if (hasTransform) {
            final int transformType = t.getTransformationType();
            transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
            concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
        }
    }
}
......
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;
    }
  ......
}

複製程式碼

在View的draw方法中我們可以看到當我們設定了動畫之後會生成transformToApply物件,當transformToApply不為null的時候會進行根據動畫的引數矩陣進行View的重新繪製。重點看到Animation產生的動畫資料實際並不是應用在View本身的,而是應用在RenderNode或者Canvas上的,這就是為什麼Animation不會改變View的屬性的根本所在。另一方面,我們知道Animation僅在View被繪製的時候才能發揮自己的價值,這也是為什麼補間動畫被放在Android.view包內。

3.屬性動畫插值器和估值器的作用?插值器和估值器分別是如何更改動畫的?

  • 插值器(Interpolator):根據時間流逝的百分比計算出當前屬性值改變的百分比。確定了動畫效果變化的模式,如勻速變化、加速變化等等。View動畫和屬性動畫均可使用。(可理解為改變動畫的速度曲線,預設使用的是先加速再減速的插值器

  • 常用的系統內建插值器:

    • 線性插值器(LinearInterpolator):勻速動畫
    • 加速減速插值器(AccelerateDecelerateInterpolator):動畫兩頭慢中間快
    • 減速插值器(DecelerateInterpolator):動畫越來越慢

自定義插值器實現Interpolator介面。

public class LinearInterpolator implements Interpolator {
    public LinearInterpolator() {
    }
    public LinearInterpolator(Context context,AttributeSet attrs) {
    }
    public float getInterpolation(float input) {
        return input;
    }
}
複製程式碼
  • 型別估值器(TypeEvaluator):根據當前屬性改變的百分比計算出改變後的屬性值(計算在百分之多少的時候返回什麼值)。針對於屬性動畫,View動畫不需要型別估值器。常用的系統內建的估值器:

    • 整形估值器(IntEvaluator)
    • 浮點型估值器(FloatEvaluator)
    • Color屬性估值器(ArgbEvaluator)

自定義估值器實現TypeEvaluator介面。

public class IntEvaluator implements TypeEvaluator<Integer> {
    public Integer evaluate(float fraction,Integer startValue,Integer endValue) {
        int startInt = startValue;      start:1,end:3 那麼在0.2的時候則為:   1+(3-1)*0.2
        return (int)(startInt + fraction * (endValue -startInt));
    }
}
複製程式碼

4.為什麼屬性動畫最後會改變View的點選事件位置而View動畫不會?

以下為例:

mView.setOnTouchListener(new View.OnTouchListener() {
    int lastX, lastY;
    Toast toast = Toast.makeText(TestActivity.this, "", Toast.LENGTH_SHORT);
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //Toolbar和狀態列的高度
            int toolbarHeight = (getWindow().getDecorView().getHeight() - findViewById(R.id.root_view).getHeight());
            int widthOffset = mView.getWidth() / 2;
            int heightOffset = mView.getHeight() / 2;
            mView.setTranslationX(x - mView.getLeft() - widthOffset);
            mView.setTranslationY(y - mView.getTop() - heightOffset - toolbarHeight);
            toast.setText(String.format("left: %d, top: %d, right: %d, bottom: %d",
                    mView.getLeft(), mView.getTop(), mView.getRight(), mView.getBottom()));
            toast.show();
        }
        lastX = x;
        lastY = y;
        return true;
    }
});
複製程式碼

Android動畫雜七雜八

當我們呼叫了setTranslationX或setRotation等方法後其實改變的並不是View的真正位置,只是對View畫布的改變。而改變View真正座標只能使用view.layout()方法,那麼為什麼屬性動畫可以通過setTranslationX等方法改變View的點選事件區域呢?

因為在事件分發中當我們去判斷View是否在手指點選區域內的時候會去判斷View是否呼叫了setTranslation,setRotation,setScale這些方法,如果呼叫的話會呼叫matrix.mapPoints這個方法將View的初始座標值和經過動畫改變的座標值進行一個融合計算從而得到最終的View座標值,以此值去判斷View是否再點選區域內,而補間動畫並沒有將矩陣設定給View,那麼最終做座標融合的時候自然不會以融合改變後的座標去判斷View是否在手指點選區域內,所以View動畫不會改變點選區域而屬性動畫可以。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
......
    Transformation transformToApply = null;
    final Animation a = getAnimation();
    if (a != null) {
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        transformToApply = parent.getChildTransformation();
    }
    if (transformToApply != null) {
        if (drawingWithRenderNode) {   //屬性動畫會設定矩陣
            renderNode.setAnimationMatrix(transformToApply.getMatrix());
        } else {
            canvas.translate(-transX, -transY); //View動畫不會設定到矩陣中
            canvas.concat(transformToApply.getMatrix());
            canvas.translate(transX, transY);
        }
    }
}
複製程式碼

當我們設定的動畫播放補間動畫的時候,我們所看到的變化,都只是臨時的。而屬性動畫呢,它所改變的東西,卻會更新到這個View所對應的矩陣中,所以當ViewGroup分派事件的時候,會正確的將當前觸控座標,轉換成矩陣變化後的座標,這就是為什麼播放補間動畫不會改變觸控區域的原因了。

5.屬性動畫記憶體洩漏的原因

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

private void addAnimationCallback(long delay) {
    if (!mSelfPulse) {
        return;
    }
    getAnimationHandler().addAnimationFrameCallback(this, delay);this就是ValueAnimator自身
}
複製程式碼

如上當我們新建ValueAnimator的時候會建立AnimationHandler這個靜態類,同時它會持有ValueAnimator,當進入Activity介面後如果有一些和控制元件繫結在一起的屬性動畫在執行同時設定成了無限迴圈模式,退出的時候要記得cancel掉這些動畫否則會造成記憶體洩漏。

引用鏈關係:Activity->View->ValueAnimator->AnimationHandler(靜態類,GCROOT)

public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }
複製程式碼

6.具體用法

參考:github.com/lvzishen/LV…

裡邊包括各種型別的動畫使用,關鍵幀等的使用例項。 下邊給一個屬性動畫的使用例項

ValueAnimator mFirstPhaseAnimator;

if (mFirstPhaseAnimator == null) {
    mFirstPhaseAnimator = ValueAnimator.ofInt(0, 100);
    mFirstPhaseAnimator.setDuration(2000);
    mFirstPhaseAnimator.setInterpolator(new DecelerateInterpolator());
    mFirstPhaseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            if (!mIsAnimationSetFinished) {
                mCurrentVal = (int) valueAnimator.getAnimatedValue(); 獲取當前的值
                if (mProcessor != null) {
                    String text = mProcessor.getText(getContext(), mCurrentVal);
                    if (TextUtils.isEmpty(text)) {
                        setValText(text);
                    }
                } else {
                    setValText(String.valueOf(mCurrentVal));
                }
            }
        }
    });
    mFirstPhaseAnimator.start();//開啟動畫
}
複製程式碼

相關文章