diff --git a/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt b/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt index 2449615ac..8eda67335 100644 --- a/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt @@ -63,7 +63,6 @@ fun MainScreen( ) { splashNavGraph( navigateToOnboarding = appState::navigateToOnboarding, - navigateToHome = appState::navigateToHome, paddingValues = innerPadding ) diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt index 02c6407f9..c816dd33e 100644 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt @@ -1,29 +1,45 @@ package com.cherrish.android.presentation.onboarding.information +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue 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.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction @@ -33,7 +49,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.cherrish.android.core.common.extension.addFocusCleaner import com.cherrish.android.core.common.extension.collectLatestSideEffect import com.cherrish.android.core.designsystem.component.button.CherrishButton import com.cherrish.android.core.designsystem.component.textfield.CherrishTextField @@ -69,6 +84,7 @@ fun OnboardingInformationRoute( ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun OnboardingInformationScreen( paddingValues: PaddingValues, @@ -84,82 +100,144 @@ private fun OnboardingInformationScreen( ) { val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current + val density = LocalDensity.current + + val nameFocusRequester = remember { FocusRequester() } val ageFocusRequester = remember { FocusRequester() } + var isNameFocused by remember { mutableStateOf(false) } var isAgeFocused by remember { mutableStateOf(false) } - Scaffold( - bottomBar = { - CherrishButton( - text = "다음", - onClick = onNextClick, - enabled = enabled, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 30.dp) - .background(CherrishTheme.colors.gray0) - .navigationBarsPadding() + val coroutineScope = rememberCoroutineScope() + + val bringIntoViewRequester = remember { BringIntoViewRequester() } - ) + LaunchedEffect(isNameFocused, isAgeFocused) { + if (isNameFocused || isAgeFocused) { + delay(300) + bringIntoViewRequester.bringIntoView() } - ) { innerPadding -> - Column( - modifier = modifier - .fillMaxSize() - .background(color = CherrishTheme.colors.gray0) - .addFocusCleaner(focusManager) - .padding(paddingValues = paddingValues) - .imePadding() + } + + val listState = rememberLazyListState() + val imeBottom = WindowInsets.ime.getBottom(density) + val imeBottomDp = with(density) { imeBottom.toDp() } + val targetBottomInset = if (imeBottomDp > 0.dp) 0.dp else paddingValues.calculateBottomPadding() + val bottomInset by animateDpAsState(targetValue = targetBottomInset, label = "bottomInset") + + Column( + modifier = modifier + .fillMaxSize() + .background(color = CherrishTheme.colors.gray0) + ) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentPadding = PaddingValues(bottom = 24.dp) ) { - Spacer(modifier = Modifier.weight(135f)) - UserInfoHeader() - Spacer(modifier = Modifier.weight(70f)) + item { + Spacer(modifier = Modifier.height(157.dp)) + } + + stickyHeader { + Column( + modifier = Modifier + .fillMaxWidth() + .background(CherrishTheme.colors.gray0) + ) { + UserInfoHeader() + } + } - UserInfoTextField( - textFieldName = "이름", - value = username, - onValueChange = onNameChange, - placeholder = "김체리", - keyboardImeAction = ImeAction.Next, - onNextAction = { ageFocusRequester.requestFocus() }, - keyboardType = KeyboardType.Text, - errorText = "이름은 최대 7자까지 입력 가능합니다.", - errorCase = nameErrorCase - ) + item { + Spacer(modifier = Modifier.height(70.dp)) + } - Spacer(modifier = Modifier.weight(30f)) + item { + Column( + modifier = Modifier + .bringIntoViewRequester(bringIntoViewRequester) + .windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom)) + ) { + UserInfoTextField( + textFieldName = "이름", + value = username, + onValueChange = onNameChange, + placeholder = "김체리", + keyboardImeAction = ImeAction.Next, + onNextAction = { ageFocusRequester.requestFocus() }, + keyboardType = KeyboardType.Text, + errorText = "이름은 최대 7자까지 입력 가능합니다.", + errorCase = nameErrorCase, + textFieldModifier = Modifier + .focusRequester(nameFocusRequester) + .pointerInput(Unit) { + awaitEachGesture { + awaitFirstDown(pass = PointerEventPass.Initial) + nameFocusRequester.requestFocus() + waitForUpOrCancellation() + } + } + .onFocusChanged { state -> + isNameFocused = state.isFocused + } + ) - UserInfoTextField( - textFieldName = "나이", - value = age, - onValueChange = onAgeChange, - placeholder = "20", - keyboardImeAction = ImeAction.Done, - onDoneAction = { - keyboardController?.hide() - kotlinx.coroutines.MainScope().launch { - delay(100) - focusManager.clearFocus() - } - }, - keyboardType = KeyboardType.Number, - visualTransformation = if (isAgeFocused) { - VisualTransformation.None - } else { - AgeSuffixTransformation(" 세") - }, - errorText = "입력 가능한 최대 나이 100세를 초과했습니다.", - errorCase = ageErrorCase, - modifier = Modifier - .focusRequester(ageFocusRequester) - .onFocusChanged { state -> - isAgeFocused = state.isFocused - } - ) + Spacer(modifier = Modifier.height(30.dp)) - Spacer(modifier = Modifier.weight(200f)) + UserInfoTextField( + textFieldName = "나이", + value = age, + onValueChange = onAgeChange, + placeholder = "20", + keyboardImeAction = ImeAction.Done, + onDoneAction = { + keyboardController?.hide() + coroutineScope.launch { + delay(100) + focusManager.clearFocus() + } + }, + keyboardType = KeyboardType.Number, + visualTransformation = if (isAgeFocused) { + VisualTransformation.None + } else { + AgeSuffixTransformation( + " 세" + ) + }, + errorText = "입력 가능한 최대 나이 100세를 초과했습니다.", + errorCase = ageErrorCase, + textFieldModifier = Modifier + .focusRequester(ageFocusRequester) + .pointerInput(Unit) { + awaitEachGesture { + awaitFirstDown(pass = PointerEventPass.Initial) + ageFocusRequester.requestFocus() + waitForUpOrCancellation() + } + } + .onFocusChanged { state -> + isAgeFocused = state.isFocused + } + ) + } - Spacer(modifier = Modifier.padding(innerPadding.calculateBottomPadding())) + Spacer(modifier = Modifier.height(24.dp)) + } } + + CherrishButton( + text = "다음", + onClick = onNextClick, + enabled = enabled, + modifier = Modifier + .fillMaxWidth() + .background(CherrishTheme.colors.gray0) + .padding(horizontal = 24.dp) + .padding(top = 30.dp, bottom = 30.dp + bottomInset) + ) } } @@ -168,7 +246,7 @@ private fun UserInfoHeader() { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 26.dp), + .padding(horizontal = 15.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( @@ -195,6 +273,7 @@ private fun UserInfoTextField( keyboardType: KeyboardType, errorText: String, modifier: Modifier = Modifier, + textFieldModifier: Modifier = Modifier, onNextAction: () -> Unit = {}, onDoneAction: () -> Unit = {}, visualTransformation: VisualTransformation = VisualTransformation.None, @@ -227,7 +306,7 @@ private fun UserInfoTextField( onDoneAction = onDoneAction, keyboardType = keyboardType, visualTransformation = visualTransformation, - modifier = Modifier.fillMaxWidth() + modifier = textFieldModifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt index a5d517364..b9659d156 100644 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt @@ -3,7 +3,6 @@ package com.cherrish.android.presentation.onboarding.information import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.cherrish.android.core.common.extension.onLogFailure -import com.cherrish.android.core.local.TokenManager import com.cherrish.android.data.model.OnboardingProfileRequestModel import com.cherrish.android.data.repository.OnboardingProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,8 +18,7 @@ import kotlinx.coroutines.launch @HiltViewModel class OnboardingInformationViewModel @Inject constructor( - private val onboardingProfileRepository: OnboardingProfileRepository, - private val tokenManager: TokenManager + private val onboardingProfileRepository: OnboardingProfileRepository ) : ViewModel() { private val _uiState = MutableStateFlow(InformationUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -43,7 +41,7 @@ class OnboardingInformationViewModel @Inject constructor( _uiState.update { it.copy(age = filtered) } } - fun onAgeErrorCase(age: String): Boolean = age.toIntOrNull() ?.let { it > 100 } ?: false + fun onAgeErrorCase(age: String): Boolean = age.toIntOrNull()?.let { it > 100 } ?: false fun onNextClicked() { val age = uiState.value.age.toIntOrNull() ?: return @@ -55,7 +53,6 @@ class OnboardingInformationViewModel @Inject constructor( age = age ) ).onSuccess { response -> - tokenManager.saveId(response.id) _sideEffect.emit(InformationSideEffect.NavigateToHome) }.onLogFailure {} } diff --git a/app/src/main/java/com/cherrish/android/presentation/splash/SplashScreen.kt b/app/src/main/java/com/cherrish/android/presentation/splash/SplashScreen.kt index 52762218b..cf5d10f9a 100644 --- a/app/src/main/java/com/cherrish/android/presentation/splash/SplashScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/splash/SplashScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -19,8 +20,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants @@ -35,23 +34,21 @@ import kotlinx.coroutines.launch @Composable fun SplashRoute( navigateToOnboarding: () -> Unit, - navigateToHome: () -> Unit, paddingValues: PaddingValues, viewModel: SplashViewModel = hiltViewModel() ) { val scope = rememberCoroutineScope() - LifecycleEventEffect(Lifecycle.Event.ON_START) { + LaunchedEffect(Unit) { scope.launch { delay(3000) - viewModel.isAutoLoginCheck() + viewModel.navigateToOnboarding() } } viewModel.sideEffect.collectLatestSideEffect { sideEffect -> when (sideEffect) { SplashSideEffect.NavigateToOnboarding -> navigateToOnboarding() - SplashSideEffect.NavigateToHome -> navigateToHome() } } diff --git a/app/src/main/java/com/cherrish/android/presentation/splash/SplashSideEffect.kt b/app/src/main/java/com/cherrish/android/presentation/splash/SplashSideEffect.kt index f1e660a08..95368757a 100644 --- a/app/src/main/java/com/cherrish/android/presentation/splash/SplashSideEffect.kt +++ b/app/src/main/java/com/cherrish/android/presentation/splash/SplashSideEffect.kt @@ -2,5 +2,4 @@ package com.cherrish.android.presentation.splash sealed interface SplashSideEffect { data object NavigateToOnboarding : SplashSideEffect - data object NavigateToHome : SplashSideEffect } diff --git a/app/src/main/java/com/cherrish/android/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/cherrish/android/presentation/splash/SplashViewModel.kt index a62445dcd..4147365e8 100644 --- a/app/src/main/java/com/cherrish/android/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/com/cherrish/android/presentation/splash/SplashViewModel.kt @@ -2,7 +2,6 @@ package com.cherrish.android.presentation.splash import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.cherrish.android.core.local.TokenManager import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -11,22 +10,14 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch @HiltViewModel -class SplashViewModel @Inject constructor( - private val tokenManager: TokenManager -) : ViewModel() { +class SplashViewModel @Inject constructor() : ViewModel() { private val _sideEffect = MutableSharedFlow() val sideEffect: SharedFlow = _sideEffect.asSharedFlow() - fun isAutoLoginCheck() { + fun navigateToOnboarding() { viewModelScope.launch { - val id = tokenManager.getId() _sideEffect.emit( SplashSideEffect.NavigateToOnboarding -// if (id != null) { -// SplashSideEffect.NavigateToHome -// } else { -// SplashSideEffect.NavigateToOnboarding -// } ) } } diff --git a/app/src/main/java/com/cherrish/android/presentation/splash/navigation/SplashNavigation.kt b/app/src/main/java/com/cherrish/android/presentation/splash/navigation/SplashNavigation.kt index 848e6d5ee..20e767005 100644 --- a/app/src/main/java/com/cherrish/android/presentation/splash/navigation/SplashNavigation.kt +++ b/app/src/main/java/com/cherrish/android/presentation/splash/navigation/SplashNavigation.kt @@ -18,13 +18,11 @@ fun NavController.navigateToSplash(navOptions: NavOptions? = null) { fun NavGraphBuilder.splashNavGraph( navigateToOnboarding: () -> Unit, - navigateToHome: () -> Unit, paddingValues: PaddingValues ) { composable { SplashRoute( navigateToOnboarding = navigateToOnboarding, - navigateToHome = navigateToHome, paddingValues = paddingValues ) }