Android屬性動(dòng)畫(huà)Animator實(shí)現(xiàn)衛(wèi)星Button

動(dòng)畫(huà)效果,衛(wèi)星Button扇形展開(kāi)和關(guān)閉

1. Animator和Animation

Animator框架是android4.0之后添加的一個(gè)動(dòng)畫(huà)框架,和之前的Animation框架相比,Animator可以進(jìn)行更多和更精細(xì)化的動(dòng)畫(huà)控制,而且比之前更簡(jiǎn)單和更高效。在4.0源碼中隨處都可以看到Animator的使用。

在3.0系統(tǒng)之前,Android給我們提供了逐幀動(dòng)畫(huà)Frame Animation和補(bǔ)間動(dòng)畫(huà)Tween Animation兩種動(dòng)畫(huà):

  • 逐幀動(dòng)畫(huà)的原理很簡(jiǎn)單,就是將一個(gè)完整的動(dòng)畫(huà)拆分成一張張單獨(dú)的圖片,然后將它們連貫起來(lái)進(jìn)行播放;
  • 補(bǔ)間動(dòng)畫(huà)是專(zhuān)門(mén)為View提供的動(dòng)畫(huà),可以實(shí)現(xiàn)View的透明度、縮放、平移和旋轉(zhuǎn)四種效果。
    比如要水平位移到200坐標(biāo),是這樣實(shí)現(xiàn)的:
ImageView image = (ImageView) findViewById(R.id.imageView);
//位移錯(cuò)標(biāo)
TranslateAnimation translateAnimation = new TranslateAnimation(0, 200, 0, 0);
//動(dòng)畫(huà)完成后保持
translateAnimation.setFillAfter(true);
//動(dòng)畫(huà)持續(xù)時(shí)間
translateAnimation.setDuration(1000);
image.startAnimation(translateAnimation);

但是補(bǔ)間動(dòng)畫(huà)還是有很多缺陷的:

  • 補(bǔ)間動(dòng)畫(huà)只能對(duì)View設(shè)置動(dòng)畫(huà),對(duì)非View的對(duì)象不能設(shè)置動(dòng)畫(huà);
  • 補(bǔ)間動(dòng)畫(huà)只是改變了View的顯示效果而沒(méi)有真正的改變View的屬性。例如,我們想使用補(bǔ)間動(dòng)畫(huà)將一個(gè)按鈕從一個(gè)位置移動(dòng)到一個(gè)新的位置,那么當(dāng)移動(dòng)完成之后我們點(diǎn)擊這個(gè)按鈕,是不會(huì)觸發(fā)其點(diǎn)擊事件的,而當(dāng)我們點(diǎn)擊移動(dòng)前的位置時(shí),會(huì)觸發(fā)其點(diǎn)擊事件,即補(bǔ)間動(dòng)畫(huà)只是在另一個(gè)地方重新繪制了這個(gè)View,其他的東西都沒(méi)有改變。

為了彌補(bǔ)以上缺陷,屬性動(dòng)畫(huà)Animator閃亮登場(chǎng)!雖然現(xiàn)在很多前端特性都是通過(guò)JS來(lái)實(shí)現(xiàn),但是還是會(huì)有很多場(chǎng)景需要使用Android原始的動(dòng)畫(huà)API,這個(gè)時(shí)候?qū)傩詣?dòng)畫(huà)就可以發(fā)揮自己強(qiáng)大的作用了!

屬性動(dòng)畫(huà),顧名思義,是對(duì)對(duì)象的屬性設(shè)置的動(dòng)畫(huà)。簡(jiǎn)單的說(shuō),只要一個(gè)對(duì)象的某個(gè)屬性有set和get方法,就可以對(duì)其設(shè)置屬性動(dòng)畫(huà)。一句話概括,屬性動(dòng)畫(huà)就是不斷的改變一個(gè)對(duì)象的某個(gè)屬性。我們只需要告訴系統(tǒng)動(dòng)畫(huà)的運(yùn)行時(shí)長(zhǎng),需要執(zhí)行哪種類(lèi)型的動(dòng)畫(huà),以及動(dòng)畫(huà)的初始值和結(jié)束值,剩下的工作就可以全部交給系統(tǒng)去完成了。

2. ObjectAnimator的使用

ObjectAnimator是屬性動(dòng)畫(huà)中最常用的一個(gè)類(lèi),我們可以通過(guò)它直接控制一個(gè)對(duì)象的屬性,十分便捷。同樣是水平位移200坐標(biāo),只需要一行代碼。

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

ofFloat()方法的第一個(gè)參數(shù)是動(dòng)畫(huà)作用的對(duì)象,這里是一個(gè)ImageView;第二個(gè)參數(shù)是屬性名稱(chēng),這里指定的是X軸的平移;第三個(gè)參數(shù)是一個(gè)不定長(zhǎng)參數(shù),指定屬性的起始值和結(jié)束值;setDuration()方法指定的是動(dòng)畫(huà)執(zhí)行的時(shí)長(zhǎng),這里是1秒鐘;最后調(diào)用start()方法,動(dòng)畫(huà)就開(kāi)始執(zhí)行了。這樣鏈?zhǔn)?/a>構(gòu)造的設(shè)計(jì)模式更為清晰方便。

需要說(shuō)明的是,ofFloat()需要的參數(shù)中包括一個(gè)對(duì)象和對(duì)象的屬性名字,但這個(gè)屬性必須有g(shù)et和set函數(shù),內(nèi)部會(huì)通過(guò)Java反射機(jī)制來(lái)調(diào)用set函數(shù)修改對(duì)象屬性值。

此外還可以為動(dòng)畫(huà)設(shè)置監(jiān)聽(tīng)器,在動(dòng)畫(huà)執(zhí)行狀態(tài)變化時(shí),執(zhí)行需要的操作。

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

        @Override //動(dòng)畫(huà)結(jié)束
        public void onAnimationEnd(Animator animation) {}

        @Override //動(dòng)畫(huà)取消
        public void onAnimationCancel(Animator animation) {}

        @Override //動(dòng)畫(huà)重復(fù)
        public void onAnimationRepeat(Animator animation) {}
        });
alpha.start();

大多數(shù)情況下我們只需要監(jiān)聽(tīng)動(dòng)畫(huà)結(jié)束,這個(gè)時(shí)候可以使用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所操作的常見(jiàn)屬性如下:

  1. translationX\translationY,水平或者縱向移動(dòng);
  2. rotation、rotationX\rotationY,這里的rotation是指3D的旋轉(zhuǎn)。rotationX是水平方向的旋轉(zhuǎn),rotationY是垂直方向的旋轉(zhuǎn);
  3. scaleX\scaleY 水平、垂直方向的縮放;
  4. X\Y 具體會(huì)移動(dòng)到的某個(gè)點(diǎn);
  5. alpha 透明度。

通過(guò)組合以上屬性,就能繪制出各種酷炫的動(dòng)畫(huà)效果。

3. 動(dòng)畫(huà)集合的使用

更多場(chǎng)景中,我們需要同時(shí)控制多個(gè)動(dòng)畫(huà)來(lái)達(dá)到更加豐富的效果,當(dāng)然最直接的方法就是多生成幾個(gè)ObjectAnimator對(duì)象:

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

這樣的效果就是三個(gè)動(dòng)畫(huà)同時(shí)執(zhí)行:旋轉(zhuǎn)360度的同時(shí)向(200,200)坐標(biāo)移動(dòng)。

可能你會(huì)說(shuō)執(zhí)行對(duì)象都是一個(gè),執(zhí)行時(shí)間也都一樣,可不可以有其他寫(xiě)法。當(dāng)然可以,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一次性賦予一個(gè)對(duì)象,當(dāng)然也可以把PropertyValuesHolder保存下來(lái)在設(shè)置給其他對(duì)象。

不過(guò)以上兩個(gè)方法都是同事執(zhí)行所有動(dòng)畫(huà),如果要控制動(dòng)畫(huà)之間的順序呢?
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();

將需要的動(dòng)畫(huà)都放入一個(gè)集合中,然后管理它們的執(zhí)行順序,可以一起執(zhí)行(playTogether),也可以順序執(zhí)行(playTogether),還可以自定義順序。

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

with代表一起執(zhí)行,after代表隨后執(zhí)行。

4. 實(shí)例:衛(wèi)星Button#

利用上面提到的AnimatorSet,將多個(gè)動(dòng)畫(huà)效果組合在一起來(lái)實(shí)現(xiàn)衛(wèi)星Buttion的效果

動(dòng)畫(huà)效果,衛(wèi)星Button扇形展開(kāi)和關(guān)閉

布局文件

布局很簡(jiǎn)單,在FrameLayout布局中7個(gè)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>

動(dòng)畫(huà)實(shí)現(xiàn)

扇形展開(kāi)動(dòng)畫(huà)實(shí)際上讓多個(gè)ImageView同時(shí)在X和Y軸上移動(dòng)到特定的位置,這些位置以一個(gè)原點(diǎn)和固定的半徑成扇形。也就是說(shuō)這些點(diǎn)的位置滿足:

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

扇形關(guān)閉的動(dòng)畫(huà)也是同樣的道理,只不過(guò)把Button移動(dòng)的起點(diǎn)和終點(diǎn)顛倒即可。
具體代碼實(shí)現(xiàn)如下:

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;
        }
    }
       
        //控件關(guān)閉
    private void closeAnimator() {
        for(int i=0;i<res.length-1;i++){
            //計(jì)算每個(gè)控件展開(kāi)的弧度
            moveBack(mImageViewList.get(i+1),(float) (Math.PI/2/(res.length-2))*i);
        }
        opened = false;
    }

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

    /**
     * 扇形展開(kāi)
     * @param objView 目標(biāo)控件
     * @param angle 展開(kāi)的角度
     */
    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();
    }
       
    /**
     * 扇形關(guān)閉
     * @param objView 目標(biāo)控件
     * @param angle 關(guān)閉的角度
     */
    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();
    }
}

類(lèi)似的效果當(dāng)然也可以使用Animation來(lái)實(shí)現(xiàn),網(wǎng)上也有很多例子,但實(shí)現(xiàn)復(fù)雜,代碼量很大。反觀ObejctAnimator實(shí)現(xiàn)起來(lái)更為簡(jiǎn)潔,清楚明白。通過(guò)結(jié)合更多屬性的變化,相信讀者朋友們能制作出更多更酷炫的效果,畢竟創(chuàng)意是無(wú)窮!

參考文獻(xiàn)

  1. ndroid之a(chǎn)nimator 和animation 的區(qū)別
  2. Android - 進(jìn)階】之Animator屬性動(dòng)畫(huà)
  3. Android 屬性動(dòng)畫(huà)(Property Animation) 完全解析 (上)
  4. android animator
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容