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