Skip to content

Scaffold 구현 #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 22 additions & 11 deletions .idea/other.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions app/src/main/kotlin/com/yourssu/handy/demo/ScaffoldPreview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.yourssu.handy.demo

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.yourssu.handy.compose.HandyTheme
import com.yourssu.handy.compose.Scaffold

@Composable
@Preview
fun ScaffoldPreview() {
HandyTheme {
Scaffold(
topBar = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.Blue)
)
},
bottomBar = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.Green)
)
},
snackbarHost = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.Red)
)
},
floatingActionButton = {
Box(
modifier = Modifier
.size(56.dp)
.background(Color.Yellow)
)
},
content = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray)
)
}
)
}
}
168 changes: 168 additions & 0 deletions compose/src/main/kotlin/com/yourssu/handy/compose/Scaffold.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.yourssu.handy.compose

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import com.yourssu.handy.compose.ScaffoldSpacingValues.FabBottomSpacing
import com.yourssu.handy.compose.ScaffoldSpacingValues.FabEndSpacing
import com.yourssu.handy.compose.ScaffoldSpacingValues.SnackBarBottomSpacing
import com.yourssu.handy.compose.ScaffoldSpacingValues.SnackBarHorizontalSpacing
import com.yourssu.handy.compose.foundation.LocalContentColor

/**
* layout을 구성하기 위한 Scaffold입니다.
*
* @param modifier : Modifier
* @param topBar : 상단 바
* @param snackbarHost : Snackbar
* @param floatingActionButton : Floating Action Button
* @param bottomBar : 하단 네비게이션 바
* @param containerColor : Scaffold의 배경색
* @param contentColor : Scaffold의 content 색상
* @param content : Scaffold의 content
*/
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
containerColor: Color = Color.Unspecified,
contentColor: Color = LocalContentColor.current,
content: @Composable (PaddingValues) -> Unit
) {
Surface(
modifier = modifier,
backgroundColor = containerColor,
contentColor = contentColor
) {
ScaffoldLayout(
topBar = topBar,
content = content,
snackbar = snackbarHost,
fab = floatingActionButton,
bottomBar = bottomBar
)
}
}

/**
* ScaffoldLayout을 구성하는 함수입니다.
* @param topBar : 상단 바
* @param content : Scaffold의 content
* @param snackbar : Snackbar
* @param fab : Floating Action Button
* @param bottomBar : 하단 네비게이션 바
*/
@Composable
private fun ScaffoldLayout(
topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit,
snackbar: @Composable () -> Unit,
fab: @Composable () -> Unit,
bottomBar: @Composable () -> Unit
) {
val snackBarPxValue = LocalDensity.current.run { SnackBarBottomSpacing.toPx() }.toInt()
val fabEndMarginPxValue = LocalDensity.current.run { FabEndSpacing.toPx() }.toInt()
val fabBottomMarginPxValue = LocalDensity.current.run { FabBottomSpacing.toPx() }.toInt()

SubcomposeLayout(
modifier = Modifier.semantics { isTraversalGroup = true }
) { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight

val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

val topBarPlaceable = subcompose(ScaffoldLayoutContent.TopBar) {
Box(
modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = 0f
}
) {
topBar()
}
}.first().measure(looseConstraints)

val mainContentPlaceable = subcompose(ScaffoldLayoutContent.MainContent) {
Box(
modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = 2f
}
) {
content(PaddingValues(0.dp))
}
}.first().measure(looseConstraints)

val snackBarPlaceable = subcompose(ScaffoldLayoutContent.Snackbar) {
Box(modifier = Modifier
.padding(horizontal = SnackBarHorizontalSpacing) // TODO : SnackBar 컴포넌트 자체에서 margin을 줄지 여기서 주어야 할지?
.semantics {
isTraversalGroup = true
traversalIndex = 4f
}
) {
snackbar()
}
}.first().measure(looseConstraints)

val fabPlaceable = subcompose(ScaffoldLayoutContent.Fab) {
Box(modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = 3f
}) {
fab()
}
}.first().measure(looseConstraints)

val bottomBarPlaceable = subcompose(ScaffoldLayoutContent.BottomBar) {
Box(modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = 1f
}) {
bottomBar()
}
}.first().measure(looseConstraints)

val bottomBarVerticalOffset = layoutHeight - bottomBarPlaceable.height
val fabVerticalOffset =
bottomBarVerticalOffset - fabPlaceable.height - fabBottomMarginPxValue
val snackBarVerticalOffset = fabVerticalOffset - snackBarPlaceable.height - snackBarPxValue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fab이 없을 땐 fabVerticalOffset이 bottomBarVerticalOffset과 같아야 할 것 같아요.

쉽게 설명하자면 지금 상황에서 fab이 없을 때 snackBar가 어디에 배치될지 생각해보심 됩니다. 아마 생각보다 높은 곳에 배치될거예요. (fab 마진 + snackBar 마진 만큼)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kangyuri1114
업데이트 해주신 프리뷰 확인했어요. 근데 FAB랑 SnackBar가 같이 있는 경우에는 SnackBar가 FAB 위에 그려지는 게 맞지 않을까요? 가장 처음에 첨부해주신 프리뷰처럼요
image

val fabHorizontalOffset = layoutWidth - fabPlaceable.width - fabEndMarginPxValue

layout(layoutWidth, layoutHeight) {
topBarPlaceable.placeRelative(0, 0)
mainContentPlaceable.placeRelative(0, topBarPlaceable.height)
snackBarPlaceable.placeRelative(0, snackBarVerticalOffset)
fabPlaceable.placeRelative(fabHorizontalOffset, fabVerticalOffset)
bottomBarPlaceable.placeRelative(0, bottomBarVerticalOffset)
}
}
}

object ScaffoldSpacingValues {
val FabBottomSpacing = 32.dp
val FabEndSpacing = 16.dp
val SnackBarBottomSpacing = 16.dp
val SnackBarHorizontalSpacing = 16.dp
}

private enum class ScaffoldLayoutContent {
TopBar,
MainContent,
Snackbar,
Fab,
BottomBar
}