-
-
Save inidamleader/7bcc273afe6b885738556d190582a815 to your computer and use it in GitHub Desktop.
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.offset | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.rememberLazyListState | |
import androidx.compose.foundation.text.KeyboardActions | |
import androidx.compose.foundation.text.KeyboardOptions | |
import androidx.compose.material3.HorizontalDivider | |
import androidx.compose.material3.LocalTextStyle | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.material3.TextField | |
import androidx.compose.material3.TextFieldDefaults | |
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.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.snapshotFlow | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.focus.FocusRequester | |
import androidx.compose.ui.focus.focusRequester | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.text.TextStyle | |
import androidx.compose.ui.text.input.ImeAction | |
import androidx.compose.ui.text.input.KeyboardType | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.text.style.TextOverflow | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.dp | |
import com.inidamleader.ovtracker.layer.ui.theme.OvTrackerTheme | |
import com.inidamleader.ovtracker.util.compose.geometry.toDp | |
import kotlinx.coroutines.flow.collectLatest | |
import kotlinx.coroutines.launch | |
import java.time.LocalDate | |
import java.time.format.DateTimeFormatter | |
import java.time.format.FormatStyle | |
import java.util.Locale | |
/** | |
* A composable function that allows users to select an item from a list using a scrollable list with a text field for editing. | |
* | |
* @param initialValue The initial value to be selected in the list. | |
* @param values The list of items. | |
* @param modifier Modifier for customizing the appearance of the `ListPicker`. | |
* @param wrapSelectorWheel Boolean flag indicating whether the list should wrap around like a selector wheel. | |
* @param format A lambda function that formats an item into a string for display. | |
* @param onValueChange A callback function that is invoked when the selected item changes. | |
* @param onIsErrorChange A callback function that is invoked when the isError changes. | |
* @param parse A lambda function that parses a string into an item. | |
* @param enableEdition Boolean flag indicating whether the user can edit the selected item using a text field. | |
* @param outOfBoundsPageCount The number of pages to display on either side of the selected item. | |
* @param textStyle The text style for the displayed items. | |
* @param verticalPadding The vertical padding between items. | |
* @param dividerColor The color of the horizontal dividers. | |
* @param dividerThickness The thickness of the horizontal dividers. | |
* | |
* @author Reda El Madini - For support, contact [email protected] | |
*/ | |
@Composable | |
fun <E> ListPicker( | |
initialValue: E, | |
values: List<E>, | |
onValueChange: (E) -> Unit, | |
modifier: Modifier = Modifier, | |
wrapSelectorWheel: Boolean = false, | |
format: E.() -> String = { toString() }, | |
parse: (String.() -> E?)? = null, | |
onIsErrorChange: (Boolean) -> Unit = {}, | |
enableEdition: Boolean = parse != null, | |
outOfBoundsPageCount: Int = 1, | |
textStyle: TextStyle = LocalTextStyle.current, | |
verticalPadding: Dp = 16.dp, | |
dividerColor: Color = MaterialTheme.colorScheme.outline, | |
dividerThickness: Dp = 1.dp, | |
keyboardType: KeyboardType = KeyboardType.Text, | |
) { | |
LogComposition() // 0 recompositions | |
val listSize = values.size | |
val coercedOutOfBoundsPageCount = outOfBoundsPageCount.coerceIn(0..listSize / 2) | |
val visibleItemsCount = 1 + coercedOutOfBoundsPageCount * 2 | |
val iteration = | |
if (wrapSelectorWheel) | |
remember(key1 = coercedOutOfBoundsPageCount, key2 = listSize) { | |
(Int.MAX_VALUE - 2 * coercedOutOfBoundsPageCount) / listSize | |
} | |
else 1 | |
val intervals = | |
remember(key1 = coercedOutOfBoundsPageCount, key2 = iteration, key3 = listSize) { | |
listOf( | |
0, | |
coercedOutOfBoundsPageCount, | |
coercedOutOfBoundsPageCount + iteration * listSize, | |
coercedOutOfBoundsPageCount + iteration * listSize + coercedOutOfBoundsPageCount, | |
) | |
} | |
val scrollOfItemIndex = { it: Int -> | |
it + (listSize * (iteration / 2)) | |
} | |
val scrollOfItem = { item: E -> | |
values.indexOf(item) | |
.takeIf { it != -1 } | |
?.let { index -> scrollOfItemIndex(index) } | |
} | |
val lazyListState = rememberLazyListState( | |
initialFirstVisibleItemIndex = remember( | |
key1 = initialValue, | |
key2 = listSize, | |
key3 = iteration, | |
) { | |
scrollOfItem(initialValue) ?: 0 | |
}, | |
) | |
LaunchedEffect(key1 = values) { | |
snapshotFlow { lazyListState.firstVisibleItemIndex }.collectLatest { | |
onValueChange(values[it % listSize]) | |
} | |
} | |
Box( | |
modifier = modifier, | |
contentAlignment = Alignment.Center, | |
) { | |
var edit by rememberSaveable { mutableStateOf(false) } | |
ComposeScope { | |
AnimatedContent( | |
targetState = edit, | |
label = "AnimatedContent", | |
) { showTextField -> | |
if (showTextField) { | |
var isError by rememberSaveable { mutableStateOf(false) } | |
val initialSelectedItem = remember { | |
values[lazyListState.firstVisibleItemIndex % listSize] | |
} | |
var value by rememberSaveable { | |
mutableStateOf(initialSelectedItem.format()) | |
} | |
val focusRequester = remember { FocusRequester() } | |
InvokedEffect(key1 = Unit) { | |
focusRequester.requestFocus() | |
} | |
val coroutineScope = rememberCoroutineScope() | |
ComposeScope { | |
TextField( | |
modifier = Modifier | |
.fillMaxWidth() | |
.focusRequester(focusRequester), | |
value = value, | |
onValueChange = { string -> | |
value = string | |
parse?.invoke(string).let { item -> | |
isError = | |
if (item != null) | |
if (values.contains(item)) false | |
else true // item not found | |
else true // string cannot be parsed | |
if (isError) onValueChange(initialSelectedItem) | |
else onValueChange(item ?: initialSelectedItem) | |
onIsErrorChange(isError) | |
} | |
}, | |
textStyle = textStyle.copy(textAlign = TextAlign.Center), | |
keyboardOptions = KeyboardOptions.Default.copy( | |
keyboardType = keyboardType, | |
imeAction = if (!isError) ImeAction.Done else ImeAction.Default, | |
), | |
keyboardActions = KeyboardActions( | |
onDone = { | |
if (!isError) { | |
parse?.invoke(value)?.let { item -> | |
scrollOfItem(item)?.let { scroll -> | |
coroutineScope.launch { | |
lazyListState.scrollToItem(scroll) | |
} | |
} | |
} | |
edit = false | |
} | |
} | |
), | |
isError = isError, | |
colors = TextFieldDefaults.colors().copy( | |
focusedContainerColor = Color.Transparent, | |
unfocusedContainerColor = Color.Transparent, | |
errorContainerColor = Color.Transparent, | |
focusedIndicatorColor = Color.Transparent, | |
unfocusedIndicatorColor = Color.Transparent, | |
errorIndicatorColor = Color.Transparent, | |
errorTextColor = MaterialTheme.colorScheme.error, | |
), | |
) | |
} | |
} else { | |
val itemHeight = textStyle.lineHeight.toDp() + verticalPadding * 2F | |
LazyColumn( | |
state = lazyListState, | |
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
modifier = Modifier | |
.fillMaxWidth() | |
.height(itemHeight * visibleItemsCount) | |
.fadingEdge( | |
brush = remember { | |
Brush.verticalGradient( | |
0F to Color.Transparent, | |
0.5F to Color.Black, | |
1F to Color.Transparent | |
) | |
}, | |
), | |
) { | |
items( | |
count = intervals.last(), | |
key = { it }, | |
) { index -> | |
val enabled by remember(key1 = index, key2 = enableEdition) { | |
derivedStateOf { | |
enableEdition && (index == (lazyListState.firstVisibleItemIndex + coercedOutOfBoundsPageCount)) | |
} | |
} | |
val textModifier = Modifier.padding(vertical = verticalPadding) | |
when (index) { | |
in intervals[0]..<intervals[1] -> Text( | |
text = if (wrapSelectorWheel) values[(index - coercedOutOfBoundsPageCount + listSize) % listSize].format() else "", | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier, | |
) | |
in intervals[1]..<intervals[2] -> { | |
Text( | |
text = values[(index - coercedOutOfBoundsPageCount) % listSize].format(), | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier.then( | |
Modifier.clickable( | |
onClick = { edit = true }, | |
enabled = enabled, | |
) | |
), | |
) | |
} | |
in intervals[2]..<intervals[3] -> Text( | |
text = if (wrapSelectorWheel) values[(index - coercedOutOfBoundsPageCount) % listSize].format() else "", | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier, | |
) | |
} | |
} | |
} | |
HorizontalDivider( | |
modifier = Modifier.offset(y = itemHeight * coercedOutOfBoundsPageCount - dividerThickness / 2), | |
thickness = dividerThickness, | |
color = dividerColor, | |
) | |
HorizontalDivider( | |
modifier = Modifier.offset(y = itemHeight * (coercedOutOfBoundsPageCount + 1) - dividerThickness / 2), | |
thickness = dividerThickness, | |
color = dividerColor, | |
) | |
} | |
} | |
} | |
} | |
} | |
@Preview(widthDp = 300) | |
@Composable | |
fun PreviewListPicker1() { | |
OvTrackerTheme { | |
Surface(color = MaterialTheme.colorScheme.primary) { | |
var value by remember { mutableStateOf(LocalDate.now()) } | |
val list = remember { | |
buildList { | |
repeat(10) { | |
add(LocalDate.now().minusDays((it - 5).toLong())) | |
} | |
} | |
} | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
wrapSelectorWheel = true, | |
format = { | |
format( | |
DateTimeFormatter | |
.ofLocalizedDate(FormatStyle.MEDIUM) | |
.withLocale(Locale.getDefault()), | |
) | |
}, | |
onIsErrorChange = {}, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
@Preview(widthDp = 100) | |
@Composable | |
fun PreviewListPicker2() { | |
OvTrackerTheme { | |
Surface(color = MaterialTheme.colorScheme.tertiary) { | |
var value by remember { mutableStateOf("5") } | |
val list = remember { (1..10).map { it.toString() } } | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
modifier = Modifier, | |
onIsErrorChange = {}, | |
outOfBoundsPageCount = 2, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
@Preview | |
@Composable | |
fun PreviewListPicker3() { | |
OvTrackerTheme { | |
Column(horizontalAlignment = Alignment.CenterHorizontally) { | |
var value by remember { mutableIntStateOf(5) } | |
val list = remember { (1..10).map { it } } | |
Surface(color = MaterialTheme.colorScheme.primary) { | |
Text( | |
text = "Selected value: $value", | |
textAlign = TextAlign.Center, | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(8.dp), | |
style = MaterialTheme.typography.bodyMedium, | |
) | |
} | |
Spacer(modifier = Modifier.height(16.dp)) | |
Surface { | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
format = { this.toString() }, | |
parse = { | |
try { | |
takeIf { | |
// check if each input string contains only integers | |
it.matches(Regex("^\\d+\$")) | |
}?.toInt() | |
} catch (e: NumberFormatException) { | |
null | |
} | |
}, | |
onIsErrorChange = {}, | |
outOfBoundsPageCount = 2, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
} |
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.runtime.Composable | |
@Composable | |
fun ComposeScope(content: @Composable () -> Unit) { | |
content() | |
} |
package com.inidamleader.ovtracker.util.compose.geometry | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.geometry.isSpecified | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.unit.Density | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.DpOffset | |
import androidx.compose.ui.unit.DpSize | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.unit.IntSize | |
import androidx.compose.ui.unit.TextUnit | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.isSpecified | |
import kotlin.math.roundToInt | |
// Those functions are designed to be used in lambdas | |
// DP | |
fun Density.dpToSp(dp: Dp) = if (dp.isSpecified) dp.toSp() else TextUnit.Unspecified | |
fun Density.dpToFloatPx(dp: Dp) = if (dp.isSpecified) dp.toPx() else Float.NaN | |
fun Density.dpToIntPx(dp: Dp) = if (dp.isSpecified) dp.toPx().toInt() else 0 | |
fun Density.dpRoundToPx(dp: Dp) = if (dp.isSpecified) dp.roundToPx() else 0 | |
@Composable | |
fun Dp.toSp() = LocalDensity.current.dpToSp(this) | |
@Composable | |
fun Dp.toFloatPx() = LocalDensity.current.dpToFloatPx(this) | |
@Composable | |
fun Dp.toIntPx() = LocalDensity.current.dpToIntPx(this) | |
@Composable | |
fun Dp.roundToPx() = LocalDensity.current.dpRoundToPx(this) | |
fun Dp.toRecDpSize() = if (isSpecified) DpSize(this, this) else DpSize.Unspecified | |
fun Dp.toRecDpOffset() = if (isSpecified) DpOffset(this, this) else DpOffset.Unspecified | |
// TEXT UNIT | |
fun Density.spToDp(sp: TextUnit) = if (sp.isSpecified) sp.toDp() else Dp.Unspecified | |
fun Density.spToFloatPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx() else Float.NaN | |
fun Density.spToIntPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx().toInt() else 0 | |
fun Density.spRoundToPx(sp: TextUnit) = if (sp.isSpecified) sp.roundToPx() else 0 | |
@Composable | |
fun TextUnit.toDp() = LocalDensity.current.spToDp(this) | |
@Composable | |
fun TextUnit.toFloatPx() = LocalDensity.current.spToFloatPx(this) | |
@Composable | |
fun TextUnit.toIntPx() = LocalDensity.current.spToIntPx(this) | |
@Composable | |
fun TextUnit.roundToPx() = LocalDensity.current.spRoundToPx(this) | |
// FLOAT | |
fun Density.floatPxToDp(px: Float) = if (px.isFinite()) px.toDp() else Dp.Unspecified | |
fun Density.floatPxToSp(px: Float) = if (px.isFinite()) px.toSp() else TextUnit.Unspecified | |
@Composable | |
fun Float.toDp() = LocalDensity.current.floatPxToDp(this) | |
@Composable | |
fun Float.toSp() = LocalDensity.current.floatPxToSp(this) | |
fun Float.toIntPx() = if (isFinite()) toInt() else 0 | |
fun Float.roundToPx() = if (isFinite()) roundToInt() else 0 | |
fun Float.toRecSize() = if (isFinite()) Size(this, this) else Size.Unspecified | |
fun Float.toRecOffset() = if (isFinite()) Offset(this, this) else Offset.Unspecified | |
// INT | |
fun Density.intPxToDp(px: Int) = px.toDp() | |
fun Density.intPxToSp(px: Int) = px.toSp() | |
@Composable | |
fun Int.toDp() = LocalDensity.current.intPxToDp(this) | |
@Composable | |
fun Int.toSp() = LocalDensity.current.intPxToSp(this) | |
fun Int.toFloatPx() = toFloat() | |
fun Int.toRecIntSize() = IntSize(this, this) | |
fun Int.toRecIntOffset() = IntOffset(this, this) | |
// DP SIZE | |
fun Density.dpSizeToIntSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) IntSize(dpSize.width.toPx().toInt(), dpSize.height.toPx().toInt()) | |
else IntSize.Zero | |
fun Density.dpSizeRoundToIntSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) IntSize(dpSize.width.roundToPx(), dpSize.height.roundToPx()) | |
else IntSize.Zero | |
fun Density.dpSizeToSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) Size(dpSize.width.toPx(), dpSize.height.toPx()) | |
else Size.Unspecified | |
@Composable | |
fun DpSize.toIntSize() = LocalDensity.current.dpSizeToIntSize(this) | |
@Composable | |
fun DpSize.roundToIntSize() = LocalDensity.current.dpSizeRoundToIntSize(this) | |
@Composable | |
fun DpSize.toSize() = LocalDensity.current.dpSizeToSize(this) | |
fun DpSize.isSpaced() = isSpecified && width > 0.dp && height > 0.dp | |
// SIZE | |
fun Density.sizeToDpSize(size: Size) = | |
if (size.isSpecified) DpSize(size.width.toDp(), size.height.toDp()) | |
else DpSize.Unspecified | |
@Composable | |
fun Size.toDpSize() = | |
if (isSpecified) LocalDensity.current.sizeToDpSize(this) | |
else DpSize.Unspecified | |
fun Size.toIntSize() = | |
if (isSpecified) IntSize(width.toInt(), height.toInt()) | |
else IntSize.Zero | |
fun Size.isSpaced() = isSpecified && width > 0F && height > 0F | |
// INT SIZE | |
fun Density.intSizeToDpSize(intSize: IntSize) = DpSize(intSize.width.toDp(), intSize.height.toDp()) | |
@Composable | |
fun IntSize.toDpSize() = LocalDensity.current.intSizeToDpSize(this) | |
@Composable | |
fun IntSize.toSize() = Size(width.toFloat(), height.toFloat()) | |
fun IntSize.isSpaced() = width > 0 && height > 0 | |
// DP OFFSET | |
fun Density.dpOffsetToIntOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) IntOffset(dpOffset.x.toPx().toInt(), dpOffset.y.toPx().toInt()) | |
else IntOffset.Zero | |
fun Density.dpOffsetRoundToIntOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) IntOffset(dpOffset.x.roundToPx(), dpOffset.y.roundToPx()) | |
else IntOffset.Zero | |
fun Density.dpOffsetToOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) Offset(dpOffset.x.toPx(), dpOffset.y.toPx()) | |
else Offset.Unspecified | |
@Composable | |
fun DpOffset.toIntOffset() = LocalDensity.current.dpOffsetToIntOffset(this) | |
@Composable | |
fun DpOffset.roundToIntOffset() = LocalDensity.current.dpOffsetRoundToIntOffset(this) | |
@Composable | |
fun DpOffset.toOffset() = LocalDensity.current.dpOffsetToOffset(this) | |
// OFFSET | |
fun Density.offsetToDpOffset(offset: Offset) = | |
if (offset.isSpecified) DpOffset(offset.x.toDp(), offset.y.toDp()) | |
else DpOffset.Unspecified | |
@Composable | |
fun Offset.toDpOffset() = LocalDensity.current.offsetToDpOffset(this) | |
fun Offset.toIntOffset() = | |
if (isSpecified) IntOffset(x.toInt(), y.toInt()) | |
else IntOffset.Zero | |
// INT OFFSET | |
fun Density.intOffsetToDpOffset(intOffset: IntOffset) = DpOffset(intOffset.x.toDp(), intOffset.y.toDp()) | |
@Composable | |
fun IntOffset.toDpOffset() = LocalDensity.current.intOffsetToDpOffset(this) | |
fun IntOffset.toOffset() = Offset(x.toFloat(), y.toFloat()) |
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.RememberObserver | |
import androidx.compose.runtime.remember | |
@Composable | |
fun InvokedEffect( | |
key1: Any?, | |
effect: () -> Unit, | |
) { | |
remember(key1 = key1) { InvokedEffectImpl(effect) } | |
} | |
@Composable | |
fun InvokedEffect( | |
key1: Any?, | |
key2: Any?, | |
effect: () -> Unit, | |
) { | |
remember(key1 = key1, key2 = key2) { InvokedEffectImpl(effect) } | |
} | |
@Composable | |
fun InvokedEffect( | |
key1: Any?, | |
key2: Any?, | |
key3: Any?, | |
effect: () -> Unit, | |
) { | |
remember(key1 = key1, key2 = key2, key3 = key3) { InvokedEffectImpl(effect) } | |
} | |
@Composable | |
fun InvokedEffect( | |
vararg keys: Any?, | |
effect: () -> Unit, | |
) { | |
remember(*keys) { InvokedEffectImpl(effect) } | |
} | |
internal class InvokedEffectImpl( | |
private val effect: () -> Unit | |
) : RememberObserver { | |
override fun onRemembered() { | |
effect() | |
} | |
override fun onForgotten() { | |
} | |
override fun onAbandoned() { | |
} | |
} |
@Stable | |
fun Modifier.fadingEdge(brush: Brush) = this | |
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) | |
.drawWithContent { | |
drawContent() | |
drawRect(brush = brush, blendMode = BlendMode.DstIn) | |
} |
inidamleader
commented
Jan 14, 2024
•
Thanks for this. Can you also post the code for your util.compose.geometry.toDp
.Thanks
Thanks for this. Can you also post the code for your
util.compose.geometry.toDp
.Thanks
val Dp.toPx: Float @composable get() = LocalDensity.current.toPx(this)
If you like don’t forget to give a star
Thanks for this. Can you also post the code for your
util.compose.geometry.toDp
.Thanksval Dp.toPx: Float @composable get() = LocalDensity.current.toPx(this)
If you like don’t forget to give a star
Thanks for the reply, but you shared Dp.toPx
(which seems to be incorrect since there isn't a toPx function that accepts a Dp) while your code looks to use TextUnit.toDp
?
Oh sorry, here is the correct code:
val TextUnit.toDp: Dp @composable get() = LocalDensity.current.toDp(this)
fun Density.toDp(sp: TextUnit): Dp = sp.toDp()
This extension property converts a TextUnit to Dp within a Composable function using the current LocalDensity.
@iadtgar Sorry, I accidentally deleted your previous post. Here it is:
Thanks, The previews fail with java.lang.IllegalStateException: Only Sp can convert to Px I assume because it is TextUnitType.Unspecified while the extension functions assume TextUnitType.Sp. If I set the textStyle on the ListPicker with a value from MaterialTheme.typography it seems to work since lineHeight is actually set. I'm just wondering how you got this to work using TextStyle(fontSize = 32.sp)
Thank you for reporting this, I’ve made the appropriate change to the code
Thank you, the last thing that I noticed - If you use some values from
MaterialTheme.typography
such asMaterialTheme.typography.bodyLarge
the initial state of the list isn't showing the selected item in the center. Once you scroll the list it seems to correct itself
Please give me also the code to be able to reproduce this issue
Thank you, the last thing that I noticed - If you use some values from
MaterialTheme.typography
such asMaterialTheme.typography.bodyLarge
the initial state of the list isn't showing the selected item in the center. Once you scroll the list it seems to correct itself
Try to use the default font. I use Inter font in the project and had the same issue. I tried to remove the line with textStyle
and it worked
What does ComposeScope and InvokedEffect refer to?
@nasserkhosravi You can replace InvokedEffect with LaunchedEffect and remove the ComposeScope function. I initially used them for performance optimization:
1- ComposeScope was used to minimize the recomposition scope size.
2- InvokedEffect is more lightweight, as LaunchedEffect creates a coroutine internally, which can introduce some overhead and delay when executing potentially suspending functions.
I'll be updating the code to include their implementation.
@inidamleader Thank you very much for sharing the code. I would like to know if it is possible to achieve an infinite scrolling, such as ... 0 1 2 ... 23 0 1 2 ...
LaunchedEffect(key1 = values) {
lazyListState.scrollToItem(scrollOfItem(initialValue) ?: 0) // +
snapshotFlow { lazyListState.firstVisibleItemIndex }.collectLatest {
onValueChange(values[it % listSize])
}
}
Also, I'd like to know if it's right to add one line of code here? In my codes, without this, after I dynamically change the parameters values
and initialValue
, the initialValue
doesn't work because the scroll state of lazyListState has not changed.
@Zeng1998 Thank you for your question! Yes, you can achieve infinite scrolling by setting the wrapSelectorWheel parameter to true. This will allow the items to scroll continuously, wrapping around once they reach the end.
Regarding your second point, you're correct—the initialValue might not update properly because the scroll state of lazyListState doesn’t reset automatically. You can modify the code based on your needs, and yes, adding the line to reset the scroll state could be the right approach, though I don’t fully recall the exact implementation at the moment. I’ll review and fix this today.
Feel free to reach out if you need any further clarification!