From d3aed23bd91a4649c50c89ef1e5e6517822fe723 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 16:12:55 +0900 Subject: [PATCH 1/7] Feat/#5 : Apply CurveEffect to PickerItem for customizable alpha and scaleY curves --- .../com/dongchyeon/timepicker/TimePicker.kt | 64 +++++++++++-------- .../dongchyeon/timepicker/ui/CurveEffect.kt | 20 ++++++ .../dongchyeon/timepicker/ui/PickerItem.kt | 18 ++---- 3 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt index 0543cac..579202f 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.dongchyeon.timepicker.model.PickerState import com.dongchyeon.timepicker.model.rememberPickerState +import com.dongchyeon.timepicker.ui.CurveEffect import com.dongchyeon.timepicker.ui.PickerItem import kotlinx.coroutines.launch import kotlinx.datetime.Clock @@ -45,6 +46,7 @@ fun TimePicker( initialTime: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time, timeFormat: TimeFormat = TimePickerDefaults.timeFormat, selector: PickerSelector = TimePickerDefaults.pickerSelector(), + curveEffect: CurveEffect = CurveEffect(), onValueChange: (LocalTime) -> Unit ) { if (timeFormat.is24Hour) { @@ -56,6 +58,7 @@ fun TimePicker( textColor = itemLabel.color, initialTime = initialTime, selector = selector, + curveEffect = curveEffect, onValueChange = onValueChange ) } else { @@ -68,6 +71,7 @@ fun TimePicker( initialTime = initialTime, localeTimeFormat = timeFormat.localeTimeFormat, selector = selector, + curveEffect = curveEffect, onValueChange = onValueChange ) } @@ -80,11 +84,37 @@ private fun TimePicker12Hour( visibleItemsCount: Int = 5, textStyle: TextStyle = MaterialTheme.typography.bodyLarge, textColor: Color = Color.White, - initialTime: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time, + initialTime: LocalTime, localeTimeFormat: LocaleTimeFormat, selector: PickerSelector, + curveEffect: CurveEffect, onValueChange: (LocalTime) -> Unit ) { + val amPmItems = remember { + listOf( + TimePeriod.AM.getLabel(localeTimeFormat), + TimePeriod.PM.getLabel(localeTimeFormat) + ) + } + val hourItems = remember { (1..12).toList() } + val minuteItems = remember { (0..59).toList() } + + val amPmPickerState = rememberPickerState( + initialIndex = if (initialTime.hour < 12) 0 else 1, + items = amPmItems + ) + val hourPickerState = rememberPickerState( + initialIndex = hourItems.indexOf(if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12), + items = hourItems + ) + val minutePickerState = rememberPickerState( + initialIndex = minuteItems.indexOf(initialTime.minute), + items = minuteItems + ) + + var previousHour by remember { mutableIntStateOf(initialTime.hour) } + val scope = rememberCoroutineScope() + Box(modifier = modifier.fillMaxWidth()) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -97,32 +127,6 @@ private fun TimePicker12Hour( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom ) { - val amPmItems = remember { - listOf( - TimePeriod.AM.getLabel(localeTimeFormat), - TimePeriod.PM.getLabel(localeTimeFormat) - ) - } - val hourItems = remember { (1..12).toList() } - val minuteItems = remember { (0..59).toList() } - - val amPmPickerState = rememberPickerState( - initialIndex = if (initialTime.hour < 12) 0 else 1, - items = amPmItems - ) - val hourPickerState = rememberPickerState( - initialIndex = hourItems.indexOf(if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12), - items = hourItems - ) - val minutePickerState = rememberPickerState( - initialIndex = minuteItems.indexOf(initialTime.minute), - items = minuteItems - ) - - var previousHour by remember { mutableIntStateOf(initialTime.hour) } - - val scope = rememberCoroutineScope() - Box( modifier = Modifier.fillMaxWidth() ) { @@ -164,6 +168,7 @@ private fun TimePicker12Hour( modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = false, + curveEffect = curveEffect, onValueChange = { onPickerValueChange( amPmPickerState, @@ -185,6 +190,7 @@ private fun TimePicker12Hour( modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, + curveEffect = curveEffect, onValueChange = { onPickerValueChange( amPmPickerState, @@ -225,6 +231,7 @@ private fun TimePicker12Hour( itemFormatter = { item -> item.toString().padStart(2, '0') }, + curveEffect = curveEffect, onValueChange = { onPickerValueChange( amPmPickerState, @@ -252,6 +259,7 @@ private fun TimePicker24Hour( textColor: Color = Color.White, initialTime: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time, selector: PickerSelector, + curveEffect: CurveEffect, onValueChange: (LocalTime) -> Unit ) { val hourItems = remember { (0..23).toList() } @@ -319,6 +327,7 @@ private fun TimePicker24Hour( modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, + curveEffect = curveEffect, onValueChange = { onPickerValueChange( hourPickerState, @@ -347,6 +356,7 @@ private fun TimePicker24Hour( itemFormatter = { item -> item.toString().padStart(2, '0') }, + curveEffect = curveEffect, onValueChange = { onPickerValueChange( hourPickerState, diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt new file mode 100644 index 0000000..bb8778c --- /dev/null +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt @@ -0,0 +1,20 @@ +package com.dongchyeon.timepicker.ui + +data class CurveEffect( + val alphaEnabled: Boolean = true, + val minAlpha: Float = 0.2f, + val scaleYEnabled: Boolean = true, + val minScaleY: Float = 0.8f +) { + fun calculateAlpha(distanceFromCenter: Float, maxDistance: Float): Float { + if (!alphaEnabled) return 1f + val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) + return ((1f - ratio) * (1f - minAlpha) + minAlpha) + } + + fun calculateScaleY(distanceFromCenter: Float, maxDistance: Float): Float { + if (!scaleYEnabled) return 1f + val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) + return ((1f - ratio) * (1f - minScaleY) + minScaleY) + } +} diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt index 892ddd1..bdb524e 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt @@ -37,16 +37,17 @@ import kotlin.math.abs @Composable internal fun PickerItem( - modifier: Modifier = Modifier, items: List, state: PickerState = rememberPickerState(items = items), visibleItemsCount: Int, + itemFormatter: (T) -> String = { it.toString() }, + modifier: Modifier = Modifier, textModifier: Modifier = Modifier, - infiniteScroll: Boolean = true, textStyle: TextStyle, textColor: Color, itemSpacing: Dp, - itemFormatter: (T) -> String = { it.toString() }, + infiniteScroll: Boolean = true, + curveEffect: CurveEffect = CurveEffect(), onValueChange: (T) -> Unit ) { val visibleItemsMiddle = visibleItemsCount / 2 @@ -125,16 +126,11 @@ internal fun PickerItem( val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index } val itemCenterOffset = itemInfo?.offset?.let { it + (itemInfo.size / 2) } ?: 0 - val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset) + val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset).toFloat() val maxDistance = totalItemHeight.toPx() * visibleItemsMiddle - val alpha = if (distanceFromCenter <= maxDistance) { - ((maxDistance - distanceFromCenter) / maxDistance).coerceIn(0.2f, 1f) - } else { - 0.2f - } - - val scaleY = 1f - (0.2f * (distanceFromCenter / maxDistance)).coerceIn(0f, 0.4f) + val alpha = curveEffect.calculateAlpha(distanceFromCenter, maxDistance) + val scaleY = curveEffect.calculateScaleY(distanceFromCenter, maxDistance) val item = getItemForIndex( index = index, From 25dd21287d37f4baf70271ee210dc67199f52fcd Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 16:53:54 +0900 Subject: [PATCH 2/7] Refactor/#5 : Add PickerStyle and CurveEffect to TimePickerDefaults for consistent styling --- .../com/dongchyeon/timepicker/TimePicker.kt | 105 ++++++------------ .../timepicker/TimePickerDefaults.kt | 91 ++++++++------- .../dongchyeon/timepicker/ui/CurveEffect.kt | 20 ---- .../dongchyeon/timepicker/ui/PickerItem.kt | 34 +++--- 4 files changed, 101 insertions(+), 149 deletions(-) delete mode 100644 timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt index 579202f..117d4be 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,13 +22,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.dongchyeon.timepicker.model.PickerState import com.dongchyeon.timepicker.model.rememberPickerState -import com.dongchyeon.timepicker.ui.CurveEffect import com.dongchyeon.timepicker.ui.PickerItem import kotlinx.coroutines.launch import kotlinx.datetime.Clock @@ -40,23 +36,20 @@ import kotlinx.datetime.toLocalDateTime @Composable fun TimePicker( modifier: Modifier = Modifier, - itemSpacing: Dp = 2.dp, - visibleItemsCount: Int = TimePickerDefaults.visibleItemsCount, - itemLabel: ItemLabel = TimePickerDefaults.itemLabel(), initialTime: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time, + visibleItemsCount: Int = TimePickerDefaults.visibleItemsCount, timeFormat: TimeFormat = TimePickerDefaults.timeFormat, + style: PickerStyle = TimePickerDefaults.pickerStyle(), selector: PickerSelector = TimePickerDefaults.pickerSelector(), - curveEffect: CurveEffect = CurveEffect(), + curveEffect: CurveEffect = TimePickerDefaults.curveEffect(), onValueChange: (LocalTime) -> Unit ) { if (timeFormat.is24Hour) { TimePicker24Hour( modifier = modifier, - itemSpacing = itemSpacing, visibleItemsCount = visibleItemsCount, - textStyle = itemLabel.style, - textColor = itemLabel.color, initialTime = initialTime, + style = style, selector = selector, curveEffect = curveEffect, onValueChange = onValueChange @@ -64,12 +57,10 @@ fun TimePicker( } else { TimePicker12Hour( modifier = modifier, - itemSpacing = itemSpacing, visibleItemsCount = visibleItemsCount, - textStyle = itemLabel.style, - textColor = itemLabel.color, initialTime = initialTime, localeTimeFormat = timeFormat.localeTimeFormat, + style = style, selector = selector, curveEffect = curveEffect, onValueChange = onValueChange @@ -80,12 +71,10 @@ fun TimePicker( @Composable private fun TimePicker12Hour( modifier: Modifier = Modifier, - itemSpacing: Dp = 2.dp, - visibleItemsCount: Int = 5, - textStyle: TextStyle = MaterialTheme.typography.bodyLarge, - textColor: Color = Color.White, initialTime: LocalTime, + visibleItemsCount: Int, localeTimeFormat: LocaleTimeFormat, + style: PickerStyle, selector: PickerSelector, curveEffect: CurveEffect, onValueChange: (LocalTime) -> Unit @@ -135,7 +124,7 @@ private fun TimePicker12Hour( .fillMaxWidth() .align(Alignment.Center) .padding(horizontal = 20.dp) - .height(with(LocalDensity.current) { textStyle.lineHeight.toDp() } + 20.dp) + .height(with(LocalDensity.current) { style.textStyle.lineHeight.toDp() } + 20.dp) .background( color = selector.color, shape = selector.shape @@ -159,12 +148,10 @@ private fun TimePicker12Hour( verticalAlignment = Alignment.CenterVertically ) { PickerItem( - state = amPmPickerState, items = amPmItems, + state = amPmPickerState, visibleItemsCount = 3, - itemSpacing = itemSpacing, - textStyle = textStyle, - textColor = textColor, + style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = false, @@ -181,12 +168,10 @@ private fun TimePicker12Hour( ) PickerItem( - state = hourPickerState, items = hourItems, + state = hourPickerState, visibleItemsCount = visibleItemsCount, - itemSpacing = itemSpacing, - textStyle = textStyle, - textColor = textColor, + style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, @@ -199,38 +184,30 @@ private fun TimePicker12Hour( localeTimeFormat, onValueChange ) - scope.launch { val currentHour = hourPickerState.selectedItem + val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size + val nextIndex = (currentIndex + 1) % amPmItems.size - if (currentHour == 12 && previousHour == 11) { - val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size - val nextIndex = (currentIndex + 1) % amPmItems.size - amPmPickerState.lazyListState.animateScrollToItem(nextIndex) - } else if (currentHour == 11 && previousHour == 12) { - val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size - val nextIndex = (currentIndex + 1) % amPmItems.size + if ((currentHour == 12 && previousHour == 11) || + (currentHour == 11 && previousHour == 12) + ) { amPmPickerState.lazyListState.animateScrollToItem(nextIndex) } - previousHour = currentHour } } ) PickerItem( - state = minutePickerState, items = minuteItems, + state = minutePickerState, visibleItemsCount = visibleItemsCount, - itemSpacing = itemSpacing, - textStyle = textStyle, - textColor = textColor, + style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, - itemFormatter = { item -> - item.toString().padStart(2, '0') - }, + itemFormatter = { it.toString().padStart(2, '0') }, curveEffect = curveEffect, onValueChange = { onPickerValueChange( @@ -253,11 +230,9 @@ private fun TimePicker12Hour( @Composable private fun TimePicker24Hour( modifier: Modifier = Modifier, - itemSpacing: Dp = 2.dp, - visibleItemsCount: Int = 5, - textStyle: TextStyle = MaterialTheme.typography.bodyLarge, - textColor: Color = Color.White, - initialTime: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time, + initialTime: LocalTime, + visibleItemsCount: Int, + style: PickerStyle, selector: PickerSelector, curveEffect: CurveEffect, onValueChange: (LocalTime) -> Unit @@ -294,7 +269,7 @@ private fun TimePicker24Hour( .fillMaxWidth() .align(Alignment.Center) .padding(horizontal = 20.dp) - .height(with(LocalDensity.current) { textStyle.lineHeight.toDp() } + 20.dp) + .height(with(LocalDensity.current) { style.textStyle.lineHeight.toDp() } + 20.dp) .background( color = selector.color, shape = selector.shape @@ -318,51 +293,37 @@ private fun TimePicker24Hour( verticalAlignment = Alignment.CenterVertically ) { PickerItem( - state = hourPickerState, items = hourItems, + state = hourPickerState, visibleItemsCount = visibleItemsCount, - itemSpacing = itemSpacing, - textStyle = textStyle, - textColor = textColor, + style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, curveEffect = curveEffect, onValueChange = { - onPickerValueChange( - hourPickerState, - minutePickerState, - onValueChange - ) + onPickerValueChange(hourPickerState, minutePickerState, onValueChange) } ) Text( text = ":", - style = textStyle, - color = textColor + style = style.textStyle, + color = style.textColor ) PickerItem( - state = minutePickerState, items = minuteItems, + state = minutePickerState, visibleItemsCount = visibleItemsCount, - itemSpacing = itemSpacing, - textStyle = textStyle, - textColor = textColor, + style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, - itemFormatter = { item -> - item.toString().padStart(2, '0') - }, + itemFormatter = { it.toString().padStart(2, '0') }, curveEffect = curveEffect, onValueChange = { - onPickerValueChange( - hourPickerState, - minutePickerState, - onValueChange - ) + onPickerValueChange(hourPickerState, minutePickerState, onValueChange) } ) } diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt index 6bc240e..cab65f3 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt @@ -7,9 +7,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp object TimePickerDefaults { + @Composable + fun pickerStyle( + textStyle: TextStyle = MaterialTheme.typography.titleMedium, + textColor: Color = Color.White, + itemSpacing: Dp = 2.dp + ): PickerStyle { + return PickerStyle( + textStyle = textStyle, + textColor = textColor, + itemSpacing = itemSpacing + ) + } + @Composable fun pickerSelector( enabled: Boolean = true, @@ -26,13 +40,17 @@ object TimePickerDefaults { } @Composable - fun itemLabel( - style: TextStyle = MaterialTheme.typography.titleMedium, - color: Color = Color.White - ): ItemLabel { - return ItemLabel( - style = style, - color = color + fun curveEffect( + alphaEnabled: Boolean = true, + minAlpha: Float = 0.2f, + scaleYEnabled: Boolean = true, + minScaleY: Float = 0.8f + ): CurveEffect { + return CurveEffect( + alphaEnabled = alphaEnabled, + minAlpha = minAlpha, + scaleYEnabled = scaleYEnabled, + minScaleY = minScaleY ) } @@ -41,43 +59,40 @@ object TimePickerDefaults { } @Immutable -class PickerSelector( - val enabled: Boolean, - val shape: RoundedCornerShape, - val color: Color, - val border: BorderStroke? -) { - fun copy( - enabled: Boolean = this.enabled, - shape: RoundedCornerShape = this.shape, - color: Color = this.color, - border: BorderStroke? = this.border - ): PickerSelector { - return PickerSelector( - enabled = enabled, - shape = shape, - color = color, - border = border - ) - } -} +data class PickerStyle( + val textStyle: TextStyle, + val textColor: Color, + val itemSpacing: Dp +) @Immutable -class ItemLabel( - val style: TextStyle, - val color: Color +data class CurveEffect( + val alphaEnabled: Boolean = true, + val minAlpha: Float = 0.2f, + val scaleYEnabled: Boolean = true, + val minScaleY: Float = 0.8f ) { - fun copy( - style: TextStyle = this.style, - color: Color = this.color - ): ItemLabel { - return ItemLabel( - style = style, - color = color - ) + fun calculateAlpha(distanceFromCenter: Float, maxDistance: Float): Float { + if (!alphaEnabled) return 1f + val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) + return ((1f - ratio) * (1f - minAlpha) + minAlpha) + } + + fun calculateScaleY(distanceFromCenter: Float, maxDistance: Float): Float { + if (!scaleYEnabled) return 1f + val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) + return ((1f - ratio) * (1f - minScaleY) + minScaleY) } } +@Immutable +data class PickerSelector( + val enabled: Boolean, + val shape: RoundedCornerShape, + val color: Color, + val border: BorderStroke? +) + enum class TimeFormat(val is24Hour: Boolean, val localeTimeFormat: LocaleTimeFormat) { DEFAULT(false, LocaleTimeFormat.ENGLISH), TWELVE_HOUR(false, LocaleTimeFormat.ENGLISH), diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt deleted file mode 100644 index bb8778c..0000000 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/CurveEffect.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.dongchyeon.timepicker.ui - -data class CurveEffect( - val alphaEnabled: Boolean = true, - val minAlpha: Float = 0.2f, - val scaleYEnabled: Boolean = true, - val minScaleY: Float = 0.8f -) { - fun calculateAlpha(distanceFromCenter: Float, maxDistance: Float): Float { - if (!alphaEnabled) return 1f - val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) - return ((1f - ratio) * (1f - minAlpha) + minAlpha) - } - - fun calculateScaleY(distanceFromCenter: Float, maxDistance: Float): Float { - if (!scaleYEnabled) return 1f - val ratio = (distanceFromCenter / maxDistance).coerceIn(0f, 1f) - return ((1f - ratio) * (1f - minScaleY) + minScaleY) - } -} diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt index bdb524e..6de88c3 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -19,15 +18,14 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import com.dongchyeon.timepicker.CurveEffect +import com.dongchyeon.timepicker.PickerStyle +import com.dongchyeon.timepicker.TimePickerDefaults import com.dongchyeon.timepicker.model.PickerState import com.dongchyeon.timepicker.model.rememberPickerState import com.dongchyeon.timepicker.ui.util.toPx @@ -37,17 +35,15 @@ import kotlin.math.abs @Composable internal fun PickerItem( + modifier: Modifier = Modifier, items: List, state: PickerState = rememberPickerState(items = items), visibleItemsCount: Int, - itemFormatter: (T) -> String = { it.toString() }, - modifier: Modifier = Modifier, + style: PickerStyle, textModifier: Modifier = Modifier, - textStyle: TextStyle, - textColor: Color, - itemSpacing: Dp, - infiniteScroll: Boolean = true, - curveEffect: CurveEffect = CurveEffect(), + itemFormatter: (T) -> String = { it.toString() }, + infiniteScroll: Boolean, + curveEffect: CurveEffect, onValueChange: (T) -> Unit ) { val visibleItemsMiddle = visibleItemsCount / 2 @@ -105,7 +101,7 @@ internal fun PickerItem( } } - val totalItemHeight = itemHeightDp + itemSpacing + val totalItemHeight = itemHeightDp + style.itemSpacing Box(modifier = modifier) { LazyColumn( @@ -142,10 +138,10 @@ internal fun PickerItem( Text( text = item?.let { itemFormatter(it) } ?: "", maxLines = 1, - style = textStyle, - color = textColor.copy(alpha = alpha), + style = style.textStyle, + color = style.textColor.copy(alpha = alpha), modifier = Modifier - .padding(vertical = itemSpacing / 2) + .padding(vertical = style.itemSpacing / 2) .graphicsLayer(scaleY = scaleY) .onSizeChanged { size -> itemHeightPixels = size.height } .then(textModifier) @@ -185,9 +181,9 @@ private fun PickerItemPreview() { PickerItem( items = (0..100).map { it }, visibleItemsCount = 5, - textStyle = MaterialTheme.typography.bodyLarge, - textColor = Color.White, - itemSpacing = 8.dp, + style = TimePickerDefaults.pickerStyle(), + curveEffect = TimePickerDefaults.curveEffect(), + infiniteScroll = true, onValueChange = {} ) } From 914680a2d617960be9a1553c24f4f29f60a3dcf6 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 17:11:19 +0900 Subject: [PATCH 3/7] Refactor/#5 : Remove @Composable from curveEffect function in TimePickerDefaults --- .../main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt index cab65f3..89569e7 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePickerDefaults.kt @@ -39,7 +39,6 @@ object TimePickerDefaults { ) } - @Composable fun curveEffect( alphaEnabled: Boolean = true, minAlpha: Float = 0.2f, From 0743076d19698a17b25aa9b825df7b1e80bf7a68 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 17:15:50 +0900 Subject: [PATCH 4/7] Fix/#5 : Adjust AM/PM picker visibleItemsCount for consistent alpha and scale with hour/minute pickers --- .../src/main/java/com/dongchyeon/timepicker/TimePicker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt index 117d4be..1196ac3 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt @@ -150,7 +150,7 @@ private fun TimePicker12Hour( PickerItem( items = amPmItems, state = amPmPickerState, - visibleItemsCount = 3, + visibleItemsCount = visibleItemsCount, style = style, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), From 63684d0f4bd288cee7d06f38719c7c8507cf68c5 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 17:27:10 +0900 Subject: [PATCH 5/7] Fix/#5 : Move totalItemHeight.toPx() calculation outside LazyColumn for performance optimization --- .../src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt index 6de88c3..99e5e8d 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt @@ -102,6 +102,7 @@ internal fun PickerItem( } val totalItemHeight = itemHeightDp + style.itemSpacing + val totalItemHeightPx = totalItemHeight.toPx() Box(modifier = modifier) { LazyColumn( @@ -123,7 +124,7 @@ internal fun PickerItem( val itemCenterOffset = itemInfo?.offset?.let { it + (itemInfo.size / 2) } ?: 0 val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset).toFloat() - val maxDistance = totalItemHeight.toPx() * visibleItemsMiddle + val maxDistance = totalItemHeightPx * visibleItemsMiddle val alpha = curveEffect.calculateAlpha(distanceFromCenter, maxDistance) val scaleY = curveEffect.calculateScaleY(distanceFromCenter, maxDistance) From b0d224908e7929b166e569260a4a0553386606ea Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 17:42:10 +0900 Subject: [PATCH 6/7] Fix/#5 : Add zero division guard for itemSize in getStartIndexForInfiniteScroll --- .../src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt index 99e5e8d..5e6d617 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt @@ -173,6 +173,10 @@ private fun getStartIndexForInfiniteScroll( visibleItemsMiddle: Int, startIndex: Int ): Int { + if (itemSize == 0) { + return listScrollMiddle - visibleItemsMiddle + startIndex + } + return listScrollMiddle - listScrollMiddle % itemSize - visibleItemsMiddle + startIndex } From d4fd8d6926b13623e2a911194fc9b84700633715 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Fri, 4 Jul 2025 17:47:26 +0900 Subject: [PATCH 7/7] Refactor/#5 : Reduce composable nesting depth in TimePicker --- .../com/dongchyeon/timepicker/TimePicker.kt | 336 ++++++++---------- 1 file changed, 146 insertions(+), 190 deletions(-) diff --git a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt index 1196ac3..89d47cc 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/TimePicker.kt @@ -104,129 +104,117 @@ private fun TimePicker12Hour( var previousHour by remember { mutableIntStateOf(initialTime.hour) } val scope = rememberCoroutineScope() - Box(modifier = modifier.fillMaxWidth()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Bottom - ) { - Box( - modifier = modifier.fillMaxWidth() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Bottom - ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - .padding(horizontal = 20.dp) - .height(with(LocalDensity.current) { style.textStyle.lineHeight.toDp() } + 20.dp) - .background( - color = selector.color, - shape = selector.shape - ) - .then( - if (selector.border != null) { - Modifier.border( - border = selector.border, - shape = selector.shape - ) - } else { - Modifier - } - ) - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 50.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PickerItem( - items = amPmItems, - state = amPmPickerState, - visibleItemsCount = visibleItemsCount, - style = style, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - infiniteScroll = false, - curveEffect = curveEffect, - onValueChange = { - onPickerValueChange( - amPmPickerState, - hourPickerState, - minutePickerState, - localeTimeFormat, - onValueChange - ) - } - ) + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + SelectorBackground( + style = style, + selector = selector + ) - PickerItem( - items = hourItems, - state = hourPickerState, - visibleItemsCount = visibleItemsCount, - style = style, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - infiniteScroll = true, - curveEffect = curveEffect, - onValueChange = { - onPickerValueChange( - amPmPickerState, - hourPickerState, - minutePickerState, - localeTimeFormat, - onValueChange - ) - scope.launch { - val currentHour = hourPickerState.selectedItem - val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size - val nextIndex = (currentIndex + 1) % amPmItems.size + Row( + modifier = Modifier.padding(horizontal = 50.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PickerItem( + items = amPmItems, + state = amPmPickerState, + visibleItemsCount = visibleItemsCount, + style = style, + modifier = Modifier.weight(1f), + textModifier = Modifier.padding(8.dp), + infiniteScroll = false, + curveEffect = curveEffect, + onValueChange = { + onPickerValueChange( + amPmPickerState, + hourPickerState, + minutePickerState, + localeTimeFormat, + onValueChange + ) + } + ) - if ((currentHour == 12 && previousHour == 11) || - (currentHour == 11 && previousHour == 12) - ) { - amPmPickerState.lazyListState.animateScrollToItem(nextIndex) - } - previousHour = currentHour - } - } - ) + PickerItem( + items = hourItems, + state = hourPickerState, + visibleItemsCount = visibleItemsCount, + style = style, + modifier = Modifier.weight(1f), + textModifier = Modifier.padding(8.dp), + infiniteScroll = true, + curveEffect = curveEffect, + onValueChange = { + onPickerValueChange( + amPmPickerState, + hourPickerState, + minutePickerState, + localeTimeFormat, + onValueChange + ) + scope.launch { + val currentHour = hourPickerState.selectedItem + val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size + val nextIndex = (currentIndex + 1) % amPmItems.size - PickerItem( - items = minuteItems, - state = minutePickerState, - visibleItemsCount = visibleItemsCount, - style = style, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - infiniteScroll = true, - itemFormatter = { it.toString().padStart(2, '0') }, - curveEffect = curveEffect, - onValueChange = { - onPickerValueChange( - amPmPickerState, - hourPickerState, - minutePickerState, - localeTimeFormat, - onValueChange - ) - } - ) + if ((currentHour == 12 && previousHour == 11) || + (currentHour == 11 && previousHour == 12) + ) { + amPmPickerState.lazyListState.animateScrollToItem(nextIndex) } + previousHour = currentHour } } - } + ) + + PickerItem( + items = minuteItems, + state = minutePickerState, + visibleItemsCount = visibleItemsCount, + style = style, + modifier = Modifier.weight(1f), + textModifier = Modifier.padding(8.dp), + infiniteScroll = true, + itemFormatter = { it.toString().padStart(2, '0') }, + curveEffect = curveEffect, + onValueChange = { + onPickerValueChange( + amPmPickerState, + hourPickerState, + minutePickerState, + localeTimeFormat, + onValueChange + ) + } + ) } } } +@Composable +private fun SelectorBackground( + modifier: Modifier = Modifier, + style: PickerStyle, + selector: PickerSelector +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(with(LocalDensity.current) { style.textStyle.lineHeight.toDp() } + 20.dp) + .background(color = selector.color, shape = selector.shape) + .then( + if (selector.border != null) { + Modifier.border(border = selector.border, shape = selector.shape) + } else { + Modifier + } + ) + ) +} + @Composable private fun TimePicker24Hour( modifier: Modifier = Modifier, @@ -249,87 +237,55 @@ private fun TimePicker24Hour( items = minuteItems ) - Box(modifier = modifier.fillMaxWidth()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Bottom - ) { - Box( - modifier = modifier.fillMaxWidth() - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Bottom - ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - .padding(horizontal = 20.dp) - .height(with(LocalDensity.current) { style.textStyle.lineHeight.toDp() } + 20.dp) - .background( - color = selector.color, - shape = selector.shape - ) - .then( - if (selector.border != null) { - Modifier.border( - border = selector.border, - shape = selector.shape - ) - } else { - Modifier - } - ) - ) + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + SelectorBackground( + style = style, + selector = selector + ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 50.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PickerItem( - items = hourItems, - state = hourPickerState, - visibleItemsCount = visibleItemsCount, - style = style, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - infiniteScroll = true, - curveEffect = curveEffect, - onValueChange = { - onPickerValueChange(hourPickerState, minutePickerState, onValueChange) - } - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 50.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PickerItem( + items = hourItems, + state = hourPickerState, + visibleItemsCount = visibleItemsCount, + style = style, + modifier = Modifier.weight(1f), + textModifier = Modifier.padding(8.dp), + infiniteScroll = true, + curveEffect = curveEffect, + onValueChange = { + onPickerValueChange(hourPickerState, minutePickerState, onValueChange) + } + ) - Text( - text = ":", - style = style.textStyle, - color = style.textColor - ) + Text( + text = ":", + style = style.textStyle, + color = style.textColor + ) - PickerItem( - items = minuteItems, - state = minutePickerState, - visibleItemsCount = visibleItemsCount, - style = style, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - infiniteScroll = true, - itemFormatter = { it.toString().padStart(2, '0') }, - curveEffect = curveEffect, - onValueChange = { - onPickerValueChange(hourPickerState, minutePickerState, onValueChange) - } - ) - } - } + PickerItem( + items = minuteItems, + state = minutePickerState, + visibleItemsCount = visibleItemsCount, + style = style, + modifier = Modifier.weight(1f), + textModifier = Modifier.padding(8.dp), + infiniteScroll = true, + itemFormatter = { it.toString().padStart(2, '0') }, + curveEffect = curveEffect, + onValueChange = { + onPickerValueChange(hourPickerState, minutePickerState, onValueChange) } - } + ) } } }