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