- 原文链接:Activity Split Animation
- 原文作者: Udi Cohen
- 译者: 小鄧子
- 校对者: 程序亦非猿
- 状态: 完成
这周,正好有时间可以写一个小而酷的Activity过渡动画。
在切换不同Activity时,系统级过渡动画是作用于整个Activity的,而我想要实现的动画效果是将Activity A分割成两部分,然后将他们向外推开,最后呈现Activity B。gif图效果如下:
我的思路很简单:
- Activity A保存为bitmap
- 把bitmap分割成两个子bitmap
- 子bitmap传递至Activity B
- 在Activity B的布局之上显示两个子bitmap
- 使用动画向外移出两个子bitmap
- Activity B呈现在用户眼前 :)
可是实现起来,并不如我预期的那样简单。我遇到了一些困难,但最终我找到了所有问题的解决办法。接下来,就让我们一步步搞定它。
提示:这种实现方式需要保存整个屏幕的内容为bitmap(译者注:源码中,作者只是保存了android.R.id.content下的内容作为bitmap,并非整个screen)。对于低内存或者大屏幕的设备来说,可能是很大的开销。如果你依然选择使用,请小心,并且不要过度使用。
保存Bitmap
为了得到整个Activity的图片,可以使用以下代码:
1 2 3 |
View root = currActivity.getWindow().getDecorView().findViewById(android.R.id.content); root.setDrawingCacheEnabled(true); mBitmap = root.getDrawingCache(); |
第一行代码中,首先拿到Activity的根View,然后通过android.R.id.content
得到一个FrameLayout,这个FrameLayout存在于每一个Activity中,并且包含了setContentView( )
中放入的布局。下图是用 HierarchyViewer观察时的样子。
为了获取根View或其他任何View视图的bitmap,可以通过调用getDrawingCache( )方法,它将返回一个缓存bitmap,但前提是这个View允许绘图缓存,这就是为什么在获取缓存bitmap之前调用 setDrawingCacheEnabled( )的原因。如果View没有缓存bitmap,则会立即创建。
分割Bitmap
分割bitmap的代码如下:
1 2 |
Bitmap mBmp1 = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), splitYCoord); Bitmap mBmp2 =Bitmap.createBitmap(bmp, 0, splitYCoord, bmp.getWidth(), bmp.getHeight() - splitYCoord); |
bmp
是整个activity A的缓存bitmap,splitYCoord
是Y轴的分割点。
生成的两个子bitmap, mBmp1
是bmp
的上半部分,mBmp2
是bmp
的下半部分,它们的高度大小取决于分割点splitYCoord
传递子bitmap到下一个Activity
得到两个子bitmap之后,我希望跳转到下一个Activity时候把就它们放在要展示的Activity的布局之上,这样用户看到的依然是Activity A的布局,而事实上程序已经跳转到Activity B了。
起初,我想将他们作为Intent的Extras传递过去,因为bitmap实现了Parcelable接口,所以理论上来说这是可行的。但是问题来了,受限于IPC的容量限制,子bitmap太大了以至于不能在Intent中传递,这是我得到的错误log:
!!! FAILED BINDER TRANSACTION !!!
还有一些其他方法,比如将子bitmap写入文件,然后在另一端读出。但是我发现,最简单的实现方式,就是将他们以成员变量的形式放到一个公共区域中。所以,我创建了一个静态类用来持有子bitmap,所有的创建操作和动画逻辑,也都在这里个类里面,稍后会详细介绍。
在Activity B中显示子bitmap
启动activity B之后,通过调用overridePendingTransition( )禁用所有默认Activity过度动画。我创建了两个Imageview去呈现之前创建的子bitmap,并将它们展示在屏幕上,为了避免提前看到Activity B的布局,这些操作要在setContentView( )
之前调用。
这两个Imageview将直接添加到activity所在的Window上。这样做不仅可以保证Imageview能够处在即将被填充的布局之上,而且还可以灵活控制每一个Imageview在屏幕上的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ImageView imageView = new ImageView(destActivity); imageView.setImageBitmap(bmp); WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(); windowParams.gravity = Gravity.TOP; windowParams.x = loc[0]; windowParams.y = loc[1]; windowParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; windowParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; windowParams.flags =WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; windowParams.format = PixelFormat.TRANSLUCENT; windowParams.windowAnimations = 0; destActivity.getWindowManager().addView(imageView, windowParams); |
简单明了。
gravity表示将把我们的layout放在window的什么位置.因为已经计算了子bitmap相对于屏幕顶部的X、Y的坐标,所以我们将gravity赋值为Top就可以了。
子bitmap动画
在Activity B中创建完Imageview并且摆放好位置后,调用setContentView( )
填充Layout布局。当布局填充完毕后,执行动画,把两个bitmap向外推出,从而呈现Activity布局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
mSetAnim = new AnimatorSet(); mTopImage.setLayerType(View.LAYER_TYPE_HARDWARE, null); mBottomImage.setLayerType(View.LAYER_TYPE_HARDWARE, null); mSetAnim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationEnd(Animator animation) { clean(destActivity); } @Override public void onAnimationCancel(Animator animation) { clean(destActivity); } ... }); // Animating the 2 parts away from each other Animator anim1 = ObjectAnimator.ofFloat(mTopImage, translationY, mTopImage.getHeight() * -1); Animator anim2 = ObjectAnimator.ofFloat(mBottomImage, translationY, mBottomImage.getHeight()); mSetAnim.setDuration(duration); mSetAnim.playTogether(anim1, anim2); mSetAnim.start(); |
这个动画仅仅是Y轴移动动画,将每个Imageview移出屏幕,不同的只是方向而已。我使用硬件加速(了解更多有关硬件加速动画,请阅读我最新发布的blog)并且在动画结束或者取消后,做了一些清理操作(如,移除硬件图层,把Imageview从Window窗口移除等等)
如何使用我的动画
我曾反复思考,在尽量不限制开发者的情况下,如何最简单便捷的使用它。不过话说回来,最简单的做法还是创建一个BaseActivity,然后开发者继承这个基类,这样就可以不必花费太多的精力去关心它了。但我并没有这样做是因为,我讨厌仅仅是为了获得扩展功能就继承其他的Activity。试想,如果你的工程有属于自己的BaseActivity,然而一些三方库却强制要求继承它们的BaseActivity,这种情况下,你一定感到特无语。
所以,我只创建了一个类,包含了一些静态方法,用来完成所有的工作,API如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/** * Utility class to create a split activity animation * * @author Udi Cohen (@udinic) */ public class ActivitySplitAnimationUtil { /** * Start a new Activity with a Split animation * * @param currActivity The current Activity * @param intent The Intent needed tot start the new Activity * @param splitYCoord The Y coordinate where we want to split the Activity on the animation. -1 * will split the Activity equally */ public static void startActivity(Activity currActivity, Intent intent, int splitYCoord); /** * Start a new Activity with a Split animation right in the middle of the Activity * * @param currActivity The current Activity * @param intent The Intent needed tot start the new Activity */ public static void startActivity(Activity currActivity, Intent intent); /** * Preparing the graphics on the destination Activity. * Should be called on the destination activity on Activity#onCreate() BEFORE setContentView() * * @param destActivity the destination Activity */ public static void prepareAnimation(final Activity destActivity); /** * Start the animation the reveals the destination Activity * Should be called on the destination activity on Activity#onCreate() AFTER setContentView() * * @param destActivity the destination Activity * @param duration The duration of the animation * @param interpolator The interpulator to use for the animation. null for no interpulation. */ public static void animate(final Activity destActivity, final int duration, final TimeInterpolator interpolator); /** * Start the animation that reveals the destination Activity * Should be called on the destination activity on Activity#onCreate() AFTER setContentView() * * @param destActivity the destination Activity * @param duration The duration of the animation */ public static void animate(final Activity destActivity, final int duration); /** * Cancel an in progress animation */ public static void cancel(); } |
使用它非常的简单,只需要在Activity A中要跳转Activity B的时候,调用这个方法就行了:
1 |
ActivitySplitAnimationUtil.startActivity(Activity1.this, new Intent(Activity1.this, Activity2.class)); |
然后在Activity B的onCreate()
方法中这样做:
1 2 3 4 5 6 7 8 9 |
// Preparing the 2 images to be split ActivitySplitAnimationUtil.prepareAnimation(this); // Setting the Activity's layout setContentView(R.layout.act_two); // Animating the items to be open, revealing the new activity. // Animation duration of 1 second ActivitySplitAnimat |
就是辣么简单。
没有什么多余的操作,只需要调用三个静态方法即可。
目前只支持API 14以上,如果想兼容更早的版本请使用NineOldAndroid。
这个是仓库地址:
https://github.com/Udinic/ActivitySplitAnimation
使用它,Fork它,丰富它。
下一步
你可以将它扩展的更丰富,比如:
- 垂直分割 – 让Activity从两侧移出。
- 把Activity分割成更多的部分。
- 做所有你能想到的事情。