Skip to content

Commit

Permalink
Merge pull request #37 from YAPP-Github/bugfix/#35-orbit-picker-item
Browse files Browse the repository at this point in the history
[BugFix] OrbitPickerItem ์ดˆ๊ธฐํ™” ๋ฐ ์žฌ๊ตฌ์„ฑ ์‹œ ์ƒํƒœ ๋™๊ธฐํ™” ๋ฌธ์ œ
  • Loading branch information
DongChyeon authored Jan 14, 2025
2 parents 62db29a + 36eaca0 commit e0d4d44
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
Expand All @@ -30,12 +29,9 @@ import java.util.Locale
fun OrbitPicker(
modifier: Modifier = Modifier,
itemSpacing: Dp = 2.dp,
selectedAmPm: String = "์˜คํ›„",
selectedHour: Int = 0,
selectedMinute: Int = 0,
amPmStartIndex: Int = 0,
hourStartIndex: Int = 5,
minuteStartIndex: Int = 0,
initialAmPm: String = "์˜ค์ „",
initialHour: String = "1",
initialMinute: String = "00",
onValueChange: (String, Int, Int) -> Unit,
) {
Surface(
Expand All @@ -54,9 +50,18 @@ fun OrbitPicker(
val hourItems = remember { (1..12).map { it.toString() } }
val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } }

val amPmListState = rememberLazyListState()
val hourListState = rememberLazyListState()
val minuteListState = rememberLazyListState()
val amPmPickerState = rememberPickerState(
selectedItem = amPmItems.indexOf(initialAmPm).toString(),
startIndex = amPmItems.indexOf(initialAmPm),
)
val hourPickerState = rememberPickerState(
selectedItem = hourItems.indexOf(initialHour).toString(),
startIndex = hourItems.indexOf(initialHour),
)
val minutePickerState = rememberPickerState(
selectedItem = minuteItems.indexOf(initialMinute).toString(),
startIndex = minuteItems.indexOf(initialMinute),
)

val scope = rememberCoroutineScope()

Expand All @@ -77,57 +82,66 @@ fun OrbitPicker(
verticalAlignment = Alignment.CenterVertically,
) {
OrbitPickerItem(
state = amPmPickerState,
items = amPmItems,
listState = amPmListState,
visibleItemsCount = 3,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = amPmStartIndex,
infiniteScroll = false,
selectedItem = selectedAmPm,
onSelectedItemChange = { amPm ->
onValueChange(amPm, selectedHour, selectedMinute)
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
},
)

OrbitPickerItem(
state = hourPickerState,
items = hourItems,
listState = hourListState,
visibleItemsCount = 5,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = hourStartIndex,
infiniteScroll = true,
selectedItem = selectedHour.toString(),
onSelectedItemChange = { hour ->
onValueChange(selectedAmPm, hour.toInt(), selectedMinute)
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
},
onScrollCompleted = {
scope.launch {
val currentIndex = amPmListState.firstVisibleItemIndex % amPmItems.size
val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size
val nextIndex = (currentIndex + 1) % amPmItems.size
amPmListState.animateScrollToItem(nextIndex)
amPmPickerState.lazyListState.animateScrollToItem(nextIndex)
}
},
)

OrbitPickerItem(
state = minutePickerState,
items = minuteItems,
listState = minuteListState,
visibleItemsCount = 5,
itemSpacing = itemSpacing,
textStyle = OrbitTheme.typography.title2Medium,
modifier = Modifier.weight(1f),
textModifier = Modifier.padding(8.dp),
startIndex = minuteStartIndex,
infiniteScroll = true,
selectedItem = selectedMinute.toString(),
onSelectedItemChange = { minute ->
onValueChange(selectedAmPm, selectedHour, minute.toInt())
onValueChange = {
onPickerValueChange(
amPmPickerState,
hourPickerState,
minutePickerState,
onValueChange,
)
},
)
}
Expand All @@ -136,10 +150,22 @@ fun OrbitPicker(
}
}

private fun onPickerValueChange(
amPmState: PickerState,
hourState: PickerState,
minuteState: PickerState,
onValueChange: (String, Int, Int) -> Unit,
) {
val amPm = amPmState.selectedItem
val hour = hourState.selectedItem.toIntOrNull() ?: 0
val minute = minuteState.selectedItem.toIntOrNull() ?: 0
onValueChange(amPm, hour, minute)
}

@Preview(showBackground = true)
@Composable
fun BottomSheetPickerPreview() {
fun OrbitPickerPreview() {
OrbitPicker { amPm, hour, minute ->
Log.d("OrbitPicker", "amPm: $amPm, hour: $hour, minute: $minute")
Log.d("OrbitPicker", "selectedAmPm: $amPm, selectedHour: $hour, selectedMinute: $minute")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,11 @@ 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.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -31,63 +26,77 @@ import androidx.compose.ui.unit.dp
import com.yapp.designsystem.theme.OrbitTheme
import com.yapp.ui.utils.toPx
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlin.math.abs

@Composable
fun OrbitPickerItem(
modifier: Modifier = Modifier,
items: List<String>,
selectedItem: String,
onSelectedItemChange: (String) -> Unit,
onScrollCompleted: (() -> Unit)? = null,
listState: LazyListState = rememberLazyListState(),
startIndex: Int = 0,
state: PickerState = rememberPickerState(),
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 = state.lazyListState
val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState)
val itemHeightPixels = remember { mutableIntStateOf(0) }
val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() }

LaunchedEffect(listState) {
if (listState.firstVisibleItemIndex != listStartIndex) {
listState.scrollToItem(listStartIndex) // ์ดˆ๊ธฐ ์Šคํฌ๋กค ์œ„์น˜ ์„ค์ •
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 = calculateCenterIndex(listState) // ์ดˆ๊ธฐ๊ฐ’ ์„ค์ •

snapshotFlow { calculateCenterIndex(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)
}?.index
}
.distinctUntilChanged()
.collect { centerIndex ->
val adjustedIndex = centerIndex % items.size

// ๋์—์„œ ๋์œผ๋กœ ์ด๋™ ๊ฐ์ง€
if (previousAdjustedIndex == items.size - 1 && adjustedIndex == 0) {
onScrollCompleted?.invoke()
} else if (previousAdjustedIndex == 0 && adjustedIndex == items.size - 1) {
onScrollCompleted?.invoke()
}

if (adjustedIndex != previousAdjustedIndex) {
onSelectedItemChange(items[adjustedIndex])
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
}

previousAdjustedIndex = adjustedIndex
}
}

Expand All @@ -113,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) == 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 Expand Up @@ -150,19 +164,6 @@ private fun calculateStartIndex(
}
}

/**
* ์ค‘์‹ฌ ์ธ๋ฑ์Šค๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
*/
private fun calculateCenterIndex(listState: LazyListState): Int {
val layoutInfo = listState.layoutInfo
val centerOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2

return layoutInfo.visibleItemsInfo.minByOrNull { item ->
val itemCenter = item.offset + (item.size / 2)
abs(itemCenter - centerOffset)
}?.index ?: 0
}

/**
* ์ฃผ์–ด์ง„ ์ธ๋ฑ์Šค์— ํ•ด๋‹นํ•˜๋Š” ํ•ญ๋ชฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
* ๋ฌดํ•œ ์Šคํฌ๋กค๊ณผ ๋ณด์ด๋Š” ํ•ญ๋ชฉ์˜ ๊ฐœ์ˆ˜๋ฅผ ๊ณ ๋ คํ•ฉ๋‹ˆ๋‹ค.
Expand All @@ -177,17 +178,15 @@ private fun getItemForIndex(index: Int, items: List<String>, infiniteScroll: Boo

@Composable
@Preview
fun OrbitPickerPreview() {
fun OrbitPickerItemPreview() {
OrbitTheme {
var selectedItem by remember { mutableStateOf("0") }

OrbitPickerItem(
items = (0..100).map { it.toString() },
selectedItem = selectedItem,
onSelectedItemChange = { selectedItem = it },
state = rememberPickerState(),
visibleItemsCount = 5,
textStyle = TextStyle.Default,
itemSpacing = 8.dp,
onValueChange = {},
)
}
}
Loading

0 comments on commit e0d4d44

Please sign in to comment.