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