Android中的動畫詳解--《Android開發藝術探索》閱讀筆記——第七章

Android中動畫分爲:View動畫、幀動畫(也屬於View動畫)、屬性動畫。
View動畫是對View做圖形變換(平移、縮放、旋轉、透明度)從而產生動畫效果。
幀動畫就是順序播放一系列圖片來產生動畫效果。
屬性動畫可以動態改變對象的屬性來達到動畫效果。

一、View動畫

View動畫的平移、縮放、旋轉、透明度 分別對應 Animation的的4個子類:TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。View可以用xml定義、也可以用代碼創建。推薦使用xml,可讀性好。

1.1 xml方式

如下所示,R.anim.animation_test 是xml定義的動畫。
其中標籤 translate、scale、alpha、rotate,就是對應四種動畫。set標籤是動畫集合,對應AnimationSet類,有多個動畫構成。

其中android:duration是指動畫時間,fillAfter爲true是動畫結束後保持,false會回到初始狀態。interpolator是指動畫的執行速度,默認是先加速後減速。其他標籤及屬性較簡單可自行研究驗證。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="5000"
    android:fillAfter="true"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"> 
    <!--set裏面的duration如果有值,會覆蓋子標籤的duration-->

    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="400" />
    <scale
        android:duration="2000"
        android:fromXScale="0.5"
        android:fromYScale="0.5"
        android:toXScale="1"
        android:toYScale="1" />
    <alpha
        android:duration="3000"
        android:fromAlpha="0.2"
        android:toAlpha="1" />

    <rotate
        android:fromDegrees="0"
        android:toDegrees="90" />
</set>

定義好動畫後,使用也很簡單,調用view的startAnimation方法即可。

		//view動畫使用,方式一:xml,建議使用。
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test);
        textView1.startAnimation(animation);

1.2 代碼動態創建

代碼創建舉例如下,也很簡單。

		//view動畫使用,方式二:new 動畫對象
        AnimationSet animationSet = new AnimationSet(false);
        animationSet.setDuration(3000);
        animationSet.addAnimation(new TranslateAnimation(0, 100, 0, 0));
        animationSet.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
        animationSet.setFillAfter(true);
        textView2.startAnimation(animationSet);

        //view動畫使用,方式二:new 動畫對象,使用setAnimation
        AnimationSet animationSet2 = new AnimationSet(false);
        animationSet2.setDuration(3000);
        animationSet2.addAnimation(new TranslateAnimation(0, 100, 0, 0));
        animationSet2.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
        animationSet2.setFillAfter(true);
        animationSet2.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }
            @Override
            public void onAnimationEnd(Animation animation) {
                MyToast.showMsg(AnimationTestActivity.this, "View動畫:代碼 set:View動畫結束~");
            }
            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        textView3.setAnimation(animationSet2);

注意點:

  1. startAnimation方法是立刻播放動畫;setAnimation是設置要播放的下一個動畫。
  2. setAnimationListener可以監聽動畫的開始、結束、重複。

1.3 自定義View動畫

通常我們不需要自定義View動畫,上面4種基本夠用。

自定義View動畫,需要繼承Animation,重寫initialize和applyTransformation方法。在initialize中做初始化工作,在applyTransformation中做相應的矩陣變換(需要用到Camera),需要用到數學知識。這個給出一個例子Rotate3dAnimation,沿Y軸旋轉並沿Z軸平移,到達3d效果。

/**
 * 沿Y軸旋轉並沿Z軸平移,到達3d效果
 * An animation that rotates the view on the Y axis between two specified angles.
 * This animation also adds a translation on the Z axis (depth) to improve the effect.
 */
public class Rotate3dAnimation extends Animation {
    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mCenterX;
    private final float mCenterY;
    private final float mDepthZ;
    private final boolean mReverse;
    private Camera mCamera;

    /**
     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
     * start angle and its end angle. Both angles are in degrees. The rotation
     * is performed around a center point on the 2D space, definied by a pair
     * of X and Y coordinates, called centerX and centerY. When the animation
     * starts, a translation on the Z axis (depth) is performed. The length
     * of the translation can be specified, as well as whether the translation
     * should be reversed in time.
     *
     * @param fromDegrees the start angle of the 3D rotation 沿y軸旋轉開始角度
     * @param toDegrees   the end angle of the 3D rotation 沿y軸旋轉結束角度
     * @param centerX     the X center of the 3D rotation 沿y軸旋轉的軸點x(相對於自身)
     * @param centerY     the Y center of the 3D rotation 沿y軸旋轉的軸點y(相對於自身)
     * @param depthZ      z軸的平移。如果>0,越大就越遠離,視覺上變得越小。
     * @param reverse     true if the translation should be reversed, false otherwise
     */
    public Rotate3dAnimation(float fromDegrees, float toDegrees,
                             float centerX, float centerY, float depthZ, boolean reverse) {
        mFromDegrees = fromDegrees;
        mToDegrees = toDegrees;
        mCenterX = centerX;
        mCenterY = centerY;
        mDepthZ = depthZ;
        mReverse = reverse;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mCamera = new Camera();
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float fromDegrees = mFromDegrees;
        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

        final float centerX = mCenterX;
        final float centerY = mCenterY;
        final Camera camera = mCamera;

        final Matrix matrix = t.getMatrix();

        camera.save();
        if (mReverse) {
            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
        } else {
            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
        }
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

1.4 幀動畫

幀動畫對應AnimationDrawable類,用來順序播放多張圖片。使用很簡單,先xml定義一個AnimationDrawable,然後作爲背景或資源設置給view並開始動畫即可。
舉例如下:

R.drawable.frame_animation

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/home_icon_guide00"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide01"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide02"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide03"
        android:duration="50"/>
    ......
</animation-list>
        tvFrameAnimation.setBackgroundResource(R.drawable.frame_animation);
        AnimationDrawable frameAnimationBackground = (AnimationDrawable) tvFrameAnimation.getBackground();
        frameAnimationBackground.start();

1.5 View動畫的特殊使用場景

1.5.1 給ViewGroup指定child的出場動畫

使用LayoutAnimation給ViewGroup指定child的出場動畫,方法如下:

1.先用xml定義標籤LayoutAnimation:

  • android:animation設置child的出場動畫
  • android:animationOrder設置child的出場順序,normal就是順序
  • delay是指:每個child延遲(在android:animation中指定的動畫時間)0.8倍後播放動畫。如果android:animation中的動畫時間是100ms,那麼每個child都會延遲800ms後播放動畫。 如果不設置delay,那麼所有child同時執行動畫。
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/enter_from_left_for_child_of_group"
    android:animationOrder="normal"
    android:delay="0.8">
</layoutAnimation>
R.anim.enter_from_left_for_child_of_group

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

2.把LayoutAnimation設置給ViewGroup

    <LinearLayout
        android:id="@+id/ll_layout_animation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layoutAnimation="@anim/layout_animation">
        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="呵呵呵"/>
        <TextView
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="qq"
            android:background="@color/colorPrimary"/>
        <TextView
            android:layout_width="30dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="啊啊"/>
    </LinearLayout>

除了xml,當然也可以使用LayoutAnimationController 指定:

        //代碼設置LayoutAnimation,實現ViewGroup的child的出場動畫
        Animation enterAnim = AnimationUtils.loadAnimation(this, R.anim.enter_from_left_for_child_of_group);
        LayoutAnimationController controller = new LayoutAnimationController(enterAnim);
        controller.setDelay(0.8f);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
        llLayoutAnimation.setLayoutAnimation(controller);

1.5.2 Activity的切換效果

Activity默認有切換動畫效果,我們也可以自定義:overridePendingTransition(int enterAnim, int exitAnim) 可以指定activity開大或暫停時的動畫效果。

  • enterAnim,指要打開的activity進入的動畫
  • exitAnim,要暫停的activity退出的動畫

注意 必須在startActivity或finish之後使用才能生效。如下所示:

    public static void launch(Activity activity) {
        Intent intent = new Intent(activity, AnimationTestActivity.class);
        activity.startActivity(intent);
        //打開的activity,從右側進入,暫停的activity退出到左側。
        activity.overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left);
    }
......

    @Override
    public void finish() {
        super.finish();
        //打開的activity,就是上一個activity從左側進入,要finish的activity退出到右側
        overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right);
    }

R.anim.enter_from_right

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="100%p"
        android:toXDelta="0"/>
</set>

R.anim.exit_to_left

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="0"
        android:toXDelta="-100%p"/>
</set>

R.anim.enter_from_left

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

R.anim.exit_to_right

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

二、屬性動畫

屬性動畫幾乎無所不能,只要對象有這個屬性,就可以對這個屬性做動畫。甚至還可以沒有對象。可用通過ObjectAnimator、ValueAnimator、AnimatorSet實現豐富的動畫。

2.1 使用方法

屬性動畫可對任意對象做動畫,不僅僅是View。默認動畫時間是300ms,10ms/幀。具體理解就是:可在給定的時間間隔內 實現 對象的某屬性值 從 value1 到 value2的改變。

使用很簡單,可以直接代碼實現(推薦),也可xml實現,舉例如下:

		//屬性動畫使用,方式一:代碼,建議使用。 橫移
        ObjectAnimator translationX = ObjectAnimator
                .ofFloat(textView6, "translationX", 0, 200)
                .setDuration(1000);
        translationX.setInterpolator(new LinearInterpolator());
        setAnimatorListener(translationX);

        //屬性動畫使用,方式二:xml。   豎移
        Animator animatorUpAndDown = AnimatorInflater.loadAnimator(this, R.animator.animator_test);
        animatorUpAndDown.setTarget(textView6);

        //文字顏色變化
        ObjectAnimator textColor = ObjectAnimator
                .ofInt(textView6, "textColor", 0xffff0000, 0xff00ffff)
                .setDuration(1000);
        textColor.setRepeatCount(ValueAnimator.INFINITE);
        textColor.setRepeatMode(ValueAnimator.REVERSE);
        //注意,這裏如果不設置 那麼顏色就是跳躍的,設置ArgbEvaluator 就是連續過度的顏色變化
        textColor.setEvaluator(new ArgbEvaluator());

        //animatorSet
        mAnimatorSet = new AnimatorSet();
        mAnimatorSet
                .play(animatorUpAndDown)
                .with(textColor)
                .after(translationX);

        mAnimatorSet.start();


    /**
     * 設置屬性動畫的監聽
     * @param translationX
     */
    private void setAnimatorListener(ObjectAnimator translationX) {
        translationX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //每播放一幀,都會調用
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            translationX.addPauseListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationResume(Animator animation) {
                    super.onAnimationResume(animation);
                }
            });
        }

        translationX.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
    }

R.animator.animator_test,是放在res/animator中。

<?xml version="1.0" encoding="utf-8"?>
<!--屬性動畫test,一般建議採用代碼實現,不用xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">


    <!--repeatCount:默認是0-1是無限循環-->
    <!--repeatMode:重複模式:restart-從頭來一遍、reverse-反向來一遍-->
    <!--valueType:指定propertyName的類型可選intType、floatType-->

    <!--android:pathData=""
        android:propertyXName=""
        android:propertyYName=""-->
    <objectAnimator
        android:propertyName="translationY"
        android:duration="1000"
        android:valueFrom="0"
        android:valueTo="120"
        android:startOffset="0"
        android:repeatCount="0"
        android:repeatMode="reverse"
        android:valueType="floatType"
        android:interpolator="@android:interpolator/accelerate_decelerate" />

    <!--animator對用vueAnimator,比objectAnimator少了propertyName-->
    <!--<animator-->
        <!--android:duration="2000"-->
        <!--android:valueFrom=""-->
        <!--android:valueTo=""-->
        <!--android:startOffset=""-->
        <!--android:repeatCount=""-->
        <!--android:repeatMode=""-->
        <!--android:valueType=""-->
        <!--android:interpolator=""-->
        <!--android:pathData=""-->
        <!--android:propertyXName=""-->
        <!--android:propertyYName=""/>-->

</set>

translationX是實現橫移,animatorUpAndDown是實現豎移、textColor是實現文字顏色變化。其中animatorUpAndDown是使用xml定義,標籤含義也很好理解。 最後使用AnimatorSet的play、with、after 實現 先橫移,然後 豎移和顏色變化 同時的動畫集合效果。

注意點

  1. 關於View動畫和屬性動畫的平移屬性動畫改變屬性值setTranslationX 的視圖效果像view動畫的平移一樣,都是view實際的layout位置沒變,只改變了視圖位置;不同點是屬性動畫 給觸摸點生效區域增加了位移(而view動畫僅改變了視圖位置)。
  2. 插值器:Interpolator,根據 時間流逝的百分比,計算當前屬性值改變的百分比。 例如duration是1000,start後過了200,那麼時間百分比是0.2,那麼如果差值器是LinearInterpolator線性差值器,那麼屬性值改變的百分比也是0.2
  3. 估值器:Evaluator,就是根據 差值器獲取的 屬性值百分比,計算改變後的屬性值。 ofInt、onFloat內部會自動設置IntEvaluator、FloatEvaluator。如果使用ofInt且是顏色相關的屬性,就要設置ArgbEvaluator。 上面例子中 文字顏色變化動畫 設置了ArgbEvaluator:textColor.setEvaluator(new ArgbEvaluator())。
  4. 動畫監聽:主要是兩個監聽接口,AnimatorUpdateListener、AnimatorListenerAdapter。AnimatorUpdateListener的回調方法在每幀更新時都會調用一次;AnimatorListenerAdapter可以監聽開始、結束、暫停、繼續、重複、取消,重寫你要關注的方法即可。

2.2對任意屬性做動畫

一個問題,針對下面的Button,如何實現 的寬度逐漸拉長的動畫,即文字不變,僅拉長背景寬度?

    <Button
        android:id="@+id/button_animator_test"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:text="任意屬性動畫-寬度拉長"/>

首先,View動畫的ScaleAnimation是無法實現的,因爲view的scale是把view的視圖放大,這樣文字也會拉長變形。那麼屬性動畫呢?試試~

        ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
        width1.setDuration(2000);
        width1.start();

但是發現,沒有效果!這是爲啥呢?解釋如下.

對object 的任意屬性做動畫 要求兩個條件:

  1. object有 對應屬性 的set方法,動畫中沒設置初始值 還要有get方法,系統要去取初始值(不滿足則會crash)。
  2. set方法要對object有所改變,如UI的變化。不滿足則會沒有動畫效果

上面Button沒有動畫效果,就是沒有滿足第二條。看下Button的setWidth方法:

    public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;
        requestLayout();
        invalidate();
    }

實際就是TextView的setWidth方法,看到設置進去的值僅影響了寬度最大值和最小值。按照官方註釋和實測,發現只有當Button/TextView在xml中設置android:layout_width爲"wrap_content"時,纔會setWidth改變寬度;而當Button/TextView在xml中設置android:layout_width爲固定dp值時,setWidth無效。 而我們上面給出的Button xml中確實是固定值180dp,所以是屬性"width"的setWidth是無效的,即不滿足第二條要求,就沒有動畫效果了。(當修改Button xml中設置android:layout_width爲"wrap_content"時,上面執行的屬性動畫是生效的。)

那麼,當不滿足條件時,如何解決此問題呢? 有如下處理方法:

  1. 給object添加set、get方法,如果有權限。(一般不行,如TextView是SDK裏面的不能直接改)
  2. 給Object包裝一層,在包裝類中提供set、get方法。
  3. 使用ValueAnimator,監聽Value變化過程,自己實現屬性的改變。
    private void testAnimatorAboutButtonWidth() {
        //Button width 屬性動畫:如果xml中寬度是wrap_content,那麼動畫有效。
        // 如果設置button確切的dp值,那麼無效,因爲對應屬性"width"的setWidth()方法就是 在wrap_content是纔有效。
        ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
        width1.setDuration(2000);
//        width1.start();

        //那麼,想要在button原本有確切dp值時,要能對width動畫,怎麼做呢?
        //方法一,包一層,然後用layoutParams
        ViewWrapper wrapper = new ViewWrapper(button);
        ObjectAnimator width2 = ObjectAnimator.ofInt(wrapper, "width", 1000);
        width2.setDuration(2000);
//        width2.start();

        //方法二,使用ValueAnimator,每一幀自己顯示寬度的變化
        ValueAnimator valueAnimator = ValueAnimator.ofInt(button.getLayoutParams().width, 1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int animatedValue = (Integer) animation.getAnimatedValue();
                Log.i("hfy", "onAnimationUpdate: animatedValue=" + animatedValue);

//                IntEvaluator intEvaluator = new IntEvaluator();
////                獲取屬性值改變比例、計算屬性值
//                float animatedFraction = animation.getAnimatedFraction();
//                Integer evaluate = intEvaluator.evaluate(animatedFraction, 300, 600);
//                Log.i("hfy", "onAnimationUpdate: evaluate="+evaluate);


                if (button != null) {
                    button.getLayoutParams().width = animatedValue;
                    button.requestLayout();
                }
            }
        });

        valueAnimator.setDuration(4000).start();

    }

    /**
     * 包一層,提供對應屬性的set、get方法
     */
    private class ViewWrapper {

        private final View mView;

        public ViewWrapper(View view) {
            mView = view;
        }

        public int getWidth() {
            return mView.getLayoutParams().width;
        }

        public void setWidth(int width) {
            ViewGroup.LayoutParams layoutParams = mView.getLayoutParams();
            layoutParams.width = width;
            mView.setLayoutParams(layoutParams);
            mView.requestLayout();
        }
    }

2.3 屬性動畫的原理

屬性動畫,要求對象有這個屬性的set方法,執行時會根據傳入的 屬性初始值、最終值,在每幀更新時調用set方法設置當前時刻的 屬性值。隨着時間推移,set的屬性值會接近最終值,從而達到動畫效果。如果沒傳入初始值,那麼對象還要有get方法,用於獲取初始值。

在獲取初始值、set屬性值時,都是使用 反射 的方式,進行 get、set方法的調用。
見PropertyValuesHolder的setupValue、setAnimatedValue方法:

    private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            Object value = convertBack(mProperty.get(target));
            kf.setValue(value);
        } else {
            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());
            }
        }
    }
    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());
            }
        }
    }

以上效果圖:

在這裏插入圖片描述

三、使用動畫的注意事項

  1. 使用幀動畫,避免OOM。因爲圖片多。
  2. 屬性動畫 如果有循環動畫,在頁面退出時要及時停止,避免內存泄漏。
  3. 使用View動畫後,調用setVisibility(View.GONE)失效時,使用view.clearAnimation()可解決。

附上之前記錄的一些動畫效果
自定義view:TextSwitcher使用
自定義view:信息飄窗/彈幕——AutoSwitchTextView
自定義view:ProgressBar 前景色、背景色、平滑顯示進度

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章