1. Animator和Animation

Animator框架是android4.0之後添加的一個動畫框架,和之前的Animation框架相比,Animator可以進行更多和更精細化的動畫控制,而且比之前更簡單和更高效。在4.0源碼中隨處都可以看到Animator的使用。

在3.0系統之前,Android給我們提供了逐幀動畫Frame Animation和補間動畫Tween Animation兩種動畫:

  • 逐幀動畫的原理很簡單,就是將一個完整的動畫拆分成一張張單獨的圖片,然後將它們連貫起來進行播放;
  • 補間動畫是專門為View提供的動畫,可以實現View的透明度、縮放、平移和旋轉四種效果。
    比如要水平位移到200坐標,是這樣實現的:
    ImageView image = (ImageView) findViewById(R.id.imageView);
    //位移錯標
    TranslateAnimation translateAnimation = new TranslateAnimation(0, 200, 0, 0);
    //動畫完成后保持
    translateAnimation.setFillAfter(true);
    //動畫持續時間
    translateAnimation.setDuration(1000);
    image.startAnimation(translateAnimation);

但是補間動畫還是有很多缺陷的:

  • 補間動畫只能對View設置動畫,對非View的對象不能設置動畫;
  • 補間動畫只是改變了View的显示效果而沒有真正的改變View的屬性。例如,我們想使用補間動畫將一個按鈕從一個位置移動到一個新的位置,那麼當移動完成之後我們點擊這個按鈕,是不會觸發其點擊事件的,而當我們點擊移動前的位置時,會觸發其點擊事件,即補間動畫只是在另一個地方重新繪製了這個View,其他的東西都沒有改變。

為了彌補以上缺陷,屬性動畫Animator閃亮登場!雖然現在很多前端特性都是通過JS來實現,但是還是會有很多場景需要使用Android原始的動畫API,這個時候屬性動畫就可以發揮自己強大的作用了!

屬性動畫,顧名思義,是對對象的屬性設置的動畫。簡單的說,只要一個對象的某個屬性有set和get方法,就可以對其設置屬性動畫。一句話概括,屬性動畫就是不斷的改變一個對象的某個屬性。我們只需要告訴系統動畫的運行時長,需要執行哪種類型的動畫,以及動畫的初始值和結束值,剩下的工作就可以全部交給系統去完成了。

2. ObjectAnimator的使用

ObjectAnimator是屬性動畫中最常用的一個類,我們可以通過它直接控制一個對象的屬性,十分便捷。同樣是水平位移200坐標,只需要一行代碼。

ObjectAnimator.ofFloat(image,"TranslationX",0f,200f).setDuration(1000).start();

ofFloat()方法的第一個參數是動畫作用的對象,這裡是一個ImageView;第二個參數是屬性名稱,這裏指定的是X軸的平移;第三個參數是一個不定長參數,指定屬性的起始值和結束值;setDuration()方法指定的是動畫執行的時長,這裡是1秒鐘;最後調用start()方法,動畫就開始執行了。這樣鏈式構造的設計模式更為清晰方便。

需要說明的是,ofFloat()需要的參數中包括一個對象和對象的屬性名字,但這個屬性必須有get和set函數,內部會通過Java反射機制來調用set函數修改對象屬性值。

此外還可以為動畫設置監聽器,在動畫執行狀態變化時,執行需要的操作。

ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).setDuration(1000);
alpha.addListener(new Animator.AnimatorListener() {
        @Override //動畫開始
        public void onAnimationStart(Animator animation) {}

        @Override //動畫結束
        public void onAnimationEnd(Animator animation) {}

        @Override //動畫取消
        public void onAnimationCancel(Animator animation) {}

        @Override //動畫重複
        public void onAnimationRepeat(Animator animation) {}
        });
alpha.start();

大多數情況下我們只需要監聽動畫結束,這個時候可以使用AnimatorListenerAdapter

alpha.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            Toast.makeText(view.getContext(),"get Animator",Toast.LENGTH_SHORT).show();
    }
});

ObjectAnimator所操作的常見屬性如下:

  1. translationX\translationY,水平或者縱向移動;
  2. rotation、rotationX\rotationY,這裏的rotation是指3D的旋轉。rotationX是水平方向的旋轉,rotationY是垂直方向的旋轉;
  3. scaleX\scaleY 水平、垂直方向的縮放;
  4. X\Y 具體會移動到的某個點;
  5. alpha 透明度。

通過組合以上屬性,就能繪製出各種酷炫的動畫效果。

3. 動畫集合的使用

更多場景中,我們需要同時控制多個動畫來達到更加豐富的效果,當然最直接的方法就是多生成幾個ObjectAnimator對象:

ObjectAnimator.ofFloat(image,"rotation",0f,360f).setDuration(1000).start();
ObjectAnimator.ofFloat(image,"TranslationX",0f,200f).setDuration(1000).start();
ObjectAnimator.ofFloat(image,"TranslationY",0f,200f).setDuration(1000).start();

這樣的效果就是三個動畫同時執行:旋轉360度的同時向(200,200)坐標移動。

可能你會說執行對象都是一個,執行時間也都一樣,可不可以有其他寫法。當然可以,PropertyValuesHolder就是一種方法:

PropertyValuesHolder p1 = PropertyValuesHolder.ofFloat("rotation",0f,360f);
PropertyValuesHolder p2 = PropertyValuesHolder.ofFloat("TranslationX",0f,300f);
PropertyValuesHolder p3 = PropertyValuesHolder.ofFloat("TranslationY",0f,300f);
ObjectAnimator.ofPropertyValuesHolder(image,p1,p2,p3).setDuration(1000).start();

PropertyValuesHolder記錄下屬性變化的情況,然後再把一系列PropertyValuesHolder一次性賦予一個對象,當然也可以把PropertyValuesHolder保存下來在設置給其他對象。

不過以上兩個方法都是同事執行所有動畫,如果要控制動畫之間的順序呢?
AnimatorSet將是首選!

ObjectAnimator rotation = ObjectAnimator.ofFloat(image, "rotation", 0f, 360f);
ObjectAnimator translationX = ObjectAnimator.ofFloat(image, "TranslationX", 0f, 200f);
ObjectAnimator translationY = ObjectAnimator.ofFloat(image, "TranslationY", 0f, 200f);
AnimatorSet animatorSet = new AnimatorSet();
//animatorSet.playSequentially(rotation,translationX,translationY);
animatorSet.playTogether(rotation,translationX,translationY);
animatorSet.setDuration(1000).start();

將需要的動畫都放入一個集合中,然後管理它們的執行順序,可以一起執行(playTogether),也可以順序執行(playTogether),還可以自定義順序。

animatorSet.play(translationX).with(translationY).after(rotation);

with代表一起執行,after代表隨後執行。

4. 實例:衛星Button

利用上面提到的AnimatorSet,將多個動畫效果組合在一起來實現衛星Buttion的效果


動畫效果,衛星Button扇形展開和關閉

布局文件

布局很簡單,在FrameLayout布局中7個ImageView相互重疊

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:layout_width="match_parent"
             android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView_h"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        app:srcCompat="@drawable/h"/>
    <ImageView
        android:id="@+id/imageView_g"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="5dp"
        app:srcCompat="@drawable/g"/>
    <ImageView
        android:id="@+id/imageView_f"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="6dp"
        app:srcCompat="@drawable/f"/>
    <ImageView
        android:id="@+id/imageView_e"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="6dp"
        app:srcCompat="@drawable/e"/>
    <ImageView
        android:id="@+id/imageView_d"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="6dp"
        app:srcCompat="@drawable/d"/>
    <ImageView
        android:id="@+id/imageView_c"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="6dp"
        app:srcCompat="@drawable/c"/>
    <ImageView
        android:id="@+id/imageView_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:paddingTop="6dp"
        app:srcCompat="@drawable/b"/>
    <ImageView
        android:id="@+id/imageView_a"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/a"/>
</FrameLayout>

動畫實現

扇形展開動畫實際上讓多個ImageView同時在X和Y軸上移動到特定的位置,這些位置以一個原點和固定的半徑成扇形。也就是說這些點的位置滿足:

x = r * cos(a)
y = r * sin(a)
a是扇形的弧度。假設一共有n個衛星Button,再加上開頭和結尾處的空位,一個要把90度平方分為n-2份,第i個控件的弧度就是(i*PI)/(2*(n-2))

扇形關閉的動畫也是同樣的道理,只不過把Button移動的起點和終點顛倒即可。
具體代碼實現如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    // an array of imageView ids.
    int[] res = {R.id.imageView_a,R.id.imageView_b,R.id.imageView_c,R.id.imageView_d,R.id.imageView_e,
            R.id.imageView_f,R.id.imageView_g};

    // a list of ImageViews
    private List<ImageView> mImageViewList = new ArrayList<>();

    // a flag to control opening action or closing action
    private boolean opened = false;
        // 軌跡半徑
    int r = 150;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        initView();
    }
        //控件初始化
    private void initView() {
        for(int i=0;i<res.length;i++){
            ImageView imageView = (ImageView) findViewById(res[i]);
            mImageViewList.add(imageView);
            imageView.setOnClickListener(this);
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.imageView_a:
                if(!opened) {
                    openAnimator();
                } else {
                    closeAnimator();
                }
                break;
            default:
                Toast.makeText(v.getContext(),"Id: "+v.getId(),Toast.LENGTH_SHORT).show();
                break;
        }
    }

        //控件關閉
    private void closeAnimator() {
        for(int i=0;i<res.length-1;i++){
            //計算每個控件展開的弧度
            moveBack(mImageViewList.get(i+1),(float) (Math.PI/2/(res.length-2))*i);
        }
        opened = false;
    }

        // 控件打開
    private void openAnimator() {
        for(int i=0;i<res.length-1;i++){
            //計算每個控件展開的弧度
            moveTo(mImageViewList.get(i+1),(float) (Math.PI/2/(res.length-2))*i);
        }
        opened = true;
    }

    /**
     * 扇形展開
     * @param objView 目標控件
     * @param angle 展開的角度
     */
    void moveTo(View objView, float angle){
        ObjectAnimator translationX = ObjectAnimator.ofFloat(objView, "translationX", 0f, (float) Math.cos(angle) * r);
        ObjectAnimator translationY = ObjectAnimator.ofFloat(objView, "translationY", 0f, (float) Math.sin(angle) * r);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(translationX,translationY);
        animatorSet.setDuration(500).start();
    }

    /**
     * 扇形關閉
     * @param objView 目標控件
     * @param angle 關閉的角度
     */
    void moveBack(View objView, float angle){
        ObjectAnimator translationX = ObjectAnimator.ofFloat(objView, "translationX", (float) Math.cos(angle) * r,0f);
        ObjectAnimator translationY = ObjectAnimator.ofFloat(objView, "translationY", (float) Math.sin(angle) * r,0f);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(translationX,translationY);
        animatorSet.setDuration(500).start();
    }
}

類似的效果當然也可以使用Animation來實現,網上也有很多例子,但實現複雜,代碼量很大。反觀ObejctAnimator實現起來更為簡潔,清楚明白。通過結合更多屬性的變化,相信讀者朋友們能製作出更多更酷炫的效果,畢竟創意是無窮!

參考文獻

  1. ndroid之animator 和animation 的區別
  2. Android – 進階】之Animator屬性動畫
  3. Android 屬性動畫(Property Animation) 完全解析 (上)
  4. android animator