diff --git a/core/designsystem/src/main/java/com/moya/funch/ui/FunchCaption.kt b/core/designsystem/src/main/java/com/moya/funch/ui/FunchCaption.kt index fb63181..6137e3b 100644 --- a/core/designsystem/src/main/java/com/moya/funch/ui/FunchCaption.kt +++ b/core/designsystem/src/main/java/com/moya/funch/ui/FunchCaption.kt @@ -16,17 +16,15 @@ import com.moya.funch.theme.Coral500 import com.moya.funch.theme.FunchTheme @Composable -fun FunchErrorCaption(modifier: Modifier = Modifier, errorText: String, description: String = "") { +fun FunchErrorCaption(modifier: Modifier = Modifier, errorText: String, errorIcon: @Composable (() -> Unit)? = null) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { - Icon( - painter = painterResource(id = FunchIconAsset.Etc.information_24), - contentDescription = description, - tint = Coral500 - ) - Spacer(modifier = Modifier.width(4.dp)) + if (errorIcon != null) { + errorIcon() + Spacer(modifier = Modifier.width(4.dp)) + } Text( text = errorText, color = Coral500, @@ -37,9 +35,26 @@ fun FunchErrorCaption(modifier: Modifier = Modifier, errorText: String, descript // ============================== Preview ================================= -@Preview("Error Caption", showBackground = true, backgroundColor = 0xFF2C2C2C) +@Preview("Error Caption With Icon", showBackground = true, backgroundColor = 0xFF2C2C2C) @Composable private fun Preview1() { + FunchTheme { + FunchErrorCaption( + errorIcon = { + Icon( + painter = painterResource(id = FunchIconAsset.Etc.information_24), + tint = Coral500, + contentDescription = null + ) + }, + errorText = "errorText" + ) + } +} + +@Preview("Only Text Error Caption", showBackground = true, backgroundColor = 0xFF2C2C2C) +@Composable +private fun Preview2() { FunchTheme { FunchErrorCaption( errorText = "errorText" diff --git a/core/designsystem/src/main/java/com/moya/funch/ui/FunchDropDown.kt b/core/designsystem/src/main/java/com/moya/funch/ui/FunchDropDown.kt index abf0633..9263f9e 100644 --- a/core/designsystem/src/main/java/com/moya/funch/ui/FunchDropDown.kt +++ b/core/designsystem/src/main/java/com/moya/funch/ui/FunchDropDown.kt @@ -13,12 +13,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.Surface @@ -32,15 +34,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Popup import com.moya.funch.icon.FunchIconAsset import com.moya.funch.modifier.ScrollBarConfig import com.moya.funch.modifier.verticalScrollWithScrollbar @@ -50,7 +50,6 @@ import com.moya.funch.theme.Gray500 import com.moya.funch.theme.Gray800 import com.moya.funch.theme.LocalBackgroundTheme import com.moya.funch.theme.White -import kotlin.math.roundToInt @Composable fun FunchDropDownButton( @@ -116,59 +115,46 @@ fun FunchDropDownButton( @Composable fun FunchDropDownMenu( - modifier: Modifier = Modifier, items: List, - buttonBounds: Rect, - onItemSelected: (String) -> Unit, - scrollState: ScrollState = rememberScrollState() + modifier: Modifier = Modifier, + scrollState: ScrollState = rememberScrollState(), + onItemSelected: (String) -> Unit ) { - Popup( - alignment = Alignment.TopStart, - offset = IntOffset( - x = 0, - y = with(LocalDensity.current) { - (buttonBounds.height).toInt() + 8.dp.toPx().roundToInt() - } - ) - ) { - Column( - modifier = modifier - .width(with(LocalDensity.current) { buttonBounds.width.toDp() }) - .height(144.dp) - .background( - color = Gray800, - shape = FunchTheme.shapes.medium - ) - .clip(FunchTheme.shapes.medium) - .verticalScrollWithScrollbar( - state = scrollState, - scrollbarConfig = ScrollBarConfig( - indicatorHeight = 39.dp, - indicatorThickness = 4.dp, - indicatorColor = Gray300, - padding = PaddingValues( - top = 16.dp, - bottom = 16.dp, - end = 4.dp - ) + Column( + modifier = modifier + .background( + color = Gray800, + shape = FunchTheme.shapes.medium + ) + .clip(FunchTheme.shapes.medium) + .verticalScrollWithScrollbar( + state = scrollState, + scrollbarConfig = ScrollBarConfig( + indicatorHeight = 39.dp, + indicatorThickness = 4.dp, + indicatorColor = Gray300, + padding = PaddingValues( + top = 16.dp, + bottom = 16.dp, + end = 4.dp ) ) - ) { - items.forEachIndexed { index, option -> - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - FunchDropDownItem( - option = option, - onItemSelected = { onItemSelected(option) }, - isPressed = isPressed, - interactionSource = interactionSource + ) + ) { + items.forEachIndexed { index, option -> + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + FunchDropDownItem( + option = option, + onItemSelected = { onItemSelected(option) }, + isPressed = isPressed, + interactionSource = interactionSource + ) + if (index < items.lastIndex) { + Divider( + color = Gray500, + thickness = 0.5f.dp ) - if (index < items.lastIndex) { - Divider( - color = Gray500, - thickness = 0.5f.dp - ) - } } } } @@ -219,35 +205,64 @@ private fun Preview1() { modifier = Modifier.fillMaxSize(), color = backgroundColor ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - val bloodTypes = listOf("A형", "B형", "O형", "AB형") - var placeHolder by remember { mutableStateOf(bloodTypes[0]) } - var isDropDownMenuExpanded by remember { mutableStateOf(true) } - val buttonBounds = remember { mutableStateOf(Rect.Zero) } + val bloodTypes = listOf("A형", "B형", "O형", "AB형") + var placeHolder by remember { mutableStateOf(bloodTypes[0]) } + var isDropDownMenuExpanded by remember { mutableStateOf(true) } + var buttonBounds by remember { mutableStateOf(Rect.Zero) } + val dropDownMenuHeight = 192.dp - Text( - text = "Hello, World!", - fontSize = 50.sp, - color = White - ) - Box { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = "Hello, World!", + fontSize = 50.sp, + color = White + ) + Text( + text = "Hello, World!", + fontSize = 50.sp, + color = White + ) + Text( + text = "Hello, World!", + fontSize = 50.sp, + color = White + ) FunchDropDownButton( placeHolder = placeHolder, onClick = { isDropDownMenuExpanded = !isDropDownMenuExpanded }, isDropDownMenuExpanded = isDropDownMenuExpanded, indication = null, modifier = Modifier.onGloballyPositioned { coordinates -> - buttonBounds.value = coordinates.boundsInWindow() + buttonBounds = coordinates.boundsInRoot() + println(buttonBounds.top) } ) - if (isDropDownMenuExpanded) { + for (i in 0 until 10) { + Text( + text = "Hello, World!", + fontSize = 50.sp, + color = White + ) + } + } + if (isDropDownMenuExpanded) { + Box( + modifier = Modifier + .absoluteOffset( + x = with(LocalDensity.current) { buttonBounds.left.toDp() }, + y = with(LocalDensity.current) { buttonBounds.top.toDp() - dropDownMenuHeight - 8.dp } + ) + .width(with(LocalDensity.current) { buttonBounds.width.toDp() }) + .height(dropDownMenuHeight) + ) { FunchDropDownMenu( items = bloodTypes, - buttonBounds = buttonBounds.value, onItemSelected = { text -> placeHolder = text isDropDownMenuExpanded = false @@ -255,11 +270,6 @@ private fun Preview1() { ) } } - Text( - text = "Hello, World!", - fontSize = 50.sp, - color = White - ) } } } diff --git a/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt b/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt index aff0f37..12a6b98 100644 --- a/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt +++ b/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt @@ -29,7 +29,7 @@ data class CreateProfileUiState( ) internal sealed class CreateProfileEvent { - data object NavigateToHome : CreateProfileEvent() + data class NavigateToHome(val message: String) : CreateProfileEvent() data class ShowError(val message: String) : CreateProfileEvent() } @@ -165,7 +165,7 @@ internal class CreateProfileViewModel @Inject constructor( ) ) createUserProfileUseCase(profile).onSuccess { - _event.emit(CreateProfileEvent.NavigateToHome) + _event.emit(CreateProfileEvent.NavigateToHome("프로필을 생성했어요")) }.onFailure { _uiState.update { currentState -> currentState.copy(isLoading = false) } _event.emit(CreateProfileEvent.ShowError(it.message ?: "Error")) diff --git a/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt b/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt index 67406cf..bf2838e 100644 --- a/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt +++ b/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt @@ -1,5 +1,6 @@ package com.moya.funch +import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -18,8 +19,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -30,6 +33,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -41,8 +45,10 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -85,15 +91,18 @@ import com.moya.funch.uimodel.MbtiItem import com.moya.funch.uimodel.ProfileLabel import com.moya.funch.uimodel.ProfileUiModel import com.moya.funch.uimodel.SubwayTextFieldState +import kotlinx.coroutines.delay @Composable internal fun CreateProfileRoute(onNavigateToHome: () -> Unit, viewModel: CreateProfileViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current LaunchedEffect(Unit) { viewModel.event.collect { event -> when (event) { is CreateProfileEvent.NavigateToHome -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() onNavigateToHome() } @@ -147,14 +156,31 @@ fun CreateProfileScreen( onSendFeedback: () -> Unit ) { val scrollState = rememberScrollState() + val focusManager = LocalFocusManager.current val backgroundColor = LocalBackgroundTheme.current.color var isKeyboardVisible by remember { mutableStateOf(false) } - val focusManager = LocalFocusManager.current + var topBarHeight by remember { mutableFloatStateOf(0f) } + val bloodDropDownMenuHeight = 192.dp + val bloodTypes = Blood.entries.filterNot { it == Blood.IDLE }.map { it.type } + var bloodTypePlaceHolder by remember { mutableStateOf(bloodTypes[0]) } + var bloodButtonRect by remember { mutableStateOf(Rect.Zero) } + var isBloodDropDownMenuExpanded by remember { mutableStateOf(false) } Scaffold( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + isBloodDropDownMenuExpanded = false + }) + }, topBar = { FunchTopBar( - modifier = Modifier.padding(end = 20.dp), + modifier = Modifier + .padding(end = 20.dp) + .onGloballyPositioned { layoutCoordinates -> + topBarHeight = layoutCoordinates.boundsInRoot().height + }, leadingIcon = null, onClickTrailingIcon = onSendFeedback ) @@ -172,52 +198,76 @@ fun CreateProfileScreen( ) { padding -> Column( modifier = Modifier - .pointerInput(Unit) { - detectTapGestures(onTap = { - focusManager.clearFocus() - }) - } .fillMaxSize() .verticalScroll(state = scrollState) .padding(padding) ) { - Column(modifier = Modifier.padding(horizontal = 20.dp)) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.create_profile_title), - color = White, - style = FunchTheme.typography.t2 - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = stringResource(id = R.string.create_profile_sub_title), - color = Gray300, - style = FunchTheme.typography.b - ) - Spacer(modifier = Modifier.height(24.dp)) - NicknameRow( - nickname = profile.name, - onNicknameChange = onNicknameChange, - isKeyboardVisible = { isKeyboardVisible = it } - ) - Spacer(modifier = Modifier.height(14.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - JobRow(profile = profile, onSelected = onSelectJob) - ClubRow(onSelectClub = onSelectClub) - MbtiRow(profile = profile, onSelectMbti = onSelectMbti) - BooldTypeRow(onSelectBloodType = onSelectBloodType) - SubwayRow( - subwayStation = profile.subway, - onSubwayStationChange = onSubwayStationChange, - isKeyboardVisible = { isKeyboardVisible = it }, - textFieldState = profile.subwayTextFieldState, - subwayStations = profile.subwayStations, - scrollState = scrollState + Box { + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.create_profile_title), + color = White, + style = FunchTheme.typography.t2 + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = stringResource(id = R.string.create_profile_sub_title), + color = Gray300, + style = FunchTheme.typography.b + ) + Spacer(modifier = Modifier.height(24.dp)) + NicknameRow( + nickname = profile.name, + onNicknameChange = onNicknameChange, + isKeyboardVisible = { isKeyboardVisible = it } ) + Spacer(modifier = Modifier.height(14.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + JobRow(profile = profile, onSelected = onSelectJob) + ClubRow(onSelectClub = onSelectClub) + MbtiRow(profile = profile, onSelectMbti = onSelectMbti) + BooldTypeRow( + isExpended = isBloodDropDownMenuExpanded, + placeHolder = bloodTypePlaceHolder, + onClickBloodButton = { isClicked -> isBloodDropDownMenuExpanded = isClicked }, + buttonOffset = { rect -> bloodButtonRect = rect } + ) + SubwayRow( + subwayStation = profile.subway, + onSubwayStationChange = onSubwayStationChange, + isKeyboardVisible = { isKeyboardVisible = it }, + textFieldState = profile.subwayTextFieldState, + subwayStations = profile.subwayStations, + scrollState = scrollState + ) + } + Spacer(modifier = Modifier.height(20.dp)) + } + if (isBloodDropDownMenuExpanded) { + Box( + modifier = Modifier + .offset( + x = with(LocalDensity.current) { bloodButtonRect.left.toDp() }, + y = with(LocalDensity.current) { + bloodButtonRect.top.toDp() - topBarHeight.toDp() - bloodDropDownMenuHeight - 8.dp + } + ) + .width(with(LocalDensity.current) { bloodButtonRect.width.toDp() }) + .height(bloodDropDownMenuHeight) + ) { + FunchDropDownMenu( + items = bloodTypes, + onItemSelected = { bloodType -> + onSelectBloodType(Blood.of(bloodType)) + bloodTypePlaceHolder = bloodType + isBloodDropDownMenuExpanded = false + } + ) + } } - Spacer(modifier = Modifier.height(20.dp)) } if (isKeyboardVisible) { BottomBar( @@ -450,39 +500,32 @@ private fun MbtiButton(mbtiItem: MbtiItem, isSelected: Boolean, onSelected: (Mbt } @Composable -private fun BooldTypeRow(onSelectBloodType: (Blood) -> Unit, focusManager: FocusManager = LocalFocusManager.current) { - val bloodTypes = Blood.entries.filterNot { it == Blood.IDLE }.map { it.type } - var placeHolder by remember { mutableStateOf(bloodTypes[0]) } - var isDropDownMenuExpanded by remember { mutableStateOf(false) } - val buttonBounds = remember { mutableStateOf(Rect.Zero) } +private fun BooldTypeRow( + isExpended: Boolean, + placeHolder: String, + buttonOffset: (Rect) -> Unit, + onClickBloodButton: (Boolean) -> Unit +) { + val focusManager = LocalFocusManager.current + val isPositioned = remember { mutableStateOf(false) } Row { FunchLargeLabel(text = ProfileLabel.BLOOD_TYPE.labelName) - Box { - FunchDropDownButton( - placeHolder = placeHolder, - onClick = { - focusManager.clearFocus() - isDropDownMenuExpanded = !isDropDownMenuExpanded - }, - isDropDownMenuExpanded = isDropDownMenuExpanded, - indication = null, - modifier = Modifier.onGloballyPositioned { coordinates -> - buttonBounds.value = coordinates.boundsInWindow() + FunchDropDownButton( + placeHolder = placeHolder, + onClick = { + focusManager.clearFocus() + onClickBloodButton(!isExpended) + }, + isDropDownMenuExpanded = isExpended, + indication = null, + modifier = Modifier.onGloballyPositioned { layoutCoordinates -> + if (!isPositioned.value) { + isPositioned.value = true + buttonOffset(layoutCoordinates.boundsInRoot()) } - ) - if (isDropDownMenuExpanded) { - FunchDropDownMenu( - items = bloodTypes, - buttonBounds = buttonBounds.value, - onItemSelected = { bloodType -> - onSelectBloodType(Blood.of(bloodType)) - placeHolder = bloodType - isDropDownMenuExpanded = false - } - ) } - } + ) } } @@ -501,13 +544,14 @@ private fun SubwayRow( if (isFocused) { LaunchedEffect(subwayStation) { + delay(100) scrollState.animateScrollTo(scrollState.maxValue) } } Row { FunchLargeLabel(text = ProfileLabel.SUBWAY.labelName) - Column(modifier = Modifier.height(97.dp)) { + Column(modifier = Modifier.height(101.dp)) { FunchIconTextField( modifier = Modifier.onFocusChanged { focusState -> isKeyboardVisible(focusState.isFocused) @@ -579,12 +623,11 @@ private fun HorizontalSubwayStations( ) { val focusManager = LocalFocusManager.current - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(4.dp) + .horizontalScroll(rememberScrollState()) ) { subwayStations.forEach { station -> val annotatedText = buildAnnotatedString {