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 com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
21 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
22 
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.view.animation.AlphaAnimation;
26 import android.view.animation.Animation;
27 import android.view.animation.AnimationSet;
28 import android.view.animation.AnimationUtils;
29 import android.view.animation.Interpolator;
30 import android.view.animation.LinearInterpolator;
31 import android.view.animation.ScaleAnimation;
32 import android.view.animation.TranslateAnimation;
33 import android.window.TransitionInfo;
34 
35 import androidx.annotation.NonNull;
36 
37 import com.android.internal.policy.TransitionAnimation;
38 import com.android.wm.shell.util.TransitionUtil;
39 
40 /** Animation spec for ActivityEmbedding transition. */
41 // TODO(b/206557124): provide an easier way to customize animation
42 class ActivityEmbeddingAnimationSpec {
43 
44     private static final String TAG = "ActivityEmbeddingAnimSpec";
45     private static final int CHANGE_ANIMATION_DURATION = 517;
46     private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
47     private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
48 
49     private final Context mContext;
50     private final TransitionAnimation mTransitionAnimation;
51     private final Interpolator mFastOutExtraSlowInInterpolator;
52     private final LinearInterpolator mLinearInterpolator;
53     private float mTransitionAnimationScaleSetting;
54 
ActivityEmbeddingAnimationSpec(@onNull Context context)55     ActivityEmbeddingAnimationSpec(@NonNull Context context) {
56         mContext = context;
57         mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
58         mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
59                 mContext, android.R.interpolator.fast_out_extra_slow_in);
60         mLinearInterpolator = new LinearInterpolator();
61     }
62 
63     /**
64      * Sets transition animation scale settings value.
65      * @param scale The setting value of transition animation scale.
66      */
setAnimScaleSetting(float scale)67     void setAnimScaleSetting(float scale) {
68         mTransitionAnimationScaleSetting = scale;
69     }
70 
71     /** For window that doesn't need to be animated. */
72     @NonNull
createNoopAnimation(@onNull TransitionInfo.Change change)73     static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
74         // Noop but just keep the window showing/hiding.
75         final float alpha = TransitionUtil.isClosingType(change.getMode()) ? 0f : 1f;
76         return new AlphaAnimation(alpha, alpha);
77     }
78 
79     /**
80      * Animation that intended to show snapshot for closing animation because the closing end bounds
81      * are changed.
82      */
83     @NonNull
createShowSnapshotForClosingAnimation()84     static Animation createShowSnapshotForClosingAnimation() {
85         return new AlphaAnimation(1f, 1f);
86     }
87 
88     /** Animation for window that is opening in a change transition. */
89     @NonNull
createChangeBoundsOpenAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)90     Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change,
91             @NonNull Rect parentBounds) {
92         // Use end bounds for opening.
93         final Rect bounds = change.getEndAbsBounds();
94         final int startLeft;
95         final int startTop;
96         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
97             // The window will be animated in from left or right depending on its position.
98             startTop = 0;
99             startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
100         } else {
101             // The window will be animated in from top or bottom depending on its position.
102             startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
103             startLeft = 0;
104         }
105 
106         // The position should be 0-based as we will post translate in
107         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
108         final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0);
109         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
110         animation.setDuration(CHANGE_ANIMATION_DURATION);
111         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
112         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
113         return animation;
114     }
115 
116     /** Animation for window that is closing in a change transition. */
117     @NonNull
createChangeBoundsCloseAnimation(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)118     Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change,
119             @NonNull Rect parentBounds) {
120         // Use start bounds for closing.
121         final Rect bounds = change.getStartAbsBounds();
122         final int endTop;
123         final int endLeft;
124         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
125             // The window will be animated out to left or right depending on its position.
126             endTop = 0;
127             endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
128         } else {
129             // The window will be animated out to top or bottom depending on its position.
130             endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
131             endLeft = 0;
132         }
133 
134         // The position should be 0-based as we will post translate in
135         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
136         final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop);
137         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
138         animation.setDuration(CHANGE_ANIMATION_DURATION);
139         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
140         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
141         return animation;
142     }
143 
144     /**
145      * Animation for window that is changing (bounds change) in a change transition.
146      * @return the return array always has two elements. The first one is for the start leash, and
147      *         the second one is for the end leash.
148      */
149     @NonNull
createChangeBoundsChangeAnimations(@onNull TransitionInfo.Change change, @NonNull Rect parentBounds)150     Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
151             @NonNull Rect parentBounds) {
152         // Both start bounds and end bounds are in screen coordinates. We will post translate
153         // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
154         final Rect startBounds = change.getStartAbsBounds();
155         final Rect endBounds = change.getEndAbsBounds();
156         float scaleX = ((float) startBounds.width()) / endBounds.width();
157         float scaleY = ((float) startBounds.height()) / endBounds.height();
158         // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
159         // be scaled up with its parent.
160         float startScaleX = 1.f / scaleX;
161         float startScaleY = 1.f / scaleY;
162 
163         // The start leash will be fade out.
164         final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
165         final Animation startAlpha = new AlphaAnimation(1f, 0f);
166         startAlpha.setInterpolator(mLinearInterpolator);
167         startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
168         startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
169         startSet.addAnimation(startAlpha);
170         final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
171                 startScaleY);
172         startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
173         startScale.setDuration(CHANGE_ANIMATION_DURATION);
174         startSet.addAnimation(startScale);
175         startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
176                 endBounds.height());
177         startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
178 
179         // The end leash will be moved into the end position while scaling.
180         final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
181         endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
182         final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
183         endScale.setDuration(CHANGE_ANIMATION_DURATION);
184         endSet.addAnimation(endScale);
185         // The position should be 0-based as we will post translate in
186         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
187         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
188                 startBounds.top - endBounds.top, 0);
189         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
190         endSet.addAnimation(endTranslate);
191         endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
192                 parentBounds.height());
193         endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
194 
195         return new Animation[]{startSet, endSet};
196     }
197 
198     @NonNull
loadOpenAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)199     Animation loadOpenAnimation(@NonNull TransitionInfo info,
200             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
201         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
202         final Animation animation;
203         if (shouldShowBackdrop(info, change)) {
204             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
205                     ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
206                     : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
207         } else {
208             // Use the same edge extension animation as regular activity open.
209             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
210                     ? com.android.internal.R.anim.activity_open_enter
211                     : com.android.internal.R.anim.activity_open_exit);
212         }
213         // Use the whole animation bounds instead of the change bounds, so that when multiple change
214         // targets are opening at the same time, the animation applied to each will be the same.
215         // Otherwise, we may see gap between the activities that are launching together.
216         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
217                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
218         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
219         return animation;
220     }
221 
222     @NonNull
loadCloseAnimation(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds)223     Animation loadCloseAnimation(@NonNull TransitionInfo info,
224             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
225         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
226         final Animation animation;
227         if (shouldShowBackdrop(info, change)) {
228             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
229                     ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
230                     : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
231         } else {
232             // Use the same edge extension animation as regular activity close.
233             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
234                     ? com.android.internal.R.anim.activity_close_enter
235                     : com.android.internal.R.anim.activity_close_exit);
236         }
237         // Use the whole animation bounds instead of the change bounds, so that when multiple change
238         // targets are closing at the same time, the animation applied to each will be the same.
239         // Otherwise, we may see gap between the activities that are finishing together.
240         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
241                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
242         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
243         return animation;
244     }
245 
shouldShowBackdrop(@onNull TransitionInfo info, @NonNull TransitionInfo.Change change)246     private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
247             @NonNull TransitionInfo.Change change) {
248         final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE,
249                 mTransitionAnimation, false);
250         return a != null && a.getShowBackdrop();
251     }
252 }
253