Skip to content

Commit a5e1e10

Browse files
authored
Merge pull request #2 from DongChyeon/refactor/#1-pickerstate-index-based
[Refactor/#1] PickerState to index-based generic state with reliable onValueChange
2 parents 4ccf851 + d72086e commit a5e1e10

File tree

3 files changed

+91
-65
lines changed

3 files changed

+91
-65
lines changed

timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import kotlinx.datetime.Clock
3535
import kotlinx.datetime.LocalTime
3636
import kotlinx.datetime.TimeZone
3737
import kotlinx.datetime.toLocalDateTime
38-
import java.util.Locale
3938

4039
@Composable
4140
fun TimePicker(
@@ -104,20 +103,20 @@ private fun TimePicker12Hour(
104103
TimePeriod.PM.getLabel(localeTimeFormat)
105104
)
106105
}
107-
val hourItems = remember { (1..12).map { it.toString() } }
108-
val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } }
106+
val hourItems = remember { (1..12).toList() }
107+
val minuteItems = remember { (0..59).toList() }
109108

110109
val amPmPickerState = rememberPickerState(
111-
selectedItem = amPmItems.indexOf(if (initialTime.hour < 12) amPmItems[0] else amPmItems[1]).toString(),
112-
startIndex = if (initialTime.hour < 12) 0 else 1
110+
initialIndex = if (initialTime.hour < 12) 0 else 1,
111+
items = amPmItems
113112
)
114113
val hourPickerState = rememberPickerState(
115-
selectedItem = hourItems.indexOf((if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12).toString()).toString(),
116-
startIndex = hourItems.indexOf((if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12).toString())
114+
initialIndex = hourItems.indexOf(if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12),
115+
items = hourItems
117116
)
118117
val minutePickerState = rememberPickerState(
119-
selectedItem = minuteItems.indexOf(initialTime.minute.toString().padStart(2, '0')).toString(),
120-
startIndex = minuteItems.indexOf(initialTime.minute.toString().padStart(2, '0'))
118+
initialIndex = minuteItems.indexOf(initialTime.minute),
119+
items = minuteItems
121120
)
122121

123122
var previousHour by remember { mutableIntStateOf(initialTime.hour) }
@@ -196,7 +195,7 @@ private fun TimePicker12Hour(
196195
)
197196

198197
scope.launch {
199-
val currentHour = hourPickerState.selectedItem.toIntOrNull() ?: 0
198+
val currentHour = hourPickerState.selectedItem
200199

201200
if (currentHour == 12 && previousHour == 11) {
202201
val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size
@@ -223,6 +222,9 @@ private fun TimePicker12Hour(
223222
modifier = Modifier.weight(1f),
224223
textModifier = Modifier.padding(8.dp),
225224
infiniteScroll = true,
225+
itemFormatter = { item ->
226+
item.toString().padStart(2, '0')
227+
},
226228
onValueChange = {
227229
onPickerValueChange(
228230
amPmPickerState,
@@ -252,16 +254,16 @@ private fun TimePicker24Hour(
252254
selector: PickerSelector,
253255
onValueChange: (LocalTime) -> Unit
254256
) {
255-
val hourItems = remember { (1..23).map { it.toString() } }
256-
val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } }
257+
val hourItems = remember { (1..23).toList() }
258+
val minuteItems = remember { (0..59).toList() }
257259

258260
val hourPickerState = rememberPickerState(
259-
selectedItem = hourItems.indexOf((if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12).toString()).toString(),
260-
startIndex = hourItems.indexOf((if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12).toString())
261+
initialIndex = hourItems.indexOf(initialTime.hour),
262+
items = hourItems
261263
)
262264
val minutePickerState = rememberPickerState(
263-
selectedItem = minuteItems.indexOf(initialTime.minute.toString().padStart(2, '0')).toString(),
264-
startIndex = minuteItems.indexOf(initialTime.minute.toString().padStart(2, '0'))
265+
initialIndex = minuteItems.indexOf(initialTime.minute),
266+
items = minuteItems
265267
)
266268

267269
Box(modifier = modifier.fillMaxWidth()) {
@@ -342,6 +344,9 @@ private fun TimePicker24Hour(
342344
modifier = Modifier.weight(1f),
343345
textModifier = Modifier.padding(8.dp),
344346
infiniteScroll = true,
347+
itemFormatter = { item ->
348+
item.toString().padStart(2, '0')
349+
},
345350
onValueChange = {
346351
onPickerValueChange(
347352
hourPickerState,
@@ -359,15 +364,15 @@ private fun TimePicker24Hour(
359364
}
360365

361366
private fun onPickerValueChange(
362-
amPmState: PickerState,
363-
hourState: PickerState,
364-
minuteState: PickerState,
367+
amPmState: PickerState<String>,
368+
hourState: PickerState<Int>,
369+
minuteState: PickerState<Int>,
365370
localeTimeFormat: LocaleTimeFormat,
366371
onValueChange: (LocalTime) -> Unit
367372
) {
368373
val amPm = amPmState.selectedItem
369-
val hour = hourState.selectedItem.toIntOrNull() ?: 0
370-
val minute = minuteState.selectedItem.toIntOrNull() ?: 0
374+
val hour = hourState.selectedItem
375+
val minute = minuteState.selectedItem
371376

372377
val adjustedHour = when (localeTimeFormat) {
373378
LocaleTimeFormat.ENGLISH -> {
@@ -396,12 +401,12 @@ private fun onPickerValueChange(
396401
}
397402

398403
private fun onPickerValueChange(
399-
hourState: PickerState,
400-
minuteState: PickerState,
404+
hourState: PickerState<Int>,
405+
minuteState: PickerState<Int>,
401406
onValueChange: (LocalTime) -> Unit
402407
) {
403-
val hour = hourState.selectedItem.toIntOrNull() ?: 0
404-
val minute = minuteState.selectedItem.toIntOrNull() ?: 0
408+
val hour = hourState.selectedItem
409+
val minute = minuteState.selectedItem
405410

406411
val newTime = LocalTime(hour, minute)
407412

timepicker/src/main/java/com/dongchyeon/timepicker/model/PickerState.kt

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,29 @@ import androidx.compose.foundation.lazy.LazyListState
44
import androidx.compose.foundation.lazy.rememberLazyListState
55
import androidx.compose.runtime.Composable
66
import androidx.compose.runtime.remember
7+
import kotlinx.coroutines.flow.MutableStateFlow
8+
import kotlinx.coroutines.flow.StateFlow
79

8-
class PickerState(
10+
class PickerState<T>(
911
val lazyListState: LazyListState,
10-
var selectedItem: String,
11-
var startIndex: Int
12-
)
12+
val initialIndex: Int,
13+
private val items: List<T>
14+
) {
15+
private val _selectedIndex = MutableStateFlow(initialIndex)
16+
val selectedIndex: StateFlow<Int>
17+
get() = _selectedIndex
18+
19+
val selectedItem: T
20+
get() = items.getOrElse(_selectedIndex.value) { items.first() }
21+
22+
fun updateSelectedIndex(newIndex: Int) {
23+
_selectedIndex.value = newIndex.coerceIn(0, items.size - 1)
24+
}
25+
}
1326

1427
@Composable
15-
fun rememberPickerState(
28+
fun <T> rememberPickerState(
1629
lazyListState: LazyListState = rememberLazyListState(),
17-
selectedItem: String = "",
18-
startIndex: Int = 0
19-
): PickerState = remember { PickerState(lazyListState, selectedItem, startIndex) }
30+
initialIndex: Int = 0,
31+
items: List<T>
32+
): PickerState<T> = remember { PickerState(lazyListState, initialIndex, items) }

timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,18 @@ import kotlinx.coroutines.flow.map
3636
import kotlin.math.abs
3737

3838
@Composable
39-
internal fun PickerItem(
39+
internal fun <T> PickerItem(
4040
modifier: Modifier = Modifier,
41-
items: List<String>,
42-
state: PickerState = rememberPickerState(),
41+
items: List<T>,
42+
state: PickerState<T> = rememberPickerState(items = items),
4343
visibleItemsCount: Int,
4444
textModifier: Modifier = Modifier,
4545
infiniteScroll: Boolean = true,
4646
textStyle: TextStyle,
4747
textColor: Color,
4848
itemSpacing: Dp,
49-
onValueChange: (String) -> Unit
49+
itemFormatter: (T) -> String = { it.toString() },
50+
onValueChange: (T) -> Unit
5051
) {
5152
val visibleItemsMiddle = visibleItemsCount / 2
5253
val listScrollCount = if (infiniteScroll) Int.MAX_VALUE else items.size + visibleItemsMiddle * 2
@@ -57,8 +58,8 @@ internal fun PickerItem(
5758
var itemHeightPixels by remember { mutableIntStateOf(0) }
5859
val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.toDp() }
5960

60-
LaunchedEffect(state.startIndex) {
61-
val safeStartIndex = state.startIndex
61+
LaunchedEffect(state.initialIndex) {
62+
val safeStartIndex = state.initialIndex
6263
val listStartIndex = if (infiniteScroll) {
6364
getStartIndexForInfiniteScroll(itemHeightPixels, listScrollMiddle, visibleItemsMiddle, safeStartIndex)
6465
} else {
@@ -67,11 +68,11 @@ internal fun PickerItem(
6768
listState.scrollToItem(listStartIndex, 0)
6869

6970
if (!infiniteScroll) {
70-
val selectedItem = items.getOrNull(safeStartIndex) ?: ""
71-
if (selectedItem != state.selectedItem) {
72-
state.selectedItem = selectedItem
73-
onValueChange(selectedItem)
71+
val selectedItem = items.getOrNull(listStartIndex) ?: items.first()
72+
if (listStartIndex != state.selectedIndex.value) {
73+
state.updateSelectedIndex(listStartIndex)
7474
}
75+
onValueChange(selectedItem)
7576
}
7677
}
7778

@@ -85,23 +86,22 @@ internal fun PickerItem(
8586
abs(itemCenter - centerOffset)
8687
}?.index
8788
}
88-
.distinctUntilChanged()
89-
.collect { centerIndex ->
90-
if (centerIndex != null) {
91-
val adjustedIndex = if (infiniteScroll) {
92-
centerIndex % items.size
89+
.map { centerIndex ->
90+
centerIndex?.let { index ->
91+
if (infiniteScroll) {
92+
index % items.size
9393
} else {
94-
centerIndex - visibleItemsMiddle
95-
}.coerceIn(0, items.size - 1)
96-
97-
val newValue = items[adjustedIndex]
98-
99-
if (newValue != state.selectedItem) {
100-
state.selectedItem = newValue
101-
onValueChange(newValue)
94+
(index - visibleItemsMiddle).coerceIn(0, items.size - 1)
10295
}
10396
}
10497
}
98+
.distinctUntilChanged()
99+
.collect { adjustedIndex ->
100+
if (adjustedIndex != null && adjustedIndex != state.selectedIndex.value) {
101+
state.updateSelectedIndex(adjustedIndex)
102+
onValueChange(items[adjustedIndex])
103+
}
104+
}
105105
}
106106

107107
val totalItemHeight = itemHeightDp + itemSpacing
@@ -136,8 +136,15 @@ internal fun PickerItem(
136136

137137
val scaleY = 1f - (0.2f * (distanceFromCenter / maxDistance)).coerceIn(0f, 0.4f)
138138

139+
val item = getItemForIndex(
140+
index = index,
141+
items = items,
142+
infiniteScroll = infiniteScroll,
143+
visibleItemsMiddle = visibleItemsMiddle
144+
)
145+
139146
Text(
140-
text = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle),
147+
text = item?.let { itemFormatter(it) } ?: "",
141148
maxLines = 1,
142149
style = textStyle,
143150
color = textColor.copy(alpha = alpha),
@@ -152,16 +159,18 @@ internal fun PickerItem(
152159
}
153160
}
154161

155-
private fun getItemForIndex(
162+
private fun <T> getItemForIndex(
156163
index: Int,
157-
items: List<String>,
164+
items: List<T>,
158165
infiniteScroll: Boolean,
159166
visibleItemsMiddle: Int
160-
): String {
167+
): T? {
168+
require(items.isNotEmpty()) { "Items list cannot be empty." }
169+
161170
return if (!infiniteScroll) {
162-
items.getOrNull(index - visibleItemsMiddle) ?: ""
171+
items.getOrNull(index - visibleItemsMiddle)
163172
} else {
164-
items.getOrNull(index % items.size) ?: ""
173+
items.getOrNull(index % items.size)
165174
}
166175
}
167176

@@ -177,9 +186,8 @@ private fun getStartIndexForInfiniteScroll(
177186
@Composable
178187
@Preview
179188
private fun PickerItemPreview() {
180-
PickerItem(
181-
items = (0..100).map { it.toString() },
182-
state = rememberPickerState(),
189+
PickerItem<Int>(
190+
items = (0..100).map { it },
183191
visibleItemsCount = 5,
184192
textStyle = MaterialTheme.typography.bodyLarge,
185193
textColor = Color.White,

0 commit comments

Comments
 (0)