Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了

前言

我們都知道 Android 自帶了 Roate Scale Translate Alpha 多種框架動畫,我們可以通過她們實現豐富的動畫效果,但是這些寬家動畫卻有一個致命的弱點,它們只是改變了 View 顯示的大小,而沒有改變 View 的響應區域。這時以 ObjectAnimator、ValueAnimator 為代表的屬性動畫也就應運而生了。


簡單效果

在這里插入圖片描述


工作原理

屬性動畫字如其名,是通過改變 View 的屬性值來改變控件的形態,說白了就是通過反射技術來獲取控件的一些屬性如寬度、高度等的 get 和 set 方法,從而實現所謂的動畫效果。所以,這就需要我們的 View (如自定義 View 中)具有 set 和 get 方法,如果沒有則會導致程序的 Clash 。
具體步驟

  1. 首先,系統通過 get 方法獲得屬性值
  2. 系統在時間插值器的作用下,更變屬性值
  3. 系統調用 set 方法,將屬性值重新賦予控件

由此也可以看出:屬性動畫直接改變了控件的屬性,所以動畫結束后控件也就發生了永久性的變化。


使用 ObjectAnimator 實現四種動畫

這里我打算通過使用 ObjectAnimator 實現四大動畫框架:

  1. alpha
  2. scaleX/scaleY
  3. translateX/translateY
  4. rotation

給大家講解下 ObjectAnimator 使用

    private void iniAnimation(){
        // 透明度動畫
        ObjectAnimator.ofFloat(mAlphaImage, "alpha", 1, 0, 1)
                .setDuration(4000)
                .start();
        
        // 縮放
        final AnimatorSet animatorSet = new AnimatorSet();
        mScaleImage.setPivotX(mScaleImage.getWidth()+250);
        mScaleImage.setPivotY(mScaleImage.getHeight()+250);
        animatorSet.playTogether(
                ObjectAnimator.ofFloat(mScaleImage, "scaleX", 1, 0)
                        .setDuration(2000),
                ObjectAnimator.ofFloat(mScaleImage, "scaleY", 1, 0)
                        .setDuration(2000)
        );
        animatorSet.start();
        
        // 平移 translation
        final AnimatorSet translationAnimatorSet = new AnimatorSet();
        translationAnimatorSet.playTogether(
                ObjectAnimator.ofFloat(mTranslationImage, "translationX", 20, 100)
                        .setDuration(2000),
                ObjectAnimator.ofFloat(mTranslationImage, "translationY", 20,100)
                        .setDuration(2000)
        );
        translationAnimatorSet.start();
        
        // 利用 ObjectAnimator 實現旋轉動畫
        final AnimatorSet rotateAnimationSet = new AnimatorSet();
        rotateAnimationSet.playTogether(
                ObjectAnimator.ofFloat(mRotationImage, "rotation",0, 360)
                        .setDuration(2000)
        );
        rotateAnimationSet.start();
    }

以上代碼就通過了 ObjectAnimator 實現了,四大效果,實現過程基本可以歸納為

  1. 創建 AnimatorSet 對象
  2. 設置,變化發生的軸心(部分需要)
  3. 設置所需要發生改變的動畫(通常在 playTogether() 方法中)
  4. 開啟動畫

最后的運行效果如開頭動畫所示
同樣的,我們可以在一個 playTogether 方法中添加多個動畫,這樣就能實現多動畫組合的效果。這里就不在贅述了,大家可以自己試試看(我 GIF 圖中,右下角的動畫,就是旋轉 + 透明度)


使用 ValueAnimator 實現屬性動畫

ValueAnimator 是 ObjectAnimator 的父類,他兩之間的區別是,ObjectAnimator 在ValueAnimator 的基礎上,通過反射技術實現了動畫功能,也就像我剛剛所舉的例子,子要給了 ObjectAnimator 兩個值(from,to),在確定動畫類型(“scale,translate”),他就能自動生成動畫。
與之形成區別,雖然我們同樣需要給 ValueAnimator 傳遞起始和最終兩個值,但是 ValueAnimator 并不會自動去執行什么,而是會通過 addUpdateListener 的監聽方法,在時間插值器的作用下,有序的返回一連串數值,然后我們就可以通過這些數值,對控件進行設置。
在這里插入圖片描述

實例

最近看各大廠商,似乎都迷上了對 FloatingActionButton 進行操作,我就也來趁波熱點。
在這里插入圖片描述


實現方法

這個效果既可以通過動畫框架實現,也可通過屬性動畫實現,這里我給大家講下實現的方法。
首先是思路
由于這里我們是采用 ValueAnimator 實現的,所以更具 ValueAnimator 的特性,在我們對其設定完時間插值器之后,它會規律的返回一系列數。所以我們只要更具這一系列數對控件的屬性進行設置即可。

    private FloatingActionButton fab;
    private ImageView imageView;
    private int buttonSize = 0, imageSize = 0;
    private float startY = 0;
    private float endY   = 0;
    
    private ValueAnimator createValueAnimate(final View view, int start, int end){
        ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ViewGroup.LayoutParams params = view.getLayoutParams();
                params.height = (int) animation.getAnimatedValue();
                params.width  = (int) animation.getAnimatedValue();
                view.setLayoutParams(params);
            }
        });
        return valueAnimator;
    }

可以看到我們傳入三個參數,這里我做的是縮放動畫,所以給的分別是控件,控件當前大小和控件目標大小。然后根據監聽器返回的值進行設置即可。
調用方面
我這里實現的是上拉隱藏和下拉顯示,所以我們需要判斷下 Y軸 滑動方向:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                startY = event.getY();
                if ((startY - endY) < 0){
                    // 縮小
                    animationDown(fab, buttonSize);
                    animationDown(imageView, imageSize);
                }else if ((startY - endY) > 0){
                    // 放大
                    animationUp(fab, buttonSize);
                    animationUp(imageView, imageSize);
                }
                break;

            case MotionEvent.ACTION_DOWN:
                endY   = event.getY();
                break;
        }
        return super.onTouchEvent(event);
    }

    private void animationDown(final View view, int originalSize){
        ValueAnimator animator = createValueAnimate(view, originalSize, 0);
        animator.addListener(new AnimatorListenerAdapter(){
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                view.setVisibility(View.GONE);
            }
        });
        animator.setInterpolator(new BounceInterpolator());
        animator.setDuration(500).start();
    }

    private void animationUp(final View view, int originalSize){
        view.setVisibility(View.VISIBLE);
        ValueAnimator animator = createValueAnimate(view, 0, originalSize);
        animator.setInterpolator(new BounceInterpolator());
        animator.setDuration(500).start();
    }

這里我們會發現,由于是屬性動畫,所以改變的直接就是控件的大小,這就導致了一個問題,如果是實時的獲取控件大小。那么我們在執行完多小動畫,也就是 animationDown 后,就無法在獲得控件原始大小了。
所以這里我們在 onResume 方法中獲取控件大小:

    @Override
    protected void onResume() {
        super.onResume();
        fab.post(new Runnable() {
            @Override
            public void run() {
                buttonSize = fab.getHeight();
            }
        });
        imageView.post(new Runnable() {
            @Override
            public void run() {
                imageSize = imageView.getHeight();
            }
        });
    }

實戰演練

屬性動畫可以作為 ViewGroup 增加活減少控件是的動畫,是的界面的變換不是那么的突兀,其實細心的同學可能有發現,android 是自帶切換效果的,但是形式比較單一,所以這里我通過自定義 ObjectAnimator 的方法。

最后效果

在這里插入圖片描述
實現過程其實非常簡單:

  1. 首先 實例化出來一個 LayoutTransition 對象
  2. 接著 通過 ObjectAnimator.ofPropertyValuesHolder() 實例化出來一個用于載入動畫的
  3. ObjectAnimator 對象
  4. 然后 在 ObjectAnimator.ofPropertyValuesHolder() 中設置一系列的動畫效果
  5. 用 setAnimation 方法將該 ObjectAnimator 對象設置為 transition 的動畫
  6. 為 ObjectAnimator 對象設置 Duration 執行時間
  7. 設置動畫延時 setStartDelay

用同樣的方法設置 remove 動畫

        LayoutTransition transition = new LayoutTransition();

        ObjectAnimator appendAnimator = ObjectAnimator.ofPropertyValuesHolder(
                (ImageView) null,
                PropertyValuesHolder.ofFloat("scaleX", 0.0f, 1.0f),
                PropertyValuesHolder.ofFloat("scaleY", 0.0f, 1.0f),
                PropertyValuesHolder.ofFloat("alpha" , 0.0f, 1.0f)
        );
        appendAnimator.setInterpolator(new BounceInterpolator());
        transition.setAnimator(LayoutTransition.APPEARING, appendAnimator);
        transition.setDuration(LayoutTransition.APPEARING, transition.getDuration(LayoutTransition.APPEARING));
        transition.setStartDelay(LayoutTransition.APPEARING, transition.getStartDelay(LayoutTransition.APPEARING));

        ObjectAnimator removeAnimator = ObjectAnimator.ofPropertyValuesHolder(
                (ImageView) null,
                PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.0f),
                PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.0f),
                PropertyValuesHolder.ofFloat("alpha", 1.0f, 0.0f)
        );
        removeAnimator.setInterpolator(new BounceInterpolator());
        transition.setAnimator(LayoutTransition.DISAPPEARING, removeAnimator);
        transition.setDuration(LayoutTransition.DISAPPEARING, transition.getDuration(LayoutTransition.DISAPPEARING));
        transition.setStartDelay(LayoutTransition.DISAPPEARING, transition.getStartDelay(LayoutTransition.DISAPPEARING));

最后通過 setLayoutTransition 將這個 LayoutTransition 對象付給你的 ViewGroup 即可

        layout = findViewById(R.id.layout);
        layout.setLayoutTransition(transition);

測試環節

測試是分為添加控件和移除控件,功能在活動中動態的執行:
添加方法

  1. 首先創建好一個控件,這里我拿的是 (ImageView 舉例)
  2. 將其 大小、內容等屬性設置完成
  3. 調用 LinearLayout 的 addView 方法添加控件到布局的指定位置

移除方法

  1. 首先判斷該線下布局中是否有控件
  2. 再有的情況下定點移除控件
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_add_image:
                ImageView imageView = new ImageView(ExtendActivity.this);
                imageView.setScaleType(ImageView.ScaleType.FIT_XY);
                imageView.setImageResource(R.drawable.heart);
                ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(200, 200);
                imageView.setLayoutParams(params);
                layout.addView(imageView,0);
                break;

            case R.id.btn_remove_image:
                int count = layout.getChildCount();
                if (count > 0){
                    layout.removeViewAt(0);
                }
                break;
        }
    }

項目 Demo 點擊前往https://github.com/FishInWater-1999/android_view_user_defined_first
到此為止所有屬性動畫的使用基本介紹完畢
由于是個人學習的總結,如果有問題或是我個人疏漏,希望大家在評論區給我留言
祝大家編程愉快,少碼 bug ,哈哈哈

posted @ 2019-06-19 10:32 水中魚之1999 閱讀(...) 評論(...) 編輯 收藏
内部期期公开一波中特