11.自定義過渡動畫
11.1 問題
應用程式需要自定義Activity切換或Fragment切換時產生的過渡動畫。
11.2 解決方案
(API Level 5)
要修改Activity間的過渡動畫,可以使用overridePendingTransition()API進行某次切換時的動畫,或者在應用程式的主題中宣告自定義動畫值來進行更多全域性設定。要修改Fragment間的過渡動畫,可以使用onCreateAnimation()或onCreateAnimator()API方法。
11.3 實現機制
1.Activity
要自定義Activity切換時的過渡動畫,可以考慮4種動畫:開啟一個新Activity時的進入動畫和退出動畫,以及當前Activity關閉時的進入動畫和退出動畫。每種動畫都會應用到過渡動畫中所涉及的兩個Activity之一。例如,當開啟一個新的Activity時,當前Activity將會執行“開啟退出”動畫,而新Activity會執行“開啟進入”動畫。由於這些動畫都是同時執行的,因此動畫間應該是互補的,否則看起來會不太協調。以下四段程式碼演示了這4種動畫。
res/anim/activity_open_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <rotate android:fromDegrees="90" android:toDegrees="0" android:pivotX="0%" android:pivotY="0%" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500"/> <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500" /> </set>
res/anim/activity_open_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <rotate android:fromDegrees="0" android:toDegrees="-90" android:pivotX="0%" android:pivotY="0%" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500"/> <alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500" /> </set>
res/anim/activity_close_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <rotate android:fromDegrees="-90" android:toDegrees="0" android:pivotX="0%p" android:pivotY="0%p" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500"/> <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500" /> </set>
res/anim/activity_close_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <rotate android:fromDegrees="0" android:toDegrees="90" android:pivotX="0%p" android:pivotY="0%p" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500" /> <alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:duration="500" /> </set>
我們建立了兩個“開啟”動畫,即舊的Activity順時針旋轉消失、新的Activity順時針旋轉進入。補足的“關閉”動畫會將當前Activity逆時針旋轉退出、之前的Activity逆時針旋轉進入。每個動畫還有漸出或漸入的效果,這樣過渡動畫看起來會更加流暢。要在特定時刻應用這些自定義動畫,可以在startActivity()或finish()後立刻呼叫overridePendingTransition()方法,如下所示:
//使用自定義過渡動畫啟動一個新的Activity Intent intent = new Intent(...); startActivity(intent); overridePendingTransition(R.anim.activity_open_enter,R.anim.activity_open_exit); //使用自定義過渡動畫關閉當前Activity finish(); overridePendingTransition(R.anim.activity_close_enter,R.anim.activity_close_exit);
這種方式在只希望在某些場合使用自定義過渡動畫的情況下非常有用。但如果希望在應用程式中自定義每個Activity的過渡動畫,到處呼叫這個方法可能會有點麻煩。反之,最好在應用程式的主題中使用自定義的動畫。以下程式碼演示了一個自定義的主題,該主題可以全域性使用這些過渡動畫。
res/values/styles.xml
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="ActivityTheme" parent="@style/Theme.AppCompat.Light"> <item name="android:windowAnimationStyle">@style/ActivityAnimation</item> </style> <style name="ActivityAnimation" parent="@android:style/Animation.Activity"> <item name="android:activityOpenEnterAnimation">@anim/activity_open_enter</item> <item name="android:activityOpenExitAnimation">@anim/activity_open_exit</item> <item name="android:activityCloseEnterAnimation">@anim/activity_close_enter</item> <item name="android:activityCloseExitAnimation">@anim/activity_close_exit</item> </style> </resources>
通過提供一個主題的自定義屬性android:windowAnimationStyle值,我們可以自定義這些過渡動畫。引用框架的父樣式也很重要,因為這4種動畫並不是該樣式中唯一定義的內容,否則可能會無意中去除現有的一些視窗動畫。
2. 支援Fragment
自定義Fragment間的過渡動畫會有些不同,這取決於你是否使用了支援庫。這是因為原生的Fragment使用了新的Animator物件,該物件在支援庫的Fragment版本中是不支援的。
使用支援庫時,可以通過呼叫setCustomAnimations()覆寫單個FragmentTransaction的過渡動畫。該方法的接受兩個引數的版本可以設定新增/替換/移除動作時的動畫效果,但在介面棧回退時不能設定相應的動畫。該方法的接受4個引數的版本則可以為介面棧的回退新增自定義的動畫。還是使用之前示例中一樣的Animation物件,下面的程式碼顯色瞭如何將這些動畫新增到FragmentTransaction中:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); //首先必須呼叫該方法! ft.setCustomAnimations(R.anim.activity_open_enter,R.anim.activity_open_exit, R.anim.activity_close_enter, R.anim.activity_close_exit); ft.replace(R.id.container_fragment, fragment); ft.addToBackStack(null); ft.commit();
重點:
setCustomAnimations()必須在add()、replace()和其他動作方法之前呼叫,否則動畫將不會執行。最好是在每個事務程式碼塊開始時就呼叫該方法。
如果希望對某個Fragment一直使用同樣的動畫,可能需要覆寫Fragment中的onCreateAnimation()方法。以下程式碼顯示了使用了這種方式定義的Fragment動畫。
使用自定義動畫的Fragment
public class SupportFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { TextView tv = new TextView(getActivity()); tv.setText("Fragment"); tv.setBackgroundColor(Color.RED); return tv; } @Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { switch (transit) { case FragmentTransaction.TRANSIT_FRAGMENT_FADE: if (enter) { return AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in); } else { return AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out); } case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE: if (enter) { return AnimationUtils.loadAnimation(getActivity(), R.anim.activity_close_enter); } else { return AnimationUtils.loadAnimation(getActivity(), R.anim.activity_close_exit); } case FragmentTransaction.TRANSIT_FRAGMENT_OPEN: default: if (enter) { return AnimationUtils.loadAnimation(getActivity(), R.anim.activity_open_enter); } else { return AnimationUtils.loadAnimation(getActivity(), R.anim.activity_open_exit); } } } }
Fragment的動畫行為和FragmentTransaction的設定有很大的關係。有很多的過渡值可以通過setTransition()方法關聯到事務上。如果沒有呼叫setTransition(),Fragment就無法知道開啟動畫集和關閉動畫集的區別,因此我們唯一知道的就是執行進入動畫還是退出動畫。
要獲得之前通過setCustomAnimations()實現相同的效果,需要將事務的過渡值設為TRANSIT_FRAGMENT_OPEN。這時會使用這個過渡值呼叫初始的事務,但同時會通過TRANSIT_FRAGMENT_CLOSE呼叫介面棧回退動作,這樣就允許Fragment提供不同的動畫。以下程式碼片段演示瞭如何用這種方式構造一個事務:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); //設定過渡值來觸發相應的動畫 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.replace(R.id.container_fragment, fragment); ft.addToBackStack(null); ft.commit();
Fragment還有第三個狀態,這在Activity中是沒有的,它是通過TRANSIT_FRAGMENT_FADE過渡值定義的。這個動畫是在過渡行為不再是變化的一部分時出現的,例如新增或替換但在Fragment隱藏或顯示是不會出現。在這個示例中,我們使用標準的系統漸變動畫來詮釋這種情形。
3.本地Fragment
如果應用程式的目標版本是API Level 11 或之後版本,則不必使用支援庫中的Fragment,而且這種情況下的自定義動畫程式碼會稍微有些不同。本地Fragment實現使用相對較新的Animator物件(而非舊的Animator物件)來建立過渡動畫。
這需要對程式碼做一些修改;首先,需要使用Animator來定義所有的XML動畫。參見以下四段程式碼:
res/animator/fragment_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <objectAnimator android:valueFrom="0" android:valueTo="-90" android:valueType="floatType" android:propertyName="rotation" android:duration="500"/> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:valueType="floatType" android:propertyName="alpha" android:duration="500"/> </set>
res/animator/fragment_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <objectAnimator android:valueFrom="90" android:valueTo="0" android:valueType="floatType" android:propertyName="rotation" android:duration="500"/> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:valueType="floatType" android:propertyName="alpha" android:duration="500"/> </set>
res/animator/fragment_pop_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <objectAnimator android:valueFrom="0" android:valueTo="90" android:valueType="floatType" android:propertyName="rotation" android:duration="500"/> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:valueType="floatType" android:propertyName="alpha" android:duration="500"/> </set>
res/animator/fragment_pop_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" > <objectAnimator android:valueFrom="-90" android:valueTo="0" android:valueType="floatType" android:propertyName="rotation" android:duration="500"/> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:valueType="floatType" android:propertyName="alpha" android:duration="500"/> </set>
除了語法的細微區別,這些動畫幾乎和之前建立的動畫一模一樣。其他僅有的差別是,這些動畫被設定為圍繞在檢視的中心(預設行為)而不是左上角。
和以前一樣,可以通過setCustomAnimations()直接設定某個FragmentTransaction單獨的過渡動畫;但是,新的版本使用了我們的Animator例項。下面的程式碼片段使用新API實現了這一過程:
FragmentTransaction ft = getFragmentManager().beginTransaction(); //首先必須呼叫該方法! ft.setCustomAnimations(R.animator.fragment_enter,R.animator.fragment_exit,R.animator.fragment_pop_enter, R.animator.fragment_pop_exit); ft.replace(R.id.container_fragment, fragment); ft.addToBackStack(null); ft.commit();
如果想要對某一特定子類總是使用相同的過渡動畫,可以像以前一樣自定義Fragment。但本地Fragment沒有onCreateAnimation()方法,而是使用了onCreateAnimator()。檢視以下程式碼,它使用新的API重新定義了我們建立的Fragment。
自定義過渡動畫的本地Fragment
public class NativeFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { TextView tv = new TextView(getActivity()); tv.setText("Fragment"); tv.setBackgroundColor(Color.BLUE); return tv; } @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { switch (transit) { case FragmentTransaction.TRANSIT_FRAGMENT_FADE: if (enter) { return AnimatorInflater.loadAnimator(getActivity(), android.R.animator.fade_in); } else { return AnimatorInflater.loadAnimator(getActivity(), android.R.animator.fade_out); } case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE: if (enter) { return AnimatorInflater.loadAnimator(getActivity(), R.animator.fragment_pop_enter); } else { return AnimatorInflater.loadAnimator(getActivity(), R.animator.fragment_pop_exit); } case FragmentTransaction.TRANSIT_FRAGMENT_OPEN: default: if (enter) { return AnimatorInflater.loadAnimator(getActivity(), R.animator.fragment_enter); } else { return AnimatorInflater.loadAnimator(getActivity(), R.animator.fragment_exit); } } } }
同樣,會像支援庫示例一樣的檢查同樣的過渡值,我們只是返回Animator例項。下面同樣的程式碼片段可以使用過渡值集開始一個事務:
FragmentTransaction ft = getFragmentManager().beginTransaction(); //設定過渡值觸發相應的動畫 ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.replace(R.id.container_fragment, fragment); ft.addToBackStack(null); ft.commit();
將這些自定義動畫全域性地應用到整個應用程式的最終方式就是將這些動畫關聯到應用程式的主題上。以下程式碼顯示了使用我們的Fragment動畫的自定義主題。
res/values/styles.xml
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="NativeFragmentTheme" parent="android:Theme.Holo.Light"> <item name="android:windowAnimationStyle">@style/FragmentAnimation</item> </style> <style name="FragmentAnimation" parent="@android:style/Animation.Activity"> <item name="android:fragmentOpenEnterAnimation">@animator/fragment_enter</item> <item name="android:fragmentOpenExitAnimation">@animator/fragment_exit</item> <item name="android:fragmentCloseEnterAnimation">@animator/fragment_pop_enter</item> <item name="android:fragmentCloseExitAnimation">@animator/fragment_pop_exit</item> <item name="android:fragmentFadeEnterAnimation">@android:animator/fade_in</item> <item name="android:fragmentFadeExitAnimation">@android:animator/fade_out</item> </style> </resources>
正如你所看到的,一個主題預設的Fragment動畫屬性就是相同的windowAnimationStyle屬性的一部分。因此,我們在自定義時要保證繼承自同樣的父樣式,否則會移除其他的一些系統預設效果(例如Activity過渡動畫)。必須依然要在FragmentTransaction中請求相應的過渡型別,從而觸發相應的動畫。
如果想要在主題中同時自定義Activity和Fragment的過度動畫,可以將它們一起放到一個相同的自定義樣式中(參見以下程式碼)。
res/values/styles.xml
<resources> <style name="AppTheme" parent="@style/Theme.Holo.Light"> <item name="android:windowAnimationStyle">@style/TransitionAnimation</item> </style> <style name="TransitionAnimation" parent="@android:style/Animation.Activity"> <item name="android:activityOpenEnterAnimation">@anim/activity_open_enter</item> <item name="android:activityOpenExitAnimation">@anim/activity_open_exit</item> <item name="android:activityCloseEnterAnimation">@anim/activity_close_enter</item> <item name="android:activityCloseExitAnimation">@anim/activity_close_exit</item> <item name="android:fragmentOpenEnterAnimation">@animator/fragment_enter</item> <item name="android:fragmentOpenExitAnimation">@animator/fragment_exit</item> <item name="android:fragmentCloseEnterAnimation">@animator/fragment_pop_enter</item> <item name="android:fragmentCloseExitAnimation">@animator/fragment_pop_exit</item> <item name="android:fragmentFadeEnterAnimation">@android:animator/fade_in</item> <item name="android:fragmentFadeExitAnimation">@android:animator/fade_out</item> </style> </resources>
警告:
對主題新增的Fragment過渡動畫只會作用於本地Fragment實現。支援庫中的Fragment因為早期的平臺版本並沒有這些屬性,所有也找不到這些屬性。