• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2021 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  *      https://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.google.accompanist.insets.ui
18 
19 import androidx.compose.foundation.layout.ColumnScope
20 import androidx.compose.foundation.layout.PaddingValues
21 import androidx.compose.foundation.layout.calculateEndPadding
22 import androidx.compose.foundation.layout.calculateStartPadding
23 import androidx.compose.material.BottomAppBar
24 import androidx.compose.material.DrawerDefaults
25 import androidx.compose.material.FabPosition
26 import androidx.compose.material.FloatingActionButton
27 import androidx.compose.material.MaterialTheme
28 import androidx.compose.material.ModalDrawer
29 import androidx.compose.material.Scaffold
30 import androidx.compose.material.ScaffoldState
31 import androidx.compose.material.Snackbar
32 import androidx.compose.material.SnackbarHost
33 import androidx.compose.material.SnackbarHostState
34 import androidx.compose.material.Surface
35 import androidx.compose.material.TopAppBar
36 import androidx.compose.material.contentColorFor
37 import androidx.compose.material.rememberScaffoldState
38 import androidx.compose.runtime.Composable
39 import androidx.compose.runtime.CompositionLocalProvider
40 import androidx.compose.runtime.Immutable
41 import androidx.compose.runtime.ProvidableCompositionLocal
42 import androidx.compose.runtime.Stable
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.remember
46 import androidx.compose.runtime.setValue
47 import androidx.compose.runtime.staticCompositionLocalOf
48 import androidx.compose.ui.Modifier
49 import androidx.compose.ui.graphics.Color
50 import androidx.compose.ui.graphics.Shape
51 import androidx.compose.ui.layout.SubcomposeLayout
52 import androidx.compose.ui.unit.Dp
53 import androidx.compose.ui.unit.LayoutDirection
54 import androidx.compose.ui.unit.dp
55 
56 /**
57  * Provides the current [Scaffold] content padding values.
58  */
59 public val LocalScaffoldPadding: ProvidableCompositionLocal<PaddingValues> =
60     staticCompositionLocalOf { PaddingValues(0.dp) }
61 
62 /**
63  * A copy of [androidx.compose.material.Scaffold] which lays out [content] behind both the top bar
64  * content, and the bottom bar content. See [androidx.compose.material.Scaffold] for more
65  * information about the features provided.
66  *
67  * A sample which demonstrates how to use this layout to achieve a edge-to-edge layout is
68  * below:
69  *
70  * @sample com.google.accompanist.sample.insets.InsetsBasics
71  *
72  * @param modifier optional Modifier for the root of the [Scaffold]
73  * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
74  * variables to provide manual control over the drawer behavior, sizes of components, etc
75  * @param topBar top app bar of the screen. Consider using [TopAppBar].
76  * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
77  * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
78  * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
79  * @param floatingActionButton Main action button of your screen. Consider using
80  * [FloatingActionButton] for this slot.
81  * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
82  * possible options available.
83  * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
84  * [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
85  * [floatingActionButton].
86  * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right
87  * for RTL).
88  * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
89  * @param drawerShape shape of the drawer sheet (if set)
90  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow
91  * below the drawer sheet (if set)
92  * @param drawerBackgroundColor background color to be used for the drawer sheet
93  * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to
94  * either the matching content color for [drawerBackgroundColor], or, if it is not a color from
95  * the theme, this will keep the same value set above this Surface.
96  * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
97  * @param backgroundColor background of the scaffold body
98  * @param contentColor color of the content in scaffold body. Defaults to either the matching
99  * content color for [backgroundColor], or, if it is not a color from the theme, this will keep
100  * the same value set above this Surface.
101  * @param contentPadding Additional content padding to apply to the [content].
102  * @param content content of your screen. The lambda receives an [PaddingValues] that should be
103  * applied to the content root via Modifier.padding to properly offset top and bottom bars. If
104  * you're using VerticalScroller, apply this modifier to the child of the scroller, and not on
105  * the scroller itself.
106  */
107 @Deprecated(
108     """
109         accompanist/insets-ui has been deprecated.
110         This functionality has been upstreamed to Material.
111         For more migration information, please visit https://google.github.io/accompanist/insets/#migration
112     """
113 )
114 @Composable
Scaffoldnull115 public fun Scaffold(
116     modifier: Modifier = Modifier,
117     scaffoldState: ScaffoldState = rememberScaffoldState(),
118     topBar: @Composable () -> Unit = {},
<lambda>null119     bottomBar: @Composable () -> Unit = {},
<lambda>null120     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
<lambda>null121     floatingActionButton: @Composable () -> Unit = {},
122     floatingActionButtonPosition: FabPosition = FabPosition.End,
123     isFloatingActionButtonDocked: Boolean = false,
124     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
125     drawerGesturesEnabled: Boolean = true,
126     drawerShape: Shape = MaterialTheme.shapes.large,
127     drawerElevation: Dp = DrawerDefaults.Elevation,
128     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
129     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
130     drawerScrimColor: Color = DrawerDefaults.scrimColor,
131     backgroundColor: Color = MaterialTheme.colors.background,
132     contentColor: Color = contentColorFor(backgroundColor),
133     contentPadding: PaddingValues = LocalScaffoldPadding.current,
134     content: @Composable (PaddingValues) -> Unit
135 ) {
childModifiernull136     val child = @Composable { childModifier: Modifier ->
137         Surface(modifier = childModifier, color = backgroundColor, contentColor = contentColor) {
138             ScaffoldLayout(
139                 isFabDocked = isFloatingActionButtonDocked,
140                 fabPosition = floatingActionButtonPosition,
141                 topBar = topBar,
142                 content = content,
143                 snackbar = { snackbarHost(scaffoldState.snackbarHostState) },
144                 fab = floatingActionButton,
145                 bottomBar = bottomBar,
146                 contentPadding = contentPadding,
147             )
148         }
149     }
150 
151     if (drawerContent != null) {
152         ModalDrawer(
153             modifier = modifier,
154             drawerState = scaffoldState.drawerState,
155             gesturesEnabled = drawerGesturesEnabled,
156             drawerContent = drawerContent,
157             drawerShape = drawerShape,
158             drawerElevation = drawerElevation,
159             drawerBackgroundColor = drawerBackgroundColor,
160             drawerContentColor = drawerContentColor,
161             scrimColor = drawerScrimColor,
<lambda>null162             content = { child(Modifier) }
163         )
164     } else {
165         child(modifier)
166     }
167 }
168 
169 /**
170  * Layout for a [Scaffold]'s content.
171  *
172  * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
173  * @param fabPosition [FabPosition] for the FAB (if present)
174  * @param topBar the content to place at the top of the [Scaffold], typically a [TopAppBar]
175  * @param content the main 'body' of the [Scaffold]
176  * @param snackbar the [Snackbar] displayed on top of the [content]
177  * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
178  * and above the [bottomBar]
179  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
180  * [content], typically a [BottomAppBar].
181  */
182 @Composable
ScaffoldLayoutnull183 private fun ScaffoldLayout(
184     isFabDocked: Boolean,
185     fabPosition: FabPosition,
186     topBar: @Composable () -> Unit,
187     content: @Composable (PaddingValues) -> Unit,
188     snackbar: @Composable () -> Unit,
189     fab: @Composable () -> Unit,
190     bottomBar: @Composable () -> Unit,
191     contentPadding: PaddingValues,
192 ) {
193     val innerPadding = remember { MutablePaddingValues() }
194 
195     SubcomposeLayout { constraints ->
196         val layoutWidth = constraints.maxWidth
197         val layoutHeight = constraints.maxHeight
198 
199         val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
200 
201         layout(layoutWidth, layoutHeight) {
202             val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map {
203                 it.measure(looseConstraints)
204             }
205 
206             val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
207 
208             val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
209                 it.measure(looseConstraints)
210             }
211 
212             val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
213 
214             val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab)
215                 .mapNotNull { measurable ->
216                     measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
217                 }
218 
219             val fabPlacement = if (fabPlaceables.isNotEmpty()) {
220                 val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
221                 val fabHeight = fabPlaceables.maxByOrNull { it.height }!!.height
222                 // FAB distance from the left of the layout, taking into account LTR / RTL
223                 val fabLeftOffset = if (fabPosition == FabPosition.End) {
224                     if (layoutDirection == LayoutDirection.Ltr) {
225                         layoutWidth - FabSpacing.roundToPx() - fabWidth
226                     } else {
227                         FabSpacing.roundToPx()
228                     }
229                 } else {
230                     (layoutWidth - fabWidth) / 2
231                 }
232 
233                 FabPlacement(
234                     isDocked = isFabDocked,
235                     left = fabLeftOffset,
236                     width = fabWidth,
237                     height = fabHeight
238                 )
239             } else {
240                 null
241             }
242 
243             val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
244                 CompositionLocalProvider(
245                     LocalFabPlacement provides fabPlacement,
246                     content = bottomBar
247                 )
248             }.map { it.measure(looseConstraints) }
249 
250             val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0
251             val fabOffsetFromBottom = fabPlacement?.let {
252                 if (bottomBarHeight == 0) {
253                     it.height +
254                         FabSpacing.roundToPx() +
255                         contentPadding.calculateBottomPadding().roundToPx()
256                 } else {
257                     if (isFabDocked) {
258                         // Total height is the bottom bar height + half the FAB height
259                         bottomBarHeight + (it.height / 2)
260                     } else {
261                         // Total height is the bottom bar height + the FAB height + the padding
262                         // between the FAB and bottom bar
263                         bottomBarHeight + it.height + FabSpacing.roundToPx()
264                     }
265                 }
266             }
267 
268             val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
269                 snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
270             } else {
271                 0
272             }
273 
274             // Update the inner padding
275             innerPadding.apply {
276                 start = contentPadding.calculateStartPadding(LayoutDirection.Ltr)
277                 top = topBarHeight.toDp() + contentPadding.calculateTopPadding()
278                 end = contentPadding.calculateEndPadding(LayoutDirection.Ltr)
279                 bottom = bottomBarHeight.toDp() + contentPadding.calculateBottomPadding()
280             }
281 
282             val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
283                 CompositionLocalProvider(LocalScaffoldPadding provides innerPadding) {
284                     content(innerPadding)
285                 }
286             }.map { it.measure(looseConstraints.copy(maxHeight = layoutHeight)) }
287 
288             // Placing to control drawing order to match default elevation of each placeable
289 
290             bodyContentPlaceables.forEach { it.place(0, 0) }
291             topBarPlaceables.forEach { it.place(0, 0) }
292             snackbarPlaceables.forEach {
293                 it.place(0, layoutHeight - snackbarOffsetFromBottom)
294             }
295             // The bottom bar is always at the bottom of the layout
296             bottomBarPlaceables.forEach {
297                 it.place(0, layoutHeight - bottomBarHeight)
298             }
299             // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
300             fabPlacement?.let { placement ->
301                 fabPlaceables.forEach {
302                     it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
303                 }
304             }
305         }
306     }
307 }
308 
309 /**
310  * Placement information for a [FloatingActionButton] inside a [Scaffold].
311  *
312  * @property isDocked whether the FAB should be docked with the bottom bar
313  * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
314  * support
315  * @property width the width of the FAB
316  * @property height the height of the FAB
317  */
318 @Immutable
319 internal class FabPlacement(
320     val isDocked: Boolean,
321     val left: Int,
322     val width: Int,
323     val height: Int
324 )
325 
326 /**
327  * CompositionLocal containing a [FabPlacement] that is read by [BottomAppBar] to calculate notch
328  * location.
329  */
<lambda>null330 internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
331 
332 // FAB spacing above the bottom bar / bottom of the Scaffold
333 private val FabSpacing = 16.dp
334 
335 private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
336 
337 @Stable
338 internal class MutablePaddingValues : PaddingValues {
339     var start: Dp by mutableStateOf(0.dp)
340     var top: Dp by mutableStateOf(0.dp)
341     var end: Dp by mutableStateOf(0.dp)
342     var bottom: Dp by mutableStateOf(0.dp)
343 
calculateLeftPaddingnull344     override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp {
345         return when (layoutDirection) {
346             LayoutDirection.Ltr -> start
347             LayoutDirection.Rtl -> end
348         }
349     }
350 
calculateTopPaddingnull351     override fun calculateTopPadding(): Dp = top
352 
353     override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp {
354         return when (layoutDirection) {
355             LayoutDirection.Ltr -> end
356             LayoutDirection.Rtl -> start
357         }
358     }
359 
calculateBottomPaddingnull360     override fun calculateBottomPadding(): Dp = bottom
361 }
362