• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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