3年前开发Rom时的一个任务,就是仿照IOS打开应用和退出应用的开发过程和思路,可能已经过时,现在拿出来看看以前的思路
目标
最终效果做到如下的形式: 点击Launcher上的icon,app从Icon的位置开始放大到全屏,观察发现Launcher也有从Icon位置放大的效果;退场时,app界面和Launcher同时缩小到Icon位置
设置方法的选择:
进出场动画和转场动画的本质是一样,都是从一个activity过渡到另一个activity的动画,所不同是进出场动画是在两个app之间的过渡,而且Launcher所在的activity是一个壁纸窗口,这是使用的时候需要注意的。
三种方法
- 设置theme中的 android:windowAnimationStyle
这是最简单方便的方式,只需要我们配置几个动画的xml就ok了,缺点是由于是在主题的里面的静态资源,不能根据实际情况改变动画类型和和设置相关参数。诸多条件限制决定了这种方式不能达到目的。 - overridePendingTransition方式
这种方式可以让我们覆盖掉第一种方式设置的动画,优点是我们可以在代码里面动态改变它,但遗憾的是这个方法只受动画的xml文件形式,也无法接受设置动画的参数。 - ActivityOptions方式
这种方法是在startActivity的时候,通过ActivityOptions构造出一个Bundle参数,传递给WindowManager,用来覆盖默认的动画,这样就为我们定义转场动画提供了扩展空间。实际查看Launcher的源代码,原生也是这么做的。
准备
进出场动画的设置流程主要与AMS和WMS有关,主要参考以下博客:
android Application Component研究之Activity(二)
WindowManagerService动画分析
为了更好的分析动画的设置流程,需要打开WMS和AMS的日志开关
通过adb shell dumpsys window -d enable a
打开WMS的日志开关
通过adb shell dumpsys activity log a on
打开AMS的日志开关
进场动画
首先观察进场动画:点击APP的Icon后,整个界面从Icon位置放大,Icon放大到一定程度后可以隐约看到APP的界面,继续放大,app界面逐渐从透明到不透明,最终覆盖全屏。
Android原生通过ActivityOptions提供了一个类似的转场动画makeScaleUpAnimation,它的使用如下:
1 | ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, left, top , width , height); |
其效果出来只有app放大,而Launcher没有动画效果,找遍资料可以发现android没有提供相关接口。
查看makeScaleUpAnimation方法的源码:1
2
3
4
5
6
7
8
9
10
11
12
13public static ActivityOptions makeScaleUpAnimation(View source,
int startX, int startY, int width, int height) {
ActivityOptions opts = new ActivityOptions();
opts.mPackageName = source.getContext().getPackageName();
opts.mAnimationType = ANIM_SCALE_UP;
int[] pts = new int[2];
source.getLocationOnScreen(pts);
opts.mStartX = pts[0] + startX;
opts.mStartY = pts[1] + startY;
opts.mWidth = width;
opts.mHeight = height;
return opts;
}
发现里面并没有构造一个Animation
,只是设置了相关位置的相关参数,以及ANIM_SCALE_UP
这个动画类型的标识,真正构造动画的方法在com.android.server.wm.AppTransition
里面,这是一个协助WindowManagerServervice
来设置转场动画的类。当真正需要执行动画时,会从AppTransition
中加载或构造合适的动画,交由WindowManagerServervice
来执行。
AppTransition
每次会加载两个动画,一个是打开(enter
)动画,另一个是退出(exit
)动画,从Launcher
打开Activity
,对Activity
来说是enter
,对Launcher
就是exit
,加载ScaleUp
的动画都会调用AppTransition的createScaleUpAnimationLocked
:
1 | private Animation createScaleUpAnimationLocked(int transit, boolean enter,int appWidth, int appHeight) { |
这个方法有几个参数:
transit
: 表示本次过渡动画的类型, 由于Laucher
是壁纸窗口,所以此时transit
的值为TRANSIT_WALLPAPER_CLOSE
enter
:表示该窗口时进入还是退出,后来加载动画的时候调用两次createScaleUpAnimationLocked
,两次就是这个参数值不同,一次为true,一次为falseappWidth
,appHeight
:顾名思义,为app最终的宽高
createScaleUpAnimationLocked
可以看到进入enter=true
时的动画,其实就是两个动画的组合,一个放大动画ScaleAnimation
,一个透明度变化动画(AlphaAnimation
),这就是App打开时的动画,而enter=false
时,这时加载的应该是App打开时Launcher的动画,却只有透明度变化(AlphaAnimation
),仿照app放大的动画写一个launcher放大的动画即可:
先看app放大的动画: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
28if (enter) {
// Entering app zooms out from the center of the initial rect.
float scaleW = mNextAppTransitionStartWidth / (float) appWidth;
float scaleH = mNextAppTransitionStartHeight / (float) appHeight;
Animation scale = new ScaleAnimation(scaleW, 1, scaleH, 1,
computePivot(mNextAppTransitionStartX, scaleW),
computePivot(mNextAppTransitionStartY, scaleH));
//modify start
if(XOS_LANCHER_TRANSITION && transit == TRANSIT_WALLPAPER_CLOSE){
scale.setInterpolator(mDecelerateXLauncherInterpolator);
}else{
scale.setInterpolator(mDecelerateInterpolator);
}
//modify end
Animation alpha = new AlphaAnimation(0, 1);
//modify start
if(XOS_LANCHER_TRANSITION && transit == TRANSIT_WALLPAPER_CLOSE){
alpha.setInterpolator(mThumbnailXLauncherFadeInInterpolator);
}else{
alpha.setInterpolator(mThumbnailFadeOutInterpolator);
}
// modify end
AnimationSet set = new AnimationSet(false);
set.addAnimation(scale);
set.addAnimation(alpha);
set.setDetachWallpaper(true);
a = set;
}
这时我更换了原来的两个动画的Interpolator
,其中mDecelerateXLauncherInterpolator = new DecelerateInterpolator(2.5f)
,将放大动画速度的因子调大,以便使减速过程更明显
1 | mThumbnailXLauncherFadeInInterpolator = new Interpolator() { |
RECENTS_THUMBNAIL_XLAUNCHER_FADEOUT_FRACTION=0.15
,这样以便app界面能够更快的将背景的Launcher界面上的Icon图标完全覆盖,不至于出现Launcher界面放大时看出变得很大的Icon图标
然后是Launcher的放大动画:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if(XOS_LANCHER_TRANSITION && transit == TRANSIT_WALLPAPER_CLOSE){
//enter app : animation for launcher exit
//mNextAppTransitionStartWidth,mNextAppTransitionStartHeight 是通过StartActivity传入的APP Icon的宽高,appWidth,appHeight是app全屏的时的大小
float scaleW = mNextAppTransitionStartWidth / (float) appWidth;
float scaleH = mNextAppTransitionStartHeight / (float) appHeight;
//因为对Launcher来说是从原来的大小放大,所以最终的大小是scaleW和scaleH的倒数
float sW = scaleW > 0.001 ? 1 / scaleW : 1000f;
float sH = scaleH > 0.001 ? 1 / scaleH : 1000f;
//第三和第四个参数是缩放点的中心位置,计算方法和app放大的位置相同,这样才能保证
Animation scale = new ScaleAnimation(1f, sW, 1f, sH,
computePivot(mNextAppTransitionStartX, scaleW),
computePivot(mNextAppTransitionStartY, scaleH));
scale.setInterpolator(mDecelerateInterpolator);
Animation alpha = new AlphaAnimation(1f, 0.0f);
alpha.setDuration(300);
alpha.setInterpolator(mThumbnailFadeOutInterpolator);
AnimationSet set = new AnimationSet(false);
set.addAnimation(scale);
set.addAnimation(alpha);
set.setDetachWallpaper(true);
a = set;
}
经过这几步修改,进场动画基本完成
总结:
ActivityOptions.makeScaleUpAnimation
来实现需要的放大动画- 原来的
App
放大的动画需要调整Interpolator
,即放大动画和透明度动画的变化速率 - 加载Launcher动画时也需要调用
createScaleUpAnimationLocked
方法,只是传入的enter值为false - Launcher的动画缩放中心位置和App缩放动画是相同的,但是放大倍数是相反的
退场动画
进场动画可以通过startActivity(intent, opts.toBundle());
传参数到AppTransition
类中构造对应的方法,而退场时却没有对应的方法,这时候面临几个问题:
- 怎么设置退场动画
- 什么时候来设置退场动画
- 退场动画的位置参数从哪里来
这里看一下进场动画时的设置流程:
可以看到是ActivityStack
在执行Resume
的流程时一步步将ScaleUp
的相关参数设置到AppTranssion中去的。
所以可以仿照这个流程在WindowMamagerService
和AppTransition
中添加一个overridePendingAppTransitionScaleUp
方法:WindowMamagerService
中添加:
1 | public void overridePendingAppTransitionScaleDown(int startX, int startY, int startWidth, |
AppTransition中添加:1
2
3
4
5
6
7
8
9
10
11
12
13
14void overridePendingAppTransitionScaleDown(int startX, int startY, int startWidth,
int startHeight) {
if (isTransitionSet()) {
mNextAppTransitionType = NEXT_TRANSIT_TYPE_SCALE_DOWN;
mNextAppTransitionPackage = null;
mNextAppTransitionThumbnail = null;
mNextAppTransitionStartX = startX;
mNextAppTransitionStartY = startY;
mNextAppTransitionStartWidth = startWidth;
mNextAppTransitionStartHeight = startHeight;
postAnimationCallback();
mNextAppTransitionCallback = null;
}
}
NEXT_TRANSIT_TYPE_SCALE_DOWN
是我自定义的一个动画,用来表示退场的动画
AppTransition类中添加:
1 | Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, |
loadAnimation
用来选择加载哪个动画,而createScaleDownAnimationLocked
便是真正实现的地方了,也是最重要的地方,需要反复微调效果,放在最后说。
怎么设置怎么设置退场动画解决了,但是什么时候来设置它呢?通过分析log发现,不管进场还是退场的时候其实都会执行一次AcitivityStack
的resumeTopActivityInnerLocked
,通过ActivityRecord.applyOptionsLocked
来试图设置过场动画,只是AcitivityOption
在用完一次后就置null了,以后无法再次使用,为达到目的,需要将进场时的AcitivityOption
保存起来。
这里不能将AcitivityOptions
保存在ActivityRecord
里面,因为一个AcitivityRecord
会对应一个Acitivity
,而我们打开app
可能会打开多个Activity
,退出的时候很有可能不是从进来的Activity
退出的。这时候就需要将ActivityOptions
保存在Task
即TaskRecord
中
所以我思路是这样的:
- 打开app的时候,判断前一个
Activity
是否是Launcher
,表示是从launcher
打开app,是的话将AcitivityOptions
保存到当前app所在的TaskRecord
- 退出app的时候,判断后一个
Activity
是否是Launcher
,表示退出app,是的话将AcitivityOptions
参数从前一个TaskRecord
中取出来放入Launcher
所在ActivityRecord
,这样ActivityRecord
就有参数来设置退场动画了
但是在实际操作过程中遇到一个问题,如果将上述的操作放在resumeTopActivityInnerLocked
中会在很多情况下这个方法是取不到前一个ActivityRecord
的对象的,但这些操作又要放在resumeTopActivityInnerLocked
之前。这里思考到在Acitivity
resume之前,肯定会执行前一个Activity
的pause
操作,最后到找一个合适的保存参数的位置即startPausingLocked
1 | final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming,boolean dontWait) { |
这样我们都顺利的记住了进入App和退出App时的位置信息
接下来看AppTransition.createScaleDownAnimationLocked
方法,它是退场动画的构造。与createScaleUpAnimationLocked
所构造的动画是相反的。不过会有几个问题:
- 退场动画需要一个合适的减速过程才能达到理想的效果
经过反复试验,最终采用以(0.1,0),(0.1,1)为控制点的三阶贝塞尔曲线
- 放慢速度会发现,在退场动画时app界面会在Launcher界面下面,这样会感觉Icon图标变得很巨大,而没有给人App界面缩小,最后变成Icon的错觉
这个需要调试动画时App界面和Launcher界面的Z轴的层次,所以App动画需要设置set.setZAdjustment(Animation.ZORDER_TOP);
,而Launcher的动画需要设置:set.setZAdjustment(Animation.ZORDER_NORMAL);
缩放时
Launcher
宽高缩放需要等比,不然会变形,但App的缩放注定又不是等比的,这样会导致App界面消失时由于比Icon
而残影严重。最后通过单独调节Y轴方向上的缩放速率来减轻残影。最后还有一个关键点,那就是app界面缩小时透明度的变化,它关系到在退场过程中能否给人感觉平滑过渡到Icon图标
下面是调节透明度变化的Interpolator1
2
3
4
5
6
7
8
9
10
11
12
13
14
15final Interpolator xInterploator = new DecelerateInterpolator(3f);
mThumbnailXLauncherFadeOutInterpolator = new Interpolator() {
@Override
public float getInterpolation(float input) {
// Linear response for first fraction, then complete after that.
if(input <= 0.10){//动画开始一段时间app界面保持完全透明
return 0f;
}
if (input <= 0.95f) {
float t = (input - 0.10f)/ 0.85f;
return xInterploator.getInterpolation(t);//透明度变化经历一个减速过程
}
return 1f;
}
};
最终createScaleDownAnimationLocked
方法如下:
1 | private Animation createScaleDownAnimationLocked(int transit, boolean enter, |
不足之处
主要表现在退场时不够完美
- 退场时app界面的残影仍然存在,在Icon图标不为方形时明显
- 由于只app退出时只能回到进场时点击的位置,所以当Launcher上的app图标改变了,然后从任务列表进入app界面,再退出是无法缩放到正确的位置的。