1 /* 2 * Copyright (C) 2018 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.systemui.statusbar.notification; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.app.ActivityManager; 23 import android.graphics.Matrix; 24 import android.graphics.Rect; 25 import android.os.RemoteException; 26 import android.util.MathUtils; 27 import android.view.IRemoteAnimationFinishedCallback; 28 import android.view.IRemoteAnimationRunner; 29 import android.view.RemoteAnimationAdapter; 30 import android.view.RemoteAnimationTarget; 31 import android.view.SyncRtSurfaceTransactionApplier; 32 import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; 33 import android.view.View; 34 35 import com.android.internal.policy.ScreenDecorationsUtils; 36 import com.android.systemui.Interpolators; 37 import com.android.systemui.statusbar.NotificationShadeDepthController; 38 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 39 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 40 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; 41 import com.android.systemui.statusbar.phone.NotificationPanelViewController; 42 import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController; 43 44 import java.util.concurrent.Executor; 45 46 /** 47 * A class that allows activities to be launched in a seamless way where the notification 48 * transforms nicely into the starting window. 49 */ 50 public class ActivityLaunchAnimator { 51 52 private static final int ANIMATION_DURATION = 400; 53 public static final long ANIMATION_DURATION_FADE_CONTENT = 67; 54 public static final long ANIMATION_DURATION_FADE_APP = 200; 55 public static final long ANIMATION_DELAY_ICON_FADE_IN = ANIMATION_DURATION - 56 CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY 57 - 16; 58 private static final long LAUNCH_TIMEOUT = 500; 59 private final NotificationPanelViewController mNotificationPanel; 60 private final NotificationListContainer mNotificationContainer; 61 private final float mWindowCornerRadius; 62 private final NotificationShadeWindowViewController mNotificationShadeWindowViewController; 63 private final NotificationShadeDepthController mDepthController; 64 private final Executor mMainExecutor; 65 private Callback mCallback; 66 private final Runnable mTimeoutRunnable = () -> { 67 setAnimationPending(false); 68 mCallback.onExpandAnimationTimedOut(); 69 }; 70 private boolean mAnimationPending; 71 private boolean mAnimationRunning; 72 private boolean mIsLaunchForActivity; 73 ActivityLaunchAnimator( NotificationShadeWindowViewController notificationShadeWindowViewController, Callback callback, NotificationPanelViewController notificationPanel, NotificationShadeDepthController depthController, NotificationListContainer container, Executor mainExecutor)74 public ActivityLaunchAnimator( 75 NotificationShadeWindowViewController notificationShadeWindowViewController, 76 Callback callback, 77 NotificationPanelViewController notificationPanel, 78 NotificationShadeDepthController depthController, 79 NotificationListContainer container, 80 Executor mainExecutor) { 81 mNotificationPanel = notificationPanel; 82 mNotificationContainer = container; 83 mDepthController = depthController; 84 mNotificationShadeWindowViewController = notificationShadeWindowViewController; 85 mCallback = callback; 86 mMainExecutor = mainExecutor; 87 mWindowCornerRadius = ScreenDecorationsUtils 88 .getWindowCornerRadius(mNotificationShadeWindowViewController.getView() 89 .getResources()); 90 } 91 getLaunchAnimation( View sourceView, boolean occluded)92 public RemoteAnimationAdapter getLaunchAnimation( 93 View sourceView, boolean occluded) { 94 if (!(sourceView instanceof ExpandableNotificationRow) 95 || !mCallback.areLaunchAnimationsEnabled() || occluded) { 96 return null; 97 } 98 AnimationRunner animationRunner = new AnimationRunner( 99 (ExpandableNotificationRow) sourceView); 100 return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION, 101 ANIMATION_DURATION - 150 /* statusBarTransitionDelay */); 102 } 103 isAnimationPending()104 public boolean isAnimationPending() { 105 return mAnimationPending; 106 } 107 108 /** 109 * Set the launch result the intent requested 110 * 111 * @param launchResult the launch result 112 * @param wasIntentActivity was this launch for an activity 113 */ setLaunchResult(int launchResult, boolean wasIntentActivity)114 public void setLaunchResult(int launchResult, boolean wasIntentActivity) { 115 mIsLaunchForActivity = wasIntentActivity; 116 setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT 117 || launchResult == ActivityManager.START_SUCCESS) 118 && mCallback.areLaunchAnimationsEnabled()); 119 } 120 isLaunchForActivity()121 public boolean isLaunchForActivity() { 122 return mIsLaunchForActivity; 123 } 124 setAnimationPending(boolean pending)125 private void setAnimationPending(boolean pending) { 126 mAnimationPending = pending; 127 mNotificationShadeWindowViewController.setExpandAnimationPending(pending); 128 if (pending) { 129 mNotificationShadeWindowViewController.getView().postDelayed(mTimeoutRunnable, 130 LAUNCH_TIMEOUT); 131 } else { 132 mNotificationShadeWindowViewController.getView().removeCallbacks(mTimeoutRunnable); 133 } 134 } 135 isAnimationRunning()136 public boolean isAnimationRunning() { 137 return mAnimationRunning; 138 } 139 140 class AnimationRunner extends IRemoteAnimationRunner.Stub { 141 142 private final ExpandableNotificationRow mSourceNotification; 143 private final ExpandAnimationParameters mParams; 144 private final Rect mWindowCrop = new Rect(); 145 private final float mNotificationCornerRadius; 146 private float mCornerRadius; 147 private boolean mIsFullScreenLaunch = true; 148 private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier; 149 AnimationRunner(ExpandableNotificationRow sourceNofitication)150 public AnimationRunner(ExpandableNotificationRow sourceNofitication) { 151 mSourceNotification = sourceNofitication; 152 mParams = new ExpandAnimationParameters(); 153 mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification); 154 mNotificationCornerRadius = Math.max(mSourceNotification.getCurrentTopRoundness(), 155 mSourceNotification.getCurrentBottomRoundness()); 156 } 157 158 @Override onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets, RemoteAnimationTarget[] remoteAnimationWallpaperTargets, IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)159 public void onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets, 160 RemoteAnimationTarget[] remoteAnimationWallpaperTargets, 161 IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback) 162 throws RemoteException { 163 mMainExecutor.execute(() -> { 164 RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget( 165 remoteAnimationTargets); 166 if (primary == null) { 167 setAnimationPending(false); 168 invokeCallback(iRemoteAnimationFinishedCallback); 169 mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */); 170 return; 171 } 172 173 setExpandAnimationRunning(true); 174 mIsFullScreenLaunch = primary.position.y == 0 175 && primary.sourceContainerBounds.height() 176 >= mNotificationPanel.getHeight(); 177 if (!mIsFullScreenLaunch) { 178 mNotificationPanel.collapseWithDuration(ANIMATION_DURATION); 179 } 180 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 181 mParams.startPosition = mSourceNotification.getLocationOnScreen(); 182 mParams.startTranslationZ = mSourceNotification.getTranslationZ(); 183 mParams.startClipTopAmount = mSourceNotification.getClipTopAmount(); 184 if (mSourceNotification.isChildInGroup()) { 185 int parentClip = mSourceNotification 186 .getNotificationParent().getClipTopAmount(); 187 mParams.parentStartClipTopAmount = parentClip; 188 // We need to calculate how much the child is clipped by the parent 189 // because children always have 0 clipTopAmount 190 if (parentClip != 0) { 191 float childClip = parentClip 192 - mSourceNotification.getTranslationY(); 193 if (childClip > 0.0f) { 194 mParams.startClipTopAmount = (int) Math.ceil(childClip); 195 } 196 } 197 } 198 int targetWidth = primary.sourceContainerBounds.width(); 199 // If the notification panel is collapsed, the clip may be larger than the height. 200 int notificationHeight = Math.max(mSourceNotification.getActualHeight() 201 - mSourceNotification.getClipBottomAmount(), 0); 202 int notificationWidth = mSourceNotification.getWidth(); 203 anim.setDuration(ANIMATION_DURATION); 204 anim.setInterpolator(Interpolators.LINEAR); 205 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 206 @Override 207 public void onAnimationUpdate(ValueAnimator animation) { 208 mParams.linearProgress = animation.getAnimatedFraction(); 209 float progress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( 210 mParams.linearProgress); 211 int newWidth = (int) MathUtils.lerp(notificationWidth, 212 targetWidth, progress); 213 mParams.left = (int) ((targetWidth - newWidth) / 2.0f); 214 mParams.right = mParams.left + newWidth; 215 mParams.top = (int) MathUtils.lerp(mParams.startPosition[1], 216 primary.position.y, progress); 217 mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1] 218 + notificationHeight, 219 primary.position.y + primary.sourceContainerBounds.bottom, 220 progress); 221 mCornerRadius = MathUtils.lerp(mNotificationCornerRadius, 222 mWindowCornerRadius, progress); 223 applyParamsToWindow(primary); 224 applyParamsToNotification(mParams); 225 applyParamsToNotificationShade(mParams); 226 } 227 }); 228 anim.addListener(new AnimatorListenerAdapter() { 229 @Override 230 public void onAnimationEnd(Animator animation) { 231 setExpandAnimationRunning(false); 232 invokeCallback(iRemoteAnimationFinishedCallback); 233 } 234 }); 235 anim.start(); 236 setAnimationPending(false); 237 }); 238 } 239 invokeCallback(IRemoteAnimationFinishedCallback callback)240 private void invokeCallback(IRemoteAnimationFinishedCallback callback) { 241 try { 242 callback.onAnimationFinished(); 243 } catch (RemoteException e) { 244 e.printStackTrace(); 245 } 246 } 247 getPrimaryRemoteAnimationTarget( RemoteAnimationTarget[] remoteAnimationTargets)248 private RemoteAnimationTarget getPrimaryRemoteAnimationTarget( 249 RemoteAnimationTarget[] remoteAnimationTargets) { 250 RemoteAnimationTarget primary = null; 251 for (RemoteAnimationTarget app : remoteAnimationTargets) { 252 if (app.mode == RemoteAnimationTarget.MODE_OPENING) { 253 primary = app; 254 break; 255 } 256 } 257 return primary; 258 } 259 setExpandAnimationRunning(boolean running)260 private void setExpandAnimationRunning(boolean running) { 261 mNotificationPanel.setLaunchingNotification(running); 262 mSourceNotification.setExpandAnimationRunning(running); 263 mNotificationShadeWindowViewController.setExpandAnimationRunning(running); 264 mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null); 265 mAnimationRunning = running; 266 if (!running) { 267 mCallback.onExpandAnimationFinished(mIsFullScreenLaunch); 268 applyParamsToNotification(null); 269 applyParamsToNotificationShade(null); 270 } 271 272 } 273 applyParamsToNotificationShade(ExpandAnimationParameters params)274 private void applyParamsToNotificationShade(ExpandAnimationParameters params) { 275 mNotificationContainer.applyExpandAnimationParams(params); 276 mNotificationPanel.applyExpandAnimationParams(params); 277 mDepthController.setNotificationLaunchAnimationParams(params); 278 } 279 applyParamsToNotification(ExpandAnimationParameters params)280 private void applyParamsToNotification(ExpandAnimationParameters params) { 281 mSourceNotification.applyExpandAnimationParams(params); 282 } 283 applyParamsToWindow(RemoteAnimationTarget app)284 private void applyParamsToWindow(RemoteAnimationTarget app) { 285 Matrix m = new Matrix(); 286 m.postTranslate(0, (float) (mParams.top - app.position.y)); 287 mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight()); 288 SurfaceParams params = new SurfaceParams.Builder(app.leash) 289 .withAlpha(1f) 290 .withMatrix(m) 291 .withWindowCrop(mWindowCrop) 292 .withLayer(app.prefixOrderIndex) 293 .withCornerRadius(mCornerRadius) 294 .withVisibility(true) 295 .build(); 296 mSyncRtTransactionApplier.scheduleApply(params); 297 } 298 299 @Override onAnimationCancelled()300 public void onAnimationCancelled() throws RemoteException { 301 mMainExecutor.execute(() -> { 302 setAnimationPending(false); 303 mCallback.onLaunchAnimationCancelled(); 304 }); 305 } 306 }; 307 308 public static class ExpandAnimationParameters { 309 public float linearProgress; 310 int[] startPosition; 311 float startTranslationZ; 312 int left; 313 int top; 314 int right; 315 int bottom; 316 int startClipTopAmount; 317 int parentStartClipTopAmount; 318 ExpandAnimationParameters()319 public ExpandAnimationParameters() { 320 } 321 getTop()322 public int getTop() { 323 return top; 324 } 325 getBottom()326 public int getBottom() { 327 return bottom; 328 } 329 getWidth()330 public int getWidth() { 331 return right - left; 332 } 333 getHeight()334 public int getHeight() { 335 return bottom - top; 336 } 337 getTopChange()338 public int getTopChange() { 339 // We need this compensation to ensure that the QS moves in sync. 340 int clipTopAmountCompensation = 0; 341 if (startClipTopAmount != 0.0f) { 342 clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount, 343 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress)); 344 } 345 return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0); 346 } 347 getProgress()348 public float getProgress() { 349 return linearProgress; 350 } 351 getProgress(long delay, long duration)352 public float getProgress(long delay, long duration) { 353 return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay) 354 / duration, 0.0f, 1.0f); 355 } 356 getStartClipTopAmount()357 public int getStartClipTopAmount() { 358 return startClipTopAmount; 359 } 360 getParentStartClipTopAmount()361 public int getParentStartClipTopAmount() { 362 return parentStartClipTopAmount; 363 } 364 getStartTranslationZ()365 public float getStartTranslationZ() { 366 return startTranslationZ; 367 } 368 } 369 370 public interface Callback { 371 372 /** 373 * Called when the launch animation was cancelled. 374 */ onLaunchAnimationCancelled()375 void onLaunchAnimationCancelled(); 376 377 /** 378 * Called when the launch animation has timed out without starting an actual animation. 379 */ onExpandAnimationTimedOut()380 void onExpandAnimationTimedOut(); 381 382 /** 383 * Called when the expand animation has finished. 384 * 385 * @param launchIsFullScreen True if this launch was fullscreen, such that now the window 386 * fills the whole screen 387 */ onExpandAnimationFinished(boolean launchIsFullScreen)388 void onExpandAnimationFinished(boolean launchIsFullScreen); 389 390 /** 391 * Are animations currently enabled. 392 */ areLaunchAnimationsEnabled()393 boolean areLaunchAnimationsEnabled(); 394 } 395 } 396