• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import static android.view.WindowManager.TRANSIT_CHANGE;
22 import static android.window.TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID;
23 
24 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
25 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
26 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
27 
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.content.Context;
31 import android.graphics.Rect;
32 import android.util.Log;
33 import android.view.WindowManager;
34 import android.view.animation.AlphaAnimation;
35 import android.view.animation.Animation;
36 import android.view.animation.AnimationSet;
37 import android.view.animation.AnimationUtils;
38 import android.view.animation.Interpolator;
39 import android.view.animation.LinearInterpolator;
40 import android.view.animation.ScaleAnimation;
41 import android.view.animation.TranslateAnimation;
42 import android.window.TransitionInfo;
43 
44 import com.android.internal.policy.TransitionAnimation;
45 import com.android.wm.shell.shared.TransitionUtil;
46 
47 /** Animation spec for ActivityEmbedding transition. */
48 class ActivityEmbeddingAnimationSpec {
49 
50     private static final String TAG = "ActivityEmbeddingAnimSpec";
51     private static final int CHANGE_ANIMATION_DURATION = 517;
52     private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
53     private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
54 
55     private final Context mContext;
56     private final TransitionAnimation mTransitionAnimation;
57     private final Interpolator mFastOutExtraSlowInInterpolator;
58     private final LinearInterpolator mLinearInterpolator;
59     private float mTransitionAnimationScaleSetting;
60 
ActivityEmbeddingAnimationSpec(@onNull Context context)61     ActivityEmbeddingAnimationSpec(@NonNull Context context) {
62         mContext = context;
63         mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
64         mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
65                 mContext, android.R.interpolator.fast_out_extra_slow_in);
66         mLinearInterpolator = new LinearInterpolator();
67     }
68 
69     /**
70      * Sets transition animation scale settings value.
71      * @param scale The setting value of transition animation scale.
72      */
setAnimScaleSetting(float scale)73     void setAnimScaleSetting(float scale) {
74         mTransitionAnimationScaleSetting = scale;
75     }
76 
77     /** For window that doesn't need to be animated. */
78     @NonNull
createNoopAnimation(@onNull TransitionInfo.Change change)79     static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
80         // Noop but just keep the window showing/hiding.
81         final float alpha = TransitionUtil.isClosingType(change.getMode()) ? 0f : 1f;
82         return new AlphaAnimation(alpha, alpha);
83     }
84 
85     /**
86      * Animation that intended to show snapshot for closing animation because the closing end bounds
87      * are changed.
88      */
89     @NonNull
createShowSnapshotForClosingAnimation()90     static Animation createShowSnapshotForClosingAnimation() {
91         return new AlphaAnimation(1f, 1f);
92     }
93 
94     /** Animation for window that is opening in a change transition. */
95     @NonNull
createChangeBoundsOpenAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)96     Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change,
97             @NonNull Rect parentBounds) {
98         final Animation customAnimation =
99                 loadCustomAnimation(change.getAnimationOptions(), TRANSIT_CHANGE);
100         if (customAnimation != null) {
101             return customAnimation;
102         }
103         // Use end bounds for opening.
104         final Rect bounds = change.getEndAbsBounds();
105         final int startLeft;
106         final int startTop;
107         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
108             // The window will be animated in from left or right depending on its position.
109             startTop = 0;
110             startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
111         } else {
112             // The window will be animated in from top or bottom depending on its position.
113             startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
114             startLeft = 0;
115         }
116 
117         // The position should be 0-based as we will post translate in
118         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
119         final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0);
120         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
121         animation.setDuration(CHANGE_ANIMATION_DURATION);
122         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
123         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
124         return animation;
125     }
126 
127     /** Animation for window that is closing in a change transition. */
128     @NonNull
createChangeBoundsCloseAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)129     Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change,
130             @NonNull Rect parentBounds) {
131         final Animation customAnimation =
132                 loadCustomAnimation(change.getAnimationOptions(), TRANSIT_CHANGE);
133         if (customAnimation != null) {
134             return customAnimation;
135         }
136         // Use start bounds for closing.
137         final Rect bounds = change.getStartAbsBounds();
138         final int endTop;
139         final int endLeft;
140         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
141             // The window will be animated out to left or right depending on its position.
142             endTop = 0;
143             endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
144         } else {
145             // The window will be animated out to top or bottom depending on its position.
146             endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
147             endLeft = 0;
148         }
149 
150         // The position should be 0-based as we will post translate in
151         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
152         final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop);
153         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
154         animation.setDuration(CHANGE_ANIMATION_DURATION);
155         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
156         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
157         return animation;
158     }
159 
160     /**
161      * Animation for window that is changing (bounds change) in a change transition.
162      * @return the return array always has two elements. The first one is for the start leash, and
163      *         the second one is for the end leash.
164      */
165     @NonNull
createChangeBoundsChangeAnimations(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)166     Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
167             @NonNull Rect parentBounds) {
168         // TODO(b/293658614): Support more complicated animations that may need more than a noop
169         // animation as the start leash.
170         final Animation noopAnimation = createNoopAnimation(change);
171         final Animation customAnimation =
172                 loadCustomAnimation(change.getAnimationOptions(), TRANSIT_CHANGE);
173         if (customAnimation != null) {
174             return new Animation[]{noopAnimation, customAnimation};
175         }
176         // Both start bounds and end bounds are in screen coordinates. We will post translate
177         // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
178         final Rect startBounds = change.getStartAbsBounds();
179         final Rect endBounds = change.getEndAbsBounds();
180         float scaleX = ((float) startBounds.width()) / endBounds.width();
181         float scaleY = ((float) startBounds.height()) / endBounds.height();
182         // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
183         // be scaled up with its parent.
184         float startScaleX = 1.f / scaleX;
185         float startScaleY = 1.f / scaleY;
186 
187         // The start leash will be fade out.
188         final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
189         final Animation startAlpha = new AlphaAnimation(1f, 0f);
190         startAlpha.setInterpolator(mLinearInterpolator);
191         startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
192         startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
193         startSet.addAnimation(startAlpha);
194         final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
195                 startScaleY);
196         startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
197         startScale.setDuration(CHANGE_ANIMATION_DURATION);
198         startSet.addAnimation(startScale);
199         startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
200                 endBounds.height());
201         startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
202 
203         // The end leash will be moved into the end position while scaling.
204         final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
205         endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
206         final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
207         endScale.setDuration(CHANGE_ANIMATION_DURATION);
208         endSet.addAnimation(endScale);
209         // The position should be 0-based as we will post translate in
210         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
211         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
212                 startBounds.top - endBounds.top, 0);
213         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
214         endSet.addAnimation(endTranslate);
215         endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
216                 parentBounds.height());
217         endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
218 
219         return new Animation[]{startSet, endSet};
220     }
221 
222     @NonNull
loadOpenAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)223     Animation loadOpenAnimation(@NonNull TransitionInfo info,
224             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
225         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
226         final Animation customAnimation =
227                 loadCustomAnimation(change.getAnimationOptions(), change.getMode());
228         final Animation animation;
229         if (customAnimation != null) {
230             animation = customAnimation;
231         } else if (shouldShowBackdrop(info, change)) {
232             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
233                     ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
234                     : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
235         } else {
236             // Use the same edge extension animation as regular activity open.
237             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
238                     ? com.android.internal.R.anim.activity_open_enter
239                     : com.android.internal.R.anim.activity_open_exit);
240         }
241         // Use the whole animation bounds instead of the change bounds, so that when multiple change
242         // targets are opening at the same time, the animation applied to each will be the same.
243         // Otherwise, we may see gap between the activities that are launching together.
244         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
245                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
246         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
247         return animation;
248     }
249 
250     @NonNull
loadCloseAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)251     Animation loadCloseAnimation(@NonNull TransitionInfo info,
252             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
253         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
254         final Animation customAnimation =
255                 loadCustomAnimation(change.getAnimationOptions(), change.getMode());
256         final Animation animation;
257         if (customAnimation != null) {
258             animation = customAnimation;
259         } else if (shouldShowBackdrop(info, change)) {
260             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
261                     ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
262                     : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
263         } else {
264             // Use the same edge extension animation as regular activity close.
265             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
266                     ? com.android.internal.R.anim.activity_close_enter
267                     : com.android.internal.R.anim.activity_close_exit);
268         }
269         // Use the whole animation bounds instead of the change bounds, so that when multiple change
270         // targets are closing at the same time, the animation applied to each will be the same.
271         // Otherwise, we may see gap between the activities that are finishing together.
272         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
273                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
274         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
275         return animation;
276     }
277 
shouldShowBackdrop(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change)278     private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
279             @NonNull TransitionInfo.Change change) {
280         final int type = getTransitionTypeFromInfo(info);
281         final Animation a = loadAttributeAnimation(type, info, change, WALLPAPER_TRANSITION_NONE,
282                 mTransitionAnimation, false);
283         return a != null && a.getShowBackdrop();
284     }
285 
286     @Nullable
loadCustomAnimation(@ullable TransitionInfo.AnimationOptions options, @WindowManager.TransitionType int mode)287     Animation loadCustomAnimation(@Nullable TransitionInfo.AnimationOptions options,
288             @WindowManager.TransitionType int mode) {
289         if (options == null || options.getType() != ANIM_CUSTOM) {
290             return null;
291         }
292         final int resId;
293         if (TransitionUtil.isOpeningType(mode)) {
294             resId = options.getEnterResId();
295         } else if (TransitionUtil.isClosingType(mode)) {
296             resId = options.getExitResId();
297         } else if (mode == TRANSIT_CHANGE) {
298             resId = options.getChangeResId();
299         } else {
300             Log.w(TAG, "Unknown transit type:" + mode);
301             resId = DEFAULT_ANIMATION_RESOURCES_ID;
302         }
303         // Use the default animation if the resources ID is not specified.
304         if (resId == DEFAULT_ANIMATION_RESOURCES_ID) {
305             return null;
306         }
307 
308         final Animation anim;
309         // TODO(b/293658614): Consider allowing custom animations from non-default packages.
310         // Enforce limiting to animations from the default "android" package for now.
311         anim = mTransitionAnimation.loadDefaultAnimationRes(resId);
312         if (anim != null) {
313             return anim;
314         }
315         // The app may be intentional to use an invalid resource as a no-op animation.
316         // ActivityEmbeddingAnimationRunner#createOpenCloseAnimationAdapters will skip the
317         // animation with duration 0. Then it will use prepareForJumpCut for empty adapters.
318         return new AlphaAnimation(1f, 1f);
319     }
320 }
321