1 /*
<lambda>null2  * Copyright (C) 2024 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.keyguard.ui.view.layout.sections.transitions
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.graphics.Rect
23 import android.transition.Transition
24 import android.transition.TransitionListenerAdapter
25 import android.transition.TransitionSet
26 import android.transition.TransitionValues
27 import android.util.Log
28 import android.view.View
29 import android.view.ViewGroup
30 import android.view.ViewTreeObserver.OnPreDrawListener
31 import com.android.app.animation.Interpolators
32 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
33 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
34 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_DOWN_MILLIS
35 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_UP_MILLIS
36 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
37 import com.android.systemui.res.R
38 import com.android.systemui.shared.R as sharedR
39 import com.google.android.material.math.MathUtils
40 import kotlin.math.abs
41 
42 internal fun View.getRect(): Rect = Rect(this.left, this.top, this.right, this.bottom)
43 
44 internal fun View.setRect(rect: Rect) =
45     this.setLeftTopRightBottom(rect.left, rect.top, rect.right, rect.bottom)
46 
47 class ClockSizeTransition(
48     config: IntraBlueprintTransition.Config,
49     clockViewModel: KeyguardClockViewModel,
50 ) : TransitionSet() {
51     init {
52         ordering = ORDERING_TOGETHER
53         if (config.type != Type.SmartspaceVisibility) {
54             addTransition(ClockFaceOutTransition(config, clockViewModel))
55             addTransition(ClockFaceInTransition(config, clockViewModel))
56         }
57         addTransition(SmartspaceMoveTransition(config, clockViewModel))
58     }
59 
60     abstract class VisibilityBoundsTransition() : Transition() {
61         abstract val captureSmartspace: Boolean
62         protected val TAG = this::class.simpleName!!
63 
64         override fun captureEndValues(transition: TransitionValues) = captureValues(transition)
65 
66         override fun captureStartValues(transition: TransitionValues) = captureValues(transition)
67 
68         override fun getTransitionProperties(): Array<String> = TRANSITION_PROPERTIES
69 
70         private fun captureValues(transition: TransitionValues) {
71             val view = transition.view
72             transition.values[PROP_VISIBILITY] = view.visibility
73             transition.values[PROP_ALPHA] = view.alpha
74             transition.values[PROP_BOUNDS] = view.getRect()
75 
76             if (!captureSmartspace) return
77             val parent = view.parent as View
78             val targetSSView =
79                 parent.findViewById<View>(sharedR.id.bc_smartspace_view)
80                     ?: parent.findViewById<View>(R.id.keyguard_slice_view)
81             if (targetSSView == null) {
82                 Log.e(TAG, "Failed to find smartspace equivalent target under $parent")
83                 return
84             }
85             transition.values[SMARTSPACE_BOUNDS] = targetSSView.getRect()
86         }
87 
88         open fun mutateBounds(
89             view: View,
90             fromIsVis: Boolean,
91             toIsVis: Boolean,
92             fromBounds: Rect,
93             toBounds: Rect,
94             fromSSBounds: Rect?,
95             toSSBounds: Rect?
96         ) {}
97 
98         override fun createAnimator(
99             sceenRoot: ViewGroup,
100             startValues: TransitionValues?,
101             endValues: TransitionValues?
102         ): Animator? {
103             if (startValues == null || endValues == null) {
104                 Log.w(
105                     TAG,
106                     "Couldn't create animator: startValues=$startValues; endValues=$endValues"
107                 )
108                 return null
109             }
110 
111             var fromVis = startValues.values[PROP_VISIBILITY] as Int
112             var fromIsVis = fromVis == View.VISIBLE
113             var fromAlpha = startValues.values[PROP_ALPHA] as Float
114             val fromBounds = startValues.values[PROP_BOUNDS] as Rect
115             val fromSSBounds = startValues.values[SMARTSPACE_BOUNDS] as Rect?
116 
117             val toView = endValues.view
118             val toVis = endValues.values[PROP_VISIBILITY] as Int
119             val toBounds = endValues.values[PROP_BOUNDS] as Rect
120             val toSSBounds = endValues.values[SMARTSPACE_BOUNDS] as Rect?
121             val toIsVis = toVis == View.VISIBLE
122             val toAlpha = if (toIsVis) 1f else 0f
123 
124             // Align starting visibility and alpha
125             if (!fromIsVis) fromAlpha = 0f
126             else if (fromAlpha <= 0f) {
127                 fromIsVis = false
128                 fromVis = View.INVISIBLE
129             }
130 
131             mutateBounds(toView, fromIsVis, toIsVis, fromBounds, toBounds, fromSSBounds, toSSBounds)
132             if (fromIsVis == toIsVis && fromBounds.equals(toBounds)) {
133                 if (DEBUG) {
134                     Log.w(
135                         TAG,
136                         "Skipping no-op transition: $toView; " +
137                             "vis: $fromVis -> $toVis; " +
138                             "alpha: $fromAlpha -> $toAlpha; " +
139                             "bounds: $fromBounds -> $toBounds; "
140                     )
141                 }
142                 return null
143             }
144 
145             val sendToBack = fromIsVis && !toIsVis
146             fun lerp(start: Int, end: Int, fract: Float): Int =
147                 MathUtils.lerp(start.toFloat(), end.toFloat(), fract).toInt()
148             fun computeBounds(fract: Float): Rect =
149                 Rect(
150                     lerp(fromBounds.left, toBounds.left, fract),
151                     lerp(fromBounds.top, toBounds.top, fract),
152                     lerp(fromBounds.right, toBounds.right, fract),
153                     lerp(fromBounds.bottom, toBounds.bottom, fract)
154                 )
155 
156             fun assignAnimValues(src: String, fract: Float, vis: Int? = null) {
157                 val bounds = computeBounds(fract)
158                 val alpha = MathUtils.lerp(fromAlpha, toAlpha, fract)
159                 if (DEBUG) {
160                     Log.i(
161                         TAG,
162                         "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;"
163                     )
164                 }
165                 toView.setVisibility(vis ?: View.VISIBLE)
166                 toView.setAlpha(alpha)
167                 toView.setRect(bounds)
168             }
169 
170             if (DEBUG) {
171                 Log.i(
172                     TAG,
173                     "transitioning: $toView; " +
174                         "vis: $fromVis -> $toVis; " +
175                         "alpha: $fromAlpha -> $toAlpha; " +
176                         "bounds: $fromBounds -> $toBounds; "
177                 )
178             }
179 
180             return ValueAnimator.ofFloat(0f, 1f).also { anim ->
181                 // We enforce the animation parameters on the target view every frame using a
182                 // predraw listener. This is suboptimal but prevents issues with layout passes
183                 // overwriting the animation for individual frames.
184                 val predrawCallback = OnPreDrawListener {
185                     assignAnimValues("predraw", anim.animatedFraction)
186                     return@OnPreDrawListener true
187                 }
188 
189                 this@VisibilityBoundsTransition.addListener(
190                     object : TransitionListenerAdapter() {
191                         override fun onTransitionStart(t: Transition) {
192                             toView.viewTreeObserver.addOnPreDrawListener(predrawCallback)
193                         }
194 
195                         override fun onTransitionEnd(t: Transition) {
196                             toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback)
197                         }
198                     }
199                 )
200 
201                 val listener =
202                     object : AnimatorListenerAdapter() {
203                         override fun onAnimationStart(anim: Animator) {
204                             assignAnimValues("start", 0f, fromVis)
205                         }
206 
207                         override fun onAnimationEnd(anim: Animator) {
208                             assignAnimValues("end", 1f, toVis)
209                             if (sendToBack) toView.translationZ = 0f
210                         }
211                     }
212 
213                 anim.addListener(listener)
214                 assignAnimValues("init", 0f, fromVis)
215             }
216         }
217 
218         companion object {
219             private const val PROP_VISIBILITY = "ClockSizeTransition:Visibility"
220             private const val PROP_ALPHA = "ClockSizeTransition:Alpha"
221             private const val PROP_BOUNDS = "ClockSizeTransition:Bounds"
222             private const val SMARTSPACE_BOUNDS = "ClockSizeTransition:SSBounds"
223             private val TRANSITION_PROPERTIES =
224                 arrayOf(PROP_VISIBILITY, PROP_ALPHA, PROP_BOUNDS, SMARTSPACE_BOUNDS)
225         }
226     }
227 
228     abstract class ClockFaceTransition(
229         config: IntraBlueprintTransition.Config,
230         val viewModel: KeyguardClockViewModel,
231     ) : VisibilityBoundsTransition() {
232         protected abstract val isLargeClock: Boolean
233         protected abstract val smallClockMoveScale: Float
234         override val captureSmartspace
235             get() = !isLargeClock
236 
237         protected fun addTargets() {
238             if (isLargeClock) {
239                 viewModel.currentClock.value?.let {
240                     if (DEBUG) Log.i(TAG, "Adding large clock views: ${it.largeClock.layout.views}")
241                     it.largeClock.layout.views.forEach { addTarget(it) }
242                 }
243                     ?: run {
244                         Log.e(TAG, "No large clock set, falling back")
245                         addTarget(R.id.lockscreen_clock_view_large)
246                     }
247             } else {
248                 if (DEBUG) Log.i(TAG, "Adding small clock")
249                 addTarget(R.id.lockscreen_clock_view)
250             }
251         }
252 
253         override fun mutateBounds(
254             view: View,
255             fromIsVis: Boolean,
256             toIsVis: Boolean,
257             fromBounds: Rect,
258             toBounds: Rect,
259             fromSSBounds: Rect?,
260             toSSBounds: Rect?
261         ) {
262             // Move normally if clock is not changing visibility
263             if (fromIsVis == toIsVis) return
264 
265             fromBounds.set(toBounds)
266             if (isLargeClock) {
267                 // Large clock shouldn't move; fromBounds already set
268             } else if (toSSBounds != null && fromSSBounds != null) {
269                 // Instead of moving the small clock the full distance, we compute the distance
270                 // smartspace will move. We then scale this to match the duration of this animation
271                 // so that the small clock moves at the same speed as smartspace.
272                 val ssTranslation =
273                     abs((toSSBounds.top - fromSSBounds.top) * smallClockMoveScale).toInt()
274                 fromBounds.top = toBounds.top - ssTranslation
275                 fromBounds.bottom = toBounds.bottom - ssTranslation
276             } else {
277                 Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds")
278             }
279         }
280     }
281 
282     class ClockFaceInTransition(
283         config: IntraBlueprintTransition.Config,
284         viewModel: KeyguardClockViewModel,
285     ) : ClockFaceTransition(config, viewModel) {
286         override val isLargeClock = viewModel.isLargeClockVisible.value
287         override val smallClockMoveScale = CLOCK_IN_MILLIS / STATUS_AREA_MOVE_DOWN_MILLIS.toFloat()
288 
289         init {
290             duration = CLOCK_IN_MILLIS
291             startDelay = CLOCK_IN_START_DELAY_MILLIS
292             interpolator = CLOCK_IN_INTERPOLATOR
293             addTargets()
294         }
295 
296         companion object {
297             const val CLOCK_IN_MILLIS = 167L
298             const val CLOCK_IN_START_DELAY_MILLIS = 133L
299             val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN
300         }
301     }
302 
303     class ClockFaceOutTransition(
304         config: IntraBlueprintTransition.Config,
305         viewModel: KeyguardClockViewModel,
306     ) : ClockFaceTransition(config, viewModel) {
307         override val isLargeClock = !viewModel.isLargeClockVisible.value
308         override val smallClockMoveScale = CLOCK_OUT_MILLIS / STATUS_AREA_MOVE_UP_MILLIS.toFloat()
309 
310         init {
311             duration = CLOCK_OUT_MILLIS
312             interpolator = CLOCK_OUT_INTERPOLATOR
313             addTargets()
314         }
315 
316         companion object {
317             const val CLOCK_OUT_MILLIS = 133L
318             val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR
319         }
320     }
321 
322     // TODO: Might need a mechanism to update this one while in-progress
323     class SmartspaceMoveTransition(
324         val config: IntraBlueprintTransition.Config,
325         viewModel: KeyguardClockViewModel,
326     ) : VisibilityBoundsTransition() {
327         private val isLargeClock = viewModel.isLargeClockVisible.value
328         override val captureSmartspace = false
329 
330         init {
331             duration =
332                 if (isLargeClock) STATUS_AREA_MOVE_UP_MILLIS else STATUS_AREA_MOVE_DOWN_MILLIS
333             interpolator = Interpolators.EMPHASIZED
334             addTarget(sharedR.id.date_smartspace_view)
335             addTarget(sharedR.id.bc_smartspace_view)
336 
337             // Notifications normally and media on split shade needs to be moved
338             addTarget(R.id.aod_notification_icon_container)
339             addTarget(R.id.status_view_media_container)
340         }
341 
342         override fun mutateBounds(
343             view: View,
344             fromIsVis: Boolean,
345             toIsVis: Boolean,
346             fromBounds: Rect,
347             toBounds: Rect,
348             fromSSBounds: Rect?,
349             toSSBounds: Rect?
350         ) {
351             // If view is changing visibility, hold it in place
352             if (fromIsVis == toIsVis) return
353             if (DEBUG) Log.i(TAG, "Holding position of ${view.id}")
354             toBounds.set(fromBounds)
355         }
356 
357         companion object {
358             const val STATUS_AREA_MOVE_UP_MILLIS = 967L
359             const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L
360         }
361     }
362 
363     companion object {
364         val DEBUG = false
365     }
366 }
367