diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml index 94c96f63..49481ad4 100644 --- a/.idea/other.xml +++ b/.idea/other.xml @@ -25,6 +25,17 @@ diff --git a/app/src/main/kotlin/com/yourssu/handy/demo/ScaffoldPreview.kt b/app/src/main/kotlin/com/yourssu/handy/demo/ScaffoldPreview.kt new file mode 100644 index 00000000..bcf60f65 --- /dev/null +++ b/app/src/main/kotlin/com/yourssu/handy/demo/ScaffoldPreview.kt @@ -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) + ) + } + ) + } +} \ No newline at end of file diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/Scaffold.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/Scaffold.kt new file mode 100644 index 00000000..0a802787 --- /dev/null +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/Scaffold.kt @@ -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 + 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 +} \ No newline at end of file