1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.activityembedding;
18 
19 
20 import static android.app.ActivityOptions.ANIM_CUSTOM;
21 
22 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
23 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
24 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.content.Context;
29 import android.graphics.Rect;
30 import android.view.animation.AlphaAnimation;
31 import android.view.animation.Animation;
32 import android.view.animation.AnimationSet;
33 import android.view.animation.AnimationUtils;
34 import android.view.animation.Interpolator;
35 import android.view.animation.LinearInterpolator;
36 import android.view.animation.ScaleAnimation;
37 import android.view.animation.TranslateAnimation;
38 import android.window.TransitionInfo;
39 
40 import com.android.internal.policy.TransitionAnimation;
41 import com.android.window.flags.Flags;
42 import com.android.wm.shell.shared.TransitionUtil;
43 
44 /** Animation spec for ActivityEmbedding transition. */
45 // TODO(b/206557124): provide an easier way to customize animation
46 class ActivityEmbeddingAnimationSpec {
47 
48     private static final String TAG = "ActivityEmbeddingAnimSpec";
49     private static final int CHANGE_ANIMATION_DURATION = 517;
50     private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
51     private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
52 
53     private final Context mContext;
54     private final TransitionAnimation mTransitionAnimation;
55     private final Interpolator mFastOutExtraSlowInInterpolator;
56     private final LinearInterpolator mLinearInterpolator;
57     private float mTransitionAnimationScaleSetting;
58 
ActivityEmbeddingAnimationSpec(@onNull Context context)59     ActivityEmbeddingAnimationSpec(@NonNull Context context) {
60         mContext = context;
61         mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
62         mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
63                 mContext, android.R.interpolator.fast_out_extra_slow_in);
64         mLinearInterpolator = new LinearInterpolator();
65     }
66 
67     /**
68      * Sets transition animation scale settings value.
69      * @param scale The setting value of transition animation scale.
70      */
setAnimScaleSetting(float scale)71     void setAnimScaleSetting(float scale) {
72         mTransitionAnimationScaleSetting = scale;
73     }
74 
75     /** For window that doesn't need to be animated. */
76     @NonNull
createNoopAnimation(@onNull TransitionInfo.Change change)77     static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
78         // Noop but just keep the window showing/hiding.
79         final float alpha = TransitionUtil.isClosingType(change.getMode()) ? 0f : 1f;
80         return new AlphaAnimation(alpha, alpha);
81     }
82 
83     /**
84      * Animation that intended to show snapshot for closing animation because the closing end bounds
85      * are changed.
86      */
87     @NonNull
createShowSnapshotForClosingAnimation()88     static Animation createShowSnapshotForClosingAnimation() {
89         return new AlphaAnimation(1f, 1f);
90     }
91 
92     /** Animation for window that is opening in a change transition. */
93     @NonNull
createChangeBoundsOpenAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)94     Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change,
95             @NonNull Rect parentBounds) {
96         // Use end bounds for opening.
97         final Rect bounds = change.getEndAbsBounds();
98         final int startLeft;
99         final int startTop;
100         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
101             // The window will be animated in from left or right depending on its position.
102             startTop = 0;
103             startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
104         } else {
105             // The window will be animated in from top or bottom depending on its position.
106             startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
107             startLeft = 0;
108         }
109 
110         // The position should be 0-based as we will post translate in
111         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
112         final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0);
113         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
114         animation.setDuration(CHANGE_ANIMATION_DURATION);
115         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
116         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
117         return animation;
118     }
119 
120     /** Animation for window that is closing in a change transition. */
121     @NonNull
createChangeBoundsCloseAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)122     Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change,
123             @NonNull Rect parentBounds) {
124         // Use start bounds for closing.
125         final Rect bounds = change.getStartAbsBounds();
126         final int endTop;
127         final int endLeft;
128         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
129             // The window will be animated out to left or right depending on its position.
130             endTop = 0;
131             endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
132         } else {
133             // The window will be animated out to top or bottom depending on its position.
134             endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
135             endLeft = 0;
136         }
137 
138         // The position should be 0-based as we will post translate in
139         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
140         final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop);
141         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
142         animation.setDuration(CHANGE_ANIMATION_DURATION);
143         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
144         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
145         return animation;
146     }
147 
148     /**
149      * Animation for window that is changing (bounds change) in a change transition.
150      * @return the return array always has two elements. The first one is for the start leash, and
151      *         the second one is for the end leash.
152      */
153     @NonNull
createChangeBoundsChangeAnimations(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)154     Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
155             @NonNull Rect parentBounds) {
156         // Both start bounds and end bounds are in screen coordinates. We will post translate
157         // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
158         final Rect startBounds = change.getStartAbsBounds();
159         final Rect endBounds = change.getEndAbsBounds();
160         float scaleX = ((float) startBounds.width()) / endBounds.width();
161         float scaleY = ((float) startBounds.height()) / endBounds.height();
162         // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
163         // be scaled up with its parent.
164         float startScaleX = 1.f / scaleX;
165         float startScaleY = 1.f / scaleY;
166 
167         // The start leash will be fade out.
168         final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
169         final Animation startAlpha = new AlphaAnimation(1f, 0f);
170         startAlpha.setInterpolator(mLinearInterpolator);
171         startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
172         startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
173         startSet.addAnimation(startAlpha);
174         final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
175                 startScaleY);
176         startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
177         startScale.setDuration(CHANGE_ANIMATION_DURATION);
178         startSet.addAnimation(startScale);
179         startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
180                 endBounds.height());
181         startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
182 
183         // The end leash will be moved into the end position while scaling.
184         final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
185         endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
186         final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
187         endScale.setDuration(CHANGE_ANIMATION_DURATION);
188         endSet.addAnimation(endScale);
189         // The position should be 0-based as we will post translate in
190         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
191         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
192                 startBounds.top - endBounds.top, 0);
193         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
194         endSet.addAnimation(endTranslate);
195         endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
196                 parentBounds.height());
197         endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
198 
199         return new Animation[]{startSet, endSet};
200     }
201 
202     @NonNull
loadOpenAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)203     Animation loadOpenAnimation(@NonNull TransitionInfo info,
204             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
205         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
206         final Animation customAnimation = loadCustomAnimation(info, change, isEnter);
207         final Animation animation;
208         if (customAnimation != null) {
209             animation = customAnimation;
210         } else if (shouldShowBackdrop(info, change)) {
211             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
212                     ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
213                     : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
214         } else {
215             // Use the same edge extension animation as regular activity open.
216             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
217                     ? com.android.internal.R.anim.activity_open_enter
218                     : com.android.internal.R.anim.activity_open_exit);
219         }
220         // Use the whole animation bounds instead of the change bounds, so that when multiple change
221         // targets are opening at the same time, the animation applied to each will be the same.
222         // Otherwise, we may see gap between the activities that are launching together.
223         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
224                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
225         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
226         return animation;
227     }
228 
229     @NonNull
loadCloseAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)230     Animation loadCloseAnimation(@NonNull TransitionInfo info,
231             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
232         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
233         final Animation customAnimation = loadCustomAnimation(info, change, isEnter);
234         final Animation animation;
235         if (customAnimation != null) {
236             animation = customAnimation;
237         } else if (shouldShowBackdrop(info, change)) {
238             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
239                     ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
240                     : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
241         } else {
242             // Use the same edge extension animation as regular activity close.
243             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
244                     ? com.android.internal.R.anim.activity_close_enter
245                     : com.android.internal.R.anim.activity_close_exit);
246         }
247         // Use the whole animation bounds instead of the change bounds, so that when multiple change
248         // targets are closing at the same time, the animation applied to each will be the same.
249         // Otherwise, we may see gap between the activities that are finishing together.
250         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
251                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
252         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
253         return animation;
254     }
255 
shouldShowBackdrop(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change)256     private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
257             @NonNull TransitionInfo.Change change) {
258         final int type = getTransitionTypeFromInfo(info);
259         final Animation a = loadAttributeAnimation(type, info, change, WALLPAPER_TRANSITION_NONE,
260                 mTransitionAnimation, false);
261         return a != null && a.getShowBackdrop();
262     }
263 
264     @Nullable
loadCustomAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, boolean isEnter)265     private Animation loadCustomAnimation(@NonNull TransitionInfo info,
266             @NonNull TransitionInfo.Change change, boolean isEnter) {
267         final TransitionInfo.AnimationOptions options;
268         if (Flags.moveAnimationOptionsToChange()) {
269             options = change.getAnimationOptions();
270         } else {
271             options = info.getAnimationOptions();
272         }
273         if (options == null || options.getType() != ANIM_CUSTOM) {
274             return null;
275         }
276         final Animation anim = mTransitionAnimation.loadAnimationRes(options.getPackageName(),
277                 isEnter ? options.getEnterResId() : options.getExitResId());
278         if (anim != null) {
279             return anim;
280         }
281         // The app may be intentional to use an invalid resource as a no-op animation.
282         // ActivityEmbeddingAnimationRunner#createOpenCloseAnimationAdapters will skip the
283         // animation with duration 0. Then it will use prepareForJumpCut for empty adapters.
284         return new AlphaAnimation(1f, 1f);
285     }
286 }
287