Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BugFix] OrbitPickerItem 초기화 및 재구성 시 상태 동기화 문제 #36

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,34 +34,43 @@ fun OrbitPickerItem(
modifier: Modifier = Modifier,
items: List<String>,
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)
Expand All @@ -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
}
}
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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) }
17 changes: 15 additions & 2 deletions feature/home/src/main/java/com/yapp/alarm/AlarmAddEditScreen.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
}
}
}
Expand Down
Loading
Loading