轉載:http://blog.csdn.net/guolin_blog/article/details/43536355
在手機上去實現一些動畫效果算是件比較炫酷的事情,因此Android系統在一開始的時候就給我們提供了兩種實現動畫效果的方式,逐幀動畫(frame-by-frame animation)和補間動畫(tweened animation)。逐幀動畫的工作原理很簡單,其實就是將一個完整的動畫拆分成一張張單獨的圖片,然后再將它們連貫起來進行播放,類似於動畫片的工作原理。補間動畫則是可以對View進行一系列的動畫操作,包括淡入淡出、縮放、平移、旋轉四種。
然而自Android 3.0版本開始,系統給我們提供了一種全新的動畫模式,屬性動畫(property animation),它的功能非常強大,彌補了之前補間動畫的一些缺陷,幾乎是可以完全替代掉補間動畫了。對於逐幀動畫和補間動畫的用法,我不想再多講,它們的技術已經比較老了,而且網上資料也非常多,那么今天我們這篇文章的主題就是對Android屬性動畫進行一次完全解析。
為什么要引入屬性動畫?
Android之前的補間動畫機制其實還算是比較健全的,在android.view.animation包下面有好多的類可以供我們操作,來完成一系列的動畫效果,比如說對View進行移動、縮放、旋轉和淡入淡出,並且我們還可以借助AnimationSet來將這些動畫效果組合起來使用,除此之外還可以通過配置Interpolator來控制動畫的播放速度等等等等。那么這里大家可能要產生疑問了,既然之前的動畫機制已經這么健全了,為什么還要引入屬性動畫呢?
其實上面所謂的健全都是相對的,如果你的需求中只需要對View進行移動、縮放、旋轉和淡入淡出操作,那么補間動畫確實已經足夠健全了。但是很顯然,這些功能是不足以覆蓋所有的場景的,一旦我們的需求超出了移動、縮放、旋轉和淡入淡出這四種對View的操作,那么補間動畫就不能再幫我們忙了,也就是說它在功能和可擴展方面都有相當大的局限性,那么下面我們就來看看補間動畫所不能勝任的場景。
注意上面我在介紹補間動畫的時候都有使用“對View進行操作”這樣的描述,沒錯,補間動畫是只能夠作用在View上的。也就是說,我們可以對一個Button、TextView、甚至是LinearLayout、或者其它任何繼承自View的組件進行動畫操作,但是如果我們想要對一個非View的對象進行動畫操作,抱歉,補間動畫就幫不上忙了。可能有的朋友會感到不能理解,我怎么會需要對一個非View的對象進行動畫操作呢?這里我舉一個簡單的例子,比如說我們有一個自定義的View,在這個View當中有一個Point對象用於管理坐標,然后在onDraw()方法當中就是根據這個Point對象的坐標值來進行繪制的。也就是說,如果我們可以對Point對象進行動畫操作,那么整個自定義View的動畫效果就有了。顯然,補間動畫是不具備這個功能的,這是它的第一個缺陷。
然后補間動畫還有一個缺陷,就是它只能夠實現移動、縮放、旋轉和淡入淡出這四種動畫操作,那如果我們希望可以對View的背景色進行動態地改變呢?很遺憾,我們只能靠自己去實現了。說白了,之前的補間動畫機制就是使用硬編碼的方式來完成的,功能限定死就是這些,基本上沒有任何擴展性可言。
最后,補間動畫還有一個致命的缺陷,就是它只是改變了View的顯示效果而已,而不會真正去改變View的屬性。什么意思呢?比如說,現在屏幕的左上角有一個按鈕,然后我們通過補間動畫將它移動到了屏幕的右下角,現在你可以去嘗試點擊一下這個按鈕,點擊事件是絕對不會觸發的,因為實際上這個按鈕還是停留在屏幕的左上角,只不過補間動畫將這個按鈕繪制到了屏幕的右下角而已。
也正是因為這些原因,Android開發團隊決定在3.0版本當中引入屬性動畫這個功能,那么屬性動畫是不是就把上述的問題全部解決掉了?下面我們就來一起看一看。
新引入的屬性動畫機制已經不再是針對於View來設計的了,也不限定於只能實現移動、縮放、旋轉和淡入淡出這幾種動畫操作,同時也不再只是一種視覺上的動畫效果了。它實際上是一種不斷地對值進行操作的機制,並將值賦值到指定對象的指定屬性上,可以是任意對象的任意屬性。所以我們仍然可以將一個View進行移動或者縮放,但同時也可以對自定義View中的Point對象進行動畫操作了。我們只需要告訴系統動畫的運行時長,需要執行哪種類型的動畫,以及動畫的初始值和結束值,剩下的工作就可以全部交給系統去完成了。
既然屬性動畫的實現機制是通過對目標對象進行賦值並修改其屬性來實現的,那么之前所說的按鈕顯示的問題也就不復存在了,如果我們通過屬性動畫來移動一個按鈕,那么這個按鈕就是真正的移動了,而不再是僅僅在另外一個位置繪制了而已。
好了,介紹了這么多,相信大家已經對屬性動畫有了一個最基本的認識了,下面我們就來開始學習一下屬性動畫的用法。
ValueAnimator
ValueAnimator是整個屬性動畫機制當中最核心的一個類,前面我們已經提到了,屬性動畫的運行機制是通過不斷地對值進行操作來實現的,而初始值和結束值之間的動畫過渡就是由ValueAnimator這個類來負責計算的。它的內部使用一種時間循環的機制來計算值與值之間的動畫過渡,我們只需要將初始值和結束值提供給ValueAnimator,並且告訴它動畫所需運行的時長,那么ValueAnimator就會自動幫我們完成從初始值平滑地過渡到結束值這樣的效果。除此之外,ValueAnimator還負責管理動畫的播放次數、播放模式、以及對動畫設置監聽器等,確實是一個非常重要的類。
但是ValueAnimator的用法卻一點都不復雜,我們先從最簡單的功能看起吧,比如說想要將一個值從0平滑過渡到1,時長300毫秒,就可以這樣寫:
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); anim.setDuration(300); anim.start();
怎么樣?很簡單吧,調用ValueAnimator的ofFloat()方法就可以構建出一個ValueAnimator的實例,ofFloat()方法當中允許傳入多個float類型的參數,這里傳入0和1就表示將值從0平滑過渡到1,然后調用ValueAnimator的setDuration()方法來設置動畫運行的時長,最后調用start()方法啟動動畫。
用法就是這么簡單,現在如果你運行一下上面的代碼,動畫就會執行了。可是這只是一個將值從0過渡到1的動畫,又看不到任何界面效果,我們怎樣才能知道這個動畫是不是已經真正運行了呢?這就需要借助監聽器來實現了,如下所示:
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); anim.setDuration(300); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float currentValue = (float) animation.getAnimatedValue(); Log.d("TAG", "cuurent value is " + currentValue); } }); anim.start();
可以看到,這里我們通過addUpdateListener()方法來添加一個動畫的監聽器,在動畫執行的過程中會不斷地進行回調,我們只需要在回調方法當中將當前的值取出並打印出來,就可以知道動畫有沒有真正運行了。運行上述代碼,控制台打印如下所示:
從打印日志的值我們就可以看出,ValueAnimator確實已經在正常工作了,值在300毫秒的時間內從0平滑過渡到了1,而這個計算工作就是由ValueAnimator幫助我們完成的。另外ofFloat()方法當中是可以傳入任意多個參數的,因此我們還可以構建出更加復雜的動畫邏輯,比如說將一個值在5秒內從0過渡到5,再過渡到3,再過渡到10,就可以這樣寫:
ValueAnimator anim = ValueAnimator.ofFloat(0f, 5f, 3f, 10f); anim.setDuration(5000); anim.start();
當然也許你並不需要小數位數的動畫過渡,可能你只是希望將一個整數值從0平滑地過渡到100,那么也很簡單,只需要調用ValueAnimator的ofInt()方法就可以了,如下所示:
ValueAnimator anim = ValueAnimator.ofInt(0, 100);
ValueAnimator當中最常用的應該就是ofFloat()和ofInt()這兩個方法了,另外還有一個ofObject()方法,我會在下篇文章進行講解。
那么除此之外,我們還可以調用setStartDelay()方法來設置動畫延遲播放的時間,調用setRepeatCount()和setRepeatMode()方法來設置動畫循環播放的次數以及循環播放的模式,循環模式包括RESTART和REVERSE兩種,分別表示重新播放和倒序播放的意思。這些方法都很簡單,我就不再進行詳細講解了。
ObjectAnimator
相比於ValueAnimator,ObjectAnimator可能才是我們最常接觸到的類,因為ValueAnimator只不過是對值進行了一個平滑的動畫過渡,但我們實際使用到這種功能的場景好像並不多。而ObjectAnimator則就不同了,它是可以直接對任意對象的任意屬性進行動畫操作的,比如說View的alpha屬性。
不過雖說ObjectAnimator會更加常用一些,但是它其實是繼承自ValueAnimator的,底層的動畫實現機制也是基於ValueAnimator來完成的,因此ValueAnimator仍然是整個屬性動畫當中最核心的一個類。那么既然是繼承關系,說明ValueAnimator中可以使用的方法在ObjectAnimator中也是可以正常使用的,它們的用法也非常類似,這里如果我們想要將一個TextView在5秒中內從常規變換成全透明,再從全透明變換成常規,就可以這樣寫:
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f); animator.setDuration(5000); animator.start();
可以看到,我們還是調用了ofFloat()方法來去創建一個ObjectAnimator的實例,只不過ofFloat()方法當中接收的參數有點變化了。這里第一個參數要求傳入一個object對象,我們想要對哪個對象進行動畫操作就傳入什么,這里我傳入了一個textview。第二個參數是想要對該對象的哪個屬性進行動畫操作,由於我們想要改變TextView的不透明度,因此這里傳入"alpha"。后面的參數就是不固定長度了,想要完成什么樣的動畫就傳入什么值,這里傳入的值就表示將TextView從常規變換成全透明,再從全透明變換成常規。之后調用setDuration()方法來設置動畫的時長,然后調用start()方法啟動動畫,效果如下圖所示:
學會了這一個用法之后,其它的用法我們就可以舉一反三了,那比如說我們想要將TextView進行一次360度的旋轉,就可以這樣寫:
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f); animator.setDuration(5000); animator.start();
可以看到,這里我們將第二個參數改成了"rotation",然后將動畫的初始值和結束值分別設置成0和360,現在運行一下代碼,效果如下圖所示:
那么如果想要將TextView先向左移出屏幕,然后再移動回來,就可以這樣寫:
float curTranslationX = textview.getTranslationX(); ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "translationX", curTranslationX, -500f, curTranslationX); animator.setDuration(5000); animator.start();
這里我們先是調用了TextView的getTranslationX()方法來獲取到當前TextView的translationX的位置,然后ofFloat()方法的第二個參數傳入"translationX",緊接着后面三個參數用於告訴系統TextView應該怎么移動,現在運行一下代碼,效果如下圖所示:
然后我們還可以TextView進行縮放操作,比如說將TextView在垂直方向上放大3倍再還原,就可以這樣寫:
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "scaleY", 1f, 3f, 1f); animator.setDuration(5000); animator.start();
這里將ofFloat()方法的第二個參數改成了"scaleY",表示在垂直方向上進行縮放,現在重新運行一下程序,效果如下圖所示:
到目前為止,ObjectAnimator的用法還算是相當簡單吧,但是我相信肯定會有不少朋友現在心里都有同樣一個疑問,就是ofFloat()方法的第二個參數到底可以傳哪些值呢?目前我們使用過了alpha、rotation、translationX和scaleY這幾個值,分別可以完成淡入淡出、旋轉、水平移動、垂直縮放這幾種動畫,那么還有哪些值是可以使用的呢?其實這個問題的答案非常玄乎,就是我們可以傳入任意的值到ofFloat()方法的第二個參數當中。任意的值?相信這很出乎大家的意料吧,但事實就是如此。因為ObjectAnimator在設計的時候就沒有針對於View來進行設計,而是針對於任意對象的,它所負責的工作就是不斷地向某個對象中的某個屬性進行賦值,然后對象根據屬性值的改變再來決定如何展現出來。
那么比如說我們調用下面這樣一段代碼:
ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f);
其實這段代碼的意思就是ObjectAnimator會幫我們不斷地改變textview對象中alpha屬性的值,從1f變化到0f。然后textview對象需要根據alpha屬性值的改變來不斷刷新界面的顯示,從而讓用戶可以看出淡入淡出的動畫效果。
那么textview對象中是不是有alpha屬性這個值呢?沒有,不僅textview沒有這個屬性,連它所有的父類也是沒有這個屬性的!這就奇怪了,textview當中並沒有alpha這個屬性,ObjectAnimator是如何進行操作的呢?其實ObjectAnimator內部的工作機制並不是直接對我們傳入的屬性名進行操作的,而是會去尋找這個屬性名對應的get和set方法,因此alpha屬性所對應的get和set方法應該就是:
public void setAlpha(float value); public float getAlpha();
那么textview對象中是否有這兩個方法呢?確實有,並且這兩個方法是由View對象提供的,也就是說不僅TextView可以使用這個屬性來進行淡入淡出動畫操作,任何繼承自View的對象都可以的。
既然alpha是這個樣子,相信大家一定已經明白了,前面我們所用的所有屬性都是這個工作原理,那么View當中一定也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()這些方法,不信的話你可以到View當中去找一下。
組合動畫
獨立的動畫能夠實現的視覺效果畢竟是相當有限的,因此將多個動畫組合到一起播放就顯得尤為重要。幸運的是,Android團隊在設計屬性動畫的時候也充分考慮到了組合動畫的功能,因此提供了一套非常豐富的API來讓我們將多個動畫組合到一起。
實現組合動畫功能主要需要借助AnimatorSet這個類,這個類提供了一個play()方法,如果我們向這個方法中傳入一個Animator對象(ValueAnimator或ObjectAnimator)將會返回一個AnimatorSet.Builder的實例,AnimatorSet.Builder中包括以下四個方法:
- after(Animator anim) 將現有動畫插入到傳入的動畫之后執行
- after(long delay) 將現有動畫延遲指定毫秒后執行
- before(Animator anim) 將現有動畫插入到傳入的動畫之前執行
- with(Animator anim) 將現有動畫和傳入的動畫同時執行
好的,有了這四個方法,我們就可以完成組合動畫的邏輯了,那么比如說我們想要讓TextView先從屏幕外移動進屏幕,然后開始旋轉360度,旋轉的同時進行淡入淡出操作,就可以這樣寫:
ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f); ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f); ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f); AnimatorSet animSet = new AnimatorSet(); animSet.play(rotate).with(fadeInOut).after(moveIn); animSet.setDuration(5000); animSet.start();
換一種思路的寫法:
public void rotateyAnimRuns(final View view) { ObjectAnimator anim = ObjectAnimator.offFloat(view,"zhy",1.0f,0.0f); anim.setDuration(500); anim.start(); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float cVal = (Float) animation.getAnimatedValue(); view.setAlpha(cVal); view.setScaleX(cVal); view.setScaleY(cVal); } }); }
把設置屬性的那個字符串,隨便寫一個該對象沒有的屬性,就是不管~~咱們只需要它按照時間插值和持續時間計算的那個值,我們自己手動調用~
再換一個思路:
public void propertyValuesHolder(View view) { PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("alpha", 1f, 0f, 1f); PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("scaleX", 1f, 0, 1f); PropertyValuesHolder pvhZ = PropertyValuesHolder.ofFloat("scaleY", 1f, 0, 1f); ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY, pvhZ).setDuration(1000).start(); }
Animator監聽器
在很多時候,我們希望可以監聽到動畫的各種事件,比如動畫何時開始,何時結束,然后在開始或者結束的時候去執行一些邏輯處理。這個功能是完全可以實現的,Animator類當中提供了一個addListener()方法,這個方法接收一個AnimatorListener,我們只需要去實現這個AnimatorListener就可以監聽動畫的各種事件了。
大家已經知道,ObjectAnimator是繼承自ValueAnimator的,而ValueAnimator又是繼承自Animator的,因此不管是ValueAnimator還是ObjectAnimator都是可以使用addListener()這個方法的。另外AnimatorSet也是繼承自Animator的,因此addListener()這個方法算是個通用的方法。
添加一個監聽器的代碼如下所示:
anim.addListener(new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } });
可以看到,我們需要實現接口中的四個方法,onAnimationStart()方法會在動畫開始的時候調用,onAnimationRepeat()方法會在動畫重復執行的時候調用,onAnimationEnd()方法會在動畫結束的時候調用,onAnimationCancel()方法會在動畫被取消的時候調用。
但是也許很多時候我們並不想要監聽那么多個事件,可能我只想要監聽動畫結束這一個事件,那么每次都要將四個接口全部實現一遍就顯得非常繁瑣。沒關系,為此Android提供了一個適配器類,叫作AnimatorListenerAdapter,使用這個類就可以解決掉實現接口繁瑣的問題了,如下所示:
anim.addListener(new AnimatorListenerAdapter() { });
這里我們向addListener()方法中傳入這個適配器對象,由於AnimatorListenerAdapter中已經將每個接口都實現好了,所以這里不用實現任何一個方法也不會報錯。那么如果我想監聽動畫結束這個事件,就只需要單獨重寫這一個方法就可以了,如下所示:
anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { } });
使用XML編寫動畫
我們可以使用代碼來編寫所有的動畫功能,這也是最常用的一種做法。不過,過去的補間動畫除了使用代碼編寫之外也是可以使用XML編寫的,因此屬性動畫也提供了這一功能,即通過XML來完成和代碼一樣的屬性動畫功能。
通過XML來編寫動畫可能會比通過代碼來編寫動畫要慢一些,但是在重用方面將會變得非常輕松,比如某個將通用的動畫編寫到XML里面,我們就可以在各個界面當中輕松去重用它。
如果想要使用XML來編寫動畫,首先要在res目錄下面新建一個animator文件夾,所有屬性動畫的XML文件都應該存放在這個文件夾當中。然后在XML文件中我們一共可以使用如下三種標簽:
- <animator> 對應代碼中的ValueAnimator
- <objectAnimator> 對應代碼中的ObjectAnimator
- <set> 對應代碼中的AnimatorSet
那么比如說我們想要實現一個從0到100平滑過渡的動畫,在XML當中就可以這樣寫:
<animator xmlns:android="http://schemas.android.com/apk/res/android" android:valueFrom="0" android:valueTo="100" android:valueType="intType"/>
而如果我們想將一個視圖的alpha屬性從1變成0,就可以這樣寫:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:valueFrom="1" android:valueTo="0" android:valueType="floatType" android:propertyName="alpha"/>
其實XML編寫動畫在可讀性方面還是挺高的,上面的內容相信不用我做解釋大家也都看得懂吧。
另外,我們也可以使用XML來完成復雜的組合動畫操作,比如將一個視圖先從屏幕外移動進屏幕,然后開始旋轉360度,旋轉的同時進行淡入淡出操作,就可以這樣寫:
<set xmlns:android="http://schemas.android.com/apk/res/android" android:ordering="sequentially" > <objectAnimator android:duration="2000" android:propertyName="translationX" android:valueFrom="-500" android:valueTo="0" android:valueType="floatType" > </objectAnimator> <set android:ordering="together" > <objectAnimator android:duration="3000" android:propertyName="rotation" android:valueFrom="0" android:valueTo="360" android:valueType="floatType" > </objectAnimator> <set android:ordering="sequentially" > <objectAnimator android:duration="1500" android:propertyName="alpha" android:valueFrom="1" android:valueTo="0" android:valueType="floatType" > </objectAnimator> <objectAnimator android:duration="1500" android:propertyName="alpha" android:valueFrom="0" android:valueTo="1" android:valueType="floatType" > </objectAnimator> </set> </set> </set>
這段XML實現的效果和我們剛才通過代碼來實現的組合動畫的效果是一模一樣的,每個參數的含義都非常清楚,相信大家都是一看就懂,我就不再一一解釋了。
最后XML文件是編寫好了,那么我們如何在代碼中把文件加載進來並將動畫啟動呢?只需調用如下代碼即可:
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.anim_file);
animator.setTarget(view);
animator.start();
調用AnimatorInflater的loadAnimator來將XML動畫文件加載進來,然后再調用setTarget()方法將這個動畫設置到某一個對象上面,最后再調用start()方法啟動動畫就可以了
ValueAnimator的高級用法
在上篇文章中介紹補間動畫缺點的時候有提到過,補間動畫是只能對View對象進行動畫操作的。而屬性動畫就不再受這個限制,它可以對任意對象進行動畫操作。那么大家應該還記得在上篇文章當中我舉的一個例子,比如說我們有一個自定義的View,在這個View當中有一個Point對象用於管理坐標,然后在onDraw()方法當中就是根據這個Point對象的坐標值來進行繪制的。也就是說,如果我們可以對Point對象進行動畫操作,那么整個自定義View的動畫效果就有了。OK,下面我們就來學習一下如何實現這樣的效果。
在開始動手之前,我們還需要掌握另外一個知識點,就是TypeEvaluator的用法。可能在大多數情況下我們使用屬性動畫的時候都不會用到TypeEvaluator,但是大家還是應該了解一下它的用法,以防止當我們遇到一些解決不掉的問題時能夠想起來還有這樣的一種解決方案。
那么TypeEvaluator的作用到底是什么呢?簡單來說,就是告訴動畫系統如何從初始值過度到結束值。我們在上一篇文章中學到的ValueAnimator.ofFloat()方法就是實現了初始值與結束值之間的平滑過度,那么這個平滑過度是怎么做到的呢?其實就是系統內置了一個FloatEvaluator,它通過計算告知動畫系統如何從初始值過度到結束值,我們來看一下FloatEvaluator的代碼實現:
public class FloatEvaluator implements TypeEvaluator { public Object evaluate(float fraction, Object startValue, Object endValue) { float startFloat = ((Number) startValue).floatValue(); return startFloat + fraction * (((Number) endValue).floatValue() - startFloat); } }
可以看到,FloatEvaluator實現了TypeEvaluator接口,然后重寫evaluate()方法。evaluate()方法當中傳入了三個參數,第一個參數fraction非常重要,這個參數用於表示動畫的完成度的,我們應該根據它來計算當前動畫的值應該是多少,第二第三個參數分別表示動畫的初始值和結束值。那么上述代碼的邏輯就比較清晰了,用結束值減去初始值,算出它們之間的差值,然后乘以fraction這個系數,再加上初始值,那么就得到當前動畫的值了。
好的,那FloatEvaluator是系統內置好的功能,並不需要我們自己去編寫,但介紹它的實現方法是要為我們后面的功能鋪路的。前面我們使用過了ValueAnimator的ofFloat()和ofInt()方法,分別用於對浮點型和整型的數據進行動畫操作的,但實際上ValueAnimator中還有一個ofObject()方法,是用於對任意對象進行動畫操作的。但是相比於浮點型或整型數據,對象的動畫操作明顯要更復雜一些,因為系統將完全無法知道如何從初始對象過度到結束對象,因此這個時候我們就需要實現一個自己的TypeEvaluator來告知系統如何進行過度。
下面來先定義一個Point類,如下所示:
public class Point { private float x; private float y; public Point(float x, float y) { this.x = x; this.y = y; } public float getX() { return x; } public float getY() { return y; } }
Point類非常簡單,只有x和y兩個變量用於記錄坐標的位置,並提供了構造方法來設置坐標,以及get方法來獲取坐標。接下來定義PointEvaluator,如下所示:
public class PointEvaluator implements TypeEvaluator{ @Override public Object evaluate(float fraction, Object startValue, Object endValue) { Point startPoint = (Point) startValue; Point endPoint = (Point) endValue; float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX()); float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY()); Point point = new Point(x, y); return point; } }
可以看到,PointEvaluator同樣實現了TypeEvaluator接口並重寫了evaluate()方法。其實evaluate()方法中的邏輯還是非常簡單的,先是將startValue和endValue強轉成Point對象,然后同樣根據fraction來計算當前動畫的x和y的值,最后組裝到一個新的Point對象當中並返回。
這樣我們就將PointEvaluator編寫完成了,接下來我們就可以非常輕松地對Point對象進行動畫操作了,比如說我們有兩個Point對象,現在需要將Point1通過動畫平滑過度到Point2,就可以這樣寫:
Point point1 = new Point(0, 0); Point point2 = new Point(300, 300); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2); anim.setDuration(5000); anim.start();
代碼很簡單,這里我們先是new出了兩個Point對象,並在構造函數中分別設置了它們的坐標點。然后調用ValueAnimator的ofObject()方法來構建ValueAnimator的實例,這里需要注意的是,ofObject()方法要求多傳入一個TypeEvaluator參數,這里我們只需要傳入剛才定義好的PointEvaluator的實例就可以了。
好的,這就是自定義TypeEvaluator的全部用法,掌握了這些知識之后,我們就可以來嘗試一下如何通過對Point對象進行動畫操作,從而實現整個自定義View的動畫效果。
新建一個MyAnimView繼承自View,代碼如下所示:
public class MyAnimView extends View { public static final float RADIUS = 50f; private Point currentPoint; private Paint mPaint; public MyAnimView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.BLUE); } @Override protected void onDraw(Canvas canvas) { if (currentPoint == null) { currentPoint = new Point(RADIUS, RADIUS); drawCircle(canvas); startAnimation(); } else { drawCircle(canvas); } } private void drawCircle(Canvas canvas) { float x = currentPoint.getX(); float y = currentPoint.getY(); canvas.drawCircle(x, y, RADIUS, mPaint); } private void startAnimation() { Point startPoint = new Point(RADIUS, RADIUS); Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); anim.setDuration(5000); anim.start(); } }
基本上還是很簡單的,總共也沒幾行代碼。首先在自定義View的構造方法當中初始化了一個Paint對象作為畫筆,並將畫筆顏色設置為藍色,接着在onDraw()方法當中進行繪制。這里我們繪制的邏輯是由currentPoint這個對象控制的,如果currentPoint對象不等於空,那么就調用drawCircle()方法在currentPoint的坐標位置畫出一個半徑為50的圓,如果currentPoint對象是空,那么就調用startAnimation()方法來啟動動畫。
那么我們來觀察一下startAnimation()方法中的代碼,其實大家應該很熟悉了,就是對Point對象進行了一個動畫操作而已。這里我們定義了一個startPoint和一個endPoint,坐標分別是View的左上角和右下角,並將動畫的時長設為5秒。然后有一點需要大家注意的,就是我們通過監聽器對動畫的過程進行了監聽,每當Point值有改變的時候都會回調onAnimationUpdate()方法。在這個方法當中,我們對currentPoint對象進行了重新賦值,並調用了invalidate()方法,這樣的話onDraw()方法就會重新調用,並且由於currentPoint對象的坐標已經改變了,那么繪制的位置也會改變,於是一個平移的動畫效果也就實現了。
下面我們只需要在布局文件當中引入這個自定義控件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.tony.myapplication.MyAnimView android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
最后運行一下程序,效果如下圖所示:
OK!這樣我們就成功實現了通過對對象進行值操作來實現動畫效果的功能,這就是ValueAnimator的高級用法。
ObjectAnimator的高級用法
ObjectAnimator的基本用法和工作原理在上一篇文章當中都已經講解過了,相信大家都已經掌握。那么大家應該都還記得,我們在吐槽補間動畫的時候有提到過,補間動畫是只能實現移動、縮放、旋轉和淡入淡出這四種動畫操作的,功能限定死就是這些,基本上沒有任何擴展性可言。比如我們想要實現對View的顏色進行動態改變,補間動畫是沒有辦法做到的。
但是屬性動畫就不會受這些條條框框的限制,它的擴展性非常強,對於動態改變View的顏色這種功能是完全可是勝任的,那么下面我們就來學習一下如何實現這樣的效果。
大家應該都還記得,ObjectAnimator內部的工作機制是通過尋找特定屬性的get和set方法,然后通過方法不斷地對值進行改變,從而實現動畫效果的。因此我們就需要在MyAnimView中定義一個color屬性,並提供它的get和set方法。這里我們可以將color屬性設置為字符串類型,使用#RRGGBB這種格式來表示顏色值,代碼如下所示:
public class MyAnimView extends View { ... private String color; public String getColor() { return color; } public void setColor(String color) { this.color = color; mPaint.setColor(Color.parseColor(color)); invalidate(); } ... }
注意在setColor()方法當中,我們編寫了一個非常簡單的邏輯,就是將畫筆的顏色設置成方法參數傳入的顏色,然后調用了invalidate()方法。這段代碼雖然只有三行,但是卻執行了一個非常核心的功能,就是在改變了畫筆顏色之后立即刷新視圖,然后onDraw()方法就會調用。在onDraw()方法當中會根據當前畫筆的顏色來進行繪制,這樣顏色也就會動態進行改變了。
那么接下來的問題就是怎樣讓setColor()方法得到調用了,毫無疑問,當然是要借助ObjectAnimator類,但是在使用ObjectAnimator之前我們還要完成一個非常重要的工作,就是編寫一個用於告知系統如何進行顏色過度的TypeEvaluator。創建ColorEvaluator並實現TypeEvaluator接口,代碼如下所示:
public class ColorEvaluator implements TypeEvaluator { private int mCurrentRed = -1; private int mCurrentGreen = -1; private int mCurrentBlue = -1; @Override public Object evaluate(float fraction, Object startValue, Object endValue) { String startColor = (String) startValue; String endColor = (String) endValue; int startRed = Integer.parseInt(startColor.substring(1, 3), 16); int startGreen = Integer.parseInt(startColor.substring(3, 5), 16); int startBlue = Integer.parseInt(startColor.substring(5, 7), 16); int endRed = Integer.parseInt(endColor.substring(1, 3), 16); int endGreen = Integer.parseInt(endColor.substring(3, 5), 16); int endBlue = Integer.parseInt(endColor.substring(5, 7), 16); // 初始化顏色的值 if (mCurrentRed == -1) { mCurrentRed = startRed; } if (mCurrentGreen == -1) { mCurrentGreen = startGreen; } if (mCurrentBlue == -1) { mCurrentBlue = startBlue; } // 計算初始顏色和結束顏色之間的差值 int redDiff = Math.abs(startRed - endRed); int greenDiff = Math.abs(startGreen - endGreen); int blueDiff = Math.abs(startBlue - endBlue); int colorDiff = redDiff + greenDiff + blueDiff; if (mCurrentRed != endRed) { mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0, fraction); } else if (mCurrentGreen != endGreen) { mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction); } else if (mCurrentBlue != endBlue) { mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff, redDiff + greenDiff, fraction); } // 將計算出的當前顏色的值組裝返回 String currentColor = "#" + getHexString(mCurrentRed) + getHexString(mCurrentGreen) + getHexString(mCurrentBlue); return currentColor; } /** * 根據fraction值來計算當前的顏色。 */ private int getCurrentColor(int startColor, int endColor, int colorDiff, int offset, float fraction) { int currentColor; if (startColor > endColor) { currentColor = (int) (startColor - (fraction * colorDiff - offset)); if (currentColor < endColor) { currentColor = endColor; } } else { currentColor = (int) (startColor + (fraction * colorDiff - offset)); if (currentColor > endColor) { currentColor = endColor; } } return currentColor; } /** * 將10進制顏色值轉換成16進制。 */ private String getHexString(int value) { String hexString = Integer.toHexString(value); if (hexString.length() == 1) { hexString = "0" + hexString; } return hexString; } }
這大概是我們整個動畫操作當中最復雜的一個類了。沒錯,屬性動畫的高級用法中最有技術含量的也就是如何編寫出一個合適的TypeEvaluator。好在剛才我們已經編寫了一個PointEvaluator,對它的基本工作原理已經有了了解,那么這里我們主要學習一下ColorEvaluator的邏輯流程吧。
首先在evaluate()方法當中獲取到顏色的初始值和結束值,並通過字符串截取的方式將顏色分為RGB三個部分,並將RGB的值轉換成十進制數字,那么每個顏色的取值范圍就是0-255。接下來計算一下初始顏色值到結束顏色值之間的差值,這個差值很重要,決定着顏色變化的快慢,如果初始顏色值和結束顏色值很相近,那么顏色變化就會比較緩慢,而如果顏色值相差很大,比如說從黑到白,那么就要經歷255*3這個幅度的顏色過度,變化就會非常快。
那么控制顏色變化的速度是通過getCurrentColor()這個方法來實現的,這個方法會根據當前的fraction值來計算目前應該過度到什么顏色,並且這里會根據初始和結束的顏色差值來控制變化速度,最終將計算出的顏色進行返回。
最后,由於我們計算出的顏色是十進制數字,這里還需要調用一下getHexString()方法把它們轉換成十六進制字符串,再將RGB顏色拼裝起來之后作為最終的結果返回。
好了,ColorEvaluator寫完之后我們就把最復雜的工作完成了,剩下的就是一些簡單調用的問題了,比如說我們想要實現從藍色到紅色的動畫過度,歷時5秒,就可以這樣寫:
ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView,"color", new ColorEvaluator(), "#0000FF", "#FF0000"); anim.setDuration(5000); anim.start();
用法非常簡單易懂,相信不需要我再進行解釋了。
接下來我們需要將上面一段代碼移到MyAnimView類當中,讓它和剛才的Point移動動畫可以結合到一起播放,這就要借助我們在上篇文章當中學到的組合動畫的技術了。修改MyAnimView中的代碼,如下所示:
public class MyAnimView extends View { ... private void startAnimation() { Point startPoint = new Point(RADIUS, RADIUS); Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(), "#0000FF", "#FF0000"); AnimatorSet animSet = new AnimatorSet(); animSet.play(anim).with(anim2); animSet.setDuration(5000); animSet.start(); } }
可以看到,我們並沒有改動太多的代碼,重點只是修改了startAnimation()方法中的部分內容。這里先是將顏色過度的代碼邏輯移動到了startAnimation()方法當中,注意由於這段代碼本身就是在MyAnimView當中執行的,因此ObjectAnimator.ofObject()的第一個參數直接傳this就可以了。接着我們又創建了一個AnimatorSet,並把兩個動畫設置成同時播放,動畫時長為五秒,最后啟動動畫。現在重新運行一下代碼,效果如下圖所示:
OK,位置動畫和顏色動畫非常融洽的結合到一起了,看上去效果還是相當不錯的,這樣我們就把ObjectAnimator的高級用法也掌握了。
Interpolator的用法
Interpolator這個東西很難進行翻譯,直譯過來的話是補間器的意思,它的主要作用是可以控制動畫的變化速率,比如去實現一種非線性運動的動畫效果。那么什么叫做非線性運動的動畫效果呢?就是說動畫改變的速率不是一成不變的,像加速運動以及減速運動都屬於非線性運動。
不過Interpolator並不是屬性動畫中新增的技術,實際上從Android 1.0版本開始就一直存在Interpolator接口了,而之前的補間動畫當然也是支持這個功能的。只不過在屬性動畫中新增了一個TimeInterpolator接口,這個接口是用於兼容之前的Interpolator的,這使得所有過去的Interpolator實現類都可以直接拿過來放到屬性動畫當中使用,那么我們來看一下現在TimeInterpolator接口的所有實現類,如下圖所示:
可以看到,TimeInterpolator接口已經有非常多的實現類了,這些都是Android系統內置好的並且我們可以直接使用的Interpolator。每個Interpolator都有它各自的實現效果,比如說AccelerateInterpolator就是一個加速運動的Interpolator,而DecelerateInterpolator就是一個減速運動的Interpolator。
我覺得細心的朋友應該早已經發現了,在前面兩篇文章當中我們所學到的所有屬性動畫,其實都不是在進行一種線程運動。比如說在“上”篇文章中使用ValueAnimator所打印的值如下所示:
可以看到,一開始的值變化速度明顯比較慢,僅0.0開頭的就打印了4次,之后開始加速,最后階段又開始減速,因此我們可以很明顯地看出這一個先加速后減速的Interpolator。
那么再來看一下在“中”篇文章中完成的小球移動加變色的功能,如下圖所示:
從上圖中我們明顯可以看出,小球一開始運動速度比較慢,然后逐漸加速,中間的部分運動速度就比較快,接下來開始減速,最后緩緩停住。另外顏色變化也是這種規律,一開始顏色變化的比較慢,中間顏色變化的很快,最后階段顏色變化的又比較慢。
從以上幾點我們就可以總結出一個結論了,使用屬性動畫時,系統默認的Interpolator其實就是一個先加速后減速的Interpolator,對應的實現類就是AccelerateDecelerateInterpolator。
當然,我們也可以很輕松地修改這一默認屬性,將它替換成任意一個系統內置好的Interpolator。就拿“中”篇文章中的代碼來舉例吧,MyAnimView中的startAnimation()方法是開啟動畫效果的入口,這里我們對Point對象的坐標稍做一下修改,讓它變成一種垂直掉落的效果,代碼如下所示:
private void startAnimation() { Point startPoint = new Point(getWidth() / 2, RADIUS); Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); anim.setDuration(2000); anim.start(); }
這里主要是對Point構造函數中的坐標值進行了一下改動,那么現在小球運動的動畫效果應該是從屏幕正中央的頂部掉落到底部。但是現在默認情況下小球的下降速度肯定是先加速后減速的,這不符合物理的常識規律,如果把小球視為一個自由落體的話,那么下降的速度應該是越來越快的。我們怎樣才能改變這一默認行為呢?其實很簡單,調用Animator的setInterpolator()方法就可以了,這個方法要求傳入一個實現TimeInterpolator接口的實例,那么比如說我們想要實現小球下降越來越快的效果,就可以使用AccelerateInterpolator,代碼如下所示:
private void startAnimation() { Point startPoint = new Point(getWidth() / 2, RADIUS); Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); anim.setInterpolator(new AccelerateInterpolator(2f)); anim.setDuration(2500); anim.start(); }
代碼很簡單,這里調用了setInterpolator()方法,然后傳入了一個AccelerateInterpolator的實例,注意AccelerateInterpolator的構建函數可以接收一個float類型的參數,這個參數是用於控制加速度的。現在運行一下代碼,效果如下圖所示:
OK,效果非常明顯,說明我們已經成功替換掉了默認的Interpolator,AccelerateInterpolator確實是生效了。但是現在的動畫效果看上去仍然是怪怪的,因為一個小球從很高的地方掉落到地面上直接就靜止了,這也是不符合物理規律的,小球撞擊到地面之后應該要反彈起來,然后再次落下,接着再反彈起來,又再次落下,以此反復,最后靜止。這個功能我們當然可以自己去寫,只不過比較復雜,所幸的是,Android系統中已經提供好了這樣一種Interpolator,我們只需要簡單地替換一下就可以完成上面的描述的效果,代碼如下所示:
private void startAnimation() { Point startPoint = new Point(getWidth() / 2, RADIUS); Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); anim.setInterpolator(new BounceInterpolator()); anim.setDuration(3000); anim.start(); }
可以看到,我們只是將設置的Interpolator換成了BounceInterpolator的實例,而BounceInterpolator就是一種可以模擬物理規律,實現反復彈起效果的Interpolator。另外還將整體的動畫時間稍微延長了一點,因為小球反復彈起需要比之前更長的時間。現在重新運行一下代碼,效果如下圖所示:
OK!效果還是非常不錯的。那么這里我們只是選了幾個系統實現好的Interpolator,由於內置Interpolator非常多,就不一一進行講解了,大家可以自己去使用一下其它的幾種Interpolator來看一看效果。
但是,只會用一下系統提供好的Interpolator,我們顯然對自己的要求就太低了,既然是學習屬性動畫的高級用法,那么自然要將它研究透了。下面我們就來看一下Interpolator的內部實現機制是什么樣的,並且來嘗試寫一個自定義的Interpolator。
首先看一下TimeInterpolator的接口定義,代碼如下所示:
/** * A time interpolator defines the rate of change of an animation. This allows animations * to have non-linear motion, such as acceleration and deceleration. */ public interface TimeInterpolator { /** * Maps a value representing the elapsed fraction of an animation to a value that represents * the interpolated fraction. This interpolated value is then multiplied by the change in * value of an animation to derive the animated value at the current elapsed animation time. * * @param input A value between 0 and 1.0 indicating our current point * in the animation where 0 represents the start and 1.0 represents * the end * @return The interpolation value. This value can be more than 1.0 for * interpolators which overshoot their targets, or less than 0 for * interpolators that undershoot their targets. */ float getInterpolation(float input); }
OK,接口還是非常簡單的,只有一個getInterpolation()方法。大家有興趣可以通過注釋來對這個接口進行詳解的了解,這里我就簡單解釋一下,getInterpolation()方法中接收一個input參數,這個參數的值會隨着動畫的運行而不斷變化,不過它的變化是非常有規律的,就是根據設定的動畫時長勻速增加,變化范圍是0到1。也就是說當動畫一開始的時候input的值是0,到動畫結束的時候input的值是1,而中間的值則是隨着動畫運行的時長在0到1之間變化的。
說到這個input的值,我覺得有不少朋友可能會聯想到我們在“中”篇文章中使用過的fraction值。那么這里的input和fraction有什么關系或者區別呢?答案很簡單,input的值決定了fraction的值。input的值是由系統經過計算后傳入到getInterpolation()方法中的,然后我們可以自己實現getInterpolation()方法中的算法,根據input的值來計算出一個返回值,而這個返回值就是fraction了。
因此,最簡單的情況就是input值和fraction值是相同的,這種情況由於input值是勻速增加的,因而fraction的值也是勻速增加的,所以動畫的運動情況也是勻速的。系統中內置的LinearInterpolator就是一種勻速運動的Interpolator,那么我們來看一下它的源碼是怎么實現的:
/** * An interpolator where the rate of change is constant */ @HasNativeInterpolator public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public LinearInterpolator() { } public LinearInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { return input; } /** @hide */ @Override public long createNativeInterpolator() { return NativeInterpolatorFactoryHelper.createLinearInterpolator(); } }
這里我們只看getInterpolation()方法,這個方法沒有任何邏輯,就是把參數中傳遞的input值直接返回了,因此fraction的值就是等於input的值的,這就是勻速運動的Interpolator的實現方式。
當然這是最簡單的一種Interpolator的實現了,我們再來看一個稍微復雜一點的。既然現在大家都知道了系統在默認情況下使用的是AccelerateDecelerateInterpolator,那我們就來看一下它的源碼吧,如下所示:
/** * An interpolator where the rate of change starts and ends slowly but * accelerates through the middle. * */ @HasNativeInterpolator public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory { public AccelerateDecelerateInterpolator() { } @SuppressWarnings({"UnusedDeclaration"}) public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } /** @hide */ @Override public long createNativeInterpolator() { return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator(); } }
代碼雖然沒有變長很多,但是getInterpolation()方法中的邏輯已經明顯變復雜了,不再是簡單地將參數中的input進行返回,而是進行了一個較為復雜的數學運算。那這里我們來分析一下它的算法實現,可以看到,算法中主要使用了余弦函數,由於input的取值范圍是0到1,那么cos函數中的取值范圍就是π到2π。而cos(π)的結果是-1,cos(2π)的結果是1,那么這個值再除以2加上0.5之后,getInterpolation()方法最終返回的結果值還是在0到1之間。只不過經過了余弦運算之后,最終的結果不再是勻速增加的了,而是經歷了一個先加速后減速的過程。我們可以將這個算法的執行情況通過曲線圖的方式繪制出來,結果如下圖所示:
可以看到,這是一個S型的曲線圖,當橫坐標從0變化到0.2的時候,縱坐標的變化幅度很小,但是之后就開始明顯加速,最后橫坐標從0.8變化到1的時候,縱坐標的變化幅度又變得很小。
OK,通過分析LinearInterpolator和AccelerateDecelerateInterpolator的源碼,我們已經對Interpolator的內部實現機制有了比較清楚的認識了,那么接下來我們就開始嘗試編寫一個自定義的Interpolator。
編寫自定義Interpolator最主要的難度都是在於數學計算方面的,由於我數學並不是很好,因此這里也就寫一個簡單點的Interpolator來給大家演示一下。既然屬性動畫默認的Interpolator是先加速后減速的一種方式,這里我們就對它進行一個簡單的修改,讓它變成先減速后加速的方式。新建DecelerateAccelerateInterpolator類,讓它實現TimeInterpolator接口,代碼如下所示:
public class DecelerateAccelerateInterpolator implements TimeInterpolator{ @Override public float getInterpolation(float input) { float result; if (input <= 0.5) { result = (float) (Math.sin(Math.PI * input)) / 2; } else { result = (float) (2 - Math.sin(Math.PI * input)) / 2; } return result; } }
這段代碼是使用正弦函數來實現先減速后加速的功能的,因為正弦函數初始弧度的變化值非常大,剛好和余弦函數是相反的,而隨着弧度的增加,正弦函數的變化值也會逐漸變小,這樣也就實現了減速的效果。當弧度大於π/2之后,整個過程相反了過來,現在正弦函數的弧度變化值非常小,漸漸隨着弧度繼續增加,變化值越來越大,弧度到π時結束,這樣從0過度到π,也就實現了先減速后加速的效果。
同樣我們可以將這個算法的執行情況通過曲線圖的方式繪制出來,結果如下圖所示:
可以看到,這也是一個S型的曲線圖,只不過曲線的方向和剛才是相反的。從上圖中我們可以很清楚地看出來,一開始縱坐標的變化幅度很大,然后逐漸變小,橫坐標到0.5的時候縱坐標變化幅度趨近於零,之后隨着橫坐標繼續增加縱坐標的變化幅度又開始變大,的確是先減速后加速的效果。
那么現在我們將DecelerateAccelerateInterpolator在代碼中進行替換,如下所示:
private void startAnimation() { Point startPoint = new Point(getWidth() / 2, RADIUS); Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); invalidate(); } }); anim.setInterpolator(new DecelerateAccelerateInterpolator()); anim.setDuration(3000); anim.start(); }
非常簡單,就是將DecelerateAccelerateInterpolator的實例傳入到setInterpolator()方法當中。重新運行一下代碼,效果如下圖所示:
OK!小球的運動確實是先減速后加速的效果,說明我們自定義的Interpolator已經可以正常工作了。通過這樣一個程度的學習,相信大家對屬性動畫Interpolator的理解和使用都達到了一個比較深刻的層次了。
ViewPropertyAnimator的用法
ViewPropertyAnimator其實算不上什么高級技巧,它的用法格外的簡單,只不過和前面所學的所有屬性動畫的知識不同,它並不是在3.0系統當中引入的,而是在3.1系統當中附增的一個新的功能,因此這里我們把它作為整個屬性動畫系列的收尾部分。
我們都知道,屬性動畫的機制已經不是再針對於View而進行設計的了,而是一種不斷地對值進行操作的機制,它可以將值賦值到指定對象的指定屬性上。但是,在絕大多數情況下,我相信大家主要都還是對View進行動畫操作的。Android開發團隊也是意識到了這一點,沒有為View的動畫操作提供一種更加便捷的用法確實是有點太不人性化了,於是在Android 3.1系統當中補充了ViewPropertyAnimator這個機制。
那我們先來回顧一下之前的用法吧,比如我們想要讓一個TextView從常規狀態變成透明狀態,就可以這樣寫:
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 0f); animator.start();
看上去復雜嗎?好像也不怎么復雜,但確實也不怎么容易理解。我們要將操作的view、屬性、變化的值都一起傳入到ObjectAnimator.ofFloat()方法當中,雖然看上去也沒寫幾行代碼,但這不太像是我們平時使用的面向對象的思維。
那么下面我們就來看一下如何使用ViewPropertyAnimator來實現同樣的效果,ViewPropertyAnimator提供了更加易懂、更加面向對象的API,如下所示:
textview.animate().alpha(0f);
果然非常簡單!不過textview.animate()這個方法是怎么回事呢?animate()方法就是在Android 3.1系統上新增的一個方法,這個方法的返回值是一個ViewPropertyAnimator對象,也就是說拿到這個對象之后我們就可以調用它的各種方法來實現動畫效果了,這里我們調用了alpha()方法並轉入0,表示將當前的textview變成透明狀態。
怎么樣?比起使用ObjectAnimator,ViewPropertyAnimator的用法明顯更加簡單易懂吧。除此之外,ViewPropertyAnimator還可以很輕松地將多個動畫組合到一起,比如我們想要讓textview運動到500,500這個坐標點上,就可以這樣寫:
textview.animate().x(500).y(500);
可以看出,ViewPropertyAnimator是支持連綴用法的,我們想讓textview移動到橫坐標500這個位置上時調用了x(500)這個方法,然后讓textview移動到縱坐標500這個位置上時調用了y(500)這個方法,將所有想要組合的動畫通過這種連綴的方式拼接起來,這樣全部動畫就都會一起被執行。
那么怎樣去設定動畫的運行時長呢?很簡單,也是通過連綴的方式設定即可,比如我們想要讓動畫運行5秒鍾,就可以這樣寫:
textview.animate().x(500).y(500).setDuration(5000);
除此之外,本篇文章第一部分所學的Interpolator技術我們也可以應用在ViewPropertyAnimator上面,如下所示:
textview.animate().x(500).y(500).setDuration(5000) .setInterpolator(new BounceInterpolator());
用法很簡單,同樣也是使用連綴的方式。相信大家現在都已經體驗出來了,ViewPropertyAnimator其實並沒有什么太多的技巧可言,用法基本都是大同小異的,需要用到什么功能就連綴一下,因此更多的用法大家只需要去查閱一下文檔,看看還支持哪些功能,有哪些接口可以調用就可以了。
那么除了用法之外,關於ViewPropertyAnimator有幾個細節還是值得大家注意一下的:
- 整個ViewPropertyAnimator的功能都是建立在View類新增的animate()方法之上的,這個方法會創建並返回一個ViewPropertyAnimator的實例,之后的調用的所有方法,設置的所有屬性都是通過這個實例完成的。
- 大家注意到,在使用ViewPropertyAnimator時,我們自始至終沒有調用過start()方法,這是因為新的接口中使用了隱式啟動動畫的功能,只要我們將動畫定義完成之后,動畫就會自動啟動。並且這個機制對於組合動畫也同樣有效,只要我們不斷地連綴新的方法,那么動畫就不會立刻執行,等到所有在ViewPropertyAnimator上設置的方法都執行完畢后,動畫就會自動啟動。當然如果不想使用這一默認機制的話,我們也可以顯式地調用start()方法來啟動動畫。
- ViewPropertyAnimator的所有接口都是使用連綴的語法來設計的,每個方法的返回值都是它自身的實例,因此調用完一個方法之后可以直接連綴調用它的另一個方法,這樣把所有的功能都串接起來,我們甚至可以僅通過一行代碼就完成任意復雜度的動畫功能。