Scaffold.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /*
  2. * 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. * 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. package eu.kanade.presentation.components
  17. import androidx.compose.foundation.layout.PaddingValues
  18. import androidx.compose.foundation.layout.WindowInsets
  19. import androidx.compose.foundation.layout.asPaddingValues
  20. import androidx.compose.foundation.layout.safeDrawing
  21. import androidx.compose.material3.ExperimentalMaterial3Api
  22. import androidx.compose.material3.MaterialTheme
  23. import androidx.compose.material3.TopAppBarDefaults
  24. import androidx.compose.material3.TopAppBarScrollBehavior
  25. import androidx.compose.material3.contentColorFor
  26. import androidx.compose.material3.rememberTopAppBarState
  27. import androidx.compose.runtime.Composable
  28. import androidx.compose.runtime.CompositionLocalProvider
  29. import androidx.compose.runtime.Immutable
  30. import androidx.compose.runtime.staticCompositionLocalOf
  31. import androidx.compose.ui.Modifier
  32. import androidx.compose.ui.graphics.Color
  33. import androidx.compose.ui.input.nestedscroll.nestedScroll
  34. import androidx.compose.ui.layout.SubcomposeLayout
  35. import androidx.compose.ui.unit.Constraints
  36. import androidx.compose.ui.unit.LayoutDirection
  37. import androidx.compose.ui.unit.dp
  38. /**
  39. * <a href="https://material.io/design/layout/understanding-layout.html" class="external" target="_blank">Material Design layout</a>.
  40. *
  41. * Scaffold implements the basic material design visual layout structure.
  42. *
  43. * This component provides API to put together several material components to construct your
  44. * screen, by ensuring proper layout strategy for them and collecting necessary data so these
  45. * components will work together correctly.
  46. *
  47. * Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]:
  48. *
  49. * @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
  50. *
  51. * To show a [Snackbar], use [SnackbarHostState.showSnackbar].
  52. *
  53. * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
  54. *
  55. * Tachiyomi changes:
  56. * * Remove height constraint for expanded app bar
  57. * * Also take account of fab height when providing inner padding
  58. *
  59. * @param modifier the [Modifier] to be applied to this scaffold
  60. * @param topBar top app bar of the screen, typically a [SmallTopAppBar]
  61. * @param bottomBar bottom bar of the screen, typically a [NavigationBar]
  62. * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
  63. * [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
  64. * @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton]
  65. * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition].
  66. * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
  67. * to have no color.
  68. * @param contentColor the preferred color for content inside this scaffold. Defaults to either the
  69. * matching content color for [containerColor], or to the current [LocalContentColor] if
  70. * [containerColor] is not a color from the theme.
  71. * @param content content of the screen. The lambda receives a [PaddingValues] that should be
  72. * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
  73. * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
  74. * the child of the scroll, and not on the scroll itself.
  75. */
  76. @ExperimentalMaterial3Api
  77. @Composable
  78. fun Scaffold(
  79. modifier: Modifier = Modifier,
  80. topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
  81. bottomBar: @Composable () -> Unit = {},
  82. snackbarHost: @Composable () -> Unit = {},
  83. floatingActionButton: @Composable () -> Unit = {},
  84. floatingActionButtonPosition: FabPosition = FabPosition.End,
  85. containerColor: Color = MaterialTheme.colorScheme.background,
  86. contentColor: Color = contentColorFor(containerColor),
  87. content: @Composable (PaddingValues) -> Unit,
  88. ) {
  89. /**
  90. * Tachiyomi: Pass scroll behavior to topBar
  91. */
  92. val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
  93. androidx.compose.material3.Surface(
  94. modifier = Modifier
  95. .nestedScroll(scrollBehavior.nestedScrollConnection)
  96. .then(modifier),
  97. color = containerColor,
  98. contentColor = contentColor,
  99. ) {
  100. ScaffoldLayout(
  101. fabPosition = floatingActionButtonPosition,
  102. topBar = { topBar(scrollBehavior) },
  103. bottomBar = bottomBar,
  104. content = content,
  105. snackbar = snackbarHost,
  106. fab = floatingActionButton,
  107. )
  108. }
  109. }
  110. /**
  111. * Layout for a [Scaffold]'s content.
  112. *
  113. * @param fabPosition [FabPosition] for the FAB (if present)
  114. * @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar]
  115. * @param content the main 'body' of the [Scaffold]
  116. * @param snackbar the [Snackbar] displayed on top of the [content]
  117. * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
  118. * and above the [bottomBar]
  119. * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  120. * [content], typically a [NavigationBar].
  121. */
  122. @OptIn(ExperimentalMaterial3Api::class)
  123. @Composable
  124. private fun ScaffoldLayout(
  125. fabPosition: FabPosition,
  126. topBar: @Composable () -> Unit,
  127. content: @Composable (PaddingValues) -> Unit,
  128. snackbar: @Composable () -> Unit,
  129. fab: @Composable () -> Unit,
  130. bottomBar: @Composable () -> Unit,
  131. ) {
  132. SubcomposeLayout { constraints ->
  133. val layoutWidth = constraints.maxWidth
  134. val layoutHeight = constraints.maxHeight
  135. val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
  136. /**
  137. * Tachiyomi: Remove height constraint for expanded app bar
  138. */
  139. val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
  140. layout(layoutWidth, layoutHeight) {
  141. val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map {
  142. it.measure(topBarConstraints)
  143. }
  144. val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
  145. val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
  146. it.measure(looseConstraints)
  147. }
  148. val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
  149. val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0
  150. val fabPlaceables =
  151. subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
  152. measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
  153. }
  154. val fabHeight = fabPlaceables.maxByOrNull { it.height }?.height ?: 0
  155. val fabPlacement = if (fabPlaceables.isNotEmpty()) {
  156. val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width
  157. // FAB distance from the left of the layout, taking into account LTR / RTL
  158. val fabLeftOffset = if (fabPosition == FabPosition.End) {
  159. if (layoutDirection == LayoutDirection.Ltr) {
  160. layoutWidth - FabSpacing.roundToPx() - fabWidth
  161. } else {
  162. FabSpacing.roundToPx()
  163. }
  164. } else {
  165. (layoutWidth - fabWidth) / 2
  166. }
  167. FabPlacement(
  168. left = fabLeftOffset,
  169. width = fabWidth,
  170. height = fabHeight,
  171. )
  172. } else {
  173. null
  174. }
  175. val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
  176. CompositionLocalProvider(
  177. LocalFabPlacement provides fabPlacement,
  178. content = bottomBar,
  179. )
  180. }.map { it.measure(looseConstraints) }
  181. val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0
  182. val fabOffsetFromBottom = fabPlacement?.let {
  183. if (bottomBarHeight == 0) {
  184. it.height + FabSpacing.roundToPx()
  185. } else {
  186. // Total height is the bottom bar height + the FAB height + the padding
  187. // between the FAB and bottom bar
  188. bottomBarHeight + it.height + FabSpacing.roundToPx()
  189. }
  190. }
  191. val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
  192. snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
  193. } else {
  194. 0
  195. }
  196. /**
  197. * Tachiyomi: Also take account of fab height when providing inner padding
  198. */
  199. val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
  200. val insets = WindowInsets.Companion.safeDrawing
  201. .asPaddingValues(this@SubcomposeLayout)
  202. val innerPadding = PaddingValues(
  203. top =
  204. if (topBarHeight == 0) insets.calculateTopPadding()
  205. else topBarHeight.toDp(),
  206. bottom =
  207. (
  208. if (bottomBarHeight == 0) insets.calculateBottomPadding()
  209. else bottomBarHeight.toDp()
  210. ) + fabHeight.toDp(),
  211. start = insets.calculateLeftPadding((this@SubcomposeLayout).layoutDirection),
  212. end = insets.calculateRightPadding((this@SubcomposeLayout).layoutDirection),
  213. )
  214. content(innerPadding)
  215. }.map { it.measure(looseConstraints) }
  216. // Placing to control drawing order to match default elevation of each placeable
  217. bodyContentPlaceables.forEach {
  218. it.place(0, 0)
  219. }
  220. topBarPlaceables.forEach {
  221. it.place(0, 0)
  222. }
  223. snackbarPlaceables.forEach {
  224. it.place(
  225. (layoutWidth - snackbarWidth) / 2,
  226. layoutHeight - snackbarOffsetFromBottom,
  227. )
  228. }
  229. // The bottom bar is always at the bottom of the layout
  230. bottomBarPlaceables.forEach {
  231. it.place(0, layoutHeight - bottomBarHeight)
  232. }
  233. // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
  234. fabPlacement?.let { placement ->
  235. fabPlaceables.forEach {
  236. it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
  237. }
  238. }
  239. }
  240. }
  241. }
  242. /**
  243. * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
  244. */
  245. @ExperimentalMaterial3Api
  246. @JvmInline
  247. value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
  248. companion object {
  249. /**
  250. * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it
  251. * exists)
  252. */
  253. val Center = FabPosition(0)
  254. /**
  255. * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it
  256. * exists)
  257. */
  258. val End = FabPosition(1)
  259. }
  260. override fun toString(): String {
  261. return when (this) {
  262. Center -> "FabPosition.Center"
  263. else -> "FabPosition.End"
  264. }
  265. }
  266. }
  267. /**
  268. * Placement information for a [FloatingActionButton] inside a [Scaffold].
  269. *
  270. * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
  271. * support
  272. * @property width the width of the FAB
  273. * @property height the height of the FAB
  274. */
  275. @Immutable
  276. internal class FabPlacement(
  277. val left: Int,
  278. val width: Int,
  279. val height: Int,
  280. )
  281. /**
  282. * CompositionLocal containing a [FabPlacement] that is used to calculate the FAB bottom offset.
  283. */
  284. internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
  285. // FAB spacing above the bottom bar / bottom of the Scaffold
  286. private val FabSpacing = 16.dp
  287. private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }