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