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