1 package com.android.systemui.statusbar.notification 2 3 import android.view.ViewGroup 4 import com.android.internal.jank.InteractionJankMonitor 5 import com.android.systemui.animation.ActivityLaunchAnimator 6 import com.android.systemui.animation.LaunchAnimator 7 import com.android.systemui.shade.NotificationShadeWindowViewController 8 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 9 import com.android.systemui.statusbar.notification.stack.NotificationListContainer 10 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone 11 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent 12 import com.android.systemui.statusbar.policy.HeadsUpUtil 13 import javax.inject.Inject 14 import kotlin.math.ceil 15 import kotlin.math.max 16 17 /** A provider of [NotificationLaunchAnimatorController]. */ 18 @CentralSurfacesComponent.CentralSurfacesScope 19 class NotificationLaunchAnimatorControllerProvider @Inject constructor( 20 private val notificationShadeWindowViewController: NotificationShadeWindowViewController, 21 private val notificationListContainer: NotificationListContainer, 22 private val headsUpManager: HeadsUpManagerPhone, 23 private val jankMonitor: InteractionJankMonitor 24 ) { 25 @JvmOverloads getAnimatorControllernull26 fun getAnimatorController( 27 notification: ExpandableNotificationRow, 28 onFinishAnimationCallback: Runnable? = null 29 ): NotificationLaunchAnimatorController { 30 return NotificationLaunchAnimatorController( 31 notificationShadeWindowViewController, 32 notificationListContainer, 33 headsUpManager, 34 notification, 35 jankMonitor, 36 onFinishAnimationCallback 37 ) 38 } 39 } 40 41 /** 42 * An [ActivityLaunchAnimator.Controller] that animates an [ExpandableNotificationRow]. An instance 43 * of this class can be passed to [ActivityLaunchAnimator.startIntentWithAnimation] to animate a 44 * notification expanding into an opening window. 45 */ 46 class NotificationLaunchAnimatorController( 47 private val notificationShadeWindowViewController: NotificationShadeWindowViewController, 48 private val notificationListContainer: NotificationListContainer, 49 private val headsUpManager: HeadsUpManagerPhone, 50 private val notification: ExpandableNotificationRow, 51 private val jankMonitor: InteractionJankMonitor, 52 private val onFinishAnimationCallback: Runnable? 53 ) : ActivityLaunchAnimator.Controller { 54 55 companion object { 56 const val ANIMATION_DURATION_TOP_ROUNDING = 100L 57 } 58 59 private val notificationEntry = notification.entry 60 private val notificationKey = notificationEntry.sbn.key 61 62 override var launchContainer: ViewGroup 63 get() = notification.rootView as ViewGroup 64 set(ignored) { 65 // Do nothing. Notifications are always animated inside their rootView. 66 } 67 createAnimatorStatenull68 override fun createAnimatorState(): LaunchAnimator.State { 69 // If the notification panel is collapsed, the clip may be larger than the height. 70 val height = max(0, notification.actualHeight - notification.clipBottomAmount) 71 val location = notification.locationOnScreen 72 73 val clipStartLocation = notificationListContainer.topClippingStartLocation 74 val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0) 75 val windowTop = location[1] + roundedTopClipping 76 val topCornerRadius = if (roundedTopClipping > 0) { 77 // Because the rounded Rect clipping is complex, we start the top rounding at 78 // 0, which is pretty close to matching the real clipping. 79 // We'd have to clipOut the overlaid drawable too with the outer rounded rect in case 80 // if we'd like to have this perfect, but this is close enough. 81 0f 82 } else { 83 notification.topCornerRadius 84 } 85 val params = LaunchAnimationParameters( 86 top = windowTop, 87 bottom = location[1] + height, 88 left = location[0], 89 right = location[0] + notification.width, 90 topCornerRadius = topCornerRadius, 91 bottomCornerRadius = notification.bottomCornerRadius 92 ) 93 94 params.startTranslationZ = notification.translationZ 95 params.startNotificationTop = location[1] 96 params.notificationParentTop = notificationListContainer 97 .getViewParentForNotification(notificationEntry).locationOnScreen[1] 98 params.startRoundedTopClipping = roundedTopClipping 99 params.startClipTopAmount = notification.clipTopAmount 100 if (notification.isChildInGroup) { 101 val locationOnScreen = notification.notificationParent.locationOnScreen[1] 102 val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0) 103 params.parentStartRoundedTopClipping = parentRoundedClip 104 105 val parentClip = notification.notificationParent.clipTopAmount 106 params.parentStartClipTopAmount = parentClip 107 108 // We need to calculate how much the child is clipped by the parent because children 109 // always have 0 clipTopAmount 110 if (parentClip != 0) { 111 val childClip = parentClip - notification.translationY 112 if (childClip > 0) { 113 params.startClipTopAmount = ceil(childClip.toDouble()).toInt() 114 } 115 } 116 } 117 118 return params 119 } 120 onIntentStartednull121 override fun onIntentStarted(willAnimate: Boolean) { 122 notificationShadeWindowViewController.setExpandAnimationRunning(willAnimate) 123 notificationEntry.isExpandAnimationRunning = willAnimate 124 125 if (!willAnimate) { 126 removeHun(animate = true) 127 onFinishAnimationCallback?.run() 128 } 129 } 130 removeHunnull131 private fun removeHun(animate: Boolean) { 132 if (!headsUpManager.isAlerting(notificationKey)) { 133 return 134 } 135 136 HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate) 137 headsUpManager.removeNotification(notificationKey, true /* releaseImmediately */, animate) 138 } 139 onLaunchAnimationCancellednull140 override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) { 141 // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started 142 // here? 143 notificationShadeWindowViewController.setExpandAnimationRunning(false) 144 notificationEntry.isExpandAnimationRunning = false 145 removeHun(animate = true) 146 onFinishAnimationCallback?.run() 147 } 148 onLaunchAnimationStartnull149 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 150 notification.isExpandAnimationRunning = true 151 notificationListContainer.setExpandingNotification(notification) 152 153 jankMonitor.begin(notification, 154 InteractionJankMonitor.CUJ_NOTIFICATION_APP_START) 155 } 156 onLaunchAnimationEndnull157 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { 158 jankMonitor.end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START) 159 160 notification.isExpandAnimationRunning = false 161 notificationShadeWindowViewController.setExpandAnimationRunning(false) 162 notificationEntry.isExpandAnimationRunning = false 163 notificationListContainer.setExpandingNotification(null) 164 applyParams(null) 165 removeHun(animate = false) 166 onFinishAnimationCallback?.run() 167 } 168 applyParamsnull169 private fun applyParams(params: LaunchAnimationParameters?) { 170 notification.applyLaunchAnimationParams(params) 171 notificationListContainer.applyLaunchAnimationParams(params) 172 } 173 onLaunchAnimationProgressnull174 override fun onLaunchAnimationProgress( 175 state: LaunchAnimator.State, 176 progress: Float, 177 linearProgress: Float 178 ) { 179 val params = state as LaunchAnimationParameters 180 params.progress = progress 181 params.linearProgress = linearProgress 182 183 applyParams(params) 184 } 185 } 186