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 com.android.internal.policy; 18 19 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; 20 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION; 21 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE; 22 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; 23 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; 24 import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE; 25 import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN; 26 import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE; 27 import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN; 28 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.app.ActivityManager; 32 import android.content.Context; 33 import android.content.res.Configuration; 34 import android.content.res.ResourceId; 35 import android.content.res.Resources; 36 import android.content.res.TypedArray; 37 import android.graphics.Rect; 38 import android.hardware.HardwareBuffer; 39 import android.os.SystemProperties; 40 import android.util.Slog; 41 import android.view.WindowManager.LayoutParams; 42 import android.view.WindowManager.TransitionOldType; 43 import android.view.animation.AlphaAnimation; 44 import android.view.animation.Animation; 45 import android.view.animation.AnimationSet; 46 import android.view.animation.AnimationUtils; 47 import android.view.animation.ClipRectAnimation; 48 import android.view.animation.Interpolator; 49 import android.view.animation.PathInterpolator; 50 import android.view.animation.ScaleAnimation; 51 import android.view.animation.TranslateAnimation; 52 53 import com.android.internal.R; 54 55 import java.util.List; 56 57 /** @hide */ 58 public class TransitionAnimation { 59 // These are the possible states for the enter/exit activities during a thumbnail transition 60 public static final int THUMBNAIL_TRANSITION_ENTER_SCALE_UP = 0; 61 public static final int THUMBNAIL_TRANSITION_EXIT_SCALE_UP = 1; 62 public static final int THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN = 2; 63 public static final int THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN = 3; 64 65 /** 66 * Maximum duration for the clip reveal animation. This is used when there is a lot of movement 67 * involved, to make it more understandable. 68 */ 69 private static final int MAX_CLIP_REVEAL_TRANSITION_DURATION = 420; 70 private static final int CLIP_REVEAL_TRANSLATION_Y_DP = 8; 71 private static final int THUMBNAIL_APP_TRANSITION_DURATION = 336; 72 73 public static final int DEFAULT_APP_TRANSITION_DURATION = 336; 74 75 /** Fraction of animation at which the recents thumbnail becomes completely transparent */ 76 private static final float RECENTS_THUMBNAIL_FADEOUT_FRACTION = 0.5f; 77 78 private static final String DEFAULT_PACKAGE = "android"; 79 80 private final Context mContext; 81 private final String mTag; 82 83 private final LogDecelerateInterpolator mInterpolator = new LogDecelerateInterpolator(100, 0); 84 /** Interpolator to be used for animations that respond directly to a touch */ 85 private final Interpolator mTouchResponseInterpolator = 86 new PathInterpolator(0.3f, 0f, 0.1f, 1f); 87 private final Interpolator mClipHorizontalInterpolator = new PathInterpolator(0, 0, 0.4f, 1f); 88 private final Interpolator mDecelerateInterpolator; 89 private final Interpolator mLinearOutSlowInInterpolator; 90 private final Interpolator mThumbnailFadeOutInterpolator; 91 private final Rect mTmpFromClipRect = new Rect(); 92 private final Rect mTmpToClipRect = new Rect(); 93 private final Rect mTmpRect = new Rect(); 94 95 private final int mClipRevealTranslationY; 96 private final int mConfigShortAnimTime; 97 private final int mDefaultWindowAnimationStyleResId; 98 99 private final boolean mDebug; 100 private final boolean mGridLayoutRecentsEnabled; 101 private final boolean mLowRamRecentsEnabled; 102 TransitionAnimation(Context context, boolean debug, String tag)103 public TransitionAnimation(Context context, boolean debug, String tag) { 104 mContext = context; 105 mDebug = debug; 106 mTag = tag; 107 108 mDecelerateInterpolator = AnimationUtils.loadInterpolator(context, 109 com.android.internal.R.interpolator.decelerate_cubic); 110 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, 111 com.android.internal.R.interpolator.linear_out_slow_in); 112 mThumbnailFadeOutInterpolator = input -> { 113 // Linear response for first fraction, then complete after that. 114 if (input < RECENTS_THUMBNAIL_FADEOUT_FRACTION) { 115 float t = input / RECENTS_THUMBNAIL_FADEOUT_FRACTION; 116 return mLinearOutSlowInInterpolator.getInterpolation(t); 117 } 118 return 1f; 119 }; 120 121 mClipRevealTranslationY = (int) (CLIP_REVEAL_TRANSLATION_Y_DP 122 * mContext.getResources().getDisplayMetrics().density); 123 mConfigShortAnimTime = context.getResources().getInteger( 124 com.android.internal.R.integer.config_shortAnimTime); 125 126 mGridLayoutRecentsEnabled = SystemProperties.getBoolean("ro.recents.grid", false); 127 mLowRamRecentsEnabled = ActivityManager.isLowRamDeviceStatic(); 128 129 final TypedArray windowStyle = mContext.getTheme().obtainStyledAttributes( 130 com.android.internal.R.styleable.Window); 131 mDefaultWindowAnimationStyleResId = windowStyle.getResourceId( 132 com.android.internal.R.styleable.Window_windowAnimationStyle, 0); 133 windowStyle.recycle(); 134 } 135 136 /** Loads keyguard animation by transition flags and check it is on wallpaper or not. */ loadKeyguardExitAnimation(int transitionFlags, boolean onWallpaper)137 public Animation loadKeyguardExitAnimation(int transitionFlags, boolean onWallpaper) { 138 if ((transitionFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) != 0) { 139 return null; 140 } 141 final boolean toShade = 142 (transitionFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0; 143 final boolean subtle = 144 (transitionFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) != 0; 145 return createHiddenByKeyguardExit(mContext, mInterpolator, onWallpaper, toShade, subtle); 146 } 147 148 @Nullable loadKeyguardUnoccludeAnimation()149 public Animation loadKeyguardUnoccludeAnimation() { 150 return loadDefaultAnimationRes(com.android.internal.R.anim.wallpaper_open_exit); 151 } 152 153 @Nullable loadVoiceActivityOpenAnimation(boolean enter)154 public Animation loadVoiceActivityOpenAnimation(boolean enter) { 155 return loadDefaultAnimationRes(enter 156 ? com.android.internal.R.anim.voice_activity_open_enter 157 : com.android.internal.R.anim.voice_activity_open_exit); 158 } 159 160 @Nullable loadVoiceActivityExitAnimation(boolean enter)161 public Animation loadVoiceActivityExitAnimation(boolean enter) { 162 return loadDefaultAnimationRes(enter 163 ? com.android.internal.R.anim.voice_activity_close_enter 164 : com.android.internal.R.anim.voice_activity_close_exit); 165 } 166 167 @Nullable loadAppTransitionAnimation(String packageName, int resId)168 public Animation loadAppTransitionAnimation(String packageName, int resId) { 169 return loadAnimationRes(packageName, resId); 170 } 171 172 @Nullable loadCrossProfileAppEnterAnimation()173 public Animation loadCrossProfileAppEnterAnimation() { 174 return loadAnimationRes(DEFAULT_PACKAGE, 175 com.android.internal.R.anim.task_open_enter_cross_profile_apps); 176 } 177 178 @Nullable loadCrossProfileAppThumbnailEnterAnimation()179 public Animation loadCrossProfileAppThumbnailEnterAnimation() { 180 return loadAnimationRes( 181 DEFAULT_PACKAGE, com.android.internal.R.anim.cross_profile_apps_thumbnail_enter); 182 } 183 184 /** Load animation by resource Id from specific package. */ 185 @Nullable loadAnimationRes(String packageName, int resId)186 public Animation loadAnimationRes(String packageName, int resId) { 187 if (ResourceId.isValid(resId)) { 188 AttributeCache.Entry ent = getCachedAnimations(packageName, resId); 189 if (ent != null) { 190 return loadAnimationSafely(ent.context, resId, mTag); 191 } 192 } 193 return null; 194 } 195 196 /** Load animation by resource Id from android package. */ 197 @Nullable loadDefaultAnimationRes(int resId)198 public Animation loadDefaultAnimationRes(int resId) { 199 return loadAnimationRes(DEFAULT_PACKAGE, resId); 200 } 201 202 /** Load animation by attribute Id from specific LayoutParams */ 203 @Nullable loadAnimationAttr(LayoutParams lp, int animAttr, int transit)204 public Animation loadAnimationAttr(LayoutParams lp, int animAttr, int transit) { 205 int resId = Resources.ID_NULL; 206 Context context = mContext; 207 if (animAttr >= 0) { 208 AttributeCache.Entry ent = getCachedAnimations(lp); 209 if (ent != null) { 210 context = ent.context; 211 resId = ent.array.getResourceId(animAttr, 0); 212 } 213 } 214 resId = updateToTranslucentAnimIfNeeded(resId, transit); 215 if (ResourceId.isValid(resId)) { 216 return loadAnimationSafely(context, resId, mTag); 217 } 218 return null; 219 } 220 221 /** Load animation by attribute Id from android package. */ 222 @Nullable loadDefaultAnimationAttr(int animAttr)223 public Animation loadDefaultAnimationAttr(int animAttr) { 224 int resId = Resources.ID_NULL; 225 Context context = mContext; 226 if (animAttr >= 0) { 227 AttributeCache.Entry ent = getCachedAnimations(DEFAULT_PACKAGE, 228 mDefaultWindowAnimationStyleResId); 229 if (ent != null) { 230 context = ent.context; 231 resId = ent.array.getResourceId(animAttr, 0); 232 } 233 } 234 if (ResourceId.isValid(resId)) { 235 return loadAnimationSafely(context, resId, mTag); 236 } 237 return null; 238 } 239 240 @Nullable getCachedAnimations(LayoutParams lp)241 private AttributeCache.Entry getCachedAnimations(LayoutParams lp) { 242 if (mDebug) { 243 Slog.v(mTag, "Loading animations: layout params pkg=" 244 + (lp != null ? lp.packageName : null) 245 + " resId=0x" + (lp != null ? Integer.toHexString(lp.windowAnimations) : null)); 246 } 247 if (lp != null && lp.windowAnimations != 0) { 248 // If this is a system resource, don't try to load it from the 249 // application resources. It is nice to avoid loading application 250 // resources if we can. 251 String packageName = lp.packageName != null ? lp.packageName : DEFAULT_PACKAGE; 252 int resId = getAnimationStyleResId(lp); 253 if ((resId & 0xFF000000) == 0x01000000) { 254 packageName = DEFAULT_PACKAGE; 255 } 256 if (mDebug) { 257 Slog.v(mTag, "Loading animations: picked package=" + packageName); 258 } 259 return AttributeCache.instance().get(packageName, resId, 260 com.android.internal.R.styleable.WindowAnimation); 261 } 262 return null; 263 } 264 265 @Nullable getCachedAnimations(String packageName, int resId)266 private AttributeCache.Entry getCachedAnimations(String packageName, int resId) { 267 if (mDebug) { 268 Slog.v(mTag, "Loading animations: package=" 269 + packageName + " resId=0x" + Integer.toHexString(resId)); 270 } 271 if (packageName != null) { 272 if ((resId & 0xFF000000) == 0x01000000) { 273 packageName = DEFAULT_PACKAGE; 274 } 275 if (mDebug) { 276 Slog.v(mTag, "Loading animations: picked package=" 277 + packageName); 278 } 279 return AttributeCache.instance().get(packageName, resId, 280 com.android.internal.R.styleable.WindowAnimation); 281 } 282 return null; 283 } 284 285 /** Returns window animation style ID from {@link LayoutParams} or from system in some cases */ getAnimationStyleResId(@onNull LayoutParams lp)286 public int getAnimationStyleResId(@NonNull LayoutParams lp) { 287 int resId = lp.windowAnimations; 288 if (lp.type == LayoutParams.TYPE_APPLICATION_STARTING) { 289 // Note that we don't want application to customize starting window animation. 290 // Since this window is specific for displaying while app starting, 291 // application should not change its animation directly. 292 // In this case, it will use system resource to get default animation. 293 resId = mDefaultWindowAnimationStyleResId; 294 } 295 return resId; 296 } 297 createRelaunchAnimation(Rect containingFrame, Rect contentInsets, Rect startRect)298 public Animation createRelaunchAnimation(Rect containingFrame, Rect contentInsets, 299 Rect startRect) { 300 setupDefaultNextAppTransitionStartRect(startRect, mTmpFromClipRect); 301 final int left = mTmpFromClipRect.left; 302 final int top = mTmpFromClipRect.top; 303 mTmpFromClipRect.offset(-left, -top); 304 // TODO: Isn't that strange that we ignore exact position of the containingFrame? 305 mTmpToClipRect.set(0, 0, containingFrame.width(), containingFrame.height()); 306 AnimationSet set = new AnimationSet(true); 307 float fromWidth = mTmpFromClipRect.width(); 308 float toWidth = mTmpToClipRect.width(); 309 float fromHeight = mTmpFromClipRect.height(); 310 // While the window might span the whole display, the actual content will be cropped to the 311 // system decoration frame, for example when the window is docked. We need to take into 312 // account the visible height when constructing the animation. 313 float toHeight = mTmpToClipRect.height() - contentInsets.top - contentInsets.bottom; 314 int translateAdjustment = 0; 315 if (fromWidth <= toWidth && fromHeight <= toHeight) { 316 // The final window is larger in both dimensions than current window (e.g. we are 317 // maximizing), so we can simply unclip the new window and there will be no disappearing 318 // frame. 319 set.addAnimation(new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect)); 320 } else { 321 // The disappearing window has one larger dimension. We need to apply scaling, so the 322 // first frame of the entry animation matches the old window. 323 set.addAnimation(new ScaleAnimation(fromWidth / toWidth, 1, fromHeight / toHeight, 1)); 324 // We might not be going exactly full screen, but instead be aligned under the status 325 // bar using cropping. We still need to account for the cropped part, which will also 326 // be scaled. 327 translateAdjustment = (int) (contentInsets.top * fromHeight / toHeight); 328 } 329 330 // We animate the translation from the old position of the removed window, to the new 331 // position of the added window. The latter might not be full screen, for example docked for 332 // docked windows. 333 TranslateAnimation translate = new TranslateAnimation(left - containingFrame.left, 334 0, top - containingFrame.top - translateAdjustment, 0); 335 set.addAnimation(translate); 336 set.setDuration(DEFAULT_APP_TRANSITION_DURATION); 337 set.setZAdjustment(Animation.ZORDER_TOP); 338 return set; 339 } 340 setupDefaultNextAppTransitionStartRect(Rect startRect, Rect rect)341 private void setupDefaultNextAppTransitionStartRect(Rect startRect, Rect rect) { 342 if (startRect == null) { 343 Slog.e(mTag, "Starting rect for app requested, but none available", new Throwable()); 344 rect.setEmpty(); 345 } else { 346 rect.set(startRect); 347 } 348 } 349 createClipRevealAnimationLocked(int transit, boolean enter, Rect appFrame, Rect displayFrame, Rect startRect)350 public Animation createClipRevealAnimationLocked(int transit, boolean enter, Rect appFrame, 351 Rect displayFrame, Rect startRect) { 352 final Animation anim; 353 if (enter) { 354 final int appWidth = appFrame.width(); 355 final int appHeight = appFrame.height(); 356 357 // mTmpRect will contain an area around the launcher icon that was pressed. We will 358 // clip reveal from that area in the final area of the app. 359 setupDefaultNextAppTransitionStartRect(startRect, mTmpRect); 360 361 float t = 0f; 362 if (appHeight > 0) { 363 t = (float) mTmpRect.top / displayFrame.height(); 364 } 365 int translationY = mClipRevealTranslationY + (int) (displayFrame.height() / 7f * t); 366 int translationX = 0; 367 int translationYCorrection = translationY; 368 int centerX = mTmpRect.centerX(); 369 int centerY = mTmpRect.centerY(); 370 int halfWidth = mTmpRect.width() / 2; 371 int halfHeight = mTmpRect.height() / 2; 372 int clipStartX = centerX - halfWidth - appFrame.left; 373 int clipStartY = centerY - halfHeight - appFrame.top; 374 boolean cutOff = false; 375 376 // If the starting rectangle is fully or partially outside of the target rectangle, we 377 // need to start the clipping at the edge and then achieve the rest with translation 378 // and extending the clip rect from that edge. 379 if (appFrame.top > centerY - halfHeight) { 380 translationY = (centerY - halfHeight) - appFrame.top; 381 translationYCorrection = 0; 382 clipStartY = 0; 383 cutOff = true; 384 } 385 if (appFrame.left > centerX - halfWidth) { 386 translationX = (centerX - halfWidth) - appFrame.left; 387 clipStartX = 0; 388 cutOff = true; 389 } 390 if (appFrame.right < centerX + halfWidth) { 391 translationX = (centerX + halfWidth) - appFrame.right; 392 clipStartX = appWidth - mTmpRect.width(); 393 cutOff = true; 394 } 395 final long duration = calculateClipRevealTransitionDuration(cutOff, translationX, 396 translationY, displayFrame); 397 398 // Clip third of the from size of launch icon, expand to full width/height 399 Animation clipAnimLR = new ClipRectLRAnimation( 400 clipStartX, clipStartX + mTmpRect.width(), 0, appWidth); 401 clipAnimLR.setInterpolator(mClipHorizontalInterpolator); 402 clipAnimLR.setDuration((long) (duration / 2.5f)); 403 404 TranslateAnimation translate = new TranslateAnimation(translationX, 0, translationY, 0); 405 translate.setInterpolator(cutOff ? mTouchResponseInterpolator 406 : mLinearOutSlowInInterpolator); 407 translate.setDuration(duration); 408 409 Animation clipAnimTB = new ClipRectTBAnimation( 410 clipStartY, clipStartY + mTmpRect.height(), 411 0, appHeight, 412 translationYCorrection, 0, 413 mLinearOutSlowInInterpolator); 414 clipAnimTB.setInterpolator(mTouchResponseInterpolator); 415 clipAnimTB.setDuration(duration); 416 417 // Quick fade-in from icon to app window 418 final long alphaDuration = duration / 4; 419 AlphaAnimation alpha = new AlphaAnimation(0.5f, 1); 420 alpha.setDuration(alphaDuration); 421 alpha.setInterpolator(mLinearOutSlowInInterpolator); 422 423 AnimationSet set = new AnimationSet(false); 424 set.addAnimation(clipAnimLR); 425 set.addAnimation(clipAnimTB); 426 set.addAnimation(translate); 427 set.addAnimation(alpha); 428 set.setZAdjustment(Animation.ZORDER_TOP); 429 set.initialize(appWidth, appHeight, appWidth, appHeight); 430 anim = set; 431 } else { 432 final long duration; 433 switch (transit) { 434 case TRANSIT_OLD_ACTIVITY_OPEN: 435 case TRANSIT_OLD_ACTIVITY_CLOSE: 436 duration = mConfigShortAnimTime; 437 break; 438 default: 439 duration = DEFAULT_APP_TRANSITION_DURATION; 440 break; 441 } 442 if (transit == TRANSIT_OLD_WALLPAPER_INTRA_OPEN 443 || transit == TRANSIT_OLD_WALLPAPER_INTRA_CLOSE) { 444 // If we are on top of the wallpaper, we need an animation that 445 // correctly handles the wallpaper staying static behind all of 446 // the animated elements. To do this, will just have the existing 447 // element fade out. 448 anim = new AlphaAnimation(1, 0); 449 anim.setDetachWallpaper(true); 450 } else { 451 // For normal animations, the exiting element just holds in place. 452 anim = new AlphaAnimation(1, 1); 453 } 454 anim.setInterpolator(mDecelerateInterpolator); 455 anim.setDuration(duration); 456 anim.setFillAfter(true); 457 } 458 return anim; 459 } 460 createScaleUpAnimationLocked(int transit, boolean enter, Rect containingFrame, Rect startRect)461 public Animation createScaleUpAnimationLocked(int transit, boolean enter, 462 Rect containingFrame, Rect startRect) { 463 Animation a; 464 setupDefaultNextAppTransitionStartRect(startRect, mTmpRect); 465 final int appWidth = containingFrame.width(); 466 final int appHeight = containingFrame.height(); 467 if (enter) { 468 // Entering app zooms out from the center of the initial rect. 469 float scaleW = mTmpRect.width() / (float) appWidth; 470 float scaleH = mTmpRect.height() / (float) appHeight; 471 Animation scale = new ScaleAnimation(scaleW, 1, scaleH, 1, 472 computePivot(mTmpRect.left, scaleW), 473 computePivot(mTmpRect.top, scaleH)); 474 scale.setInterpolator(mDecelerateInterpolator); 475 476 Animation alpha = new AlphaAnimation(0, 1); 477 alpha.setInterpolator(mThumbnailFadeOutInterpolator); 478 479 AnimationSet set = new AnimationSet(false); 480 set.addAnimation(scale); 481 set.addAnimation(alpha); 482 set.setDetachWallpaper(true); 483 a = set; 484 } else if (transit == TRANSIT_OLD_WALLPAPER_INTRA_OPEN 485 || transit == TRANSIT_OLD_WALLPAPER_INTRA_CLOSE) { 486 // If we are on top of the wallpaper, we need an animation that 487 // correctly handles the wallpaper staying static behind all of 488 // the animated elements. To do this, will just have the existing 489 // element fade out. 490 a = new AlphaAnimation(1, 0); 491 a.setDetachWallpaper(true); 492 } else { 493 // For normal animations, the exiting element just holds in place. 494 a = new AlphaAnimation(1, 1); 495 } 496 497 // Pick the desired duration. If this is an inter-activity transition, 498 // it is the standard duration for that. Otherwise we use the longer 499 // task transition duration. 500 final long duration; 501 switch (transit) { 502 case TRANSIT_OLD_ACTIVITY_OPEN: 503 case TRANSIT_OLD_ACTIVITY_CLOSE: 504 duration = mConfigShortAnimTime; 505 break; 506 default: 507 duration = DEFAULT_APP_TRANSITION_DURATION; 508 break; 509 } 510 a.setDuration(duration); 511 a.setFillAfter(true); 512 a.setInterpolator(mDecelerateInterpolator); 513 a.initialize(appWidth, appHeight, appWidth, appHeight); 514 return a; 515 } 516 517 /** 518 * This animation is created when we are doing a thumbnail transition, for the activity that is 519 * leaving, and the activity that is entering. 520 */ createThumbnailEnterExitAnimationLocked(int thumbTransitState, Rect containingFrame, int transit, HardwareBuffer thumbnailHeader, Rect startRect)521 public Animation createThumbnailEnterExitAnimationLocked(int thumbTransitState, 522 Rect containingFrame, int transit, HardwareBuffer thumbnailHeader, 523 Rect startRect) { 524 final int appWidth = containingFrame.width(); 525 final int appHeight = containingFrame.height(); 526 Animation a; 527 setupDefaultNextAppTransitionStartRect(startRect, mTmpRect); 528 final int thumbWidthI = thumbnailHeader != null ? thumbnailHeader.getWidth() : appWidth; 529 final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1; 530 final int thumbHeightI = thumbnailHeader != null ? thumbnailHeader.getHeight() : appHeight; 531 final float thumbHeight = thumbHeightI > 0 ? thumbHeightI : 1; 532 533 switch (thumbTransitState) { 534 case THUMBNAIL_TRANSITION_ENTER_SCALE_UP: { 535 // Entering app scales up with the thumbnail 536 float scaleW = thumbWidth / appWidth; 537 float scaleH = thumbHeight / appHeight; 538 a = new ScaleAnimation(scaleW, 1, scaleH, 1, 539 computePivot(mTmpRect.left, scaleW), 540 computePivot(mTmpRect.top, scaleH)); 541 break; 542 } 543 case THUMBNAIL_TRANSITION_EXIT_SCALE_UP: { 544 // Exiting app while the thumbnail is scaling up should fade or stay in place 545 if (transit == TRANSIT_OLD_WALLPAPER_INTRA_OPEN) { 546 // Fade out while bringing up selected activity. This keeps the 547 // current activity from showing through a launching wallpaper 548 // activity. 549 a = new AlphaAnimation(1, 0); 550 } else { 551 // noop animation 552 a = new AlphaAnimation(1, 1); 553 } 554 break; 555 } 556 case THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN: { 557 // Entering the other app, it should just be visible while we scale the thumbnail 558 // down above it 559 a = new AlphaAnimation(1, 1); 560 break; 561 } 562 case THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN: { 563 // Exiting the current app, the app should scale down with the thumbnail 564 float scaleW = thumbWidth / appWidth; 565 float scaleH = thumbHeight / appHeight; 566 Animation scale = new ScaleAnimation(1, scaleW, 1, scaleH, 567 computePivot(mTmpRect.left, scaleW), 568 computePivot(mTmpRect.top, scaleH)); 569 570 Animation alpha = new AlphaAnimation(1, 0); 571 572 AnimationSet set = new AnimationSet(true); 573 set.addAnimation(scale); 574 set.addAnimation(alpha); 575 set.setZAdjustment(Animation.ZORDER_TOP); 576 a = set; 577 break; 578 } 579 default: 580 throw new RuntimeException("Invalid thumbnail transition state"); 581 } 582 583 return prepareThumbnailAnimation(a, appWidth, appHeight, transit); 584 } 585 586 /** 587 * This alternate animation is created when we are doing a thumbnail transition, for the 588 * activity that is leaving, and the activity that is entering. 589 */ createAspectScaledThumbnailEnterExitAnimationLocked(int thumbTransitState, int orientation, int transit, Rect containingFrame, Rect contentInsets, @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean freeform, Rect startRect, Rect defaultStartRect)590 public Animation createAspectScaledThumbnailEnterExitAnimationLocked(int thumbTransitState, 591 int orientation, int transit, Rect containingFrame, Rect contentInsets, 592 @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean freeform, 593 Rect startRect, Rect defaultStartRect) { 594 Animation a; 595 final int appWidth = containingFrame.width(); 596 final int appHeight = containingFrame.height(); 597 setupDefaultNextAppTransitionStartRect(defaultStartRect, mTmpRect); 598 final int thumbWidthI = mTmpRect.width(); 599 final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1; 600 final int thumbHeightI = mTmpRect.height(); 601 final float thumbHeight = thumbHeightI > 0 ? thumbHeightI : 1; 602 final int thumbStartX = mTmpRect.left - containingFrame.left - contentInsets.left; 603 final int thumbStartY = mTmpRect.top - containingFrame.top; 604 605 switch (thumbTransitState) { 606 case THUMBNAIL_TRANSITION_ENTER_SCALE_UP: 607 case THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN: { 608 final boolean scaleUp = thumbTransitState == THUMBNAIL_TRANSITION_ENTER_SCALE_UP; 609 if (freeform && scaleUp) { 610 a = createAspectScaledThumbnailEnterFreeformAnimationLocked( 611 containingFrame, surfaceInsets, startRect, defaultStartRect); 612 } else if (freeform) { 613 a = createAspectScaledThumbnailExitFreeformAnimationLocked( 614 containingFrame, surfaceInsets, startRect, defaultStartRect); 615 } else { 616 AnimationSet set = new AnimationSet(true); 617 618 // In portrait, we scale to fit the width 619 mTmpFromClipRect.set(containingFrame); 620 mTmpToClipRect.set(containingFrame); 621 622 // Containing frame is in screen space, but we need the clip rect in the 623 // app space. 624 mTmpFromClipRect.offsetTo(0, 0); 625 mTmpToClipRect.offsetTo(0, 0); 626 627 // Exclude insets region from the source clip. 628 mTmpFromClipRect.inset(contentInsets); 629 630 if (shouldScaleDownThumbnailTransition(orientation)) { 631 // We scale the width and clip to the top/left square 632 float scale = 633 thumbWidth / (appWidth - contentInsets.left - contentInsets.right); 634 if (!mGridLayoutRecentsEnabled) { 635 int unscaledThumbHeight = (int) (thumbHeight / scale); 636 mTmpFromClipRect.bottom = mTmpFromClipRect.top + unscaledThumbHeight; 637 } 638 639 Animation scaleAnim = new ScaleAnimation( 640 scaleUp ? scale : 1, scaleUp ? 1 : scale, 641 scaleUp ? scale : 1, scaleUp ? 1 : scale, 642 containingFrame.width() / 2f, 643 containingFrame.height() / 2f + contentInsets.top); 644 final float targetX = (mTmpRect.left - containingFrame.left); 645 final float x = containingFrame.width() / 2f 646 - containingFrame.width() / 2f * scale; 647 final float targetY = (mTmpRect.top - containingFrame.top); 648 float y = containingFrame.height() / 2f 649 - containingFrame.height() / 2f * scale; 650 651 // During transition may require clipping offset from any top stable insets 652 // such as the statusbar height when statusbar is hidden 653 if (mLowRamRecentsEnabled && contentInsets.top == 0 && scaleUp) { 654 mTmpFromClipRect.top += stableInsets.top; 655 y += stableInsets.top; 656 } 657 final float startX = targetX - x; 658 final float startY = targetY - y; 659 Animation clipAnim = scaleUp 660 ? new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect) 661 : new ClipRectAnimation(mTmpToClipRect, mTmpFromClipRect); 662 Animation translateAnim = scaleUp 663 ? createCurvedMotion(startX, 0, startY - contentInsets.top, 0) 664 : createCurvedMotion(0, startX, 0, startY - contentInsets.top); 665 666 set.addAnimation(clipAnim); 667 set.addAnimation(scaleAnim); 668 set.addAnimation(translateAnim); 669 670 } else { 671 // In landscape, we don't scale at all and only crop 672 mTmpFromClipRect.bottom = mTmpFromClipRect.top + thumbHeightI; 673 mTmpFromClipRect.right = mTmpFromClipRect.left + thumbWidthI; 674 675 Animation clipAnim = scaleUp 676 ? new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect) 677 : new ClipRectAnimation(mTmpToClipRect, mTmpFromClipRect); 678 Animation translateAnim = scaleUp 679 ? createCurvedMotion(thumbStartX, 0, 680 thumbStartY - contentInsets.top, 0) 681 : createCurvedMotion(0, thumbStartX, 0, 682 thumbStartY - contentInsets.top); 683 684 set.addAnimation(clipAnim); 685 set.addAnimation(translateAnim); 686 } 687 a = set; 688 a.setZAdjustment(Animation.ZORDER_TOP); 689 } 690 break; 691 } 692 case THUMBNAIL_TRANSITION_EXIT_SCALE_UP: { 693 // Previous app window during the scale up 694 if (transit == TRANSIT_OLD_WALLPAPER_INTRA_OPEN) { 695 // Fade out the source activity if we are animating to a wallpaper 696 // activity. 697 a = new AlphaAnimation(1, 0); 698 } else { 699 a = new AlphaAnimation(1, 1); 700 } 701 break; 702 } 703 case THUMBNAIL_TRANSITION_ENTER_SCALE_DOWN: { 704 // Target app window during the scale down 705 if (transit == TRANSIT_OLD_WALLPAPER_INTRA_OPEN) { 706 // Fade in the destination activity if we are animating from a wallpaper 707 // activity. 708 a = new AlphaAnimation(0, 1); 709 } else { 710 a = new AlphaAnimation(1, 1); 711 } 712 break; 713 } 714 default: 715 throw new RuntimeException("Invalid thumbnail transition state"); 716 } 717 718 return prepareThumbnailAnimationWithDuration(a, appWidth, appHeight, 719 THUMBNAIL_APP_TRANSITION_DURATION, mTouchResponseInterpolator); 720 } 721 722 /** 723 * Prepares the specified animation with a standard duration, interpolator, etc. 724 */ prepareThumbnailAnimation(Animation a, int appWidth, int appHeight, int transit)725 private Animation prepareThumbnailAnimation(Animation a, int appWidth, int appHeight, 726 int transit) { 727 // Pick the desired duration. If this is an inter-activity transition, 728 // it is the standard duration for that. Otherwise we use the longer 729 // task transition duration. 730 final int duration; 731 switch (transit) { 732 case TRANSIT_OLD_ACTIVITY_OPEN: 733 case TRANSIT_OLD_ACTIVITY_CLOSE: 734 duration = mConfigShortAnimTime; 735 break; 736 default: 737 duration = DEFAULT_APP_TRANSITION_DURATION; 738 break; 739 } 740 return prepareThumbnailAnimationWithDuration(a, appWidth, appHeight, duration, 741 mDecelerateInterpolator); 742 } 743 744 createAspectScaledThumbnailEnterFreeformAnimationLocked(Rect frame, @Nullable Rect surfaceInsets, @Nullable Rect startRect, @Nullable Rect defaultStartRect)745 private Animation createAspectScaledThumbnailEnterFreeformAnimationLocked(Rect frame, 746 @Nullable Rect surfaceInsets, @Nullable Rect startRect, 747 @Nullable Rect defaultStartRect) { 748 getNextAppTransitionStartRect(startRect, defaultStartRect, mTmpRect); 749 return createAspectScaledThumbnailFreeformAnimationLocked(mTmpRect, frame, surfaceInsets, 750 true); 751 } 752 createAspectScaledThumbnailExitFreeformAnimationLocked(Rect frame, @Nullable Rect surfaceInsets, @Nullable Rect startRect, @Nullable Rect defaultStartRect)753 private Animation createAspectScaledThumbnailExitFreeformAnimationLocked(Rect frame, 754 @Nullable Rect surfaceInsets, @Nullable Rect startRect, 755 @Nullable Rect defaultStartRect) { 756 getNextAppTransitionStartRect(startRect, defaultStartRect, mTmpRect); 757 return createAspectScaledThumbnailFreeformAnimationLocked(frame, mTmpRect, surfaceInsets, 758 false); 759 } 760 getNextAppTransitionStartRect(Rect startRect, Rect defaultStartRect, Rect rect)761 private void getNextAppTransitionStartRect(Rect startRect, Rect defaultStartRect, Rect rect) { 762 if (startRect == null && defaultStartRect == null) { 763 Slog.e(mTag, "Starting rect for container not available", new Throwable()); 764 rect.setEmpty(); 765 } else { 766 rect.set(startRect != null ? startRect : defaultStartRect); 767 } 768 } 769 createAspectScaledThumbnailFreeformAnimationLocked(Rect sourceFrame, Rect destFrame, @Nullable Rect surfaceInsets, boolean enter)770 private AnimationSet createAspectScaledThumbnailFreeformAnimationLocked(Rect sourceFrame, 771 Rect destFrame, @Nullable Rect surfaceInsets, boolean enter) { 772 final float sourceWidth = sourceFrame.width(); 773 final float sourceHeight = sourceFrame.height(); 774 final float destWidth = destFrame.width(); 775 final float destHeight = destFrame.height(); 776 final float scaleH = enter ? sourceWidth / destWidth : destWidth / sourceWidth; 777 final float scaleV = enter ? sourceHeight / destHeight : destHeight / sourceHeight; 778 AnimationSet set = new AnimationSet(true); 779 final int surfaceInsetsH = surfaceInsets == null 780 ? 0 : surfaceInsets.left + surfaceInsets.right; 781 final int surfaceInsetsV = surfaceInsets == null 782 ? 0 : surfaceInsets.top + surfaceInsets.bottom; 783 // We want the scaling to happen from the center of the surface. In order to achieve that, 784 // we need to account for surface insets that will be used to enlarge the surface. 785 final float scaleHCenter = ((enter ? destWidth : sourceWidth) + surfaceInsetsH) / 2; 786 final float scaleVCenter = ((enter ? destHeight : sourceHeight) + surfaceInsetsV) / 2; 787 final ScaleAnimation scale = enter 788 ? new ScaleAnimation(scaleH, 1, scaleV, 1, scaleHCenter, scaleVCenter) 789 : new ScaleAnimation(1, scaleH, 1, scaleV, scaleHCenter, scaleVCenter); 790 final int sourceHCenter = sourceFrame.left + sourceFrame.width() / 2; 791 final int sourceVCenter = sourceFrame.top + sourceFrame.height() / 2; 792 final int destHCenter = destFrame.left + destFrame.width() / 2; 793 final int destVCenter = destFrame.top + destFrame.height() / 2; 794 final int fromX = enter ? sourceHCenter - destHCenter : destHCenter - sourceHCenter; 795 final int fromY = enter ? sourceVCenter - destVCenter : destVCenter - sourceVCenter; 796 final TranslateAnimation translation = enter ? new TranslateAnimation(fromX, 0, fromY, 0) 797 : new TranslateAnimation(0, fromX, 0, fromY); 798 set.addAnimation(scale); 799 set.addAnimation(translation); 800 return set; 801 } 802 803 /** 804 * @return whether the transition should show the thumbnail being scaled down. 805 */ shouldScaleDownThumbnailTransition(int orientation)806 private boolean shouldScaleDownThumbnailTransition(int orientation) { 807 return mGridLayoutRecentsEnabled 808 || orientation == Configuration.ORIENTATION_PORTRAIT; 809 } 810 updateToTranslucentAnimIfNeeded(int anim, @TransitionOldType int transit)811 private static int updateToTranslucentAnimIfNeeded(int anim, @TransitionOldType int transit) { 812 if (transit == TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN 813 && anim == R.anim.activity_open_enter) { 814 return R.anim.activity_translucent_open_enter; 815 } 816 if (transit == TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE 817 && anim == R.anim.activity_close_exit) { 818 return R.anim.activity_translucent_close_exit; 819 } 820 return anim; 821 } 822 823 /** 824 * Calculates the duration for the clip reveal animation. If the clip is "cut off", meaning that 825 * the start rect is outside of the target rect, and there is a lot of movement going on. 826 * 827 * @param cutOff whether the start rect was not fully contained by the end rect 828 * @param translationX the total translation the surface moves in x direction 829 * @param translationY the total translation the surfaces moves in y direction 830 * @param displayFrame our display frame 831 * 832 * @return the duration of the clip reveal animation, in milliseconds 833 */ calculateClipRevealTransitionDuration(boolean cutOff, float translationX, float translationY, Rect displayFrame)834 private static long calculateClipRevealTransitionDuration(boolean cutOff, float translationX, 835 float translationY, Rect displayFrame) { 836 if (!cutOff) { 837 return DEFAULT_APP_TRANSITION_DURATION; 838 } 839 final float fraction = Math.max(Math.abs(translationX) / displayFrame.width(), 840 Math.abs(translationY) / displayFrame.height()); 841 return (long) (DEFAULT_APP_TRANSITION_DURATION + fraction 842 * (MAX_CLIP_REVEAL_TRANSITION_DURATION - DEFAULT_APP_TRANSITION_DURATION)); 843 } 844 845 /** 846 * Prepares the specified animation with a standard duration, interpolator, etc. 847 */ prepareThumbnailAnimationWithDuration(Animation a, int appWidth, int appHeight, long duration, Interpolator interpolator)848 private static Animation prepareThumbnailAnimationWithDuration(Animation a, int appWidth, 849 int appHeight, long duration, Interpolator interpolator) { 850 if (duration > 0) { 851 a.setDuration(duration); 852 } 853 a.setFillAfter(true); 854 if (interpolator != null) { 855 a.setInterpolator(interpolator); 856 } 857 a.initialize(appWidth, appHeight, appWidth, appHeight); 858 return a; 859 } 860 createCurvedMotion(float fromX, float toX, float fromY, float toY)861 private static Animation createCurvedMotion(float fromX, float toX, float fromY, float toY) { 862 return new TranslateAnimation(fromX, toX, fromY, toY); 863 } 864 865 /** 866 * Compute the pivot point for an animation that is scaling from a small 867 * rect on screen to a larger rect. The pivot point varies depending on 868 * the distance between the inner and outer edges on both sides. This 869 * function computes the pivot point for one dimension. 870 * @param startPos Offset from left/top edge of outer rectangle to 871 * left/top edge of inner rectangle. 872 * @param finalScale The scaling factor between the size of the outer 873 * and inner rectangles. 874 */ computePivot(int startPos, float finalScale)875 public static float computePivot(int startPos, float finalScale) { 876 877 /* 878 Theorem of intercepting lines: 879 880 + + +-----------------------------------------------+ 881 | | | | 882 | | | | 883 | | | | 884 | | | | 885 x | y | | | 886 | | | | 887 | | | | 888 | | | | 889 | | | | 890 | + | +--------------------+ | 891 | | | | | 892 | | | | | 893 | | | | | 894 | | | | | 895 | | | | | 896 | | | | | 897 | | | | | 898 | | | | | 899 | | | | | 900 | | | | | 901 | | | | | 902 | | | | | 903 | | | | | 904 | | | | | 905 | | | | | 906 | | | | | 907 | | | | | 908 | | +--------------------+ | 909 | | | 910 | | | 911 | | | 912 | | | 913 | | | 914 | | | 915 | | | 916 | +-----------------------------------------------+ 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 + ++ 927 p ++ 928 929 scale = (x - y) / x 930 <=> x = -y / (scale - 1) 931 */ 932 final float denom = finalScale - 1; 933 if (Math.abs(denom) < .0001f) { 934 return startPos; 935 } 936 return -startPos / denom; 937 } 938 939 @Nullable loadAnimationSafely(Context context, int resId, String tag)940 public static Animation loadAnimationSafely(Context context, int resId, String tag) { 941 try { 942 return AnimationUtils.loadAnimation(context, resId); 943 } catch (Resources.NotFoundException e) { 944 Slog.w(tag, "Unable to load animation resource", e); 945 return null; 946 } 947 } 948 createHiddenByKeyguardExit(Context context, LogDecelerateInterpolator interpolator, boolean onWallpaper, boolean goingToNotificationShade, boolean subtleAnimation)949 public static Animation createHiddenByKeyguardExit(Context context, 950 LogDecelerateInterpolator interpolator, boolean onWallpaper, 951 boolean goingToNotificationShade, boolean subtleAnimation) { 952 if (goingToNotificationShade) { 953 return AnimationUtils.loadAnimation(context, R.anim.lock_screen_behind_enter_fade_in); 954 } 955 956 final int resource; 957 if (subtleAnimation) { 958 resource = R.anim.lock_screen_behind_enter_subtle; 959 } else if (onWallpaper) { 960 resource = R.anim.lock_screen_behind_enter_wallpaper; 961 } else { 962 resource = R.anim.lock_screen_behind_enter; 963 } 964 965 AnimationSet set = (AnimationSet) AnimationUtils.loadAnimation(context, resource); 966 967 // TODO: Use XML interpolators when we have log interpolators available in XML. 968 final List<Animation> animations = set.getAnimations(); 969 for (int i = animations.size() - 1; i >= 0; --i) { 970 animations.get(i).setInterpolator(interpolator); 971 } 972 973 return set; 974 } 975 } 976