原文地址
Android Shared-Element Transitions for all
我們都希望我們的app有自己特殊的地方,轉場動畫就是一個比較好的方式讓用戶記住我們的應用。在Lollipop+ 上的版本實現起來十分的簡單,但是如果想兼容低于5.0的版本,你或許需要檢查Android系統的版本來做一些功能上的削減,或者你可以勇敢的手動來實現這個轉換,瘋狂的想法,但是我們可以來這么嘗試一下。
共享元素變換步驟
當你想要從一個Activity A轉換到Activity B,而且他們共享一個元素(比如是一個view),在這種場景下,最好的用戶體驗可能就是將共享的元素直接變換到最終的地方和大小,這會使用戶專注于應用而且有一種連貫性的表達。那么怎么實現這樣的過渡呢?可能需要一些步驟
- Activity A解析共享元素的開始值然后通過intent傳遞給Activity B
- Activity B開始的時候是完全透明的
- Activity B從Bundle里取出值并準備場景
- Activity B開始做共享元素變換的動畫
我將會在這篇文章中展示如何實現這些步驟的細節和一些示例代碼。首先,先進行命名,我們將在 Activity A中的共享view稱之為origin view(初始視圖),并將Activity B中的共享view稱之為destination view(目標視圖)。雖然這兩個view被稱之為共享view,但實際上他們只是碰巧有相同內容的完全不相關的view。
Activity A解析開始值并傳遞給Activity B
當我們想要創建一個從Activity A到Activity B轉換的視覺過渡效果,第一步就是解析出兩個Activity中的共享視圖內開始和結束值這樣,后期需要這個數據來做變換。
在Activity B中我們只能解析后目標視圖的屬性,對于初始視圖的屬性需要通過intent傳遞過來
Intent intent = new Intent(context, ArticleImageActivity.class);
intent.putExtra(IMAGE_URL_EXTRA, imageUrl);
intent.putExtra(VIEW_INFO_EXTRA, /* start values */ captureValues(originView));
startActivity(intent);
overridePendingTransition(0, 0);
為了去掉默認的轉場效果,我們需要在 startActivity
后面調用一次 overridePendingTransition(0,0)
,然后就能實現我們自己的效果。
這個 captureValues(View)
的方法會將view的大小和位置打包成一個bundle返回出來
private Bundle captureValues(@NonNull View view) {
Bundle b = new Bundle();
int[] screenLocation = new int[2];
view.getLocationOnScreen(screenLocation);
b.putInt(PROPNAME_SCREENLOCATION_LEFT, screenLocation[0]);
b.putInt(PROPNAME_SCREENLOCATION_TOP, screenLocation[1]);
b.putInt(PROPNAME_WIDTH, view.getWidth());
b.putInt(PROPNAME_HEIGHT, view.getHeight());
return b;
}
Activity B設置背景透明
在startActivity之后就跳轉到了Activity B中,但是我們不希望用戶一開始就能看到整個布局直到我們的過渡動畫準備好開始運動。解決方法十分的簡單,就是保證這個activity一開始是透明的,而且將layout都設置為 INVISIBLE
。
<style name="Transparent" parent="AppTheme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
Activity B場景布置
這一步是最為重要的,首先,確保所有的資源文件準備完畢,假設這個共享view是一個 ImageView 而且圖片已經從URL中加載出來了。這個并不難可以使用比如 Picasso, Glide, 或者其他的library。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_article_image);
...
// start the information passed in the Bundle
extractViewInfoFromBundle(getIntent());
// only now we load the image
Picasso.load(mImageUrl)
.into(mDestinationView, new Callback() {
@Override
public void onSuccess() {
// we've got the image loaded, we can start prepping the scene
onUiReady();
}
@Override
public void onError() {...}
});
...
}
這個 onUiReady()
方法會在所有的資源獲取之后準備界面以及過渡動畫的實現,現在已經通過intent傳遞過來了初始視圖的屬性值,然后我們需要拿到目標視圖的屬性值,所以需要一個在view被layout之后還沒有draw之前的時機,官方給我們提供了一個很好的回調方式 onPreDraw()
private void onUiReady() {
mDestinationView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// remove previous listener
mDestinationView.getViewTreeObserver().removeOnPreDrawListener(this);
// prep the scene
prepareScene();
// run the animation
runEnterAnimation();
return true;
}
});
}
在 prepareScene()
中我們需要拿到目標視圖的屬性,并且根據在屏幕上的位置和大小做一個差值的屬性轉換
private void prepareScene() {
// capture the end values in the destionation view
mEndValues = captureValues(mDestinationView);
// calculate the scale and positoin deltas
float scaleX = scaleDelta(mStartValues, mEndValues);
float scaleY = scaleDelta(mStartValues, mEndValues);
int deltaX = translationDelta(mStartValues, mEndValues);
int deltaY = translationDelta(mStartValues, mEndValues);
// scale and reposition the image
mDestinationView.setScaleX(scaleX);
mDestinationView.setScaleY(scaleY);
mDestinationView.setTranslationX(deltaX);
mDestinationView.setTranslationY(deltaY);
}
準備好之后就是設置過渡動畫了
Activity B過渡動畫
目標視圖現在和初始視圖在同樣的地方大小也一樣,然后我們做動畫配合千米那設置的屬性變換來讓它移動到最后的位置,并且縮放到適當的大小
private void runEnterAnimation() {
// We can now make it visible
mDestinationView.setVisibility(View.VISIBLE);
// finally, run the animation
mDestinationView.animate()
.setDuration(DEFAULT_DURATION)
.setInterpolator(DEFAULT_INTERPOLATOR)
.scaleX(1f)
.scaleY(1f)
.translationX(0)
.translationY(0)
.start();
}
在我們需要從Activity B返回到Activity A,只需要再做一個反轉的動畫,你可以在 onBackPressed()
的回調或者是其他會返回到前面的Activity的地方調用下面這段代碼。
private void runExitAnimation() {
mDestinationView.animate()
.setDuration(DEFAULT_DURATION)
.setInterpolator(DEFAULT_INTERPOLATOR)
.scaleX(scaleX)
.scaleY(scaleY)
.translationX(deltaX)
.translationY(deltaY)
.withEndAction(new Runnable() {
@Override
public void run() {
finish();
overridePendingTransition(0, 0);
}
}).start();
}
到這里整個步驟就結束了,而且這種方式能兼容所有的Android版本,不限于5.0以上,下面的采取這種方式實踐的效果。
備注
推薦一個圖片手勢操作的library,其中包含了這個共享元素的使用,可以看一下具體的實現方式