Android 中的轉場動畫及相容處理
Android 中的動畫有很多,除了在一個介面上使用幀動畫、屬性動畫將一個或多個 View 進行動畫處理以外,還可以用於兩個介面之間過渡、跳轉。在 Android 5.0 之前,我們已經有了 overridePendingTransition() 方法來實現一些轉場效果。然而,在 Android 5.0 以後,轉場效果更加炫酷。 比如下面的動畫:
本篇文章,主要就是解說如何實現上述的效果。主要內容包括:
- Android 5.0+ 的轉場動畫
- Android 4.X 模擬實現 Android 5.0+ 轉場效果。
Android 5.0+ 中的轉場動畫
實現轉場動畫只需三步:
- 在 res/
- 在 res/value/style 檔案中為每個 Activity 指定轉場動畫的 style ,並在
AndroidManifest.xml
檔案中為每個 Activity 設定對應的 android:theme。 - 在 Activity 呼叫 startActivity() 切換動畫前,使用 ActivityOptionsCompat 來建立轉場動畫時的共享物件。
下面就來對這三步進行詳細講解。
定義轉場動畫
在 res/ 目錄下建立了 transition 資原始檔夾後,就可以在該資料夾下對每一種動畫進行定義。
一般來說,對 Activity 定義一個過渡動畫可以寫成下面的形式:
<explode xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
</targets>
</explode >
其中,<explode/>
是動畫效果的名稱,Android 5.0(API 級別 21)支援這些進入與退出轉換:
- 分解(explode):從場景中心移入或移出檢視。
- 滑動(slide):從場景邊緣移入或移出檢視。
- 淡入淡出(fade):通過調整透明度在場景中增添或移除檢視。
而每一種動畫效果,都有額外的屬性。比如滑動 slide,可以使用 android:slideEdge="top"
設定滑動的方向;淡入淡出(fade)可以使用 android:fadingMode="fade_in"
設定具體是淡入(fade_in)還是淡出(fade_out)等。
<targets/>
標籤裡面定義需要轉場(或者不需要轉場)的目標 id ,這個 id 可以使系統自帶的,也可以是我們自己檢視中的 view 的 id,每一個 id 需要單獨在 <target/>
標籤中定義,android:targetId 表示目標 ID 需要進行過渡轉換的 view,而 android:excludeId 表示我們不需要該 ID 的 view 進行過渡轉場。上面的那段程式碼的意思是說,除了狀態列和導航欄以外所有的 view,都執行 explode 動畫。
如果我們想要在同一個過渡狀態中實現兩種或多種動畫效果怎麼辦?也簡單,將根標籤替換為 <transitionSet/>
,然後定義每一種動畫效果,最後記得在根標籤中使用 android:transitionOrdering
註明這幾種動畫的演示順序,sequential
表示順序執行,而 together
表示同時執行。比如像下面的程式碼:
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<slide android:slideEdge="bottom">
<targets>
<target android:targetId="@id/cardview"/>
</targets>
</slide>
<fade>
<targets>
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
<target android:excludeId="@id/cardview"/>
</targets>
</fade>
</transitionSet>
這段程式碼的意思就很簡單了,該 xml 定義了兩個過渡動畫,並且同時執行。第一個動畫是針對 id 為 cardView 的 view 進行滑動,第二個動畫將除了狀態列、導航欄和 cardview 以外的 view,進行淡入淡出。
為每個 Activity 定義轉場樣式
這裡的每一種動畫,指的是在進行介面跳轉過渡時,兩個介面的狀態。比如對於 Activity A 和 Activity B 這兩個介面,可能的狀態如下:
- 介面 A 跳轉至介面 B :這時介面 A 是 退出(exit )過渡狀態,而對應的介面B是進入(enter)過渡狀態。
- 介面 B 返回到介面 A :這時介面 A 是重新進入(reenter)過渡,而對應的介面B則是返回(return)過渡。
一般來說,所有的 Activity 過渡動畫都可以定義成如下的形式:
<style name="BaseAppTheme" parent="android:Theme.Material">
<!-- 開啟過渡效果 -->
<item name="android:windowContentTransitions">true</item>
<!-- 指定介面進入/退出動畫效果 -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
<!-- 指定共享元素進入/退出的動畫效果 -->
<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">
@transition/change_image_transform</item>
</style>
當然,你可以不用寫全,比如在我的 Demo 中一個介面的轉場動畫檔案如下:
<style name="AppTheme.Detail">
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowEnterTransition">@transition/detail_enter</item>
</style>
呼叫 ActivityOptionsCompat
轉場動畫是在兩個介面的跳轉返回時發生的,所以,當使用 intent 跳轉介面時,需要呼叫 ActivityOptionsCompat
來指定動畫的執行。
一般來說,呼叫 ActivityOptionsCompat
的模板程式碼如下:
// 建立一個包含過渡動畫資訊的 ActivityOptions 物件
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, getString(R.string.image_transition_name));
// 使用 Intent 跳轉介面,並傳遞共享物件資訊
Intent intent = new Intent(this, DetailActivity.class);
startActivity(intent, optionsCompat.toBundle());
ActivityOptionsCompat 是在support v4 包裡面的,其實它是 ActivityOptions 的一個相容(ActivityOptions是API 16引入的)。
然後,我們需要在第二個 Activity 中,將轉場的圖片獲取並顯示到介面中就可以了。
多個共享元素的過渡實現
有時候我們需要讓多個元素產生動畫效果,可以使用 Pair 來實現:
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, Pair.create(view1, "agreedName1"), Pair.create(view2, "agreedName2"));
手動實現一個轉場動畫
現在市面上,Android 5.0 以下的手機系統還有一定的市場份額,所以為了照顧這些使用者,我們只能手動實現一下共享元素的轉場動畫效果。
實現的思路也比較簡單,大概的步驟如下:
- 確定第一個介面的共享元素,將其資訊傳遞個第二個介面
- 第二個介面接收資訊,開始的時候將介面設定為透明,並只顯示共享元素。
- 將第二個介面的共享元素進行動畫處理。
那麼我們開始一步步實現上面的步驟。
獲取共享元素位置資訊
在第一個介面中,我們需要獲取到共享元素的位置資訊,並將其傳遞給下一個介面。於是乎,我們可以在第一個介面元素點選事件中,這麼寫:
public void imageClick(View view) {
Intent intent = new Intent(AnimeActivity.this, AnimeDetailActivity.class);
// 建立一個 rect 物件來儲存共享元素位置資訊
Rect rect = new Rect();
// 獲取元素位置資訊
view.getGlobalVisibleRect(rect);
// 將位置資訊附加到 intent 上
intent.setSourceBounds(rect);
CustomImage customImage = (CustomImage) view;
intent.putExtra(AnimeDetailActivity.EXTRA_IMAGE, customImage.getImageId());
startActivity(intent);
// 遮蔽 Activity 預設轉場效果
overridePendingTransition(0, 0);
}
其中,getGlobalVisibleRect() 方法的含義是,獲取 可見的狀態列高度+可見的標題欄高度+Rect左上角到標題欄底部的距離,如果標題欄被隱藏了,那麼可見標題欄高度為0。
接下來,就在在第二個介面接收位置資訊並將該圖片展示出來了。
模擬轉場動畫
在第二個介面中,我們需要做如下的操作:
- 獲取上共享元素資訊。
- 計算共享元素縮放比例和位移距離。
- 呼叫動畫,完成模擬轉場效果。
我將上面三個步驟的程式碼如下,你也可以下載我完整的 Demo 來檢視。
private void initial() {
// 獲取上一個介面傳入的資訊
mRect = getIntent().getSourceBounds();
mRescourceId = getIntent().getExtras().getInt(EXTRA_IMAGE);
// 獲取上一個介面中,圖片的寬度和高度
mOriginWidth = mRect.right - mRect.left;
mOriginHeight = mRect.bottom - mRect.top;
// 設定 ImageView 的位置,使其和上一個介面中圖片的位置重合
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mOriginWidth, mOriginHeight);
params.setMargins(mRect.left, mRect.top - getStatusBarHeight(), mRect.right, mRect.bottom);
mImageView.setLayoutParams(params);
// 設定 ImageView 的圖片和縮放型別
mImageView.setImageResource(mRescourceId);
mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
// 根據上一個介面傳入的圖片資源 ID,獲取圖片的 Bitmap 物件。
BitmapDrawable bitmapDrawable = (BitmapDrawable) getResources().getDrawable(mRescourceId);
Bitmap bitmap = bitmapDrawable.getBitmap();
// 計算圖片縮放比例和位移距離
getBundleInfo(bitmap);
// 建立一個 Pallette 物件
mImagePalette = Palette.from(bitmap).generate();
// 使用 Palette 設定背景顏色
mContainer.setBackgroundColor(
mImagePalette.getVibrantColor(ContextCompat.getColor(this, android.R.color.black)));
}
在12行,通過設定 Margin 的形式來確定圖片的位置,需要注意的是,由於狀態列是在父控制元件 FramLayout 之外的,因此我們要將 Rect.top 的值減去狀態列的高度,這樣才是相對於螢幕的絕對位置。然後,getBundleInfo() 方法的程式碼如下:
private void getBundleInfo(Bitmap bitmap) {
// 計算圖片縮放比例,並存儲在 bundle 中
if (bitmap.getWidth() >= bitmap.getHeight()) {
mScaleBundle.putFloat(SCALE_WIDTH, (float) mScreenWidth / mOriginWidth);
mScaleBundle.putFloat(SCALE_HEIGHT, (float) bitmap.getHeight() / mOriginHeight);
} else {
mScaleBundle.putFloat(SCALE_WIDTH, (float) bitmap.getWidth() / mOriginWidth);
mScaleBundle.putFloat(SCALE_HEIGHT, (float) mScreenHeight / mOriginHeight);
}
// 計算位移距離,並將資料儲存到 bundle 中
mTransitionBundle.putFloat(TRANSITION_X, mScreenWidth / 2 - (mRect.left + (mRect.right - mRect.left) / 2));
mTransitionBundle.putFloat(TRANSITION_Y, mScreenHeight / 2 - (mRect.top + (mRect.bottom - mRect.top) / 2));
}
動畫處理
最後我們需要使用動畫來模擬轉場效果,程式碼如下:
private void runEnterAnim() {
mImageView.animate()
.setInterpolator(DEFAULT_INTERPOLATOR)
.setDuration(DURATION)
.scaleX(mScaleBundle.getFloat(SCALE_WIDTH))
.scaleY(mScaleBundle.getFloat(SCALE_HEIGHT))
.translationX(mTransitionBundle.getFloat(TRANSITION_X))
.translationY(mTransitionBundle.getFloat(TRANSITION_Y))
.start();
}
很簡單,至此,入場動畫效果基本模擬完畢。
而退場動畫就更簡單了,直接上程式碼:
private void runExitAnim() {
mImageView.animate()
.setInterpolator(DEFAULT_INTERPOLATOR)
.setDuration(DURATION)
.scaleX(1)
.scaleY(1)
.translationX(0)
.translationY(0)
.withEndAction(new Runnable() {
@Override
public void run() {
finish();
overridePendingTransition(0, 0);
}
})
.start();
}
當然,這只是簡單的模擬,如果想要和 Android 5.0+ 的轉場效果相同,還需要做很多的動畫處理。
本篇文章的 Demo 地址:Demo