diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt index ceff77c..874e898 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt @@ -1,6 +1,5 @@ package com.yapp.ui.component.timepicker -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,19 +13,31 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme +import kotlinx.coroutines.launch import java.util.Locale @Composable fun OrbitPicker( modifier: Modifier = Modifier, itemSpacing: Dp = 2.dp, + initialAmPm: String = "오전", + initialHour: String = "1", + initialMinute: String = "00", + selectedAmPm: String, + selectedHour: Int, + selectedMinute: Int, onValueChange: (String, Int, Int) -> Unit, ) { Surface( @@ -45,9 +56,20 @@ fun OrbitPicker( val hourItems = remember { (1..12).map { it.toString() } } val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } } - val amPmPickerState = rememberPickerState() - val hourPickerState = rememberPickerState() - val minutePickerState = rememberPickerState() + val amPmPickerState = rememberPickerState( + selectedItem = selectedAmPm, + startIndex = amPmItems.indexOf(initialAmPm), + ) + val hourPickerState = rememberPickerState( + selectedItem = selectedHour.toString(), + startIndex = hourItems.indexOf(initialHour), + ) + val minutePickerState = rememberPickerState( + selectedItem = selectedMinute.toString(), + startIndex = minuteItems.indexOf(initialMinute), + ) + + val scope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxWidth()) { Box( @@ -101,6 +123,13 @@ fun OrbitPicker( onValueChange, ) }, + onScrollCompleted = { + scope.launch { + val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size + val nextIndex = (currentIndex + 1) % amPmItems.size + amPmPickerState.lazyListState.animateScrollToItem(nextIndex) + } + }, ) OrbitPickerItem( @@ -142,7 +171,17 @@ private fun onPickerValueChange( @Preview(showBackground = true) @Composable fun BottomSheetPickerPreview() { - OrbitPicker { amPm, hour, minute -> - Log.d("OrbitPicker", "amPm: $amPm, hour: $hour, minute: $minute") + var selectedAmPm by remember { mutableStateOf("오전") } + var selectedHour by remember { mutableIntStateOf(6) } + var selectedMinute by remember { mutableIntStateOf(0) } + + OrbitPicker( + selectedAmPm = selectedAmPm, + selectedHour = selectedHour, + selectedMinute = selectedMinute, + ) { amPm, hour, minute -> + selectedAmPm = amPm + selectedHour = hour + selectedMinute = minute } } diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt index 8b4695a..fce02a9 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,34 +34,43 @@ fun OrbitPickerItem( modifier: Modifier = Modifier, items: List, state: PickerState = rememberPickerState(), - startIndex: Int = 0, visibleItemsCount: Int, textModifier: Modifier = Modifier, infiniteScroll: Boolean = true, textStyle: TextStyle, itemSpacing: Dp, onValueChange: (String) -> Unit, + onScrollCompleted: () -> Unit = {}, ) { val visibleItemsMiddle = visibleItemsCount / 2 val listScrollCount = if (infiniteScroll) Int.MAX_VALUE else items.size + visibleItemsMiddle * 2 val listScrollMiddle = listScrollCount / 2 - val listStartIndex = calculateStartIndex( - infiniteScroll, - items.size, - listScrollMiddle, - visibleItemsMiddle, - startIndex, - ) - val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex) + + val listState = state.lazyListState val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) val itemHeightPixels = remember { mutableIntStateOf(0) } val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() } + LaunchedEffect(key1 = state.initialized) { + if (!state.initialized) { + val listStartIndex = calculateStartIndex( + infiniteScroll, + items.size, + listScrollMiddle, + visibleItemsMiddle, + state.startIndex, + ) + listState.scrollToItem(listStartIndex) + state.initialized = true + } + } + LaunchedEffect(listState) { + var previousAdjustedIndex = -1 + snapshotFlow { listState.layoutInfo } .map { layoutInfo -> val centerOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 - layoutInfo.visibleItemsInfo.minByOrNull { item -> val itemCenter = item.offset + (item.size / 2) abs(itemCenter - centerOffset) @@ -73,10 +81,21 @@ fun OrbitPickerItem( if (centerIndex != null) { val adjustedIndex = centerIndex % items.size val newValue = items[adjustedIndex] + + if (infiniteScroll) { + val lastIndex = items.size - 1 + if ((previousAdjustedIndex == 0 && adjustedIndex == lastIndex) || + (previousAdjustedIndex == lastIndex && adjustedIndex == 0) + ) { + onScrollCompleted() + } + } + if (newValue != state.selectedItem) { state.selectedItem = newValue onValueChange(newValue) } + previousAdjustedIndex = adjustedIndex } } } @@ -103,15 +122,20 @@ fun OrbitPickerItem( val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset) val maxDistance = totalItemHeight.toPx() * visibleItemsMiddle - val alpha = ((maxDistance - distanceFromCenter) / maxDistance).coerceIn(0.2f, 1f) + + val alpha = if (distanceFromCenter <= maxDistance) { + ((maxDistance - distanceFromCenter) / maxDistance).coerceIn(0.2f, 1f) + } else { + 0.2f + } + val scaleY = 1f - (0.4f * (distanceFromCenter / maxDistance)).coerceIn(0f, 0.4f) - val isSelected = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle) == state.selectedItem Text( text = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle), maxLines = 1, style = textStyle, - color = if (isSelected) OrbitTheme.colors.white else OrbitTheme.colors.white.copy(alpha = alpha), + color = OrbitTheme.colors.white.copy(alpha = alpha), modifier = Modifier .padding(vertical = itemSpacing / 2) .graphicsLayer(scaleY = scaleY) diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt index 3ced241..e1778f0 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt @@ -1,6 +1,5 @@ package com.yapp.ui.component.timepicker -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,7 +14,11 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -29,6 +32,14 @@ import java.util.Locale fun OrbitYearMonthPicker( modifier: Modifier = Modifier, itemSpacing: Dp = 12.dp, + initialLunar: String = "음력", + initialYear: String = "1900", + initialMonth: String = "1", + initialDay: String = "01", + selectedLunar: String, + selectedYear: Int, + selectedMonth: Int, + selectedDay: Int, onValueChange: (String, Int, Int, Int) -> Unit, ) { val screenWidth = LocalConfiguration.current.screenWidthDp.dp @@ -50,10 +61,22 @@ fun OrbitYearMonthPicker( val monthItems = remember { (1..12).map { it.toString() } } val dayItems = remember { (1..31).map { String.format(Locale.ROOT, "%02d", it) } } - val lunarPickerState = rememberPickerState() - val yearPickerState = rememberPickerState() - val monthPickerState = rememberPickerState() - val dayPickerState = rememberPickerState() + val lunarPickerState = rememberPickerState( + selectedItem = selectedLunar, + startIndex = lunarItems.indexOf(initialLunar), + ) + val yearPickerState = rememberPickerState( + selectedItem = selectedYear.toString(), + startIndex = yearItems.indexOf(initialYear), + ) + val monthPickerState = rememberPickerState( + selectedItem = selectedMonth.toString(), + startIndex = monthItems.indexOf(initialMonth), + ) + val dayPickerState = rememberPickerState( + selectedItem = selectedDay.toString(), + startIndex = dayItems.indexOf(initialDay), + ) Box( modifier = Modifier.fillMaxWidth(), @@ -89,7 +112,6 @@ fun OrbitYearMonthPicker( state = yearPickerState, items = yearItems, visibleItemsCount = 5, - startIndex = 90, itemSpacing = itemSpacing, textStyle = OrbitTheme.typography.title2SemiBold, modifier = Modifier.width(screenWidth * 0.25f), @@ -143,7 +165,20 @@ private fun onPickerValueChange( @Preview(showBackground = true) @Composable fun OrbitYearMonthPickerPreview() { - OrbitYearMonthPicker { lunar, year, month, day -> - Log.d("OrbitYearMonthPicker", "lunar: $lunar, year: $year, month: $month, day: $day") + var selectedLunar by remember { mutableStateOf("음력") } + var selectedYear by remember { mutableIntStateOf(1900) } + var selectedMonth by remember { mutableIntStateOf(1) } + var selectedDay by remember { mutableIntStateOf(1) } + + OrbitYearMonthPicker( + selectedLunar = selectedLunar, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + selectedDay = selectedDay, + ) { lunar, year, month, day -> + selectedLunar = lunar + selectedYear = year + selectedMonth = month + selectedDay = day } } diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt index a2e42a5..bd8258d 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt @@ -1,16 +1,20 @@ package com.yapp.ui.component.timepicker +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -@Stable -class PickerState { - var selectedItem: String by mutableStateOf("") -} +class PickerState( + val lazyListState: LazyListState, + var selectedItem: String, + var startIndex: Int, + var initialized: Boolean = false, +) @Composable -fun rememberPickerState() = remember { PickerState() } +fun rememberPickerState( + lazyListState: LazyListState = rememberLazyListState(), + selectedItem: String = "", + startIndex: Int = 0, +): PickerState = remember { PickerState(lazyListState, selectedItem, startIndex) } diff --git a/feature/home/src/main/java/com/yapp/alarm/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/alarm/AlarmAddEditScreen.kt index 62c5055..86db95f 100644 --- a/feature/home/src/main/java/com/yapp/alarm/AlarmAddEditScreen.kt +++ b/feature/home/src/main/java/com/yapp/alarm/AlarmAddEditScreen.kt @@ -1,6 +1,5 @@ package com.yapp.alarm -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,6 +11,11 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -27,6 +31,10 @@ fun AlarmAddEditRoute() { @Composable fun AlarmAddEditScreen() { + var selectedAmPm by remember { mutableStateOf("오전") } + var selectedHour by remember { mutableIntStateOf(1) } + var selectedMinute by remember { mutableIntStateOf(0) } + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -37,8 +45,13 @@ fun AlarmAddEditScreen() { ) OrbitPicker( modifier = Modifier.padding(top = 40.dp), + selectedAmPm = selectedAmPm, + selectedHour = selectedHour, + selectedMinute = selectedMinute, ) { amPm, hour, minute -> - Log.d("AlarmAddEditScreen", "amPm: $amPm, hour: $hour, minute: $minute") + selectedAmPm = amPm + selectedHour = hour + selectedMinute = minute } } } diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt index 473fa66..d5cd45e 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/BirthdayScreen.kt @@ -1,6 +1,5 @@ package com.kms.onboarding -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -8,6 +7,11 @@ 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.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -25,6 +29,11 @@ fun BirthdayScreen( onNextClick: () -> Unit, onBackClick: () -> Unit, ) { + var selectedLunar by remember { mutableStateOf("음력") } + var selectedYear by remember { mutableIntStateOf(1900) } + var selectedMonth by remember { mutableIntStateOf(1) } + var selectedDay by remember { mutableIntStateOf(1) } + OnboardingScreen( currentStep = currentStep, totalSteps = totalSteps, @@ -43,8 +52,15 @@ fun BirthdayScreen( ) OrbitYearMonthPicker( modifier = Modifier.padding(top = 60.dp), + selectedLunar = selectedLunar, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + selectedDay = selectedDay, ) { lunar, year, month, day -> - Log.d("BirthdayScreen", "lunar: $lunar, year: $year, month: $month, day: $day") + selectedLunar = lunar + selectedYear = year + selectedMonth = month + selectedDay = day } } } diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingAlarmTimeSelectionScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingAlarmTimeSelectionScreen.kt index 58a138d..ba1b96a 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingAlarmTimeSelectionScreen.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingAlarmTimeSelectionScreen.kt @@ -1,6 +1,5 @@ package com.kms.onboarding -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -8,6 +7,11 @@ 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.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -25,6 +29,10 @@ fun OnboardingAlarmTimeSelectionScreen( onNextClick: () -> Unit, onBackClick: () -> Unit, ) { + var selectedAmPm by remember { mutableStateOf("오전") } + var selectedHour by remember { mutableIntStateOf(1) } + var selectedMinute by remember { mutableIntStateOf(0) } + OnboardingScreen( currentStep = currentStep, totalSteps = totalSteps, @@ -52,8 +60,13 @@ fun OnboardingAlarmTimeSelectionScreen( ) OrbitPicker( modifier = Modifier.padding(top = 90.dp), + selectedAmPm = selectedAmPm, + selectedHour = selectedHour, + selectedMinute = selectedMinute, ) { amPm, hour, minute -> - Log.d("OnboardingAlarmTimeSelectionScreen", "amPm: $amPm, hour: $hour, minute: $minute") + selectedAmPm = amPm + selectedHour = hour + selectedMinute = minute } } } diff --git a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingBirthdayScreen.kt b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingBirthdayScreen.kt index b2541b6..4e7bc29 100644 --- a/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingBirthdayScreen.kt +++ b/feature/onboarding/src/main/java/com/kms/onboarding/OnboardingBirthdayScreen.kt @@ -1,6 +1,5 @@ package com.kms.onboarding -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -8,6 +7,11 @@ 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.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -26,6 +30,11 @@ fun OnboardingBirthdayScreen( onNextClick: () -> Unit, onBackClick: () -> Unit, ) { + var selectedLunar by remember { mutableStateOf("음력") } + var selectedYear by remember { mutableIntStateOf(1900) } + var selectedMonth by remember { mutableIntStateOf(1) } + var selectedDay by remember { mutableIntStateOf(1) } + OnboardingScreen( currentStep = currentStep, totalSteps = totalSteps, @@ -44,8 +53,15 @@ fun OnboardingBirthdayScreen( ) OrbitYearMonthPicker( modifier = Modifier.padding(top = 60.dp), + selectedLunar = selectedLunar, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + selectedDay = selectedDay, ) { lunar, year, month, day -> - Log.d("BirthdayScreen", "lunar: $lunar, year: $year, month: $month, day: $day") + selectedLunar = lunar + selectedYear = year + selectedMonth = month + selectedDay = day } } }