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