From 57ebbfacf0a994b51e2cd0d3fe7795bbd9142468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:11:48 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20Custom=20SubView=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat]: Compose 스낵바 기능 구현 * [Fix]: Compose 스낵바 ui 수정 * [Feat]: Compose Dialog 기능 구현 * [Feat]: Compose BottomSheet 기능 구현 * [Refactor]: ApplicationState에 SnackBar 기능 추가 * [Feat]: SnackBar 기능 적용 * [Style]: 코드 포맷 변경, Optimize imports * [Refactor]: SnackbarScreen -> SnackBarHost로 수정, 기능 적용 * [Refactor]: 네이밍, ApplicationState 기능 수정 --- .../common/view/BottomSheetScreen.kt | 149 +++++++++++++++++ .../common/view/CustomSnackBarHost.kt | 14 ++ .../presentation/common/view/DialogScreen.kt | 156 ++++++++++++++++++ .../common/view/SnackBarScreen.kt | 33 ++++ .../presentation/ui/main/home/HomeScreen.kt | 25 ++- .../ui/main/home/setting/SettingScreen.kt | 55 +++++- 6 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/BottomSheetScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/CustomSnackBarHost.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/DialogScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/BottomSheetScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/BottomSheetScreen.kt new file mode 100644 index 00000000..94b889e9 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/BottomSheetScreen.kt @@ -0,0 +1,149 @@ +package ac.dnd.bookkeeping.android.presentation.common.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialog +import com.holix.android.bottomsheetdialog.compose.BottomSheetDialogProperties + +@Composable +fun BottomSheetScreen( + bottomSheetIsShowingState: MutableState, + content: @Composable () -> Unit = { SampleBottomSheetComponent(bottomSheetIsShowingState) } +) { + val animateIn = remember { mutableStateOf(false) } + + if (bottomSheetIsShowingState.value) { + LaunchedEffect(Unit) { animateIn.value = true } + AnimatedVisibility( + visible = animateIn.value && bottomSheetIsShowingState.value, + enter = fadeIn(), + exit = fadeOut() + ) { + BottomSheetDialog( + onDismissRequest = { + bottomSheetIsShowingState.value = false + }, + properties = BottomSheetDialogProperties( + dismissOnClickOutside = false, + dismissOnBackPress = true + ) + ) { + Box( + modifier = Modifier + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp) + ) + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)) + .background(color = Color.LightGray) + ) { + content() + } + } + } + } +} + +@Composable +fun SampleBottomSheetComponent( + bottomSheetState: MutableState +) { + + Column( + Modifier + .padding(horizontal = 8.dp) + .background(Color.Transparent) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + Color.LightGray, + RoundedCornerShape(13.dp) + ) + ) { + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "권한 추가", + color = Color.Black, + ) + Text( + text = "사용 권한이 필요합니다.", + color = Color.Black + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Divider( + modifier = Modifier.height(1.dp), + color = Color.Black + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { + bottomSheetState.value = false + } + ) { + Text( + text = "권한 허용", + color = Color.Black, + modifier = Modifier.padding(vertical = 15.dp) + ) + } + Divider( + modifier = Modifier.height(1.dp), + color = Color.Black + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { + bottomSheetState.value = false + } + ) { + Text( + text = "취소", + color = Color.Black, + modifier = Modifier.padding(vertical = 15.dp) + ) + } + } + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/CustomSnackBarHost.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/CustomSnackBarHost.kt new file mode 100644 index 00000000..989067c2 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/CustomSnackBarHost.kt @@ -0,0 +1,14 @@ +package ac.dnd.bookkeeping.android.presentation.common.view + +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.runtime.Composable + +val CustomSnackBarHost: @Composable (SnackbarHostState) -> Unit = + { snackBarHostState: SnackbarHostState -> + SnackbarHost( + hostState = snackBarHostState, + ) { snackBarData -> + SnackBarScreen(snackBarData.message) + } + } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/DialogScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/DialogScreen.kt new file mode 100644 index 00000000..51b08480 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/DialogScreen.kt @@ -0,0 +1,156 @@ +package ac.dnd.bookkeeping.android.presentation.common.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog + +@Composable +fun DialogScreen( + dialogIsShowingState: MutableState, + width: Dp = 300.dp, + content: @Composable () -> Unit = { SampleDialogComponent(dialogIsShowingState) } +) { + val animateIn = remember { mutableStateOf(false) } + + if (dialogIsShowingState.value) { + Dialog( + onDismissRequest = { + dialogIsShowingState.value = false + } + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LaunchedEffect(Unit) { animateIn.value = true } + AnimatedVisibility( + visible = animateIn.value && dialogIsShowingState.value, + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + modifier = Modifier + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(10.dp) + ) + .width(width) + .clip(RoundedCornerShape(16.dp)) + .background( + color = Color.LightGray + ), + contentAlignment = Alignment.Center + ) { + content() + } + } + } + } + } +} + +@Composable +fun SampleDialogComponent( + dialogState: MutableState +) { + Surface( + modifier = Modifier.background( + color = Color.White, + shape = RoundedCornerShape(8.dp) + ), + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Sample Dialog Component", + fontSize = 16.sp, + color = Color.Black + ) + Spacer(modifier = Modifier.height(50.dp)) + Row(modifier = Modifier.wrapContentSize()) { + Box( + modifier = Modifier + .background( + color = Color.Gray, + shape = RoundedCornerShape(4.dp) + ) + .weight(1f) + .clickable { + dialogState.value = false + }, + contentAlignment = Alignment.Center + ) { + Text( + "취소", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier + .padding( + vertical = 10.5.dp, + horizontal = 12.dp + ), + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Box( + modifier = Modifier + .background( + color = Color.Gray, + shape = RoundedCornerShape(4.dp) + ) + .weight(1f) + .clickable { + dialogState.value = false + }, + contentAlignment = Alignment.Center + ) { + Text( + "확인", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier + .padding( + vertical = 10.5.dp, + horizontal = 12.dp + ) + ) + } + } + } + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt new file mode 100644 index 00000000..b3576fd5 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt @@ -0,0 +1,33 @@ +package ac.dnd.bookkeeping.android.presentation.common.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SnackBarScreen(message: String) { + Box( + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + .background( + shape = RoundedCornerShape(10.dp), + color = Color.LightGray + ), + ) { + Text( + text = message, + fontSize = 20.sp, + color = Color.White, + modifier = Modifier.padding(20.dp), + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt index 572fba7b..e17e5158 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home +import ac.dnd.bookkeeping.android.presentation.common.view.CustomSnackBarHost import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState import ac.dnd.bookkeeping.android.presentation.ui.main.home.bookkeeping.BookkeepingScreen import ac.dnd.bookkeeping.android.presentation.ui.main.home.setting.SettingScreen @@ -9,12 +10,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState +import androidx.compose.material.SnackbarResult import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -26,8 +29,9 @@ fun HomeScreen( appState: ApplicationState, viewModel: HomeViewModel = hiltViewModel() ) { - val scaffoldState: ScaffoldState = rememberScaffoldState() + val scaffoldState = rememberScaffoldState() var selectedItem by rememberSaveable { mutableIntStateOf(0) } + var snackBarIsShowingState by remember { mutableStateOf(false) } val pagerState = rememberPagerState( pageCount = { 2 } ) @@ -39,6 +43,7 @@ fun HomeScreen( Scaffold( modifier = Modifier.fillMaxSize(), scaffoldState = scaffoldState, + snackbarHost = CustomSnackBarHost, bottomBar = { HomeBottomBarScreen( selectedItem = selectedItem, @@ -63,10 +68,24 @@ fun HomeScreen( 1 -> { SettingScreen( - appState = appState + appState = appState, + onShowSnackBar = { + snackBarIsShowingState = true + } ) } } } } + + if (snackBarIsShowingState) { + LaunchedEffect(Unit) { + scaffoldState.snackbarHostState.showSnackbar("show snackBar") + .let { + if (it == SnackbarResult.Dismissed) { + snackBarIsShowingState = false + } + } + } + } } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/setting/SettingScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/setting/SettingScreen.kt index dda4d456..5c916f56 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/setting/SettingScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/setting/SettingScreen.kt @@ -1,32 +1,79 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home.setting +import ac.dnd.bookkeeping.android.presentation.common.view.BottomSheetScreen +import ac.dnd.bookkeeping.android.presentation.common.view.DialogScreen import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel @Composable fun SettingScreen( appState: ApplicationState, + onShowSnackBar: () -> Unit, viewModel: SettingViewModel = hiltViewModel() ) { - Box( + + val dialogIsShowingState = remember { mutableStateOf(false) } + val bottomSheetIsShowingState = remember { mutableStateOf(false) } + + Column( modifier = Modifier .fillMaxSize() .background(color = Color.White), - contentAlignment = Alignment.Center + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Setting", fontSize = 20.sp, - color = Color.Black + color = Color.Black, + modifier = Modifier.padding(vertical = 10.dp) + ) + Text( + text = "snackBarState", + fontSize = 20.sp, + color = Color.Black, + modifier = Modifier + .padding(vertical = 10.dp) + .clickable { + onShowSnackBar() + } + ) + Text( + text = "dialogState", + fontSize = 20.sp, + color = Color.Black, + modifier = Modifier + .padding(vertical = 10.dp) + .clickable { + dialogIsShowingState.value = true + } + ) + Text( + text = "bottomSheetState", + fontSize = 20.sp, + color = Color.Black, + modifier = Modifier + .padding(vertical = 10.dp) + .clickable { + bottomSheetIsShowingState.value = true + } ) } + BottomSheetScreen(bottomSheetIsShowingState = bottomSheetIsShowingState) + DialogScreen(dialogIsShowingState = dialogIsShowingState) }