• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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