Android中彈簧動畫的那些事 - SpringAnimation

SpringAnimation彈簧動畫

Android最近更新了Support Library包,在25.3中新增了一個動畫效果,名為SpringAnimation(彈簧動畫)。

使用步驟一:

需要25的編譯環境,同時要求最低版本為16及以上,所以使用上還是有一些限制。如果有特殊需要,可以通過tools:overrideLibrary來做對應的兼容適配。

build.xml:

minSdkVersion 16
compileSdkVersion 25

compile 'com.android.support:support-dynamic-animation:25.3.0'

導入之后就能看到對應的動畫類SpringAnimation

android.support.animation.SpringAnimation

使用步驟二:

SpringAnimation提供了兩個構造方法:

public SpringAnimation(View v, ViewProperty property)
public SpringAnimation(View v, ViewProperty property, float finalPosition)

分別是操作對應的View,對應的變化屬性及最終的位置。

ViewProperty從現有支持的看,包括(Z軸的支持需要API >= 21):

TRANSLATION_X
TRANSLATION_Y
TRANSLATION_Z
SCALE_X
SCALE_Y
ROTATION
ROTATION_X
ROTATION_Y
X
Y
Z
ALPHA
SCROLL_X
SCROLL_Y

然后在SpringAnimation中有一個SpringForce對象,負責對應的變量設置及位置計算。其中包括兩個個關鍵變量

  1. Stiffness剛度(勁度/彈性),剛度越大,形變產生的里也就越大,體現在效果上就是運動越快
  2. DampingRatio阻尼系數,系數越大,動畫停止的越快。從理論上講分為三種情況 Overdamped過阻尼(ζ > 1)、Critically damped臨界阻尼(ζ = 1)、Underdamped欠阻尼狀態(0<ζ <1)。

不過估計看到這,大家肯定還是一臉懵逼。所以,這里先稍微解釋一下彈簧跟阻尼的概念:

  1. 從原理上看,當彈簧處于平衡位置(不受力的自然位置)時,如果受到擠壓或者拉伸后,當放手后彈簧會恢復原狀。
  2. 彈回來的過程是「彈性勢能」轉化成「動能」,當重新恢復到平衡點時,因為還有速度,所以會繼續向前運動,這個時候「動能」又對應的轉化為「彈性勢能」。
  3. 如果能量轉換沒有損失,將不斷的伸長縮短,也就是不斷的來來回回。
  4. 但能量轉換是有損失的,所以縮回來的距離會不斷縮小,彈出去的距離也一樣,這個每次損耗縮小的距離比,其實就是阻尼。

這樣解釋后,大家對于關鍵的參數應該有了一定的了解。那接下來我們就繼續實戰。

使用步驟三:

嘗試模仿Dribbble上的一個作品,設計效果圖如下:

分析整個動畫效果,主體上分為兩個部分:

  1. 各元素從下往上移動的彈簧效果;SpringAnimation可以實現對應的效果。
  2. 移動過程中的透明度變化;ValueAnimation用來實現透明度Alpha從0到1的變化,從而實現淡入的感覺。

然后簡單的說明下SpringAnimation的使用方法:

SpringAnimation signUpBtnAnimY = new SpringAnimation(mSignUpBtn,SpringAnimation.TRANSLATION_Y,0);
        signUpBtnAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
signUpBtnAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
signUpBtnAnimY.setStartVelocity(10000);
signUpBtnAnimY.start();

之前介紹SpringAnimation的時候聊過了幾個影響實際效果的因素,這里再根據具體使用來說明下:
(1) DampingRatio阻尼系數,通過getSpring().setDampingRatio方法來設置。
從效果上看,ζ = 0的時候就是無限來回運動,0< ζ <1的時候會出現來回減弱的振蕩最后停止,ζ >= 1的時候會在靠近原位置的時候提前減速后停止。
在SpringForce中有對應的常量設置:

/**
* Damping ratio for a very bouncy spring. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
*/
public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;

/**
* Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
* force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
* the more bouncy the spring.
*/
public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;

/**
* Damping ratio for a spring with low bounciness. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
*/
public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;

/**
* Damping ratio for a spring with no bounciness. This damping ratio will create a critically
* damped spring that returns to equilibrium within the shortest amount of time without
* oscillating.
*/
public static final float DAMPING_RATIO_NO_BOUNCY = 1f;

(2) Stiffness剛度,通過getSpring().setStiffness方法來設置。
在SpringForce中有對應的常量設置:

/**
* Stiffness constant for extremely stiff spring.
*/
public static final float STIFFNESS_HIGH = 10_000f;

/**
* Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
*/
public static final float STIFFNESS_MEDIUM = 1500f;
    
/**
* Stiffness constant for a spring with low stiffness.
*/
public static final float STIFFNESS_LOW = 200f;

/**
* Stiffness constant for a spring with very low stiffness.
*/
public static final float STIFFNESS_VERY_LOW = 50f;

(3) StartVelocity開始速度,單位是px/second. 正數是彈簧收縮的方向,負數則相反。

根據效果要求設置完對應的參數后,調用start方法后即可執行對應的動畫。
需要注意的是SpringAnimation動畫是無法設置執行時間的,所以如果有同期需要執行的動畫,可以評估對應的執行時間或者通過updateListener來做對應的處理。

簡略的實現

public class MainActivity extends AppCompatActivity {

    private Button mSignUpBtn;

    private ImageView mLeftLogoImg;

    private ImageView mRightLogoImg;

    private TextView mDescTitleTextView;

    private LinearLayout mLettersLayout;

    private LinearLayout mSignInLayout;

    private ArrayList<SpringAnimation> mLetterAnims;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // hide the status ui.
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        setContentView(R.layout.activity_main);

        // get the screen height.
        DisplayMetrics dm = getResources().getDisplayMetrics();
        int screenHeight = dm.heightPixels;

        // letters about 'Converse'
        mLettersLayout = (LinearLayout) findViewById(R.id.letter_layout);

        mLetterAnims = new ArrayList<>();
        for (int i = 0; i < mLettersLayout.getChildCount(); i++) {
            View letterView = mLettersLayout.getChildAt(i);
            letterView.setTranslationY(screenHeight);
            SpringAnimation letterAnimY = new SpringAnimation(letterView, SpringAnimation.TRANSLATION_Y, 0);
            letterAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
            letterAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
            mLetterAnims.add(letterAnimY);
        }
        
        // text about 'Native messaging'
        mDescTitleTextView = (TextView) findViewById(R.id.desc_title_textview);
        mDescTitleTextView.setTranslationY(500f);
        mDescTitleTextView.setAlpha(0f);
        final SpringAnimation descTitleAnimY = new SpringAnimation(mDescTitleTextView, DynamicAnimation.TRANSLATION_Y, 0);
        descTitleAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
        descTitleAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
        
        final ValueAnimator descTitleAlphaAnim = ObjectAnimator.ofFloat(0f, 1f);
        descTitleAlphaAnim.setDuration(300);
        descTitleAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mDescTitleTextView.setAlpha((Float) valueAnimator.getAnimatedValue());
            }
        });
        
        // the button of sign up
        mSignUpBtn = (Button) findViewById(R.id.sign_up_btn);
        mSignUpBtn.setTranslationY(500f);
        final SpringAnimation signUpBtnAnimY = new SpringAnimation(mSignUpBtn, SpringAnimation.TRANSLATION_Y, 0);
        signUpBtnAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
        signUpBtnAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);

        // the bottom text about 'Have an account? sign in'
        mSignInLayout = (LinearLayout) findViewById(R.id.signin_layout);
        mSignInLayout.setTranslationY(500f);
        final SpringAnimation signInLayoutAnimY = new SpringAnimation(mSignInLayout, SpringAnimation.TRANSLATION_Y, 0);
        signInLayoutAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
        signInLayoutAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);


        // top logo by left
        mLeftLogoImg = (ImageView) findViewById(R.id.left_logo_imageview);
        mLeftLogoImg.setTranslationY(400f);
        mLeftLogoImg.setAlpha(0f);
        final SpringAnimation leftLogoAnimY = new SpringAnimation(mLeftLogoImg, SpringAnimation.TRANSLATION_Y, 0);
        leftLogoAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
        leftLogoAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
        leftLogoAnimY.setStartVelocity(-2000);

        // top logo by right
        mRightLogoImg = (ImageView) findViewById(R.id.right_logo_imageview);
        mRightLogoImg.setTranslationY(400f);
        mRightLogoImg.setAlpha(0f);
        final SpringAnimation rightLogoAnimY = new SpringAnimation(mRightLogoImg, SpringAnimation.TRANSLATION_Y, 0);
        rightLogoAnimY.getSpring().setStiffness(SpringForce.STIFFNESS_VERY_LOW);
        rightLogoAnimY.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
        rightLogoAnimY.setStartVelocity(-2000);
        
        final ValueAnimator logoAlphaAnim = ObjectAnimator.ofFloat(0f, 1f);
        logoAlphaAnim.setDuration(600);
        logoAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mLeftLogoImg.setAlpha((Float) valueAnimator.getAnimatedValue());
                mRightLogoImg.setAlpha((Float) valueAnimator.getAnimatedValue());
            }
        });

        mRightLogoImg.postDelayed(new Runnable() {
            @Override
            public void run() {
                leftLogoAnimY.start();
                mRightLogoImg.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        rightLogoAnimY.start();
                    }
                }, 150);

                mDescTitleTextView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        descTitleAlphaAnim.setStartDelay(100);
                        descTitleAlphaAnim.start();

                        descTitleAnimY.start();
                        signUpBtnAnimY.start();
                        signInLayoutAnimY.start();
                    }
                }, 300);

                for (final SpringAnimation letterAnim : mLetterAnims) {
                    mLettersLayout.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            letterAnim.start();
                        }
                    }, 12 * mLetterAnims.indexOf(letterAnim));
                }

                logoAlphaAnim.start();
            }
        }, 1000);
    }
}

最終實現的效果如下圖,實現的有點粗糙,希望能給大家帶來解決需求上的新思路。


原文鏈接: Android中彈簧動畫的那些事 - SpringAnimation

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容