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.systemui.statusbar.notification 18 19 import android.util.Log 20 import android.view.ViewGroup 21 import com.android.internal.jank.InteractionJankMonitor 22 import com.android.systemui.animation.ActivityTransitionAnimator 23 import com.android.systemui.animation.TransitionAnimator 24 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor 25 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 26 import com.android.systemui.statusbar.notification.stack.NotificationListContainer 27 import com.android.systemui.statusbar.policy.HeadsUpManager 28 import com.android.systemui.statusbar.policy.HeadsUpUtil 29 import kotlin.math.ceil 30 import kotlin.math.max 31 32 private const val TAG = "NotificationLaunchAnimatorController" 33 34 /** A provider of [NotificationTransitionAnimatorController]. */ 35 class NotificationLaunchAnimatorControllerProvider( 36 private val notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor, 37 private val notificationListContainer: NotificationListContainer, 38 private val headsUpManager: HeadsUpManager, 39 private val jankMonitor: InteractionJankMonitor 40 ) { 41 @JvmOverloads getAnimatorControllernull42 fun getAnimatorController( 43 notification: ExpandableNotificationRow, 44 onFinishAnimationCallback: Runnable? = null 45 ): NotificationTransitionAnimatorController { 46 return NotificationTransitionAnimatorController( 47 notificationLaunchAnimationInteractor, 48 notificationListContainer, 49 headsUpManager, 50 notification, 51 jankMonitor, 52 onFinishAnimationCallback 53 ) 54 } 55 } 56 57 /** 58 * An [ActivityTransitionAnimator.Controller] that animates an [ExpandableNotificationRow]. An 59 * instance of this class can be passed to [ActivityTransitionAnimator.startIntentWithAnimation] to 60 * animate a notification expanding into an opening window. 61 */ 62 class NotificationTransitionAnimatorController( 63 private val notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor, 64 private val notificationListContainer: NotificationListContainer, 65 private val headsUpManager: HeadsUpManager, 66 private val notification: ExpandableNotificationRow, 67 private val jankMonitor: InteractionJankMonitor, 68 private val onFinishAnimationCallback: Runnable? 69 ) : ActivityTransitionAnimator.Controller { 70 71 companion object { 72 const val ANIMATION_DURATION_TOP_ROUNDING = 100L 73 } 74 75 private val notificationEntry = notification.entry 76 private val notificationKey = notificationEntry.sbn.key 77 78 override val isLaunching: Boolean = true 79 80 override var transitionContainer: ViewGroup 81 get() = notification.rootView as ViewGroup 82 set(ignored) { 83 // Do nothing. Notifications are always animated inside their rootView. 84 } 85 createAnimatorStatenull86 override fun createAnimatorState(): TransitionAnimator.State { 87 // If the notification panel is collapsed, the clip may be larger than the height. 88 val height = max(0, notification.actualHeight - notification.clipBottomAmount) 89 val location = notification.locationOnScreen 90 91 val clipStartLocation = notificationListContainer.topClippingStartLocation 92 val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0) 93 val windowTop = location[1] + roundedTopClipping 94 val topCornerRadius = 95 if (roundedTopClipping > 0) { 96 // Because the rounded Rect clipping is complex, we start the top rounding at 97 // 0, which is pretty close to matching the real clipping. 98 // We'd have to clipOut the overlaid drawable too with the outer rounded rect in 99 // case 100 // if we'd like to have this perfect, but this is close enough. 101 0f 102 } else { 103 notification.topCornerRadius 104 } 105 val params = 106 LaunchAnimationParameters( 107 top = windowTop, 108 bottom = location[1] + height, 109 left = location[0], 110 right = location[0] + notification.width, 111 topCornerRadius = topCornerRadius, 112 bottomCornerRadius = notification.bottomCornerRadius 113 ) 114 115 params.startTranslationZ = notification.translationZ 116 params.startNotificationTop = location[1] 117 params.notificationParentTop = 118 notificationListContainer 119 .getViewParentForNotification(notificationEntry) 120 .locationOnScreen[1] 121 params.startRoundedTopClipping = roundedTopClipping 122 params.startClipTopAmount = notification.clipTopAmount 123 if (notification.isChildInGroup) { 124 val locationOnScreen = notification.notificationParent.locationOnScreen[1] 125 val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0) 126 params.parentStartRoundedTopClipping = parentRoundedClip 127 128 val parentClip = notification.notificationParent.clipTopAmount 129 params.parentStartClipTopAmount = parentClip 130 131 // We need to calculate how much the child is clipped by the parent because children 132 // always have 0 clipTopAmount 133 if (parentClip != 0) { 134 val childClip = parentClip - notification.translationY 135 if (childClip > 0) { 136 params.startClipTopAmount = ceil(childClip.toDouble()).toInt() 137 } 138 } 139 } 140 141 return params 142 } 143 onIntentStartednull144 override fun onIntentStarted(willAnimate: Boolean) { 145 if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) { 146 Log.d(TAG, "onIntentStarted(willAnimate=$willAnimate)") 147 } 148 notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(willAnimate) 149 notificationEntry.isExpandAnimationRunning = willAnimate 150 151 if (!willAnimate) { 152 removeHun(animate = true) 153 onFinishAnimationCallback?.run() 154 } 155 } 156 157 private val headsUpNotificationRow: ExpandableNotificationRow? 158 get() { 159 val summaryEntry = notificationEntry.parent?.summary 160 161 return when { 162 headsUpManager.isHeadsUpEntry(notificationKey) -> notification 163 summaryEntry == null -> null 164 headsUpManager.isHeadsUpEntry(summaryEntry.key) -> summaryEntry.row 165 else -> null 166 } 167 } 168 removeHunnull169 private fun removeHun(animate: Boolean) { 170 val row = headsUpNotificationRow ?: return 171 172 // TODO: b/297247841 - Call on the row we're removing, which may differ from notification. 173 HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate) 174 175 headsUpManager.removeNotification(row.entry.key, true /* releaseImmediately */, animate) 176 } 177 onTransitionAnimationCancellednull178 override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { 179 if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) { 180 Log.d(TAG, "onLaunchAnimationCancelled()") 181 } 182 183 // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started 184 // here? 185 notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false) 186 notificationEntry.isExpandAnimationRunning = false 187 removeHun(animate = true) 188 onFinishAnimationCallback?.run() 189 } 190 onTransitionAnimationStartnull191 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { 192 notification.isExpandAnimationRunning = true 193 notificationListContainer.setExpandingNotification(notification) 194 195 jankMonitor.begin(notification, InteractionJankMonitor.CUJ_NOTIFICATION_APP_START) 196 } 197 onTransitionAnimationEndnull198 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { 199 if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) { 200 Log.d(TAG, "onLaunchAnimationEnd()") 201 } 202 jankMonitor.end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START) 203 204 notification.isExpandAnimationRunning = false 205 notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false) 206 notificationEntry.isExpandAnimationRunning = false 207 notificationListContainer.setExpandingNotification(null) 208 applyParams(null) 209 removeHun(animate = false) 210 onFinishAnimationCallback?.run() 211 } 212 applyParamsnull213 private fun applyParams(params: LaunchAnimationParameters?) { 214 notification.applyLaunchAnimationParams(params) 215 notificationListContainer.applyLaunchAnimationParams(params) 216 } 217 onTransitionAnimationProgressnull218 override fun onTransitionAnimationProgress( 219 state: TransitionAnimator.State, 220 progress: Float, 221 linearProgress: Float 222 ) { 223 val params = state as LaunchAnimationParameters 224 params.progress = progress 225 params.linearProgress = linearProgress 226 227 applyParams(params) 228 } 229 } 230