diff --git a/core/ui/src/main/java/com/yapp/ui/component/textfield/OrbitTextField.kt b/core/ui/src/main/java/com/yapp/ui/component/textfield/OrbitTextField.kt index 06b61588..83ac88c8 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/textfield/OrbitTextField.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/textfield/OrbitTextField.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -46,6 +45,7 @@ fun OrbitTextField( hint: String, modifier: Modifier = Modifier, showWarning: Boolean = false, + warningMessage: String, onFocusChanged: (Boolean) -> Unit = {}, ) { val focusRequester = remember { FocusRequester() } @@ -83,7 +83,7 @@ fun OrbitTextField( Spacer(modifier = Modifier.height(12.dp)) if (showWarning) { - WarningMessage() + WarningMessage(warningMessage) } } } @@ -91,7 +91,7 @@ fun OrbitTextField( } @Composable -private fun WarningMessage() { +private fun WarningMessage(message: String) { Box( modifier = Modifier .fillMaxWidth() @@ -99,7 +99,7 @@ private fun WarningMessage() { contentAlignment = Alignment.Center, ) { Text( - text = "입력한 숫자를 확인해 주세요", + text = message, color = OrbitTheme.colors.alert, style = OrbitTheme.typography.label1SemiBold, ) @@ -121,17 +121,17 @@ private fun TextFieldContainer( Box( modifier = Modifier .border( - width = 3.dp, + width = 1.dp, color = when { isFocused && showWarning -> OrbitTheme.colors.alert isFocused -> OrbitTheme.colors.main.copy(alpha = 0.2f) showWarning -> OrbitTheme.colors.alert - else -> Color.Transparent + else -> OrbitTheme.colors.gray_500 }, - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(16.dp), ) - .background(OrbitTheme.colors.gray_800, shape = RoundedCornerShape(10.dp)) - .height(52.dp) + .background(OrbitTheme.colors.gray_800, shape = RoundedCornerShape(16.dp)) + .height(54.dp) .fillMaxWidth() .focusRequester(focusRequester) .clickable( @@ -197,10 +197,11 @@ private fun TextFieldContainer( fun OrbitTextFieldPreview() { OrbitTheme { OrbitTextField( - text = "123", + text = "", onTextChange = {}, - showWarning = true, + showWarning = false, hint = "이름을 입력해주세요", + warningMessage = "이름을 입력해주세요", modifier = Modifier .fillMaxWidth(), ) diff --git a/core/ui/src/main/java/com/yapp/ui/utils/ScreenPercentage.kt b/core/ui/src/main/java/com/yapp/ui/utils/ScreenPercentage.kt index 6056a838..6956e813 100644 --- a/core/ui/src/main/java/com/yapp/ui/utils/ScreenPercentage.kt +++ b/core/ui/src/main/java/com/yapp/ui/utils/ScreenPercentage.kt @@ -21,13 +21,25 @@ fun Modifier.widthForScreenPercentage(percentage: Float): Modifier { } @Composable -fun Modifier.paddingForScreenPercentage(horizontalPercentage: Float = 0f, verticalPercentage: Float = 0f): Modifier { +fun Modifier.paddingForScreenPercentage( + horizontalPercentage: Float = 0f, + verticalPercentage: Float = 0f, + topPercentage: Float = 0f, + bottomPercentage: Float = 0f, +): Modifier { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp val screenHeight = configuration.screenHeightDp.dp val horizontalPadding = screenWidth * horizontalPercentage val verticalPadding = screenHeight * verticalPercentage + val topPadding = screenHeight * topPercentage + verticalPadding + val bottomPadding = screenHeight * bottomPercentage + verticalPadding - return this.padding(horizontal = horizontalPadding, vertical = verticalPadding) + return this.padding( + start = horizontalPadding, + top = topPadding, + end = horizontalPadding, + bottom = bottomPadding, + ) } diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt new file mode 100644 index 00000000..4e827fba --- /dev/null +++ b/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt @@ -0,0 +1,48 @@ +package com.kms.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.ui.component.timepicker.OrbitYearMonthPicker +import com.yapp.ui.utils.heightForScreenPercentage +import feature.onboarding.R + +@Composable +fun BirthdayScreen( + state: OnboardingContract.State, + currentStep: Int, + totalSteps: Int, + onNextClick: () -> Unit, + onBackClick: () -> Unit, +) { + OnboardingScreen( + currentStep = currentStep, + totalSteps = totalSteps, + isButtonEnabled = true, + onNextClick = onNextClick, + onBackClick = onBackClick, + ) { + Column(modifier = Modifier.fillMaxSize()) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.05f)) + Text( + text = stringResource(id = R.string.onboarding_step3_text_title), + style = OrbitTheme.typography.heading1SemiBold, + color = OrbitTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + OrbitYearMonthPicker( + modifier = Modifier.padding(top = 60.dp), + ) + } + } +} diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnBoardingScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnBoardingScreen.kt index 20e87c64..80038d66 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/OnBoardingScreen.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnBoardingScreen.kt @@ -1,13 +1,15 @@ package com.kms.onboarding +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -66,31 +68,31 @@ fun OnboardingScreen( showTopAppBar: Boolean = true, content: @Composable () -> Unit, ) { - Scaffold( - topBar = { - if (showTopAppBar) { - OnBoardingTopAppBar( - currentStep = currentStep, - totalSteps = totalSteps, - onBackClick = onBackClick, - ) - } - }, - bottomBar = { - OnboardingBottomBar( - isButtonEnabled = isButtonEnabled, - onNextClick = onNextClick, + Column( + modifier = Modifier + .fillMaxSize() + .background(OrbitTheme.colors.gray_900) + .statusBarsPadding() + .navigationBarsPadding() + .imePadding(), + ) { + if (showTopAppBar) { + OnBoardingTopAppBar( + currentStep = currentStep, + totalSteps = totalSteps, + onBackClick = onBackClick, ) - }, - containerColor = OrbitTheme.colors.gray_900, - ) { innerPadding -> + } + Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - contentAlignment = Alignment.TopCenter, + modifier = Modifier.weight(1f), ) { content() } + + OnboardingBottomBar( + isButtonEnabled = isButtonEnabled, + onNextClick = onNextClick, + ) } } diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingNameScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingNameScreen.kt new file mode 100644 index 00000000..290309e9 --- /dev/null +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingNameScreen.kt @@ -0,0 +1,86 @@ +package com.kms.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.ui.component.textfield.OrbitTextField +import com.yapp.ui.extensions.customClickable +import com.yapp.ui.utils.heightForScreenPercentage +import com.yapp.ui.utils.paddingForScreenPercentage +import feature.onboarding.R + +@Composable +fun OnboardingNameScreen( + state: OnboardingContract.State, + currentStep: Int, + totalSteps: Int, + onNextClick: () -> Unit, + onBackClick: () -> Unit, + onTextChange: (String) -> Unit, +) { + val focusManager = LocalFocusManager.current + + OnboardingScreen( + currentStep = currentStep, + totalSteps = totalSteps, + isButtonEnabled = state.isButtonEnabled, + onNextClick = onNextClick, + onBackClick = onBackClick, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .customClickable( + rippleEnabled = false, + onClick = { focusManager.clearFocus() }, + ), + ) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.05f)) + Text( + text = stringResource(id = R.string.onboarding_step5_text_title), + style = OrbitTheme.typography.heading1SemiBold, + color = OrbitTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + OrbitTextField( + text = state.textFieldValue, + onTextChange = { value -> + onTextChange(value) + }, + hint = "이름 입력", + showWarning = state.showWarning, + warningMessage = stringResource(id = R.string.onboarding_step5_textfield_warning), + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(horizontalPercentage = 0.192f, topPercentage = 0.086f), + ) + } + } +} + +@Composable +@Preview +fun PreviewOnboardingNameScreen() { + OnboardingNameScreen( + state = OnboardingContract.State( + textFieldValue = "김민수", + isButtonEnabled = true, + showWarning = false, + ), + currentStep = 1, + totalSteps = 5, + onNextClick = {}, + onBackClick = {}, + onTextChange = {}, + ) +} diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingTimeOfBirthScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingTimeOfBirthScreen.kt new file mode 100644 index 00000000..6eb78d7e --- /dev/null +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingTimeOfBirthScreen.kt @@ -0,0 +1,146 @@ +package com.kms.onboarding + +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.ui.component.textfield.OrbitTextField +import com.yapp.ui.extensions.customClickable +import com.yapp.ui.utils.heightForScreenPercentage +import com.yapp.ui.utils.paddingForScreenPercentage +import feature.onboarding.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun OnboardingTimeOfBirthScreen( + state: OnboardingContract.State, + currentStep: Int, + totalSteps: Int, + onNextClick: () -> Unit, + onBackClick: () -> Unit, + onTextChange: (String) -> Unit, +) { + val focusManager = LocalFocusManager.current + var isTextPressed by remember { mutableStateOf(false) } + + OnboardingScreen( + currentStep = currentStep, + totalSteps = totalSteps, + isButtonEnabled = state.isButtonEnabled, + onNextClick = onNextClick, + onBackClick = onBackClick, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .customClickable( + rippleEnabled = false, + onClick = { focusManager.clearFocus() }, + ), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.05f)) + Text( + text = stringResource(id = R.string.onboarding_step4_text_title), + style = OrbitTheme.typography.heading1SemiBold, + color = OrbitTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + OrbitTextField( + text = state.textFieldValue, + onTextChange = { value -> + onTextChange(value) + }, + hint = "23:59", + showWarning = state.showWarning, + warningMessage = stringResource(id = R.string.onboarding_step4_textfield_warning), + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(horizontalPercentage = 0.192f, topPercentage = 0.086f), + ) + } + + Row( + modifier = Modifier + .wrapContentWidth() + .paddingForScreenPercentage(bottomPercentage = 0.017f) + .align(Alignment.CenterHorizontally) + .pointerInteropFilter { event -> + when (event.action) { + android.view.MotionEvent.ACTION_DOWN -> { + isTextPressed = true + true + } + + android.view.MotionEvent.ACTION_UP, + android.view.MotionEvent.ACTION_CANCEL, + -> { + isTextPressed = false + true + } + + else -> false + } + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_check), + contentDescription = "Check", + tint = if (isTextPressed) OrbitTheme.colors.main else OrbitTheme.colors.white, + ) + Text( + text = stringResource(id = R.string.onboarding_step4_text_check), + style = OrbitTheme.typography.body1Medium, + color = if (isTextPressed) OrbitTheme.colors.main else OrbitTheme.colors.white, + modifier = Modifier.padding(start = 4.dp), + textAlign = TextAlign.Center, + ) + } + } + } +} + +@Composable +@Preview +fun OnboardingTimeOfBirthScreenPreview() { + OnboardingTimeOfBirthScreen( + state = OnboardingContract.State( + textFieldValue = "23:59", + showWarning = false, + isButtonEnabled = true, + ), + currentStep = 4, + totalSteps = 4, + onNextClick = {}, + onBackClick = {}, + onTextChange = {}, + ) +} diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingViewModel.kt index 81896107..27fd8e29 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingViewModel.kt @@ -1,9 +1,12 @@ package com.kms.onboarding import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import com.kms.onboarding.navigation.OnboardingDestination import com.yapp.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -60,7 +63,10 @@ class OnboardingViewModel @Inject constructor( } private fun resetFields() { - updateState { copy(textFieldValue = "", showWarning = false, isButtonEnabled = false) } + viewModelScope.launch { + delay(150) + updateState { copy(textFieldValue = "", showWarning = false, isButtonEnabled = false) } + } } private fun handleSubmission(stepData: Map) { diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingDestination.kt b/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingDestination.kt index 80a9cb15..d33f5f92 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingDestination.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingDestination.kt @@ -4,9 +4,11 @@ sealed class OnboardingDestination(val route: String) { data object Explain : OnboardingDestination(Routes.EXPLAIN) data object AlarmTimeSelection : OnboardingDestination(Routes.ALARM_TIME_SELECTION) data object Birthday : OnboardingDestination(Routes.BIRTHDAY) + data object TimeOfBirth : OnboardingDestination(Routes.TIME_OF_BIRTH) + data object Name : OnboardingDestination(Routes.NAME) companion object { - private val routes = listOf(Explain, AlarmTimeSelection, Birthday) + private val routes = listOf(Explain, AlarmTimeSelection, Birthday, TimeOfBirth, Name) fun nextRoute(currentStep: Int): String? { return routes.getOrNull(currentStep)?.route diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingNavGraph.kt b/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingNavGraph.kt index e5ca9842..f685a2a5 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingNavGraph.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/navigation/OnboardingNavGraph.kt @@ -1,11 +1,14 @@ package com.kms.onboarding.navigation +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.kms.onboarding.OnboardingAlarmTimeSelectionScreen import com.kms.onboarding.OnboardingBirthdayScreen import com.kms.onboarding.OnboardingContract import com.kms.onboarding.OnboardingExplainScreen +import com.kms.onboarding.OnboardingNameScreen +import com.kms.onboarding.OnboardingTimeOfBirthScreen fun NavGraphBuilder.onboardingNavGraph( stateProvider: () -> OnboardingContract.State, @@ -25,7 +28,7 @@ fun NavGraphBuilder.onboardingNavGraph( OnboardingAlarmTimeSelectionScreen( state = stateProvider(), currentStep = 1, - totalSteps = 2, + totalSteps = 4, onNextClick = { eventDispatcher(OnboardingContract.Action.NextStep) }, @@ -39,7 +42,7 @@ fun NavGraphBuilder.onboardingNavGraph( OnboardingBirthdayScreen( state = stateProvider(), currentStep = 2, - totalSteps = 2, + totalSteps = 4, onNextClick = { eventDispatcher(OnboardingContract.Action.NextStep) }, @@ -48,4 +51,40 @@ fun NavGraphBuilder.onboardingNavGraph( }, ) } + + composable(OnboardingDestination.TimeOfBirth.route) { + val keyboardContract = LocalSoftwareKeyboardController.current + OnboardingTimeOfBirthScreen( + state = stateProvider(), + currentStep = 3, + totalSteps = 4, + onNextClick = { + eventDispatcher(OnboardingContract.Action.NextStep) + eventDispatcher(OnboardingContract.Action.Reset) + keyboardContract?.hide() + }, + onBackClick = { + eventDispatcher(OnboardingContract.Action.PreviousStep) + }, + onTextChange = { value -> + eventDispatcher(OnboardingContract.Action.UpdateField(value, OnboardingContract.FieldType.TIME)) + }, + ) + } + composable(OnboardingDestination.Name.route) { + OnboardingNameScreen( + state = stateProvider(), + currentStep = 4, + totalSteps = 4, + onNextClick = { + eventDispatcher(OnboardingContract.Action.NextStep) + }, + onBackClick = { + eventDispatcher(OnboardingContract.Action.PreviousStep) + }, + onTextChange = { value -> + eventDispatcher(OnboardingContract.Action.UpdateField(value, OnboardingContract.FieldType.NAME)) + }, + ) + } }