原文地址
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,其中包含了这个共享元素的使用,可以看一下具体的实现方式
网友评论
PS 我说的是人们用的系统APi实现的