在 Android 中,基本的動畫共有三種型別:
- View 動畫:也叫檢視動畫或者補間動畫,主要是指
android.view.animation
包下面的一些類,只能被用來設定給 View,缺點是比如當控制元件移動之後,接收點選的控制元件的位置不會跟隨移動,並且能夠實現的效果只有移動、縮放、旋轉和淡入淡出操作四種及其組合。 - Drawable 動畫:也叫 Frame 動畫或者幀動畫,其實可以劃分到檢視動畫的類別,實現方式是將一些列的 Drawable 像幻燈片一樣一個一個地顯示。
- Property 動畫: 屬性動畫主要是指
android.animation
包下面的一些類,只對API 11
以上版本的Android 系統才有效,但我們可以通過相容庫做低版本相容。這種動畫可以設定給任何 Object,包括那些還沒有渲染到螢幕上的物件。這種動畫是可擴充套件的,可以讓你自定義任何型別和屬性的動畫。
1、Drawable 動畫
在這裡,我們先對 Drawable 動畫進行講解,因為它相對於後面的兩種動畫比較簡單。在示例程式我們準備了一系列圖片資源,並在 drawable 資料夾下面定義了動畫資源 record_anim.xml:
<?xml version="1.0" encoding="utf-8"?>
<animation-list android:oneshot="false"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/record0" android:duration="500"/>
<item android:drawable="@drawable/record1" android:duration="500"/>
<item android:drawable="@drawable/record2" android:duration="450"/>
<item android:drawable="@drawable/record3" android:duration="400"/>
<item android:drawable="@drawable/record4" android:duration="350"/>
<item android:drawable="@drawable/record5" android:duration="400"/>
<item android:drawable="@drawable/record6" android:duration="400"/>
</animation-list>
複製程式碼
然後,我們在程式碼中使用該資源,並將其賦值給 ImageView。然後,我們從該控制元件中獲取該 Drawable 並將其轉換成 AnimationDrawable,隨後我們呼叫它的 start()
方法就開啟了 Drawable 動畫:
getBinding().ivRecord.setImageResource(R.drawable.record_anim);
animDraw = (AnimationDrawable) getBinding().ivRecord.getDrawable();
animDraw.start();
複製程式碼
此外,我們可以呼叫該 Drawable 的 stop()
方法停止動畫。
幀動畫的注意事項
使用幀動畫的時候要注意設定的圖片不宜過多、過大,以防止因為記憶體不夠而出現 OOM。
2、View 動畫
2.1 基本 View 動畫
該動畫的資源處在 android.view.animation
包下,主要有以下幾個類,它們都繼承自 Animation
,我們可以使用它們來實現複雜的動畫。這些動畫類分別有對應的 xml 標籤,所以,我們可以在 xml 中定義動畫,也可以在程式碼中實現動畫效果。這裡的 AnimationSet
可以用來將多個動畫效果進行組合,各預定義動畫的對照可以參考下面這張圖表:
2.2 View 動畫屬性
當然,要實現一種動畫效果會有許多屬性需要指定,在 xml 中,我們用標籤的屬性指定,在程式碼中我們用物件的 setter 方法指定。於是,我們可以得到下面這個對應關係:
上面的對應關係是所有的 View 動畫共用的,對各個具體的動畫型別還有其獨有的屬性。你可以在各個動畫的構造方法中,通過它們從 AttributeSet
中獲取了哪些欄位來了解它們都定義了哪些屬性,這裡我們不對其一一進行說明。各預定義的屬性動畫分別按照不同的方式實現了 Animation
的 applyTransformation
方法,具體的這些屬性如何使用以及 View 動畫的效果是如何實現的,都可通過閱讀該方法的定義得知。
對於 AnimationSet
,它內部維護了一個 Animation
列表,並且其本身也是一個 Animation
,所以,AnimationSet
內部可以新增子 AnimationSet
。
2.3 插值器
上文中我們提到過,View 動畫的具體實現是通過覆寫 Animation
的 applyTransformation
方法來完成的。這裡我們以 AlphaAnimation
為例來看它是如何作用的,同時你應該注意插值器的作用原理。該方法會在 Animation
中被迴圈呼叫,呼叫的時候會根據插值器計算出一個時間,並將其傳遞到 applyTransformation
方法中。
Animation
的 getTransformation
方法片段:
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
if (NoImagePreloadHolder.USE_CLOSEGUARD) {
guard.open("cancel or detach or getTransformation");
}
}
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
}
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
複製程式碼
AlphaAnimation
的 applyTransformation
方法:
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float alpha = mFromAlpha;
t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
}
複製程式碼
顯然,這裡的 interpolatedTime
的是一個比例。比如,假如一個透明動畫需要持續 10s
,透明度需要從 0.5f
到 1.0f
,而插值的規則是一個二次函式。那麼第 t (0<t<10)
秒的時候控制元件的透明度應該是:
alpha = 0.5f + (1.0f - 0.5f) * t^2 / 100
複製程式碼
以上就是插值器的作用原理,你也可以按照自己的需求實現自己的插值器,從而實現期待的動畫效果。
2.4 使用 View 動畫
作為一個例子,這裡我們實現一個讓控制元件抖動的動畫。在 anim 資料夾下面,我們定義一個平移的動畫,並使用插值器使其重複:
anim/shake.xml
的定義:
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="700"
android:fromXDelta="0.0"
android:interpolator="@anim/cycle_7"
android:toXDelta="15.0" />
複製程式碼
插值器 anim/cicle_7.xml
的定義:
<?xml version="1.0" encoding="utf-8"?>
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:cycles="4.0" />
複製程式碼
然後,我們在程式碼中載入 Animation
並呼叫控制元件的 startAnimation()
方法開啟動畫:
getBinding().v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.shake));
複製程式碼
對於 View
,我們有 startAnimation()
用來對 View
開始動畫;有 clearAnimation()
用來取消 View
在執行的動畫。
不使用 xml,僅使用程式碼我們一樣可以實現上述的效果,這裡我們不再進行說明。
2.5 View 動畫的特殊使用場景
2.5.1 LayoutAnimation
LayoutAnimation
作用於 ViewGroup
,可以使其子元素出場時都均有某種動畫效果,通常用於 ListView
。我們可以像下面這樣定義佈局動畫:
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation
android:delay="500"
android:animation="@anim/shake"
xmlns:android="http://schemas.android.com/apk/res/android" />
複製程式碼
顯然,這裡我們需要引用一個其他的動畫。然後,我們可以在 ListView
的 layoutAnimation
屬性中指定佈局動畫。或者呼叫 ListView
的 setLayoutAnimation()
方法應用上述動畫。
2.5.2 Activity 切換
我們可以通過在 Activity
中呼叫 overridePendingTransition(R.anim.shake, R.anim.shake);
方法來重寫 Activity
的切換動畫。注意這個方法應該在 startActivity(Intent)
或者 finish()
之後立即呼叫。
3、屬性動畫
3.1 基礎梳理
我們可以對比 View 動畫來學習屬性動畫。
- 屬性動畫主要是指
android.animation
包下面的一些類。 - 屬性動畫基礎的動畫類是
Animator
;屬性動畫也為我們提供了幾個預定義的類:AnimatorSet
,ObjectAnimator
,TimeAnimator
和ValueAnimator
;這幾個預定義類之間的繼承關係是,AnimatorSet
和ValueAnimator
直接繼承自Animator
,而ObjectAnimator
和TimeAnimator
繼承自ValueAnimator
。 - 與 View 動畫不同的是,屬性動畫的使用範圍更加寬泛,它不侷限於 View,本質上它是通過修改控制元件的屬性值實現的動畫。當你嘗試對某個物件的某個屬性進行修改的時候會有一些限制,即屬性動畫要求該物件必須提供了該屬性的
setter
方法。 - 屬性動畫也有
xml
和程式碼兩種定義方式,它的xml
通常定義在animator
資料夾下面,而 View 動畫定義在anim
資料夾下面。 - 屬性動畫提供了類似於
AnimationUtils
的方法用來從佈局資料夾中載入屬性動畫:AnimatorInflater
類的loadAnimator()
方法。 - 屬性動畫也有自己的插值器:
TimeInterpolator
,並且也提供了幾個預定義的插值器。 - 我們也可以呼叫
View
的方法來使用屬性動畫,我們可以通過 View 的animate()
方法獲取一個ViewPropertyAnimator
,然後呼叫ViewPropertyAnimator
的其他方法進行鏈式呼叫以實現複雜的屬性動畫效果。
下面是屬性動畫的程式碼實現和 xml
實現兩種方式的對比:
上文中,我們總結了屬性動畫的一些知識,並將其與 View 動畫進行了對比。這裡是一個簡單的梳理,在下文中我們會對屬性動畫進行更加詳細的介紹。
3.2 使用屬性動畫
3.2.1 ValueAnimator
上面說過 ValueAnimator
是 ObjectAnimator
和 TimeAnimator
的基類,我們可以這樣使用它:
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();
複製程式碼
這裡我們使用 log
輸出了值漸變的過程,從日誌中可以看出它的效果是值從 0
不斷遞增直到 1
。如果我們在這個監聽方法中根據值修改控制元件的屬性一樣可以實現動畫效果。除了 ofFloat()
還有 ofInt()
等方法,它們的效果相似。
3.2.2 ObjectAnimator
上面,如果我們想要實現動畫效果,需要在 ValueAnimator
的監聽事件中修改物件的屬性,這裡的 ObjectAnimator
,我們只需要傳入物件例項和屬性的字串名稱,修改物件屬性的操作就可以自動完成。比如下面的程式的效果是控制元件 textview
的透明度會在 5s
之內從 1 變成 0 再變回 1.
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
animator.setDuration(5000);
animator.start();
複製程式碼
注意這裡我們傳入的是 alpha
,這個欄位本身並不存在於控制元件中,而是有一個 setAlpha()
的方法。也就是說,ObjectAnimator
作用的原理是通過反射觸發 setter
方法而不是修改屬性來實現的。你可以在類 PropertyValuesHolder
中更詳細地瞭解這方面的內容。
PropertyValuesHolder
包裝了我們要修改的屬性的物件和方法等資訊,然後會使用反射觸發指定物件的方法來完成對物件屬性的修改。其中
void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}
複製程式碼
會去尋找我們要修改屬性的 setter
方法,然後
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
複製程式碼
會去觸發 setter
方法,以修改物件的屬性。
3.2.3 AnimatorSet
AnimatorSet
內部提供了一個構建者 AnimatorSet.Builder
來幫助我們構建組合動畫,AnimatorSet.Builder
提供了下面四種方法:
after(Animator anim)
:將現有動畫插入到傳入的動畫之後執行after(long delay)
:將現有動畫延遲指定毫秒後執行before(Animator anim)
:將現有動畫插入到傳入的動畫之前執行with(Animator anim)
:將現有動畫和傳入的動畫同時執行
當我們呼叫 AnimatorSet
的 play()
方法的時候就能獲取一個 AnimatorSet.Builder
例項,然後我們就可以使用構建者的方法進行鏈式呼叫了:
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();
複製程式碼
3.2.4 TypeEvaluator
正如前文所述,屬性動畫也有自己的插值器,我們可以通過插值函式指定在某個時間段內屬性改變的速率。插值函式得到的是一個比例,是沒有意義的。在 View 動畫的 AlphaAnimation
中,如果我們指定了起止的透明度,那麼我們可以通過透明度的計算規則得到某個時刻的透明度。但是對於屬性動畫,因為它可以應用於任何屬性,這個屬性又可能是任何型別的,那麼這個屬性將採用什麼樣的計算規則呢?這就需要我們使用 TypeEvaluator
來指定一個計算規則。也就是說,TypeEvaluator
是屬性動畫的屬性的計算規則。
下面是 TypeEvaluator
的定義,這裡的三個引數的含義分別是,fraction
是當前的比例,可以通過插值器計算得到;startValue
和 endValue
分別是屬性變化的起止值。它的返回結果就是在某個時刻某個屬性的值。
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
複製程式碼
屬性動畫中已經為我們提供了幾個預定義的 TypeEvaluator
,比如 FloatEvaluator
:
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
複製程式碼
在屬性動畫的 PropertyValuesHolder
中會根據屬性的型別選擇預定義的 TypeEvaluator
。但是如果我們的屬性的型別不在預定義的範圍之內就需要自己實現一個 TypeEvaluator
。下面我們以日期型別為例來實現一個 TypeEvaluator
。
當我們使用 ValueAnimator
的 ofObject()
方法獲取 ValueAnimator
例項的時候,要求我們傳入一個 TypeEvaluator
,於是我們可以像下面這樣定義:
private static class DateEvaluator implements TypeEvaluator<Date> {
@Override
public Date evaluate(float fraction, Date startValue, Date endValue) {
long startTime = startValue.getTime();
return new Date((long) (startTime + fraction * (endValue.getTime() - startTime)));
}
}
複製程式碼
然後,我們可以這樣使用它:
ValueAnimator animator = ValueAnimator.ofObject(new DateEvaluator(), new Date(0), new Date());
animator.setDuration(5000);
animator.addUpdateListener(animation -> {
Date date = (Date) animation.getAnimatedValue();
LogUtils.d(date);
});
animator.start();
複製程式碼
這樣就可以得到在 5s 之內輸出的從時間戳為0,到當前時刻的所有的日期變化。
3.2.5 TimeInterpolator
就像 View 動畫一樣,我們可以為屬性動畫指定一個插值器。插值器的作用是用來設定指定時間段內數值的變化的速率。在屬性動畫中,插值器是 TimeInterpolator
,同樣也有幾個預設的實現:
AccelerateDecelerateInterolator
:先加速後減速。AccelerateInterpolator
:加速。DecelerateInterpolator
:減速。AnticipateInterpolator
:先向相反方向改變一段再加速播放。AnticipateOvershootInterpolator
:先向相反方向改變,再加速播放,會超出目標值然後緩慢移動至目標值,類似於彈簧回彈。BounceInterpolator
:快到目標值時值會跳躍。CycleIinterpolator
:動畫迴圈一定次數,值的改變為一正弦函式:Math.sin(2 * mCycles * Math.PI * input)。LinearInterpolator
:線性均勻改變。OvershottInterpolator
:最後超出目標值然後緩慢改變到目標值。
3.2.6 在 xml 中使用屬性動畫
我們可以像下面這樣定義一個屬性動畫,
<set android:ordering=["together" | "sequentially"]>
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<set>
...
</set>
</set>
複製程式碼
這裡的android:ordering
用於控制子動畫啟動方式是先後有序的還是同時進行,兩個可選引數: sequentially
表示動畫按照先後順序;together
(預設)表示動畫同時啟動。
這裡的 <objectAnimator>
標籤的含義如下:
這樣在 XML 中定義了屬性動畫之後,我們可以在程式碼中通過工具類獲取到動畫例項並使用:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext, R.animtor.property_animator);
set.setTarget(myObject);
set.start();
複製程式碼
4、使用動畫的注意事項
- 記憶體耗盡:使用幀動畫的時候防止因為圖片過多導致 OOM。
- View 動畫並沒有真正改變 View 的位置:
View
動畫並沒有真正改變View
的屬性,即View
動畫執行之後並未改變View
的真實佈局屬性值。譬如我們在佈局中有一個Button
在螢幕上方,我們設定了平移動畫移動到螢幕下方然後保持動畫最後執行狀態呆在螢幕下方,這時如果點選螢幕下方動畫執行之後的Button
是沒有任何反應的,而點選原來螢幕上方沒有Button
的地方卻響應的是點選Button
的事件。 - 記憶體洩漏:使用屬性動畫的時候,當使用無限迴圈動畫,需要在 Activity 退出的時候停止動畫,不然可能會因為無法釋放資源而導致 Activity 記憶體洩漏。
- 動畫相容:當 APP 需要相容到 API 11 以下的時候就需要注意動畫的相容問題。
- 使用 dp 而不是 px:因為
px
在不同裝置上面的相容問題,使用動畫的時候儘量使用dp
作為單位。 - 硬體加速:使用硬體加速可以提升動畫的流暢性。
原始碼
你可以在Github獲取以上程式的原始碼: Android-references。