Activity From To From 技术方案

281 阅读13分钟

基于Android R版本分析

应用场景:车载

Activity From To From需求分析

  • 需求:需要实现Dock栏点击应用icon、Launcher点击桌面卡片开启、关闭相应的应用时,开关动画需要实现缩放效果,同时缩放位置为icon或者是卡片的中心位置;

  • 场景

    • open animation:从dock或者是launcher界面中点击icon图标或者是桌面卡片;
    • close animation:从应用侧返回到Launcher界面或者再次点击该应用对应dock栏中的icon图标时需要触发;
  • 触发条件

    • open:dock栏或者launcher点击对应的应用icon或卡片;

    • close:

      • back key返回或者是手势back
      • 在应用开启的状态下,再次点击dock栏中该应用程序对应的icon图标
  • 中断场景

    1. 从dock中开启对应的应用程序,然后dock栏进入编辑态,将对应的应用程序对应的icon移除,则该应用程序退出动画需要居中缩放,不需要再缩放至dock栏位置;
    2. 从dock栏或者是launcher开始应用程序之后,出现异常crash或者是主动kill,这种场景下,无退出动画的执行,因为是异常退出,无法执行正常的退出逻辑;

AOSP R工作流程分析

架构

AppTransition架构及方案.png

整体的架构如上所示,这里需要明确一点核心点:

ActivityRecord和Animation之间不是强耦合的关系,即可以简单的理解为:ActivityRecord的加载和Animation的加载时序没有强关联,是两个独立的模块;

Activity Transit Type

TRANSIT Typevaluedesc
TRANSIT_UNSET-1初始值,未设置过渡动画
TRANSIT_NONE0没有过渡动画
TRANSIT_ACTIVITY_OPEN6新Activity的窗口正在同一任务中的现有窗口之上打开
TRANSIT_ACTIVITY_CLOSE7最顶层Activity的窗口正在关闭以显示同一任务中的前一个Activity
TRANSIT_TASK_OPEN8新Activity的窗口正在另一个Activity所属的Task的现有窗口之上打开
TRANSIT_TASK_CLOSE9最顶层Activity的一个窗口正在关闭,以显示不同任务中的前一个Activity
TRANSIT_TASK_TO_FRONT10将Task移至最顶端,即现有Task的窗口显示在另一个Activity所属Task的顶部
TRANSIT_TASK_TO_BACK11将Task移至最低端,即现有Task的窗口被放置在所有其他Task下方
TRANSIT_WALLPAPER_CLOSE12新Activity没有Wallpaper的窗口会在有Wallpaper的窗口之上打开,从而有效地关闭Wallpaper
TRANSIT_WALLPAPER_OPEN13新Activity带有Wallpaper的窗口会在没有Wallpaper的窗口上打开,从而有效地打开Wallpaper
TRANSIT_WALLPAPER_INTRA_OPEN14新Activity的一个窗口在现有Activity的顶部打开,并且两者都在Wallpaper的顶部
TRANSIT_WALLPAPER_INTRA_CLOSE15最顶部Activity的窗口正在关闭以显示之前的Activity,并且两者都位于Wallpaper的顶部
TRANSIT_TASK_OPEN_BEHIND16新Task中的一个窗口在另一个Activity的Task中的现有窗口后面打开 新窗口将短暂显示,然后消失
TRANSIT_ACTIVITY_RELAUNCH18正在重新启动Activity(例如,由于配置更改)
TRANSIT_DOCK_TASK_FROM_RECENTS19正在对接最近的Task
TRANSIT_KEYGUARD_GOING_AWAY20Keyguard 即将消失
TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER21Keyguard 将消失,显示背后请求Wallpaper的Activity
TRANSIT_KEYGUARD_OCCLUDE22Keyguard 被遮挡
TRANSIT_KEYGUARD_UNOCCLUDE23Keyguard 未被遮挡
TRANSIT_TRANSLUCENT_ACTIVITY_OPEN24正在打开一个半透明的Activity
TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE25一个半透明的Activity正在关闭
TRANSIT_CRASHING_ACTIVITY_CLOSE26正在关闭崩溃的Activity
TRANSIT_TASK_CHANGE_WINDOWING_MODE27一项Task正在改变窗口模式
TRANSIT_SHOW_SINGLE_TASK_DISPLAY28由于第一个Activity已启动或正在打开,因此正在显示只能包含一个Task的显示

OpenAnimation & CloseAnimation 流程分析

参考:

  1. Android11 WMS 之 AppTransition
  2. android R版本AppTransition动效源码分析

其中有一个过称为:handleAppTransitionReady,这个过程中涉及几个模块概念;

动画类型

我们知道,在Framework中定义了两种类型的动画:

  • RemoteAnimation
  • LocalAnimation

同时还有一种动画定义类型,就是直接通过xml的方式定义的动画参数信息,通过loadAnimation的方式加载:

  • xml Animation
/**
  * Gets the {@link AnimationAdapter} according the given window layout properties in the window
  * hierarchy.
  *
  * @return The return value will always contain two elements, one for normal animations and the
  *         other for thumbnail animation, both can be {@code null}.
  *
  * @See com.android.server.wm.RemoteAnimationController.RemoteAnimationRecord
  * @See LocalAnimationAdapter
  */
Pair<AnimationAdapter, AnimationAdapter> getAnimationAdapter(WindowManager.LayoutParams lp,
                                                             int transit, boolean enter, boolean isVoiceInteraction) {
    final Pair<AnimationAdapter, AnimationAdapter> resultAdapters;
    final int appStackClipMode = getDisplayContent().mAppTransition.getAppStackClipMode();
​
    // Separate position and size for use in animators.
    final Rect screenBounds = getAnimationBounds(appStackClipMode);
    mTmpRect.set(screenBounds);
    getAnimationPosition(mTmpPoint);
    if (!sHierarchicalAnimations) {
        // Non-hierarchical animation uses position in global coordinates.
        mTmpPoint.set(mTmpRect.left, mTmpRect.top);
    }
    mTmpRect.offsetTo(0, 0);-
​
    final RemoteAnimationController controller =
        getDisplayContent().mAppTransition.getRemoteAnimationController();
    final boolean isChanging = AppTransition.isChangeTransit(transit) && enter
        && isChangingAppTransition();
​
    // Delaying animation start isn't compatible with remote animations at all.
    if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) {
        final Rect localBounds = new Rect(mTmpRect);
        localBounds.offsetTo(mTmpPoint.x, mTmpPoint.y);
        // Remote Animation类型
        final RemoteAnimationController.RemoteAnimationRecord adapters =
            controller.createRemoteAnimationRecord(this, mTmpPoint, localBounds,
                                                   screenBounds, (isChanging ? mSurfaceFreezer.mFreezeBounds : null));
        resultAdapters = new Pair<>(adapters.mAdapter, adapters.mThumbnailAdapter);
    } else if (isChanging) {
        final float durationScale = mWmService.getTransitionAnimationScaleLocked();
        final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo();
        mTmpRect.offsetTo(mTmpPoint.x, mTmpPoint.y);
​
        final AnimationAdapter adapter = new LocalAnimationAdapter(
            new WindowChangeAnimationSpec(mSurfaceFreezer.mFreezeBounds, mTmpRect,
                                          displayInfo, durationScale, true /* isAppAnimation */,
                                          false /* isThumbnail */),
            getSurfaceAnimationRunner());
​
        // Local Animation
        final AnimationAdapter thumbnailAdapter = mSurfaceFreezer.mSnapshot != null
            ? new LocalAnimationAdapter(new WindowChangeAnimationSpec(
                mSurfaceFreezer.mFreezeBounds, mTmpRect, displayInfo, durationScale,
                true /* isAppAnimation */, true /* isThumbnail */), getSurfaceAnimationRunner())
            : null;
        resultAdapters = new Pair<>(adapter, thumbnailAdapter);
        mTransit = transit;
        mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags();
    } else {
        mNeedsAnimationBoundsLayer = (appStackClipMode == STACK_CLIP_AFTER_ANIM);
        // load animation
        final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
​
        if (a != null) {
            // Only apply corner radius to animation if we're not in multi window mode.
            // We don't want rounded corners when in pip or split screen.
            final float windowCornerRadius = !inMultiWindowMode()
                ? getDisplayContent().getWindowCornerRadius()
                : 0;
            AnimationAdapter adapter = new LocalAnimationAdapter(
                new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
                                        getDisplayContent().mAppTransition.canSkipFirstFrame(),
                                        appStackClipMode, true /* isAppAnimation */, windowCornerRadius),
                getSurfaceAnimationRunner());
​
            resultAdapters = new Pair<>(adapter, null);
            mNeedsZBoost = a.getZAdjustment() == Animation.ZORDER_TOP
                || AppTransition.isClosingTransit(transit);
            mTransit = transit;
            mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags();
        } else {
            resultAdapters = new Pair<>(null, null);
        }
    }
    return resultAdapters;
}
动画参数

这个方法就是用于设置应用开启和关闭的过渡动画的,其中包含了两个类型为ArraySet集合列表,其中保存的元素类型为ActivityRecord

  • openingApps:应用过渡动画的打开应用列表
  • closingApps:应用程序过渡动画所应用的关闭应用程序列表

同时还包含一些关键的参数信息:

  • transit:描述动画的过渡类型,这个参考Activity Transit Type的定义类型;

  • enter:描述当前的动画类型为开启场景还是关闭场景;

    • true:开启场景
    • false:关闭场景

AppTransition

大致的工作流程:在open和close的时候,获取或者是应用层传入执行过渡动画需要的参数;

custom_closingAnimation.png

openingApps

针对open场景的过渡动画,Android原生提供了相应的功能;

/**
 * Create an ActivityOptions specifying an animation where the new
 * activity is scaled from a small originating area of the screen to
 * its final full representation.
 *
 * <p>If the Intent this is being used with has not set its
 * {@link android.content.Intent#setSourceBounds Intent.setSourceBounds},
 * those bounds will be filled in for you based on the initial
 * bounds passed in here.
 *
 * @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 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;
}

ActivityOptions向应用层提供了定制ScaleUpAnimation的接口;

  • source:起始View
  • startX / startY:拉伸开始的坐标,一般情况下为0
  • width / height:初始的宽高,这里一般指的就是source的width和height;

上述的参数为应用层传入的参数含义,我们需要重点关注一下ActivityOptions内部定义的变量含义:

  • opts.mPackageName:代表当前view所属的package;
  • opts.mAnimationType:过渡动画类型,当前场景下为ANIM_SCALE_UP,这个变量会在后续getAnimationAdapter过程中会使用到,需要特别注意;
  • opts.mStartX:拉伸开始的坐标,这里的startX和应用层中的startX含义是不同的,这里的startX = source.getX + app.startX的和,这里的opts.mStartX代表的其实就是实际view在屏幕的中真实坐标;
  • opts.mStartY:拉伸开始的坐标,同理startY = source.getY + app.startY的和,这里的opts.mStartY代表的其实就是实际view在屏幕的中真实坐标;
  • opts.mWidth / opts.mHeight:width和height和应用层传入的含义一致;
ScaleUpAnimation
/**
 *
 * @param frame These are the bounds of the window when it finishes the animation. This is where
 *              the animation must usually finish in entrance animation, as the next frame will
 *              display the window at these coordinates. In case of exit animation, this is
 *              where the animation must start, as the frame before the animation is displaying
 *              the window at these bounds.
 * @param insets Knowing where the window will be positioned is not enough. Some parts of the
 *               window might be obscured, usually by the system windows (status bar and
 *               navigation bar) and we use content insets to convey that information. This
 *               usually affects the animation aspects vertically, as the system decoration is
 *               at the top and the bottom. For example when we animate from full screen to
 *               recents, we want to exclude the covered parts, because they won't match the
 *               thumbnail after the last frame is executed.
 * @param surfaceInsets In rare situation the surface is larger than the content and we need to
 *                      know about this to make the animation frames match. We currently use
 *                      this for freeform windows, which have larger surfaces to display
 *                      shadows. When we animate them from recents, we want to match the content
 *                      to the recents thumbnail and hence need to account for the surface being
 *                      bigger.
 */
Animation loadAnimation(LayoutParams lp, int transit, boolean enter, int uiMode,
        int orientation, Rect frame, Rect displayFrame, Rect insets,
        @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean isVoiceInteraction,
        boolean freeform, WindowContainer container) {
    ActivityRecord record = container.getTopActivity(true, true);
    Animation a = null;
    if (isKeyguardGoingAwayTransit(transit) && enter) {
        a = loadKeyguardExitAnimation(transit);
    } ……………………
    } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_SCALE_UP) {
        a = createScaleUpAnimationLocked(transit, enter, frame);
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
                "applyAnimation: anim=%s nextAppTransition=ANIM_SCALE_UP transit=%s "
                        + "isEntrance=%s Callers=%s",
                a, appTransitionToString(transit), enter, Debug.getCallers(3));
    } ……………………
    return a;
}
private Animation createScaleUpAnimationLocked(int transit, boolean enter,
                                               Rect containingFrame) {
    Animation a;
    getDefaultNextAppTransitionStartRect(mTmpRect);
    final int appWidth = containingFrame.width();
    final int appHeight = containingFrame.height();
    // true:代表进入动画
    // false:代表退出动画
    if (enter) {
        // Entering app zooms out from the center of the initial rect.
        float scaleW = mTmpRect.width() / (float) appWidth;
        float scaleH = mTmpRect.height() / (float) appHeight;
        // 针对ScaleUp类型的进入动画,使用ScaleAnimation的Animation
        Animation scale = new ScaleAnimation(scaleW, 1, scaleH, 1,
                                             computePivot(mTmpRect.left, scaleW),
                                             computePivot(mTmpRect.top, scaleH));
        scale.setInterpolator(mDecelerateInterpolator);
​
        Animation alpha = new AlphaAnimation(0, 1);
        alpha.setInterpolator(mThumbnailFadeOutInterpolator);
​
        AnimationSet set = new AnimationSet(false);
        set.addAnimation(scale);
        set.addAnimation(alpha);
        set.setDetachWallpaper(true);
        a = set;
    } else  if (transit == TRANSIT_WALLPAPER_INTRA_OPEN ||
                transit == TRANSIT_WALLPAPER_INTRA_CLOSE) {
        // If we are on top of the wallpaper, we need an animation that
        // correctly handles the wallpaper staying static behind all of
        // the animated elements.  To do this, will just have the existing
        // element fade out.
        a = new AlphaAnimation(1, 0);
        a.setDetachWallpaper(true);
    } else {
        // For normal animations, the exiting element just holds in place.
        a = new AlphaAnimation(1, 1);
    }
​
    // Pick the desired duration.  If this is an inter-activity transition,
    // it  is the standard duration for that.  Otherwise we use the longer
    // task transition duration.
    final long duration;
    switch (transit) {
        case TRANSIT_ACTIVITY_OPEN:
        case TRANSIT_ACTIVITY_CLOSE:
            duration = mConfigShortAnimTime;
            break;
        default:
            duration = DEFAULT_APP_TRANSITION_DURATION;
            break;
    }
    a.setDuration(duration);
    a.setFillAfter(true);
    a.setInterpolator(mDecelerateInterpolator);
    a.initialize(appWidth, appHeight, appWidth, appHeight);
    return a;
}
closingApps

针对close退回至Launcher场景,我们默认是使用xml Anim;

Animation loadAnimation(LayoutParams lp, int transit, boolean enter, int uiMode,
        int orientation, Rect frame, Rect displayFrame, Rect insets,
        @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean isVoiceInteraction,
        boolean freeform, WindowContainer container) {
    ActivityRecord record = container.getTopActivity(true, true);
    Animation a = null;
    if (isKeyguardGoingAwayTransit(transit) && enter) {
        a = loadKeyguardExitAnimation(transit);
    } ……………………
    } else {
        int animAttr = 0;
        switch (transit) {
            …………
            case TRANSIT_WALLPAPER_OPEN:
                animAttr = enter
                    ? WindowAnimation_wallpaperOpenEnterAnimation
                    : WindowAnimation_wallpaperOpenExitAnimation;
                break;
            case …………
    }
    return a;
}

判断条件:transit = TRANSIT_WALLPAPER_OPEN

技术方案

根据上述的工作原理分析,我们可以分析出几个核心的关键点:

  • mNextAppTransitionType:用于描述新开启的Activity的过渡动画类型
  • transit:用于描述当前Activity的过渡动画类型
  • ScaleAnimation:Scale类型Animation实例

我们需要注意mNextAppTransitionType和transit参数的区别;

mNextAppTransitionType

我们通过分析代码可知,这个变量是在开启新的Activity的时候,才需要关注的;

当应用层通过startActivity启动新的Activity时绑定了ScaleUpAnimation,则mNextAppTransitionType变量会被赋值为NEXT_TRANSIT_TYPE_SCALE_UP;当启动动画执行完成之后,这个mNextAppTransitionType参数又会被赋值为NEXT_TRANSIT_TYPE_NONE,即使用完毕之后需要复位;

针对从哪儿来回哪儿去的需求定义,如果Activity启动动画为SCALE_UP类型,则关闭时也需要执行Scale操作,我们暂且定义为SCALE_DOWN

技术方案

针对NEXT_TRANSIT_TYPE_SCALE_UP变量的使用方式,我们需要在赋值之后以及其复位之前,我们需要记录一下当前所属的ActivityRecord,并给其设置一个flag保存其状态,为其后续close的时候使用做准备;

而当其执行close的时候,判断新定义的flag是否属于NEXT_TRANSIT_TYPE_SCALE_UP,如果属于的话,则执行我们自定义的逻辑,同时也需要复位该flag,代表本次open/close动画已经全部完成,清楚本次的record,等待下一次startActivity;

transit

针对transit参数,我们主要是在App返回至Launcher的时候需要特别关注,在App返回Launcher中,transit = TRANSIT_WALLPAPER_OPEN;

技术方案

针对close场景,我们需要新增一个判断条件,即在判断transit类型的基础上,我们需要再判断上述新增的flag是否属于NEXT_TRANSIT_TYPE_SCALE_UP类型,如果可以对应的上,则需要执行我们自定义的Animation;

ScaleAnimation

我们在创建ScaleAnimation的时候,传入的多个参数:

Animation scale = new ScaleAnimation(scaleW, 1, scaleH, 1,
                                     computePivot(mTmpRect.left, scaleW),
                                     computePivot(mTmpRect.top, scaleH));
/**
 * Constructor to use when building a ScaleAnimation from code
 * 
 * @param fromX Horizontal scaling factor to apply at the start of the
 *        animation
 * @param toX Horizontal scaling factor to apply at the end of the animation
 * @param fromY Vertical scaling factor to apply at the start of the
 *        animation
 * @param toY Vertical scaling factor to apply at the end of the animation
 * @param pivotX The X coordinate of the point about which the object is
 *        being scaled, specified as an absolute number where 0 is the left
 *        edge. (This point remains fixed while the object changes size.)
 * @param pivotY The Y coordinate of the point about which the object is
 *        being scaled, specified as an absolute number where 0 is the top
 *        edge. (This point remains fixed while the object changes size.)
 */
public ScaleAnimation(float fromX, float toX, float fromY, float toY,
        float pivotX, float pivotY) {
    mResources = null;
    mFromX = fromX;
    mToX = toX;
    mFromY = fromY;
    mToY = toY;
​
    mPivotXType = ABSOLUTE;
    mPivotYType = ABSOLUTE;
    mPivotXValue = pivotX;
    mPivotYValue = pivotY;
    initializePivotPoint();
}
  • fromX:表示x坐标轴上动画的起始比例,比如fromX=0.5f,可以简单的理解为scale起始的比例大小;
  • toX:表示动画结束时控件x轴方向的比例,比如toX = 1f,结合fromX参数,可以理解为,缩放比例大小是从0.5f放大至1f,1f代表的就是View的原始大小;
  • fromY:同fromX;
  • toY:同toY;
  • pivotX:表示x坐标轴上动画的缩放位置
  • pivotY:表示y坐标轴上动画的缩放位置

切记:pivotX和pivotY这两个参数代表的才是真正的缩放坐标,它的作用就是作为一个固定点,在动画播放的过程中,这个点保持不动,而周围的点围绕着这点进行缩放

我们知道,在open场景下,上述的参数是通过应用层传入的,但是在close场景下,我们无法获取到对应的预期参数值;同时,针对dock栏可编辑态修改icon图标的位置信息时,底层也是无法获取到的;

技术方案
icon位置不变场景

针对icon位置不变的场景,我们可以按照上述保存NEXT_TRANSIT_TYPE_SCALE_UP的方式,新增几个变量,将ScaleAnimation所使用到的参数都一一保存,在后续close的时候,将根据我们保存的这些参数创建我们自己的SCALE_DOWN类型的ScaleAnimation实例;

icon位置changed场景

针对dock栏调整icon位置的场景,解决方案为:

当应用层icon坐标发生变化(包括:位置调整、icon新增、icon移除)时,需要将变化后的位置信息都一并传入底层,使其底层在执行close的时候,优先判断应用层icon是否发生变化,如果没有发生变化,执行上述的逻辑,如果发生变化的话,则需要重新获取最新的ScaleAnimation参数信息创建对应的SCALE_DOWN类型的ScaleAnimation实例;