1 /*
<lambda>null2 * Copyright (C) 2023 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.ui.composable
18
19 import android.view.ViewGroup
20 import android.widget.FrameLayout
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.fillMaxHeight
23 import androidx.compose.foundation.layout.fillMaxWidth
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.DisposableEffect
26 import androidx.compose.runtime.LaunchedEffect
27 import androidx.compose.runtime.getValue
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.draw.drawWithContent
30 import androidx.compose.ui.layout.layout
31 import androidx.compose.ui.platform.LocalContext
32 import androidx.compose.ui.unit.dp
33 import androidx.compose.ui.viewinterop.AndroidView
34 import androidx.lifecycle.compose.collectAsStateWithLifecycle
35 import com.android.compose.animation.scene.ElementKey
36 import com.android.compose.animation.scene.MovableElementScenePicker
37 import com.android.compose.animation.scene.SceneScope
38 import com.android.compose.animation.scene.TransitionState
39 import com.android.compose.animation.scene.ValueKey
40 import com.android.compose.modifiers.thenIf
41 import com.android.systemui.compose.modifiers.sysuiResTag
42 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
43 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
44 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
45 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
46 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
47 import com.android.systemui.scene.shared.model.Scenes
48
49 object QuickSettings {
50 private val SCENES =
51 setOf(
52 Scenes.QuickSettings,
53 Scenes.Shade,
54 )
55
56 object Elements {
57 val Content =
58 ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
59 val QuickQuickSettings = ElementKey("QuickQuickSettings")
60 val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
61 val FooterActions = ElementKey("QuickSettingsFooterActions")
62 }
63
64 object SharedValues {
65 val TilesSquishiness = ValueKey("QuickSettingsTileSquishiness")
66
67 object SquishinessValues {
68 val Default = 1f
69 val LockscreenSceneStarting = 0f
70 val GoneSceneStarting = 0.3f
71 }
72
73 val MediaLandscapeTopOffset = ValueKey("MediaLandscapeTopOffset")
74
75 object MediaOffset {
76 val InQQS = 0.dp
77 // Brightness + padding
78 val InQS = 92.dp
79 val Default = 0.dp
80 }
81 }
82 }
83
stateForQuickSettingsContentnull84 private fun SceneScope.stateForQuickSettingsContent(
85 isSplitShade: Boolean,
86 squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default }
87 ): QSSceneAdapter.State {
transitionStatenull88 return when (val transitionState = layoutState.transitionState) {
89 is TransitionState.Idle -> {
90 when (transitionState.currentScene) {
91 Scenes.Shade ->
92 QSSceneAdapter.State.QQS.takeUnless { isSplitShade } ?: QSSceneAdapter.State.QS
93 Scenes.QuickSettings -> QSSceneAdapter.State.QS
94 else -> QSSceneAdapter.State.CLOSED
95 }
96 }
97 is TransitionState.Transition ->
98 with(transitionState) {
99 when {
100 isSplitShade -> UnsquishingQS(squishiness)
101 fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
102 Expanding(progress)
103 }
104 fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
105 Collapsing(progress)
106 }
107 fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
108 UnsquishingQQS(squishiness)
109 }
110 fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
111 QSSceneAdapter.State.QS
112 }
113 else ->
114 error(
115 "Bad transition for QuickSettings: fromScene=$fromScene," +
116 " toScene=$toScene"
117 )
118 }
119 }
120 }
121 }
122
123 /**
124 * This composable will show QuickSettingsContent in the correct state (as determined by its
125 * [SceneScope]).
126 *
127 * If adding to scenes not in:
128 * * QuickSettingsScene
129 * * ShadeScene
130 *
131 * amend:
132 * * [stateForQuickSettingsContent],
133 * * [QuickSettings.SCENES],
134 * * this doc.
135 */
136 @Composable
QuickSettingsnull137 fun SceneScope.QuickSettings(
138 qsSceneAdapter: QSSceneAdapter,
139 heightProvider: () -> Int,
140 isSplitShade: Boolean,
141 modifier: Modifier = Modifier,
142 squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
143 ) {
<lambda>null144 val contentState = { stateForQuickSettingsContent(isSplitShade, squishiness) }
145 val transitionState = layoutState.transitionState
146 val isClosing =
147 transitionState is TransitionState.Transition &&
148 transitionState.progress >= 0.9f && // almost done closing
149 !(layoutState.isTransitioning(to = Scenes.Shade) ||
150 layoutState.isTransitioning(to = Scenes.QuickSettings))
151
152 if (isClosing) {
<lambda>null153 DisposableEffect(Unit) {
154 onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
155 }
156 }
157
158 MovableElement(
159 key = QuickSettings.Elements.Content,
160 modifier =
161 modifier.sysuiResTag("quick_settings_panel").fillMaxWidth().layout {
measurablenull162 measurable,
163 constraints ->
164 val placeable = measurable.measure(constraints)
165 // Use the height of the correct view based on the scene it is being composed in
166 val height = heightProvider().coerceAtLeast(0)
167
168 layout(placeable.width, height) { placeable.placeRelative(0, 0) }
169 }
<lambda>null170 ) {
171 content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
172 }
173 }
174
175 @Composable
QuickSettingsContentnull176 private fun QuickSettingsContent(
177 qsSceneAdapter: QSSceneAdapter,
178 state: () -> QSSceneAdapter.State,
179 modifier: Modifier = Modifier,
180 ) {
181 val qsView by qsSceneAdapter.qsView.collectAsStateWithLifecycle()
182 val isCustomizing by qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
183 QuickSettingsTheme {
184 val context = LocalContext.current
185
186 LaunchedEffect(key1 = context) {
187 if (qsView == null) {
188 qsSceneAdapter.inflate(context)
189 }
190 }
191 qsView?.let { view ->
192 Box(
193 modifier =
194 modifier
195 .fillMaxWidth()
196 .thenIf(isCustomizing) { Modifier.fillMaxHeight() }
197 .drawWithContent {
198 qsSceneAdapter.applyLatestExpansionAndSquishiness()
199 drawContent()
200 }
201 ) {
202 AndroidView(
203 modifier = Modifier.fillMaxWidth(),
204 factory = { context ->
205 qsSceneAdapter.setState(state())
206 FrameLayout(context).apply {
207 (view.parent as? ViewGroup)?.removeView(view)
208 addView(view)
209 }
210 },
211 // When the view changes (e.g. due to a theme change), this will be recomposed
212 // if needed and the new view will be attached to the FrameLayout here.
213 update = {
214 qsSceneAdapter.setState(state())
215 if (view.parent != it) {
216 it.removeAllViews()
217 (view.parent as? ViewGroup)?.removeView(view)
218 it.addView(view)
219 }
220 },
221 onRelease = { it.removeAllViews() }
222 )
223 }
224 }
225 }
226 }
227