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.qs.composefragment.viewmodel
18
19 import android.content.res.Resources
20 import android.graphics.Rect
21 import androidx.annotation.FloatRange
22 import androidx.annotation.VisibleForTesting
23 import androidx.compose.runtime.derivedStateOf
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.setValue
27 import androidx.compose.runtime.snapshotFlow
28 import androidx.lifecycle.LifecycleCoroutineScope
29 import com.android.app.animation.Interpolators
30 import com.android.app.tracing.coroutines.launchTraced as launch
31 import com.android.internal.logging.UiEventLogger
32 import com.android.keyguard.BouncerPanelExpansionCalculator
33 import com.android.systemui.Dumpable
34 import com.android.systemui.animation.ShadeInterpolation
35 import com.android.systemui.classifier.Classifier
36 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
37 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
38 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
39 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
40 import com.android.systemui.keyguard.shared.model.Edge
41 import com.android.systemui.keyguard.shared.model.KeyguardState
42 import com.android.systemui.lifecycle.ExclusiveActivatable
43 import com.android.systemui.lifecycle.Hydrator
44 import com.android.systemui.log.table.TableLogBuffer
45 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
46 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
47 import com.android.systemui.media.controls.ui.view.MediaHost
48 import com.android.systemui.media.controls.ui.view.MediaHostState
49 import com.android.systemui.media.dagger.MediaModule.QS_PANEL
50 import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
51 import com.android.systemui.plugins.statusbar.StatusBarStateController
52 import com.android.systemui.qs.FooterActionsController
53 import com.android.systemui.qs.QSEvent
54 import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeLog
55 import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule
56 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
57 import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
58 import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel
59 import com.android.systemui.qs.panels.ui.viewmodel.MediaInRowInLandscapeViewModel
60 import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
61 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
62 import com.android.systemui.res.R
63 import com.android.systemui.scene.shared.model.Overlays
64 import com.android.systemui.shade.LargeScreenHeaderHelper
65 import com.android.systemui.shade.ShadeDisplayAware
66 import com.android.systemui.shade.domain.interactor.ShadeInteractor
67 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
68 import com.android.systemui.statusbar.StatusBarState
69 import com.android.systemui.statusbar.SysuiStatusBarStateController
70 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
71 import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
72 import com.android.systemui.util.LargeScreenUtils
73 import com.android.systemui.util.asIndenting
74 import com.android.systemui.util.kotlin.emitOnStart
75 import com.android.systemui.util.printSection
76 import com.android.systemui.util.println
77 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
78 import dagger.assisted.Assisted
79 import dagger.assisted.AssistedFactory
80 import dagger.assisted.AssistedInject
81 import java.io.PrintWriter
82 import javax.inject.Named
83 import kotlinx.coroutines.awaitCancellation
84 import kotlinx.coroutines.channels.awaitClose
85 import kotlinx.coroutines.coroutineScope
86 import kotlinx.coroutines.flow.Flow
87 import kotlinx.coroutines.flow.callbackFlow
88 import kotlinx.coroutines.flow.flowOf
89 import kotlinx.coroutines.flow.map
90 import kotlinx.coroutines.flow.onStart
91
92 class QSFragmentComposeViewModel
93 @AssistedInject
94 constructor(
95 containerViewModelFactory: QuickSettingsContainerViewModel.Factory,
96 @ShadeDisplayAware private val resources: Resources,
97 quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory,
98 footerActionsViewModelFactory: FooterActionsViewModel.Factory,
99 private val footerActionsController: FooterActionsController,
100 private val sysuiStatusBarStateController: SysuiStatusBarStateController,
101 deviceEntryInteractor: DeviceEntryInteractor,
102 disableFlagsInteractor: DisableFlagsInteractor,
103 keyguardTransitionInteractor: KeyguardTransitionInteractor,
104 private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
105 shadeInteractor: ShadeInteractor,
106 @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
107 private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
108 private val squishinessInteractor: TileSquishinessInteractor,
109 private val falsingInteractor: FalsingInteractor,
110 private val inFirstPageViewModel: InFirstPageViewModel,
111 @QSFragmentComposeLog private val tableLogBuffer: TableLogBuffer,
112 mediaInRowInLandscapeViewModelFactory: MediaInRowInLandscapeViewModel.Factory,
113 @Named(QUICK_QS_PANEL) val qqsMediaHost: MediaHost,
114 @Named(QS_PANEL) val qsMediaHost: MediaHost,
115 @Named(QSFragmentComposeModule.QS_USING_MEDIA_PLAYER) private val usingMedia: Boolean,
116 private val uiEventLogger: UiEventLogger,
117 @Assisted private val lifecycleScope: LifecycleCoroutineScope,
118 ) : Dumpable, ExclusiveActivatable() {
119
120 val containerViewModel = containerViewModelFactory.create(supportsBrightnessMirroring = true)
121 val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create()
122
123 private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS)
124 private val qsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QS)
125
126 private val hydrator = Hydrator("QSFragmentComposeViewModel.hydrator", tableLogBuffer)
127
128 val footerActionsViewModel =
129 footerActionsViewModelFactory.create(lifecycleScope).also {
130 lifecycleScope.launch { footerActionsController.init() }
131 }
132
133 var isQsExpanded by mutableStateOf(false)
134
135 var isQsVisible by mutableStateOf(false)
136
137 val isQsVisibleAndAnyShadeExpanded: Boolean
138 get() = anyShadeExpanded && isQsVisible
139
140 // This can only be negative if undefined (in which case it will be -1f), else it will be
141 // in [0, 1]. In some cases, it could be set back to -1f internally to indicate that it's
142 // different to every value in [0, 1].
143 private var qsExpansion by mutableStateOf(-1f)
144
145 fun setQsExpansionValue(value: Float) {
146 if (value < 0f) {
147 qsExpansion = -1f
148 } else {
149 qsExpansion = value.coerceIn(0f, 1f)
150 }
151 }
152
153 val isQsFullyCollapsed by derivedStateOf { qsExpansion <= 0f }
154
155 var panelExpansionFraction by mutableStateOf(0f)
156
157 var squishinessFraction by mutableStateOf(1f)
158
159 val qqsHeaderHeight by
160 hydrator.hydratedStateOf(
161 traceName = "qqsHeaderHeight",
162 initialValue = 0,
163 source =
164 configurationInteractor.onAnyConfigurationChange.map {
165 if (LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)) {
166 0
167 } else {
168 largeScreenHeaderHelper.getLargeScreenHeaderHeight()
169 }
170 },
171 )
172
173 val qqsBottomPadding by
174 hydrator.hydratedStateOf(
175 traceName = "qqsBottomPadding",
176 initialValue = resources.getDimensionPixelSize(R.dimen.qqs_layout_padding_bottom),
177 source = configurationInteractor.dimensionPixelSize(R.dimen.qqs_layout_padding_bottom),
178 )
179
180 // Starting with a non-zero value makes it so that it has a non-zero height on first expansion
181 // This is important for `QuickSettingsControllerImpl.mMinExpansionHeight` to detect a "change".
182 var qqsHeight by mutableStateOf(1)
183
184 var qsScrollHeight by mutableStateOf(0)
185
186 val heightDiff: Int
187 get() = qsScrollHeight - qqsHeight + qqsBottomPadding
188
189 var isStackScrollerOverscrolling by mutableStateOf(false)
190
191 var proposedTranslation by mutableStateOf(0f)
192
193 /**
194 * Whether QS is enabled by policy. This is normally true, except when it's disabled by some
195 * policy. See [DisableFlagsRepository].
196 */
197 val isQsEnabled by
198 hydrator.hydratedStateOf(
199 traceName = "isQsEnabled",
200 initialValue = disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(),
201 source = disableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() },
202 )
203
204 var isInSplitShade by mutableStateOf(false)
205
206 var isTransitioningToFullShade by mutableStateOf(false)
207
208 var lockscreenToShadeProgress by mutableStateOf(0f)
209
210 var isSmallScreen by mutableStateOf(false)
211
212 var heightOverride by mutableStateOf(-1)
213
214 val expansionState by derivedStateOf {
215 if (forceQs) {
216 QSExpansionState(1f)
217 } else {
218 QSExpansionState(qsExpansion.coerceIn(0f, 1f))
219 }
220 }
221
222 val isQsFullyExpanded by derivedStateOf { expansionState.progress >= 1f && isQsExpanded }
223
224 /**
225 * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
226 * determining the correct action based on the expansion state.
227 */
228 var collapseExpandAccessibilityAction: Runnable? = null
229
230 var overScrollAmount by mutableStateOf(0)
231
232 val viewTranslationY by derivedStateOf {
233 if (isOverscrolling) {
234 overScrollAmount.toFloat()
235 } else {
236 if (onKeyguardAndExpanded) {
237 translationScaleY * qqsHeight
238 } else {
239 headerTranslation
240 }
241 }
242 }
243
244 val qsScrollTranslationY by derivedStateOf {
245 val panelTranslationY = translationScaleY * heightDiff
246 if (onKeyguardAndExpanded) panelTranslationY else 0f
247 }
248
249 val viewAlpha by derivedStateOf {
250 when {
251 isInBouncerTransit ->
252 BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(alphaProgress)
253 isKeyguardState -> alphaProgress
254 isSmallScreen -> ShadeInterpolation.getContentAlpha(alphaProgress)
255 else -> largeScreenShadeInterpolator.getQsAlpha(alphaProgress)
256 }
257 }
258
259 val showingMirror: Boolean
260 get() = containerViewModel.brightnessSliderViewModel.showMirror
261
262 // The initial values in these two are not meaningful. The flow will emit on start the correct
263 // values. This is because we need to lazily fetch them after initMediaHosts.
264 val qqsMediaVisible by
265 hydrator.hydratedStateOf(
266 traceName = "qqsMediaVisible",
267 initialValue = usingMedia,
268 source =
269 if (usingMedia) {
270 mediaHostVisible(qqsMediaHost)
271 } else {
272 flowOf(false)
273 },
274 )
275
276 val qqsMediaInRow: Boolean
277 get() = qqsMediaInRowViewModel.shouldMediaShowInRow
278
279 val qsMediaVisible by
280 hydrator.hydratedStateOf(
281 traceName = "qsMediaVisible",
282 initialValue = usingMedia,
283 source = if (usingMedia) mediaHostVisible(qsMediaHost) else flowOf(false),
284 )
285
286 val qsMediaInRow: Boolean
287 get() = qsMediaInRowViewModel.shouldMediaShowInRow
288
289 var shouldUpdateSquishinessOnMedia by mutableStateOf(false)
290
291 val qsMediaTranslationY by derivedStateOf {
292 if (
293 qsExpansion > 0f &&
294 !isKeyguardState &&
295 !qqsMediaVisible &&
296 !qsMediaInRow &&
297 !isInSplitShade
298 ) {
299 val interpolation = Interpolators.ACCELERATE.getInterpolation(1f - qsExpansion)
300 -qsMediaHost.hostView.height * 1.3f * interpolation
301 } else {
302 0f
303 }
304 }
305
306 val animateTilesExpansion: Boolean
307 get() = inFirstPage && !mediaSuddenlyAppearingInLandscape
308
309 val isEditing by
310 hydrator.hydratedStateOf(
311 traceName = "isEditing",
312 source = containerViewModel.editModeViewModel.isEditing,
313 )
314
315 /** True if we are not in an expansion (from Gone to QQS/QS) animation. */
316 val isNotTransitioning by derivedStateOf {
317 viewTranslationY == 0f && viewAlpha == 1f && constrainedSquishinessFraction == 1f
318 }
319
320 private val inFirstPage: Boolean
321 get() = inFirstPageViewModel.inFirstPage
322
323 private val mediaSuddenlyAppearingInLandscape: Boolean
324 get() = !qqsMediaInRow && qsMediaInRow
325
326 private val collapsedLandscapeMedia by
327 hydrator.hydratedStateOf(
328 traceName = "collapsedLandscapeMedia",
329 initialValue = resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed),
330 source =
331 configurationInteractor.onAnyConfigurationChange.emitOnStart().map {
332 resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed)
333 },
334 )
335
336 private val qqsMediaExpansion: Float
337 get() =
338 if (qqsMediaInRow && collapsedLandscapeMedia) {
339 MediaHostState.COLLAPSED
340 } else {
341 MediaHostState.EXPANDED
342 }
343
344 private val shouldApplySquishinessToMedia by derivedStateOf {
345 shouldUpdateSquishinessOnMedia || (isInSplitShade && statusBarState == StatusBarState.SHADE)
346 }
347
348 private val mediaSquishiness by derivedStateOf {
349 if (shouldApplySquishinessToMedia) {
350 squishinessFraction
351 } else {
352 1f
353 }
354 }
355
356 private var qsBounds by mutableStateOf(Rect())
357
358 private val constrainedSquishinessFraction: Float
359 get() = squishinessFraction.constrainSquishiness()
360
361 private var _headerAnimating by mutableStateOf(false)
362
363 /**
364 * Tracks the current [StatusBarState]. It will switch early if the upcoming state is
365 * [StatusBarState.KEYGUARD]
366 */
367 @get:VisibleForTesting
368 val statusBarState by
369 hydrator.hydratedStateOf(
370 traceName = "statusBarState",
371 initialValue = sysuiStatusBarStateController.state,
372 source =
373 conflatedCallbackFlow {
374 val callback =
375 object : StatusBarStateController.StateListener {
376 override fun onStateChanged(newState: Int) {
377 trySend(newState)
378 }
379
380 override fun onUpcomingStateChanged(upcomingState: Int) {
381 if (upcomingState == StatusBarState.KEYGUARD) {
382 trySend(upcomingState)
383 }
384 }
385 }
386 sysuiStatusBarStateController.addCallback(callback)
387
388 awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
389 }
390 .onStart { emit(sysuiStatusBarStateController.state) },
391 )
392
393 private val isKeyguardState: Boolean
394 get() = statusBarState == StatusBarState.KEYGUARD
395
396 private var viewHeight by mutableStateOf(0)
397
398 private val isBypassEnabled by
399 hydrator.hydratedStateOf(
400 traceName = "isBypassEnabled",
401 source = deviceEntryInteractor.isBypassEnabled,
402 )
403
404 private val showCollapsedOnKeyguard by derivedStateOf {
405 isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade)
406 }
407
408 private val onKeyguardAndExpanded: Boolean
409 get() = isKeyguardState && !showCollapsedOnKeyguard
410
411 private val isOverscrolling: Boolean
412 get() = overScrollAmount != 0
413
414 private val forceQs by derivedStateOf {
415 (isQsExpanded || isStackScrollerOverscrolling) &&
416 (isKeyguardState && !showCollapsedOnKeyguard)
417 }
418
419 private val translationScaleY: Float
420 get() = ((qsExpansion - 1) * (if (isInSplitShade) 1f else SHORT_PARALLAX_AMOUNT))
421
422 private val headerTranslation by derivedStateOf {
423 if (isTransitioningToFullShade) 0f else proposedTranslation
424 }
425
426 private val alphaProgress by derivedStateOf {
427 when {
428 isSmallScreen -> 1f
429 isInSplitShade ->
430 if (isTransitioningToFullShade || isKeyguardState) {
431 lockscreenToShadeProgress
432 } else {
433 panelExpansionFraction
434 }
435 isTransitioningToFullShade -> lockscreenToShadeProgress
436 else -> panelExpansionFraction
437 }
438 }
439
440 private val isInBouncerTransit by
441 hydrator.hydratedStateOf(
442 traceName = "isInBouncerTransit",
443 initialValue = false,
444 source =
445 keyguardTransitionInteractor.isInTransition(
446 Edge.create(to = Overlays.Bouncer),
447 Edge.create(to = KeyguardState.PRIMARY_BOUNCER),
448 ),
449 )
450
451 private val anyShadeExpanded by
452 hydrator.hydratedStateOf(
453 traceName = "anyShadeExpanded",
454 source = shadeInteractor.isAnyExpanded,
455 )
456
457 fun applyNewQsScrollerBounds(left: Float, top: Float, right: Float, bottom: Float) {
458 if (usingMedia) {
459 qsMediaHost.currentClipping.set(
460 left.toInt(),
461 top.toInt(),
462 right.toInt(),
463 bottom.toInt(),
464 )
465 }
466 }
467
468 fun emitMotionEventForFalsingSwipeNested() {
469 falsingInteractor.isFalseTouch(Classifier.QS_SWIPE_NESTED)
470 }
471
472 fun onQQSOpen() {
473 uiEventLogger.log(QSEvent.QQS_PANEL_EXPANDED)
474 }
475
476 fun onQSOpen() {
477 uiEventLogger.log(QSEvent.QS_PANEL_EXPANDED)
478 }
479
480 override suspend fun onActivated(): Nothing {
481 initMediaHosts() // init regardless of using media (same as current QS).
482 coroutineScope {
483 launch { hydrateSquishinessInteractor() }
484 if (usingMedia) {
485 launch { hydrateQqsMediaExpansion() }
486 launch { hydrateMediaSquishiness() }
487 launch { hydrateMediaDisappearParameters() }
488 }
489 launch { hydrator.activate() }
490 launch { containerViewModel.activate() }
491 launch { quickQuickSettingsViewModel.activate() }
492 launch { qqsMediaInRowViewModel.activate() }
493 launch { qsMediaInRowViewModel.activate() }
494 awaitCancellation()
495 }
496 }
497
498 private fun initMediaHosts() {
499 qqsMediaHost.apply {
500 expansion = qqsMediaExpansion
501 showsOnlyActiveMedia = true
502 init(LOCATION_QQS)
503 }
504 qsMediaHost.apply {
505 expansion = MediaHostState.EXPANDED
506 showsOnlyActiveMedia = false
507 init(LOCATION_QS)
508 }
509 }
510
511 private suspend fun hydrateSquishinessInteractor() {
512 snapshotFlow { constrainedSquishinessFraction }
513 .collect { squishinessInteractor.setSquishinessValue(it) }
514 }
515
516 private suspend fun hydrateQqsMediaExpansion() {
517 snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it }
518 }
519
520 private suspend fun hydrateMediaSquishiness() {
521 snapshotFlow { mediaSquishiness }.collect { qsMediaHost.squishFraction = it }
522 }
523
524 private suspend fun hydrateMediaDisappearParameters() {
525 coroutineScope {
526 launch {
527 snapshotFlow { qqsMediaInRow }.collect { qqsMediaHost.applyDisappearParameters(it) }
528 }
529 launch {
530 snapshotFlow { qsMediaInRow }.collect { qsMediaHost.applyDisappearParameters(it) }
531 }
532 }
533 }
534
535 override fun dump(pw: PrintWriter, args: Array<out String>) {
536 pw.asIndenting().run {
537 printSection("Quick Settings state") {
538 println("isQSExpanded", isQsExpanded)
539 println("isQSVisible", isQsVisible)
540 println("anyShadeExpanded", anyShadeExpanded)
541 println("isQSVisibleAndAnyShadeExpanded", isQsVisibleAndAnyShadeExpanded)
542 println("isQSEnabled", isQsEnabled)
543 println("isCustomizing", containerViewModel.editModeViewModel.isEditing.value)
544 println("inFirstPage", inFirstPage)
545 }
546 printSection("Expansion state") {
547 println("qsExpansion", qsExpansion)
548 println("panelExpansionFraction", panelExpansionFraction)
549 println("squishinessFraction", squishinessFraction)
550 println("proposedTranslation", proposedTranslation)
551 println("expansionState", expansionState)
552 println("forceQS", forceQs)
553 printSection("Derived values") {
554 println("headerTranslation", headerTranslation)
555 println("translationScaleY", translationScaleY)
556 println("viewTranslationY", viewTranslationY)
557 println("qsScrollTranslationY", qsScrollTranslationY)
558 println("viewAlpha", viewAlpha)
559 }
560 }
561 printSection("Shade state") {
562 println("stackOverscrolling", isStackScrollerOverscrolling)
563 println("overscrollAmount", overScrollAmount)
564 println("statusBarState", StatusBarState.toString(statusBarState))
565 println("isKeyguardState", isKeyguardState)
566 println("isSmallScreen", isSmallScreen)
567 println("heightOverride", "${heightOverride}px")
568 println("qqsHeaderHeight", "${qqsHeaderHeight}px")
569 println("qqsBottomPadding", "${qqsBottomPadding}px")
570 println("isSplitShade", isInSplitShade)
571 println("showCollapsedOnKeyguard", showCollapsedOnKeyguard)
572 println("qqsHeight", "${qqsHeight}px")
573 println("qsScrollHeight", "${qsScrollHeight}px")
574 }
575 printSection("Media") {
576 println("qqsMediaVisible", qqsMediaVisible)
577 println("qqsMediaInRow", qqsMediaInRow)
578 println("qsMediaVisible", qsMediaVisible)
579 println("qsMediaInRow", qsMediaInRow)
580 println("collapsedLandscapeMedia", collapsedLandscapeMedia)
581 println("qqsMediaExpansion", qqsMediaExpansion)
582 println("shouldUpdateSquishinessOnMedia", shouldUpdateSquishinessOnMedia)
583 println("mediaSquishiness", mediaSquishiness)
584 println("qsMediaTranslationY", qsMediaTranslationY)
585 }
586 }
587 }
588
589 @AssistedFactory
590 interface Factory {
591 fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel
592 }
593
594 // In the future, this may have other relevant elements.
595 data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float)
596 }
597
Floatnull598 private fun Float.constrainSquishiness(): Float {
599 return (0.1f + this * 0.9f).coerceIn(0f, 1f)
600 }
601
602 private val SHORT_PARALLAX_AMOUNT = 0.1f
603
604 /**
605 * Returns a flow to track the visibility of a [MediaHost]. The flow will emit on start the visible
606 * state of the view.
607 */
mediaHostVisiblenull608 private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> {
609 return callbackFlow {
610 val listener: (Boolean) -> Unit = { visible: Boolean -> trySend(visible) }
611 mediaHost.addVisibilityChangeListener(listener)
612
613 awaitClose { mediaHost.removeVisibilityChangeListener(listener) }
614 }
615 // Need to use this to set initial state because on creation of the media host, the
616 // view visibility is not in sync with [MediaHost.visible], which is what we track with
617 // the listener. The correct state is set as part of init, so we need to get the state
618 // lazily.
619 .onStart { emit(mediaHost.visible) }
620 }
621
622 // Taken from QSPanelControllerBase
applyDisappearParametersnull623 private fun MediaHost.applyDisappearParameters(inRow: Boolean) {
624 disappearParameters.apply {
625 fadeStartPosition = 0.95f
626 disappearStart = 0f
627 if (inRow) {
628 disappearSize.set(0f, 0.4f)
629 gonePivot.set(1f, 0f)
630 contentTranslationFraction.set(0.25f, 1f)
631 disappearEnd = 0.6f
632 } else {
633 disappearSize.set(1f, 0f)
634 gonePivot.set(0f, 0f)
635 contentTranslationFraction.set(0f, 1f)
636 disappearEnd = 0.95f
637 }
638 }
639 }
640