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.derivedStateOf
28 import androidx.compose.runtime.getValue
29 import androidx.compose.runtime.remember
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.draw.drawWithContent
32 import androidx.compose.ui.layout.layout
33 import androidx.compose.ui.platform.LocalContext
34 import androidx.compose.ui.res.dimensionResource
35 import androidx.compose.ui.unit.Dp
36 import androidx.compose.ui.unit.dp
37 import androidx.compose.ui.viewinterop.AndroidView
38 import androidx.lifecycle.compose.collectAsStateWithLifecycle
39 import com.android.compose.animation.scene.ContentScope
40 import com.android.compose.animation.scene.ElementKey
41 import com.android.compose.animation.scene.MovableElementContentPicker
42 import com.android.compose.animation.scene.MovableElementKey
43 import com.android.compose.animation.scene.SceneTransitionLayoutState
44 import com.android.compose.animation.scene.ValueKey
45 import com.android.compose.animation.scene.content.state.TransitionState
46 import com.android.compose.modifiers.thenIf
47 import com.android.systemui.compose.modifiers.sysuiResTag
48 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
49 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
50 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
51 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
52 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
53 import com.android.systemui.res.R
54 import com.android.systemui.scene.shared.model.Scenes
55
56 object QuickSettings {
57 private val SCENES = setOf(Scenes.QuickSettings, Scenes.Shade)
58
59 object Elements {
60 val Content =
61 MovableElementKey(
62 "QuickSettingsContent",
63 contentPicker = MovableElementContentPicker(SCENES),
64 )
65 val QuickQuickSettings = ElementKey("QuickQuickSettings")
66 val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
67 val FooterActions = ElementKey("QuickSettingsFooterActions")
68 }
69
70 object SharedValues {
71 val TilesSquishiness = ValueKey("QuickSettingsTileSquishiness")
72
73 object SquishinessValues {
74 val Default = 1f
75 val LockscreenSceneStarting = 0f
76 val GoneSceneStarting = 0.3f
77 }
78
79 val MediaLandscapeTopOffset = ValueKey("MediaLandscapeTopOffset")
80
81 object MediaOffset {
82 // Brightness
83 val InQS = 60.dp
84 val Default = 0.dp
85
86 @Composable
87 fun inQqs(isMediaInRow: Boolean): Dp {
88 return if (isMediaInRow) {
89 // Tiles are laid out in a center of a container, that has this
90 // margin on the bottom. This compensates this margin, so that the Media
91 // Carousel can be properly centered
92 -dimensionResource(id = R.dimen.qqs_layout_padding_bottom) / 2
93 } else {
94 0.dp
95 }
96 }
97 }
98 }
99 }
100
stateForQuickSettingsContentnull101 private fun ContentScope.stateForQuickSettingsContent(
102 isSplitShade: Boolean,
103 squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
104 ): QSSceneAdapter.State {
transitionStatenull105 return when (val transitionState = layoutState.transitionState) {
106 is TransitionState.Idle -> {
107 when (transitionState.currentScene) {
108 Scenes.Shade ->
109 QSSceneAdapter.State.QQS.takeUnless { isSplitShade } ?: QSSceneAdapter.State.QS
110 Scenes.QuickSettings -> QSSceneAdapter.State.QS
111 else -> QSSceneAdapter.State.CLOSED
112 }
113 }
114 is TransitionState.Transition.ChangeScene ->
115 with(transitionState) {
116 when {
117 isSplitShade -> UnsquishingQS(squishiness)
118 fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
119 Expanding { progress }
120 }
121 fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
122 Collapsing { progress }
123 }
124 fromContent == Scenes.Shade || toContent == Scenes.Shade -> {
125 UnsquishingQQS(squishiness)
126 }
127 fromContent == Scenes.QuickSettings || toContent == Scenes.QuickSettings -> {
128 QSSceneAdapter.State.QS
129 }
130 else ->
131 // We are not in a transition between states that have QS, so just make
132 // sure it's closed. This could be an issue if going from SplitShade to
133 // a folded device.
134 QSSceneAdapter.State.CLOSED
135 }
136 }
137 is TransitionState.Transition.OverlayTransition ->
138 // Currently, no overlays use this QS impl, so we should make sure it's closed.
139 QSSceneAdapter.State.CLOSED
140 }
141 }
142
143 /**
144 * This composable will show QuickSettingsContent in the correct state (as determined by its
145 * [ContentScope]).
146 *
147 * If adding to scenes not in:
148 * * QuickSettingsScene
149 * * ShadeScene
150 *
151 * amend:
152 * * [stateForQuickSettingsContent],
153 * * [QuickSettings.SCENES],
154 * * this doc.
155 */
156 @Composable
QuickSettingsnull157 fun ContentScope.QuickSettings(
158 qsSceneAdapter: QSSceneAdapter,
159 heightProvider: () -> Int,
160 isSplitShade: Boolean,
161 modifier: Modifier = Modifier,
162 squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
163 ) {
<lambda>null164 val contentState = { stateForQuickSettingsContent(isSplitShade, squishiness) }
165
166 // Note: We use derivedStateOf {} here because isClosing() is reading the current transition
167 // progress and we don't want to recompose this scene each time the progress has changed.
<lambda>null168 val isClosing by remember(layoutState) { derivedStateOf { isClosing(layoutState) } }
169
170 if (isClosing) {
<lambda>null171 DisposableEffect(Unit) {
172 onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
173 }
174 }
175
176 MovableElement(
177 key = QuickSettings.Elements.Content,
178 modifier =
179 modifier.sysuiResTag("quick_settings_panel").fillMaxWidth().layout {
measurablenull180 measurable,
181 constraints ->
182 val placeable = measurable.measure(constraints)
183 // Use the height of the correct view based on the scene it is being composed in
184 val height = heightProvider().coerceAtLeast(0)
185
186 layout(placeable.width, height) { placeable.placeRelative(0, 0) }
187 },
<lambda>null188 ) {
189 content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
190 }
191 }
192
isClosingnull193 private fun isClosing(layoutState: SceneTransitionLayoutState): Boolean {
194 val transitionState = layoutState.transitionState
195 return transitionState is TransitionState.Transition &&
196 !(layoutState.isTransitioning(to = Scenes.Shade) ||
197 layoutState.isTransitioning(to = Scenes.QuickSettings)) &&
198 transitionState.progress >= 0.9f // almost done closing
199 }
200
201 @Composable
QuickSettingsContentnull202 private fun QuickSettingsContent(
203 qsSceneAdapter: QSSceneAdapter,
204 state: () -> QSSceneAdapter.State,
205 modifier: Modifier = Modifier,
206 ) {
207 val qsView by qsSceneAdapter.qsView.collectAsStateWithLifecycle()
208 val isCustomizing by qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
209 QuickSettingsTheme {
210 val context = LocalContext.current
211
212 LaunchedEffect(context) {
213 if (qsView == null) {
214 qsSceneAdapter.inflate(context)
215 }
216 }
217 qsView?.let { view ->
218 Box(
219 modifier =
220 modifier
221 .fillMaxWidth()
222 .thenIf(isCustomizing) { Modifier.fillMaxHeight() }
223 .drawWithContent {
224 qsSceneAdapter.applyLatestExpansionAndSquishiness()
225 drawContent()
226 }
227 ) {
228 AndroidView(
229 modifier = Modifier.fillMaxWidth(),
230 factory = { context ->
231 qsSceneAdapter.setState(state())
232 FrameLayout(context).apply {
233 (view.parent as? ViewGroup)?.removeView(view)
234 addView(view)
235 }
236 },
237 // When the view changes (e.g. due to a theme change), this will be
238 // recomposed
239 // if needed and the new view will be attached to the FrameLayout here.
240 update = {
241 qsSceneAdapter.setState(state())
242 if (view.parent != it) {
243 it.removeAllViews()
244 (view.parent as? ViewGroup)?.removeView(view)
245 it.addView(view)
246 }
247 },
248 onRelease = { it.removeAllViews() },
249 )
250 }
251 }
252 }
253 }
254