本篇文章已授權微信公眾號 安卓巴士Android開發者門戶 獨家發布
emmmm,這次來梳理一下 Activity 切換動畫的研究。首先,老規矩,看一下效果圖:
效果圖
這次要實現的動畫效果就是類似於上圖那樣,點擊某個 view,就從那個 view 展開下個 Activity,Activity 退出時原路返回,即縮放到點擊的那個 view。
實現思路
emmm,如果要你來做這樣一個效果,你會怎么做呢?
我們就一步步的來思考。
首先來說說,要給 Activity 的切換寫動畫的話,可以通過什么來實現?也許這種場景比較少,但相信大家多多少少知道一些,嗯,如果你還是不大清楚的話,可以先看看這篇實現Activity跳轉動畫的五種方式,這個大神總結了幾種方式,大概過一下有哪些方案即可,我也沒深入閱讀,感興趣的話再慢慢看就可以了。
這里就大概總結一下幾種方式:
1.使用 style 的方式定義 Activity 的切換動畫
2.使用 overridePendingTransition 方法實現 Activity 跳轉動畫
3.使用 ActivityOptions 切換動畫實現 Activity 跳轉動畫(部分動畫可支持到 api >= 16)
4.使用 ActivityOptions 動畫共享組件的方式實現跳轉 Activity 動畫(api >= 21)
目前我了解的也大概就是以上幾種方式,前兩種使用方式很簡單,只需要在 xml 中寫相應的動畫(滑進滑出動畫、漸變動畫、放大動畫等),然后應用到相應的 activity 即可。而且還不需要考慮兼容低版本問題。
<!--style方式-->
<item name="android:activityOpenEnterAnimation">@anim/anim_activity_enter</item>
<item name="android:activityCloseExitAnimation">@anim/anim_activity_exit</item>
//代碼方式
startActivity(intent)
overridePendingTransition(R.anim.anim_activity_enter, R.anim.anim_activity_exit);
//anim_activity_enter.xml 和 anim_activity_exit.xml 就是在 xml 中寫動畫
上述兩種方式使用很簡單,效果也很好。缺點就是,不夠靈活,只能實現 xml 寫出的動畫,即平移、漸變、縮放等基本動畫的組合,無法實現炫酷的動畫。
所以,顯然,我們開頭效果圖展示的動畫,用這兩種 xml 實現的動畫方式並沒有辦法做到,因為放大動畫的中心點位置是需要動態計算的。xml 中寫縮放動畫時,中心點只能是寫死的。
這樣的話, style 的動畫方案和 overridePendingTransition 的方案就只能先拋棄了,那么再繼續看看其他的方案。
ActivityOptions 動畫實現方案應該是 Google 在 Android 5.0 之后推出 Material Design 系列里的一個轉場動畫方案。當然,Google 在后續也推出了一些內置動畫,方便開發者直接使用。
上圖就是 Google 推出的 Material Design 規范的動畫實現里一個示例。關於 Android 5.0 后的動畫,網上一大堆相關文章,我也沒在這方面里去深入研究過,所以這里就不打算介紹動畫要怎么用(不然誤導大家就不好了),感興趣的可以自己去網上找找哈,這里就說下如果要實現開頭介紹的動畫,用這種方式可行不可行,可行的話又該怎么做。
Android 5.0+ Activity 轉場動畫
開個小標題,因為覺得下面會講比較多的東西。
開頭效果圖的動畫:新的 Activity 在點擊的 View 的中心點放大。
看上圖 MaterialDesign 動畫示例中,好像動畫效果也是某個 View 展開下個 Activity?那這么說的話,這種方式應該就是可行的了?
對 5.0+ 動畫有所了解的話,示例中的動畫應該有個名稱叫:共享元素切換動畫。意思就是字面上說的,兩個 Activity 切換,可以設置它們的共享元素,也就是可以讓上個界面的某個 View 在下個界面上做動畫的一種效果。
既然這樣,我們就先來看看 5.0+ 動畫,用代碼怎么寫。
那些動畫要怎么實現的我們就不看了,直接看怎么使用。上圖的代碼是個例子,如果要使用 5.0+ 的 Activity 轉場動畫,那就不能再繼續使用 startActivity(Intent intent) 了,而是要使用 startActivity(Intent intent, Bundle options) 這個方法了。而 options 參數要傳入的就通過 ActivityOptions 類指定的一些轉場動畫了,Google 為我們封裝了一些動畫接口,我們就來看看它支持哪些轉場。
所以,下面就來講講 makeScaleUpAnimation() 放大動畫和 makeSceneTransitionAnimation() 共享元素動畫。因為好像只有這兩個可以實現開頭效果圖展示的動畫效果。
對了,上上圖中的 ActivityOptionsCompat 類作用的 ActivityOptions 一樣,只是前者是 Google 為我們提供的一個兼容實現,因為這是 5.0+ 動畫,那么在 5.0 以下的版本就不能使用了,所以 Google 提供了兼容處理,讓有些動畫可以支持更低版本,動畫效果都一致,至於內部具體是怎么實現,有興趣可以去看看。但也不是所有的動畫都做到兼容處理的,像 ActivityOptions 提供的幾種動畫,基本都可以兼容,但共享元素動畫就不行了。至於哪些動畫可以兼容,哪些不行,打開 ActivityOptionsCompat 類就清楚了,這個類在 support v4包里,下面就貼張圖看看:
makeScaleUpAnimation()
接口參數的作用都在上圖里注釋了,理解了之后有沒有發現,這個接口實現的動畫效果就是我們想要的!!從哪放大,寬高從多少開始放大都可以自己設定,完美是不是!
不是,還是別高興太早了,這個接口確實可以實現點擊哪個 View,就從哪個 View 放大的效果。但是返回呢,Activity 退出時要按原路縮小至點擊的 View,這個要怎么做?是吧,找遍了所有接口都沒有。
不止這點,還有我們常見的 setDuration() 有找到么,setInterpolator() 有找到么?沒有,都沒有,也就是說如果要用這個接口做動畫的話,動畫的執行時間,還有插值器我們都沒辦法設置,那這肯定沒法滿足產品的需求啊,哪里有不修改執行時間和插值器的動畫!所以,這個方案也拋棄。
makeSceneTransitionAnimation()
共享元素動畫就復雜多了,不管是我們要使用它的方式還是它內部做的事。總之,我對這個接觸也不多,這里就大概概括一下使用的一些步驟:
-
需要對共享的元素設置 transitionName,在 xml 中設置 android:transitionName 或代碼里調用 View.setTransitionName()。
-
startActivity(Intent) 換成 startActivity(Intent intent, Bundle options),options 需要通過 ActivityOptions.makeSceneTransitionAnimation() 設置。
優點和缺點一會再說,先看看效果:
效果貌似就是我們想要的,那我們就來說說這種方式的優缺點,然后再做決定。
優點:
- 進入和退出時的動畫都是由內部實現了,我們只需要設置參數就行。
缺點:
-
共享的元素需要設置相同的 transitionName,我們點擊的 View 和打開的 Activity 是動態的,不確定性的。所以,如果對這些 View 都設置相同的 transitionName 不知道會不會有新的問題產生。
-
新 Activity 的起始寬高和位置無法設置,默認位置是共享的 View,也可以理解成點擊的 View,這點沒問題。但起始寬高默認是點擊 View 的大小,上面 gif 圖演示可能效果不太好。也就是說,放大動畫開始時,新 Activity 是從點擊 View 的寬高作為起始放大至全屏,返回時從全屏縮小至點擊 View 的寬高。上圖中點擊的 view 都很小,所以看不出什么,但在 Tv 應用的頁面中,經常有那種特別大的 view,如果是這種情況,那動畫就很難看了。
-
第2點缺點也許可以自己寫繼續 Transition 寫動畫來解決,但沒研究過共享動畫的原理,還不懂怎么修改。
-
最大的缺點是只支持 api >= 21 的。
基於目前能力不夠,不足以解決以上缺點所列問題,所以暫時拋棄該方案,但后期會利用時間來學習下 5.0+ 轉場動畫原理。
emmm,這樣一來,豈不是就沒辦法實現效果圖所需要的動畫了?別急,方案還是有的,繼續往下看。
Github 開源庫方案
其實,Github 上有很多這種動畫效果的開源庫,我找了幾個把項目下載下來看了下代碼,發現有的人思路是這樣的:
Activity 跳轉時,先把當前界面截圖,然后將這張圖傳給下個 Activity,然后下個 Activity 打開時將背景設置成上個界面截圖傳過來的圖片,然后再對根布局做放大動畫,動畫結束后將背景取消掉。
Activity 退出時有兩種方案:
方案一:將當前 Activity 背景設置成上個界面的截圖(這需要對這張圖片進行緩存處理,不然圖片很大可能已經被回收了),然后對根布局做縮小動畫,動畫結束之后再執行真正的 finish() 操作。
方案二:將當前 Activity 界面截圖,然后傳給新展示到界面的 Activity,然后做縮小動畫。(這需要 Activity 有一個置於頂層的 View 來設置截圖為背景,然后對這個 View 做動畫。
用 View 動畫來實現 Activity 轉場動畫效果
(該集中注意力啦,親愛的讀者們,上面其實都是廢話啦,就是我自己在做這個動畫效果過程中的一些摸索階段啦,跟本篇要講的動畫實現方案其實關系不大了,不想看廢話的可以略過,但下面就是本篇要講的 Activity 切換動畫的實現方案了)
受到了 Github 上大神開源庫的啟發,我在想,Activity 界面其實也就是個 View,那既然這樣我要打開的 Activity 設置成透明的,然后對根布局做放大動畫,這樣不就行了?
想到就做,先是在 style.xml 中設置透明:
<item name="android:windowBackground">@android:color/transparent</item>
然后實例化一個放大動畫:
ScaleAnimation scaleAnimation = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, x, y);
寬高從 0 開始放大至全屏,x,y 是放大的中心點,這個可以根據點擊的 View 來計算,先看看效果行不行,x,y 就先隨便傳個值。
動畫也有了,那需要找到 Activity 的根布局。想了下,這動畫的代碼要么是寫在基類里,要么是寫個專門的輔助類,不管怎樣,代碼都需要有共用性,那怎么用相同的代碼找到所有不同 Activity 的根布局呢?
規定一個相同的 id,然后設置到每個 Activity 布局文件的第一個 ViewGroup 里?---是可行,但太麻煩了,要改動的地方也太多了。
別忘了,每個 Activity 最底層就是一個 DecorView,雖然這個 DecorView 沒有 id,但我們可以通過 getWindow().getDecorView() 來獲取到它的引用啊。
再不然,我們 setContentLayout() 都是將自己寫的布局文件設置到一個 FrameLayout 里,記得吧,這個 FrameLayout 是有 id 的,是 Window 的一個靜態常量 ID_ANDROID_CONTENT, 所以我們可以通過下面方式來獲取到:
View view = activity.findViewById(activity.getWindow().ID_ANDROID_CONTENT);
//View view = activity.getWindow().getDecorView();
透明屬性,動畫,View 都有了,那接下去就是執行了,在哪里執行好呢,onCreate() 里或 onStart() 里應該都可以。那就先在 onCreate() 里執行試試看好了。
噢,對了,很重要一點,別忘了,Activity 轉場是有默認動畫的,不同系統可能實現的不同,所以得把這個默認動畫關掉,所以可以在 BaseActivity 里重寫下 startActivity(),如下:
@Override
public void startActivity(Intent intent) {
super.startActivity(intent);
overridePendingTransition(0, 0);
}
overridePendingTransition(0, 0) 傳入 0 表示不執行切換動畫,呈現出來的效果就是下個 Activity 瞬間就顯示在屏幕上了,而我們又對下個 Activity 設置了寬高從 0 開始放大的效果,那么理想中實現的效果應該是:當前 Activity 呈現在界面上,然后下個 Activity 逐漸放大到覆蓋住全屏。
好,運行,看下效果:
咦~,為什么周圍會是黑色的呢,都設置了 windowBackground 是透明的了啊,emmm,上網查了下,發現還需要一個半透明屬性 windowIsTranslucent,所以去 style.xml 中再加上:
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
再運行試一下,看下效果:
嗯,效果出來了。那就下去就是退出時的動畫了。退出動畫跟打開動畫其實就是反過程,動畫變成縮小動畫:
ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f, x, y);
之前從 0 開始放大,現在換成從全屏開始縮小,x,y 就保存在 intent 攜帶的數據里。那么也就只剩最后一個問題,縮小動畫該什么時候執行呢?
我們退出一個頁面時一般都是用 finish() 的吧,既然這樣,在基類里重寫一下這個方法:
@Override
public void finish() {
ActivityAnimationHelper.animScaleDown(this, new AbsAnimationListener() {
@Override
public void onAnimationEnd() {
BaseActivity.super.finish();
}
});
}
x,y 的計算,動畫的實現、執行我都是寫在一個輔助類里,然后在 BaseActivity 里調用。這個不重要,思想比較重要。我們重寫了 finish(),然后去執行縮小動畫,同樣動畫是應用在 Activity 的根布局,然后寫一個動畫進度的回調,但動畫結束時再去調用 super.finish()。也就是說,但調用了 finish() 時,實際上 Activity 並沒有 finish() 掉,而是先去執行縮小動畫,動畫執行完畢再真正的去執行 finish() 操作。
至此,開頭所展示的效果圖的動畫效果已經實現。
但你以為事情做完了么?不,填坑之路才剛開始!(哭喪臉)
優化之路,又名填坑之路
我前面說過,這種方案只能算是一種暫時性的替代方案,知道我什么這么說么?因為這種方案實現是會碰到太多坑了。
1.動畫的流暢性問題
首先是動畫的流暢性問題,本篇里演示的 gif 圖之所以看起來還很流暢,是因為切換的兩個 Activity 界面都太簡單了,但界面布局復雜一點時,打開一個 Activity 界面的測量、布局、繪制以及我們在 onCreate() 里寫的一些加載數據、網絡請求操作跟放大動畫都擠到一起去了,甚至網絡請求回來后更新界面時動畫都還有可能在執行中,這樣動畫的流暢性就更慘了。
在優化時,找到一個大神的一篇文章:一種新的Activity轉換動畫實現方式
這篇文章里講的實現原理正是本篇介紹的方案,而且講得更詳細,可以繼續去這篇看一下,相信你對本篇介紹的方案會更理解。
有一點不同的是,大神的放大動畫的執行時機是在 onPreDraw() 時機開啟的,如下:
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
if (view.getAnimation() == animation && !animation.hasEnded()) {
return false;
}
view.startAnimation(animation);
return true;
}
});
emmm,說實話,這個回調第一次見,我也不大清楚它的回調時機是什么,作用是什么,網上的解釋也摸棱兩可,沒看明白,待后續有時間自己看看源碼好了。
但我可以跟你們肯定的是,我看了一部分 5.0+ 動畫源碼,它內部也是在一個 Activity 的 onStart() 方法里注冊了 onPreDraw() 回調監聽,然后在回調時執行 5.0+ 的動畫。但它內部做的事,遠不止這些,實在是太多了,估計是進行的一些優化操作,我目前是還沒有能力去搞懂。
但我們動畫執行的時機是需要換一下了,想一下也知道,在 onCreate() 里做動畫,聽着就感覺有點奇怪。既然大神,還有 Google 官方都是在 onPreDraw() 里執行,那我們當然可以模仿學習。
看 5.0+ 源碼過程中,發現它在動畫開始和結束前會調用一個 ViewGroup 的 suppressLayout() 方法,這個方法隱藏的:
這是一個隱藏的方法,我們要調用的話,就需要通過反射的方式。這個方法的注釋大概是說禁止 ViewGroup 進行 layout() 操作。這樣的話,我們有一個可以優化的地方,我們可以在動畫開始時調用這個方法禁止 layout() 操作,動畫結束時恢復。
這樣做的好處是,動畫執行過程中,如果網絡或本地數據已經回調,通知 adapter 去刷新 view 時,這樣會導致動畫很卡頓。所以,當我們用 suppressLayout() 做了優化之后,就只有等動畫結束的時候界面才會去重新 layout 刷新布局,優化動畫流暢性。
但這樣做也有一個問題是,如果你在 onCreate() 或 onResume() 之類的方法發起一個 requestFocus() 操作的話,很有可能這個操作會被丟棄掉,導致界面理應獲得焦點的 view 發生錯亂問題。
至於原因,因為對 suppressLayout() 也還不是很理解,打算等對 onPreDraw() 理解了之后一起研究一下。
2.windowIsTranslucent 半透明引發的問題
哇,這個屬性,真的是。。。
你們好奇的話,就網上搜一下這個半透明屬性,一堆各種問題。但其實,網上碰到的那些問題,我基本都沒遇到過,但我遇到的是更奇葩,網上沒找到解決方案的問題,哭瞎。
emmm,我是做 Tv 應用開發的,windowIsTranslucent 這個在不同的盒子上表現的效果不一樣,簡直了。
在設置了 android:windowIsTranslucent=true 時,有的盒子界面就會是透明的,即使你設置了一張不透明的背景圖,但透明度不會很明顯。
有的盒子則是在新的 Activity 打開時,如果 view 沒有完全加載出來,則會顯示上個 Activity 的界面,造成的現象就是打開新 Activity 時,會一瞬間閃過上個界面的畫面。
還有,Tv 應用一般都會跟視頻播放有關,那就涉及到播放器。而播放器需要一個 surfaceview,而 surfaceview 遇到半透明屬性時,問題更多。
原因,都不清楚(哎,可悲)。但只要不使用半透明這個屬性,就一切正常了,但如果不用這個屬性,本篇介紹的動畫方案又沒法實現。這真的是魚和熊掌不可兼得啊。
所以,我就在想,既然 windowIsTranslucent 為 false 時,一切正常;為 true 時,動畫正常。那是否有辦法在動畫過程中設置為 true,動畫結束之后設置為 false 呢?如果可以的話,按理來說應該正好解決問題。
但找了半天,沒有找到相關的接口來動態設置這個屬性的值,這個半透明屬性值是設置在 style.xml 里的。網上有一些介紹說:在代碼動態修改 style 的,但打開那些文章你會發現,說的是動態修改,但基本都要求要么在 super.onCreate() 之前調用,要么在 setContentLayout() 之前,要么重寫 setTheme(),這么多限制,那哪里有用。
后來,在找播放器黑屏的問題時,找到一篇大神寫的博客:Android版與微信Activity側滑后退效果完全相同的SwipeBackLayout。
題目雖然看起來跟本篇一點關系都沒有,但作者遇到的問題跟我的問題本質上是一個的,也是 windowIsTranslucent 屬性導致的問題。很開心的是,作者介紹了利用反射去調用 Activity 里的 convertFromTranslucent() 和 convertToTranslucent() 方法來動態修改這個半透明屬性值,這兩個方法是對外隱藏的。
后來,我很好奇 5.0+ 的動畫到底是怎么實現的這種動畫效果,因為它明明不需要設置 windowIsTranslucent 為 true,但它的動畫,Activity 在跳轉時,上個 Activity 是可見的,這是怎么做到的。
我跟蹤了一部分源碼,也很開心的發現,原來它內部也是用的 Activity 里的這兩個方法,在動畫開始前將 Activity 設置成半透明的,動畫結束后設置回去。當然,內部它有權限調用 Activity 的方法,而我們沒有權限,所以只能通過反射來調用。
開心,問題解決了。我們只要通過反射,在動畫開始之前調用 Activity 的 convertToTranslucent() 將 Activity 設置成半透明的,動畫結束再調用 convertFromTranslucent() 設置回去,這樣動畫的效果達到了,又不會因為設置了 windowIsTranslucent 為 true 而引入各種問題。
但是,測試時發現,在 api 21 以下的盒子上,這個方法沒啟作用。
我去查看,比較了下 21 以上和以下 Activity 的代碼,發現 convertToTranslucent() 這個方法它的內部實現是不一樣的,21及以上是一套代碼,21以下至19是一套代碼,19以下則是沒有這兩個方法。
后來又仔細看了上面大神那篇文章,發現說,原來 19-21 的版本,這兩個方法要能夠生效的話,需要默認在 style.xml 先將 Activity 設置成半透明的,而 21 及以上的,則不需要。至於19以下的,就完全不能用這個方法了。
解決方法也很簡單,那就在 style.xml 默認設置 Activity 是半透明的,這樣動畫結束之后再設回去就可以了。
但是,這樣播放器就會有問題---黑屏。原因是因為調用了 convertFromTranslucent() 設置不透明,一旦調用這個方法,如果該界面有播放器,那么就會黑屏。至於具體原因,還是不清楚,上面那個大神的文章里也提到了這個現象,但他也不知道如何解決,我也不知道。
最后,為了解決黑屏的問題,只能是如果界面有播放器的話,那個這個界面的動畫就換另外一種方法來實現,至於是什么方案也可以實現開頭介紹的動畫效果,我就不說了,Github 上很多,但都有同一個特點,那就是賊麻煩。
稍微總結一下,本篇提的動畫方案適用於以下幾種場景:
-
如果你的應用設置了 windowIsTranslucent 為 true 時,沒有發現什么問題的話,那恭喜你,該動畫方案可以兼容各種版本。
-
如果你的應用設置了 windowIsTranslucent 為 true 時會有一些問題,但你的應用里沒有播放器的話,那恭喜你,該動畫方案可以兼容 19 及以上版本。
-
如果你的應用設置了 windowIsTranslucent 為 true 時會有一些問題,而且應用里也有播放器的話,那如果你實在走投無路想使用該動畫方案的話,那你再來找我吧,在研究出其他方案之前,咱們一起來慢慢填坑。
注:本篇側重點是介紹一種 Activity 動畫方案的實現思路,注意,是思路!因為本篇所介紹的動畫方案並不成熟,仍有很多坑,所以,學習、探討就可以,慎用!
Github 鏈接
上傳了一個 demo,如果對這種動畫方案感興趣的話,可以去看看代碼。跟動畫有關的代碼都在 ui/anim 文件夾里。
一種 Activity 轉場動畫----點擊哪里從哪放大
遺留問題
老樣子,最后再留幾個問題給大家思考一下(其實我也不懂,還望有大神能解答一下)
Q1:overridePendingTransition() 實現的轉場動畫一點都不卡,但用 View 動畫方案來實現 Activity 轉場動畫有時會有些卡頓,感覺是 Activity 啟動做的那一大堆事跟動畫擠一起了,那 overridePendingTransition() 原理到底是怎么實現?跟着源碼跳進去看感覺有點懵,有時間得再研究一下這部分的源碼。
Q2:Activity 切換時,一般下個 Activity 直接覆蓋在本 Activity 上了,按我的理解,如果對要打開的 Activity 的 window 設置成透明屬性,那應該就可以看到下層的 Activity 才對,為什么不行呢?為什么一定要設置 android:windowIsTranslucent = true 才可以呢?android 5.0 的共享元素動畫很明顯可以看到下個 Activity 在縮放時,上個 Activity 是可見的,那么它又是怎么實現的呢?原理是什么呢?這部分源碼看了一部分了,等理解透了點,在梳理出來。
最近剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支持~~