Android 应用的退场和入场动画分析
一、情景
在Android14上,由于新增了需求,需要定制自己的打开应用和退出应用动画。因此着手调查,应用打开和退出动画的出处。
二、任务
调查创建应用动画的创建流程和播放流程
三、行动
根据网上的的回答大多数都说应用的默认入场动画都在目录frameworks/base/core/res/res/anim这下面,分别是入场进入动画activity_open_enter.xml、入场退出动画activity_open_exit.xml、退场进入动画activity_close_enter.xml、退场退出动画activity_close_exit.xml。但是以为改这个就能改变就太想当然了,当然是不生效,既然是默认的,肯定是没人用的,最后还是得去撸代码,优先去看Launcher,应用的入场动画肯定是launcher侧创建的,需要ActivityOption去传递,基于这个猜测我找到launcher打开应用的操作:
应用打开动画
ActivityContext.java
/**
* Safely starts an activity.
*
* @param v View starting the activity.
* @param intent Base intent being launched.
* @param item Item associated with the view.
* @return RunnableList for listening for animation finish if the activity was properly
* or started, {@code null} if the launch finished
*/
default RunnableList startActivitySafely(
View v, Intent intent, @Nullable ItemInfo item) {
Preconditions.assertUIThread();
Context context = (Context) this;
if (isAppBlockedForSafeMode() && !PackageManagerHelper.isSystemApp(context, intent)) {
Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
return null;
}
//此处开始创建打开桌面应用的一些属性和动作,很显然v不为null,此处调用的是getActivityLaunchOptions,开始引入一些动画。
ActivityOptionsWrapper options = v != null ? getActivityLaunchOptions(v, item)
: makeDefaultActivityOptions(item != null && item.animationType == DEFAULT_NO_ICON
? SPLASH_SCREEN_STYLE_SOLID_COLOR : -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */);
UserHandle user = item == null ? null : item.user;
Bundle optsBundle = options.toBundle();
// Prepare intent
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (v != null) {
intent.setSourceBounds(Utilities.getViewBounds(v));
}
try {
boolean isShortcut = (item instanceof WorkspaceItemInfo)
&& (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
|| item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
&& !((WorkspaceItemInfo) item).isPromise();
if (isShortcut) {
// Shortcuts need some special checks due to legacy reasons.
startShortcutIntentSafely(intent, optsBundle, item);
} else if (user == null || user.equals(Process.myUserHandle())) {
// Could be launching some bookkeeping activity
context.startActivity(intent, optsBundle);
} else {
context.getSystemService(LauncherApps.class).startMainActivity(
intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
}
if (item != null) {
InstanceId instanceId = new InstanceIdSequence().newInstanceId();
logAppLaunch(getStatsLogManager(), item, instanceId);
}
return options.onEndCallback;
} catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
}
return null;
}
发现getActivityLaunchOptions继续往下看
ActivityContext.java
/**
* Returns launch options for an Activity.
*
* @param v View initiating a launch.
* @param item Item associated with the view.
*/
default ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
int left = 0, top = 0;
int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
if (v instanceof BubbleTextView) {
// Launch from center of icon, not entire view
Drawable icon = ((BubbleTextView) v).getIcon();
if (icon != null) {
Rect bounds = icon.getBounds();
left = (width - bounds.width()) / 2;
top = v.getPaddingTop();
width = bounds.width();
height = bounds.height();
}
}
//发现此处开始制作剪辑显示动画了
ActivityOptions options =
ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
RunnableList callback = new RunnableList();
return new ActivityOptionsWrapper(options, callback);
}
看到ActivityOptions.makeClipRevealAnimation开始制作动画了,继续看makeClipRevealAnimation这个方法
ActivityOptions.java
/**
* Create an ActivityOptions specifying an animation where the new
* activity is revealed from a small originating area of the screen to
* its final full representation.
*
* @param source The View that the new activity is animating from. This
* defines the coordinate space for <var>startX</var> and <var>startY</var>.
* @param startX The x starting location of the new activity, relative to <var>source</var>.
* @param startY The y starting location of the activity, relative to <var>source</var>.
* @param width The initial width of the new activity.
* @param height The initial height of the new activity.
* @return Returns a new ActivityOptions object that you can use to
* supply these options as the options Bundle when starting an activity.
*/
public static ActivityOptions makeClipRevealAnimation(View source,
int startX, int startY, int width, int height) {
ActivityOptions opts = new ActivityOptions();
opts.mAnimationType = ANIM_CLIP_REVEAL;
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;
}
可以看到设置动画的入场图标和位置以及大小。所以第一次打开动画会有个应用图标居中白底或者黑底的效果。
但是以上都是默认Launcher打开应用动画方式,现在由于QuickstepLauncher 继承Launcher,而Launcher继承ActivityContext.java,所以 getActivityLaunchOptions这个方法不是默认的,是由QuickstepLauncher实现的
所以继续往下看
QuickstepLauncher.java
@Override
public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
ActivityOptionsWrapper activityOptions =
mAppTransitionManager.hasControlRemoteAppTransitionPermission()
? mAppTransitionManager.getActivityLaunchOptions(v)
: super.getActivityLaunchOptions(v, item);
if (mLastTouchUpTime > 0) {
activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER,
mLastTouchUpTime);
}
if (item != null && (item.animationType == DEFAULT_NO_ICON
|| item.animationType == VIEW_BACKGROUND)) {
activityOptions.options.setSplashScreenStyle(
SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR);
} else {
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
activityOptions.options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
addLaunchCookie(item, activityOptions.options);
return activityOptions;
}
此处hasControlRemoteAppTransitionPermission是用来判断Launcher是否有远程过度动画控制权限,当然默认launcher都是有的,所以这里继续看mAppTransitionManager.getActivityLaunchOptions(v)这个方法。
QuickstepTransitionManager.java
300 public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
301 boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
302 RunnableList onEndCallback = new RunnableList();
303
304 // Handle the case where an already visible task is launched which results in no transition
305 TaskRestartedDuringLaunchListener restartedListener =
306 new TaskRestartedDuringLaunchListener();
307 restartedListener.register(onEndCallback::executeAllAndDestroy);
308 onEndCallback.add(restartedListener::unregister);
309
310 mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback);
311 ItemInfo tag = (ItemInfo) v.getTag();
312 if (tag != null && tag.shouldUseBackgroundAnimation()) {
313 ContainerAnimationRunner containerAnimationRunner =
314 ContainerAnimationRunner.from(v, mStartingWindowListener, onEndCallback);
315 if (containerAnimationRunner != null) {
316 mAppLaunchRunner = containerAnimationRunner;
317 }
318 }
//本地生成的一个动画执行的runner。
319 RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(
320 mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
321
322 // Note that this duration is a guess as we do not know if the animation will be a
323 // recents launch or not for sure until we know the opening app targets.
324 long duration = fromRecents
325 ? RECENTS_LAUNCH_DURATION
326 : APP_LAUNCH_DURATION;
327
328 long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
329 - STATUS_BAR_TRANSITION_PRE_DELAY;
//向远端注册这个runner,过度动画开始时会回掉runner。
330 ActivityOptions options = ActivityOptions.makeRemoteAnimation(
331 new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
332 new RemoteTransition(runner.toRemoteTransition(),
333 mLauncher.getIApplicationThread(), "QuickstepLaunch"));
334 return new ActivityOptionsWrapper(options, onEndCallback);
335 }
这里向远端注册一个过度动画runner,过度动画开始执行后,就会调用LauncherAnimationRunner里的onAnimationStart方法,而这里就是应用执行动画和播放位置,后续不在详解,可以自己往下看。
应用关闭动画
这后续补充有点忙嘿嘿