1 /*
2  * Copyright (C) 2021 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 androidx.window.extensions.embedding;
18 
19 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
20 
21 import android.app.ActivityThread;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.graphics.Rect;
26 import android.os.Handler;
27 import android.provider.Settings;
28 import android.view.RemoteAnimationTarget;
29 import android.view.WindowManager;
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.ClipRectAnimation;
35 import android.view.animation.Interpolator;
36 import android.view.animation.LinearInterpolator;
37 import android.view.animation.ScaleAnimation;
38 import android.view.animation.TranslateAnimation;
39 
40 import androidx.annotation.NonNull;
41 
42 import com.android.internal.R;
43 import com.android.internal.policy.AttributeCache;
44 import com.android.internal.policy.TransitionAnimation;
45 
46 /** Animation spec for TaskFragment transition. */
47 // TODO(b/206557124): provide an easier way to customize animation
48 class TaskFragmentAnimationSpec {
49 
50     private static final String TAG = "TaskFragAnimationSpec";
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 
TaskFragmentAnimationSpec(@onNull Handler handler)61     TaskFragmentAnimationSpec(@NonNull Handler handler) {
62         mContext = ActivityThread.currentActivityThread().getApplication();
63         mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
64         // Initialize the AttributeCache for the TransitionAnimation.
65         AttributeCache.init(mContext);
66         mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
67                 mContext, android.R.interpolator.fast_out_extra_slow_in);
68         mLinearInterpolator = new LinearInterpolator();
69 
70         // The transition animation should be adjusted based on the developer option.
71         final ContentResolver resolver = mContext.getContentResolver();
72         mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
73         resolver.registerContentObserver(
74                 Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
75                 new SettingsObserver(handler));
76     }
77 
78     /** For target that doesn't need to be animated. */
79     @NonNull
createNoopAnimation(@onNull RemoteAnimationTarget target)80     static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
81         // Noop but just keep the target showing/hiding.
82         final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
83         return new AlphaAnimation(alpha, alpha);
84     }
85 
86     /** Animation for target that is opening in a change transition. */
87     @NonNull
createChangeBoundsOpenAnimation(@onNull RemoteAnimationTarget target)88     Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
89         final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds();
90         final Rect bounds = target.screenSpaceBounds;
91         final int startLeft;
92         final int startTop;
93         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
94             // The window will be animated in from left or right depending on its position.
95             startTop = 0;
96             startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
97         } else {
98             // The window will be animated in from top or bottom depending on its position.
99             startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
100             startLeft = 0;
101         }
102 
103         // The position should be 0-based as we will post translate in
104         // TaskFragmentAnimationAdapter#onAnimationUpdate
105         final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0);
106         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
107         animation.setDuration(CHANGE_ANIMATION_DURATION);
108         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
109         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
110         return animation;
111     }
112 
113     /** Animation for target that is closing in a change transition. */
114     @NonNull
createChangeBoundsCloseAnimation(@onNull RemoteAnimationTarget target)115     Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
116         final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds();
117         // Use startBounds if the window is closing in case it may also resize.
118         final Rect bounds = target.startBounds;
119         final int endTop;
120         final int endLeft;
121         if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) {
122             // The window will be animated out to left or right depending on its position.
123             endTop = 0;
124             endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width();
125         } else {
126             // The window will be animated out to top or bottom depending on its position.
127             endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height();
128             endLeft = 0;
129         }
130 
131         // The position should be 0-based as we will post translate in
132         // TaskFragmentAnimationAdapter#onAnimationUpdate
133         final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop);
134         animation.setInterpolator(mFastOutExtraSlowInInterpolator);
135         animation.setDuration(CHANGE_ANIMATION_DURATION);
136         animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
137         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
138         return animation;
139     }
140 
141     /**
142      * Animation for target that is changing (bounds change) in a change transition.
143      * @return the return array always has two elements. The first one is for the start leash, and
144      *         the second one is for the end leash.
145      */
146     @NonNull
createChangeBoundsChangeAnimations(@onNull RemoteAnimationTarget target)147     Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
148         // Both start bounds and end bounds are in screen coordinates. We will post translate
149         // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate
150         final Rect startBounds = target.startBounds;
151         final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds();
152         final Rect endBounds = target.screenSpaceBounds;
153         float scaleX = ((float) startBounds.width()) / endBounds.width();
154         float scaleY = ((float) startBounds.height()) / endBounds.height();
155         // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
156         // be scaled up with its parent.
157         float startScaleX = 1.f / scaleX;
158         float startScaleY = 1.f / scaleY;
159 
160         // The start leash will be fade out.
161         final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
162         final Animation startAlpha = new AlphaAnimation(1f, 0f);
163         startAlpha.setInterpolator(mLinearInterpolator);
164         startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
165         startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
166         startSet.addAnimation(startAlpha);
167         final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
168                 startScaleY);
169         startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
170         startScale.setDuration(CHANGE_ANIMATION_DURATION);
171         startSet.addAnimation(startScale);
172         startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
173                 endBounds.height());
174         startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
175 
176         // The end leash will be moved into the end position while scaling.
177         final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
178         endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
179         final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
180         endScale.setDuration(CHANGE_ANIMATION_DURATION);
181         endSet.addAnimation(endScale);
182         // The position should be 0-based as we will post translate in
183         // TaskFragmentAnimationAdapter#onAnimationUpdate
184         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
185                 startBounds.top - endBounds.top, 0);
186         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
187         endSet.addAnimation(endTranslate);
188         // The end leash is resizing, we should update the window crop based on the clip rect.
189         final Rect startClip = new Rect(startBounds);
190         final Rect endClip = new Rect(endBounds);
191         startClip.offsetTo(0, 0);
192         endClip.offsetTo(0, 0);
193         final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
194         clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
195         endSet.addAnimation(clipAnim);
196         endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
197                 parentBounds.height());
198         endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
199 
200         return new Animation[]{startSet, endSet};
201     }
202 
203     @NonNull
loadOpenAnimation(@onNull RemoteAnimationTarget target, @NonNull Rect wholeAnimationBounds)204     Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
205             @NonNull Rect wholeAnimationBounds) {
206         final boolean isEnter = target.mode != MODE_CLOSING;
207         final Animation animation;
208         // Background color on TaskDisplayArea has already been set earlier in
209         // WindowContainer#getAnimationAdapter.
210         if (target.showBackdrop) {
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             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
216                     ? com.android.internal.R.anim.task_fragment_open_enter
217                     : com.android.internal.R.anim.task_fragment_open_exit);
218         }
219         // Use the whole animation bounds instead of the change bounds, so that when multiple change
220         // targets are opening at the same time, the animation applied to each will be the same.
221         // Otherwise, we may see gap between the activities that are launching together.
222         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
223                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
224         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
225         return animation;
226     }
227 
228     @NonNull
loadCloseAnimation(@onNull RemoteAnimationTarget target, @NonNull Rect wholeAnimationBounds)229     Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
230             @NonNull Rect wholeAnimationBounds) {
231         final boolean isEnter = target.mode != MODE_CLOSING;
232         final Animation animation;
233         if (target.showBackdrop) {
234             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
235                     ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
236                     : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
237         } else {
238             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
239                     ? com.android.internal.R.anim.task_fragment_close_enter
240                     : com.android.internal.R.anim.task_fragment_close_exit);
241         }
242         // Use the whole animation bounds instead of the change bounds, so that when multiple change
243         // targets are closing at the same time, the animation applied to each will be the same.
244         // Otherwise, we may see gap between the activities that are finishing together.
245         animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(),
246                 wholeAnimationBounds.width(), wholeAnimationBounds.height());
247         animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
248         return animation;
249     }
250 
getTransitionAnimationScaleSetting()251     private float getTransitionAnimationScaleSetting() {
252         return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
253                 Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
254                                 R.dimen.config_appTransitionAnimationDurationScaleDefault)));
255     }
256 
257     private class SettingsObserver extends ContentObserver {
SettingsObserver(@onNull Handler handler)258         SettingsObserver(@NonNull Handler handler) {
259             super(handler);
260         }
261 
262         @Override
onChange(boolean selfChange)263         public void onChange(boolean selfChange) {
264             mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
265         }
266     }
267 }
268