From 5fbe7310515444d985b1916aca6ba3f2a8fc3efe Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Sun, 9 Nov 2025 22:38:12 +0900 Subject: [PATCH 1/2] Refactor/#7: Extract curve effect logic into a Modifier extension using graphicsLayer lambda --- .../dongchyeon/timepicker/ui/PickerItem.kt | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) 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 5e6d617..0487b0c 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt @@ -7,10 +7,10 @@ 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.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -115,20 +115,6 @@ internal fun PickerItem( .pointerInput(Unit) { detectVerticalDragGestures { change, _ -> change.consume() } } ) { items(listScrollCount, key = { index -> index }) { index -> - val layoutInfo by remember { derivedStateOf { listState.layoutInfo } } - - val viewportCenterOffset = layoutInfo.viewportStartOffset + - (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 - - val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index } - val itemCenterOffset = itemInfo?.offset?.let { it + (itemInfo.size / 2) } ?: 0 - - val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset).toFloat() - val maxDistance = totalItemHeightPx * visibleItemsMiddle - - val alpha = curveEffect.calculateAlpha(distanceFromCenter, maxDistance) - val scaleY = curveEffect.calculateScaleY(distanceFromCenter, maxDistance) - val item = getItemForIndex( index = index, items = items, @@ -140,10 +126,10 @@ internal fun PickerItem( text = item?.let { itemFormatter(it) } ?: "", maxLines = 1, style = style.textStyle, - color = style.textColor.copy(alpha = alpha), + color = style.textColor, modifier = Modifier .padding(vertical = style.itemSpacing / 2) - .graphicsLayer(scaleY = scaleY) + .curvedPickerEffect(listState, index, curveEffect, totalItemHeightPx, visibleItemsMiddle) .onSizeChanged { size -> itemHeightPixels = size.height } .then(textModifier) ) @@ -180,6 +166,24 @@ private fun getStartIndexForInfiniteScroll( return listScrollMiddle - listScrollMiddle % itemSize - visibleItemsMiddle + startIndex } +private fun Modifier.curvedPickerEffect( + listState: LazyListState, + index: Int, + curveEffect: CurveEffect, + totalItemHeightPx: Float, + visibleItemsMiddle: Int, +): Modifier = graphicsLayer { + val layoutInfo = listState.layoutInfo + val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2 + val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index } + val itemCenter = itemInfo?.offset?.plus(itemInfo.size / 2) ?: 0 + val distance = abs(viewportCenter - itemCenter).toFloat() + val maxDistance = totalItemHeightPx * visibleItemsMiddle + + alpha = curveEffect.calculateAlpha(distance, maxDistance) + scaleY = curveEffect.calculateScaleY(distance, maxDistance) +} + @Composable @Preview private fun PickerItemPreview() { From 39791bde34d7fd802932ebd72da683f3a08ad1b2 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Sun, 9 Nov 2025 23:25:27 +0900 Subject: [PATCH 2/2] Refactor/#7: Optimize recomposition and frame performance using graphicsLayer and rememberUpdatedState --- .../dongchyeon/timepicker/ui/PickerItem.kt | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) 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 0487b0c..b18fe3e 100644 --- a/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt +++ b/timepicker/src/main/java/com/dongchyeon/timepicker/ui/PickerItem.kt @@ -7,13 +7,14 @@ 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.LazyListItemInfo 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.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -104,6 +105,14 @@ internal fun PickerItem( val totalItemHeight = itemHeightDp + style.itemSpacing val totalItemHeightPx = totalItemHeight.toPx() + val layoutInfo by rememberUpdatedState(listState.layoutInfo) + + val itemInfoMap = remember(layoutInfo) { + layoutInfo.visibleItemsInfo.associateBy { it.index } + } + + val viewportCenterOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 + Box(modifier = modifier) { LazyColumn( state = listState, @@ -129,8 +138,14 @@ internal fun PickerItem( color = style.textColor, modifier = Modifier .padding(vertical = style.itemSpacing / 2) - .curvedPickerEffect(listState, index, curveEffect, totalItemHeightPx, visibleItemsMiddle) - .onSizeChanged { size -> itemHeightPixels = size.height } + .curvedPickerEffect( + index = index, + viewportCenterOffset = viewportCenterOffset, + itemInfoMap = itemInfoMap, + totalItemHeightPx = totalItemHeightPx, + visibleItemsMiddle = visibleItemsMiddle, + curveEffect = curveEffect + ).onSizeChanged { size -> itemHeightPixels = size.height } .then(textModifier) ) } @@ -166,22 +181,25 @@ private fun getStartIndexForInfiniteScroll( return listScrollMiddle - listScrollMiddle % itemSize - visibleItemsMiddle + startIndex } -private fun Modifier.curvedPickerEffect( - listState: LazyListState, +fun Modifier.curvedPickerEffect( index: Int, - curveEffect: CurveEffect, + viewportCenterOffset: Int, + itemInfoMap: Map, totalItemHeightPx: Float, visibleItemsMiddle: Int, + curveEffect: CurveEffect ): Modifier = graphicsLayer { - val layoutInfo = listState.layoutInfo - val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2 - val itemInfo = layoutInfo.visibleItemsInfo.find { it.index == index } - val itemCenter = itemInfo?.offset?.plus(itemInfo.size / 2) ?: 0 - val distance = abs(viewportCenter - itemCenter).toFloat() + val itemInfo = itemInfoMap[index] + val itemCenterOffset = itemInfo?.let { it.offset + (it.size / 2) } ?: 0 + + val distanceFromCenter = abs(viewportCenterOffset - itemCenterOffset).toFloat() val maxDistance = totalItemHeightPx * visibleItemsMiddle - alpha = curveEffect.calculateAlpha(distance, maxDistance) - scaleY = curveEffect.calculateScaleY(distance, maxDistance) + val alpha = curveEffect.calculateAlpha(distanceFromCenter, maxDistance) + val scaleY = curveEffect.calculateScaleY(distanceFromCenter, maxDistance) + + this.alpha = alpha + this.scaleY = scaleY } @Composable