Last active
September 10, 2026 18:19
-
-
Save inidamleader/7bcc273afe6b885738556d190582a815 to your computer and use it in GitHub Desktop.
ListPicker: Generic compose picker (Number picker, date or any other type) function to select an item from a list by scrolling through the list with: possibility of editing and wrapSelectorWheel parameter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.inidamleader.util.ui.picker | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.foundation.clickable | |
| import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior | |
| import androidx.compose.foundation.interaction.MutableInteractionSource | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.Spacer | |
| import androidx.compose.foundation.layout.height | |
| 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.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.material3.ripple | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.SideEffect | |
| 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.saveable.rememberSaveable | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.runtime.snapshotFlow | |
| import androidx.compose.runtime.structuralEqualityPolicy | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.clip | |
| import androidx.compose.ui.focus.FocusRequester | |
| import androidx.compose.ui.focus.focusRequester | |
| import androidx.compose.ui.focus.onFocusChanged | |
| import androidx.compose.ui.graphics.Brush | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.platform.LocalLocale | |
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | |
| import androidx.compose.ui.semantics.onClick | |
| import androidx.compose.ui.semantics.semantics | |
| import androidx.compose.ui.semantics.stateDescription | |
| import androidx.compose.ui.text.TextRange | |
| 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.input.TextFieldValue | |
| 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 androidx.compose.ui.unit.isSpecified | |
| import androidx.compose.ui.unit.sp | |
| import androidx.compose.ui.util.fastCoerceIn | |
| import com.inidamleader.util.BuildConfig | |
| import com.inidamleader.util.format | |
| import com.inidamleader.util.geometry.toDp | |
| import com.inidamleader.util.global.fullWidthModifier | |
| import com.inidamleader.util.global.integerOnlyRegex | |
| import com.inidamleader.util.minusDays | |
| import com.inidamleader.util.now | |
| import com.inidamleader.util.performance.BackHandler | |
| import com.inidamleader.util.performance.ComposeScope | |
| import com.inidamleader.util.ui.fadingEdge | |
| import kotlinx.coroutines.flow.collectLatest | |
| import kotlinx.coroutines.flow.filter | |
| import kotlinx.coroutines.flow.map | |
| import java.time.format.FormatStyle | |
| /** | |
| * A controlled selector-wheel picker for choosing an item from [getValues]. | |
| * | |
| * The caller owns the value exposed by [getValue]. User-driven wheel changes and valid text edits | |
| * are reported through [onValueChange]; external changes are reflected by scrolling the wheel | |
| * without sending the same value back to the caller. Providers are read only by the picker areas | |
| * that need them, keeping caller observation scopes small. | |
| * | |
| * @param getValue Provides the currently selected value. It must be present in [getValues]. | |
| * @param getValues Provides the non-empty list of selectable values. | |
| * @param onValueChange Called when a user selects a different value. | |
| * @param modifier Modifier for customizing the appearance of the picker. | |
| * @param wrapSelectorWheel Whether the wheel wraps around at the beginning and end of [values]. | |
| * @param format Formats an item for display and accessibility state descriptions. | |
| * @param parse Parses text entered while editing. A parsed item must also be in [values]. | |
| * @param getIsError Whether the inline-edit field is in an error state. | |
| * @param onIsErrorChange Called when inline-edit validation changes while editing. | |
| * @param beyondViewportPageCount Number of rows shown above and below the selected row. | |
| * @param textStyle Text style for wheel rows and inline editing. | |
| * @param verticalPadding Vertical padding within each row. | |
| * @param keyboardType Keyboard type used for inline editing. | |
| * @param enableEditing Whether tapping the selected row opens inline editing. Requires [parse]. | |
| * @param selectionContainerColor Background color behind the selected row. | |
| * @param selectedTextColor Text color for the selected row. | |
| * @param unselectedTextColor Text color for unselected rows. | |
| * | |
| * @throws IllegalArgumentException if the list from [getValues] is empty, the value from | |
| * [getValue] is absent from it, or inline editing is enabled without a parser. | |
| */ | |
| @Composable | |
| fun <E> ListPicker( | |
| getValue: () -> E, | |
| getValues: () -> List<E>, | |
| onValueChange: (E) -> Unit, | |
| getIsError: () -> Boolean, | |
| onIsErrorChange: (Boolean) -> Unit, | |
| modifier: Modifier = Modifier, | |
| wrapSelectorWheel: Boolean = false, | |
| format: E.() -> String = { toString() }, | |
| parse: (String.() -> E?)? = null, | |
| enableEditing: Boolean = parse != null, | |
| beyondViewportPageCount: Int = 1, | |
| textStyle: TextStyle = LocalTextStyle.current, | |
| verticalPadding: Dp = 16.dp, | |
| keyboardType: KeyboardType = KeyboardType.Text, | |
| selectionContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer, | |
| selectedTextColor: Color = MaterialTheme.colorScheme.onSecondaryContainer, | |
| unselectedTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| ) { | |
| if (BuildConfig.DEBUG) { | |
| require(!enableEditing || parse != null) { "ListPicker requires parse when enableEditing is true." } | |
| LaunchedEffect(Unit) { | |
| snapshotFlow { getValue() to getValues() }.collectLatest { (value, values) -> | |
| require(values.isNotEmpty()) { "ListPicker values must not be empty." } | |
| require(value in values) { "ListPicker value must be present in values." } | |
| } | |
| } | |
| } | |
| val wheelConfiguration by remember(key1 = beyondViewportPageCount, key2 = wrapSelectorWheel) { | |
| derivedStateOf(policy = structuralEqualityPolicy()) { | |
| WheelConfiguration( | |
| listSize = getValues().size, | |
| beyondViewportPageCount = beyondViewportPageCount, | |
| wrapSelectorWheel = wrapSelectorWheel, | |
| ) | |
| } | |
| } | |
| val lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = remember { wheelConfiguration.scrollIndexFor(getValues().indexOf(getValue())) }) | |
| val resolvedTextStyle = LocalTextStyle.current.merge(textStyle) | |
| val rowContentHeight = when { | |
| resolvedTextStyle.lineHeight.isSpecified -> resolvedTextStyle.lineHeight | |
| resolvedTextStyle.fontSize.isSpecified -> resolvedTextStyle.fontSize | |
| else -> 20.sp | |
| }.toDp() | |
| val itemHeight = (rowContentHeight + verticalPadding * 2f).coerceAtLeast(56.dp) | |
| val pickerHeight = itemHeight * wheelConfiguration.visibleItemsCount | |
| val selectionShape = MaterialTheme.shapes.small | |
| var editing by rememberSaveable { mutableStateOf(false) } | |
| var hasEditingFocus by remember { mutableStateOf(false) } | |
| var editValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } | |
| val keyboardController = LocalSoftwareKeyboardController.current | |
| val dismissEditing: () -> Unit = { | |
| editing = false | |
| hasEditingFocus = false | |
| onIsErrorChange(false) | |
| keyboardController?.hide() | |
| } | |
| LaunchedEffect(key1 = Unit) { | |
| snapshotFlow { lazyListState.firstVisibleItemIndex to lazyListState.isScrollInProgress }.filter { !it.second }.map { it.first }.collectLatest { firstVisibleItemIndex -> | |
| getValues().let { onValueChange(it[firstVisibleItemIndex % it.size]) } | |
| } | |
| } | |
| LaunchedEffect(key1 = Unit) { | |
| snapshotFlow { getValue() to getValues() }.collectLatest { (value, values) -> | |
| val valueIndex = values.indexOf(value) | |
| val currentIndex = lazyListState.firstVisibleItemIndex % values.size | |
| if (valueIndex != currentIndex) lazyListState.scrollToItem(index = wheelConfiguration.scrollIndexFor(valueIndex)) | |
| } | |
| } | |
| LaunchedEffect(key1 = Unit) { | |
| snapshotFlow { editing }.filter { it }.collectLatest { | |
| val initialText = getValue().format() | |
| editValue = TextFieldValue( | |
| text = initialText, | |
| selection = TextRange(start = 0, end = initialText.length), | |
| ) | |
| onIsErrorChange(false) | |
| } | |
| } | |
| BackHandler(getIsEnabled = { editing }) { | |
| dismissEditing() | |
| } | |
| Box( | |
| modifier = modifier | |
| .height(pickerHeight) | |
| .semantics { | |
| stateDescription = getValue().format() | |
| if (enableEditing && !editing) { | |
| onClick(label = "Edit selected value") { | |
| editing = true | |
| true | |
| } | |
| } | |
| }, | |
| contentAlignment = Alignment.Center, | |
| ) { | |
| Surface( | |
| color = selectionContainerColor, | |
| shape = selectionShape, | |
| modifier = fullWidthModifier.height(itemHeight), | |
| ) {} | |
| ComposeScope { | |
| LazyColumn( | |
| state = lazyListState, | |
| flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| userScrollEnabled = !editing, | |
| modifier = fullWidthModifier | |
| .height(pickerHeight) | |
| .fadingEdge( | |
| brush = remember { | |
| Brush.verticalGradient( | |
| 0f to Color.Transparent, | |
| 0.5f to Color.Black, | |
| 1f to Color.Transparent, | |
| ) | |
| }, | |
| ), | |
| ) { | |
| items( | |
| count = wheelConfiguration.itemCount, | |
| key = { it }, | |
| ) { index -> | |
| val getHasValue = { wrapSelectorWheel or (index in wheelConfiguration.listStartIndex..<wheelConfiguration.listEndIndex) } | |
| val item by remember(key1 = index) { | |
| derivedStateOf(policy = structuralEqualityPolicy()) { | |
| if (getHasValue()) { | |
| val values = getValues() | |
| values[(index - wheelConfiguration.listStartIndex) % values.size] | |
| } else null | |
| } | |
| } | |
| val isSelected by remember(key1 = index) { | |
| derivedStateOf(policy = structuralEqualityPolicy()) { | |
| getHasValue() && index == lazyListState.firstVisibleItemIndex + wheelConfiguration.outOfBoundsPageCount | |
| } | |
| } | |
| ComposeScope { | |
| Text( | |
| text = item?.format().orEmpty(), | |
| maxLines = 1, | |
| overflow = TextOverflow.Ellipsis, | |
| textAlign = TextAlign.Center, | |
| style = resolvedTextStyle, | |
| color = if (isSelected) selectedTextColor else unselectedTextColor.copy(alpha = 0.62f), | |
| modifier = kotlin.run { | |
| val rowModifier = fullWidthModifier | |
| .height(itemHeight) | |
| .padding(vertical = verticalPadding) | |
| if (isSelected && enableEditing) { | |
| rowModifier.clickable( | |
| onClick = { editing = true }, | |
| indication = ripple(bounded = false), | |
| interactionSource = remember { MutableInteractionSource() }, | |
| ) | |
| } else rowModifier | |
| }, | |
| ) | |
| } | |
| } | |
| } | |
| AnimatedVisibility( | |
| visible = editing, | |
| enter = fadeIn(), | |
| exit = fadeOut(), | |
| modifier = Modifier.align(Alignment.Center), | |
| ) { | |
| val focusRequester = remember { FocusRequester() } | |
| SideEffect(key1 = Unit) { | |
| focusRequester.requestFocus() | |
| } | |
| ComposeScope { | |
| TextField( | |
| modifier = fullWidthModifier | |
| .height(itemHeight) | |
| .clip(selectionShape) | |
| .focusRequester(focusRequester) | |
| .onFocusChanged { focusState -> | |
| when { | |
| focusState.isFocused -> hasEditingFocus = true | |
| hasEditingFocus -> dismissEditing() | |
| } | |
| }, | |
| value = editValue, | |
| onValueChange = { enteredValue -> | |
| editValue = enteredValue | |
| val parsedValue = parse?.invoke(enteredValue.text) | |
| if (parsedValue != null && parsedValue in getValues()) { | |
| onIsErrorChange(false) | |
| if (parsedValue != getValue()) onValueChange(parsedValue) | |
| } else onIsErrorChange(true) | |
| }, | |
| textStyle = resolvedTextStyle.copy(textAlign = TextAlign.Center), | |
| keyboardOptions = KeyboardOptions.Default.copy( | |
| keyboardType = keyboardType, | |
| imeAction = ImeAction.Done, | |
| ), | |
| keyboardActions = KeyboardActions(onDone = { dismissEditing() }), | |
| singleLine = true, | |
| isError = getIsError(), | |
| colors = TextFieldDefaults.colors( | |
| focusedContainerColor = selectionContainerColor, | |
| unfocusedContainerColor = selectionContainerColor, | |
| errorContainerColor = selectionContainerColor, | |
| focusedIndicatorColor = Color.Transparent, | |
| unfocusedIndicatorColor = Color.Transparent, | |
| errorIndicatorColor = Color.Transparent, | |
| focusedTextColor = selectedTextColor, | |
| unfocusedTextColor = selectedTextColor, | |
| errorTextColor = MaterialTheme.colorScheme.error, | |
| ), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private data class WheelConfiguration( | |
| val listSize: Int, | |
| val outOfBoundsPageCount: Int, | |
| val iteration: Int, | |
| ) { | |
| constructor( | |
| listSize: Int, | |
| beyondViewportPageCount: Int, | |
| wrapSelectorWheel: Boolean, | |
| ) : this( | |
| listSize = listSize, | |
| outOfBoundsPageCount = beyondViewportPageCount.fastCoerceIn(0, listSize / 2), | |
| iteration = if (wrapSelectorWheel) (Int.MAX_VALUE - 2 * beyondViewportPageCount.fastCoerceIn(0, listSize / 2)) / listSize else 1, | |
| ) | |
| val visibleItemsCount: Int = 1 + outOfBoundsPageCount * 2 | |
| val listStartIndex: Int = outOfBoundsPageCount | |
| val listEndIndex: Int = listStartIndex + iteration * listSize | |
| val itemCount: Int = listEndIndex + outOfBoundsPageCount | |
| fun scrollIndexFor(valueIndex: Int): Int = valueIndex + listSize * (iteration / 2) | |
| } | |
| @Preview(widthDp = 300) | |
| @Composable | |
| private fun Preview1() { | |
| MaterialTheme { | |
| Surface(color = MaterialTheme.colorScheme.primary) { | |
| var value by remember { mutableStateOf(now().date) } | |
| val list = remember { | |
| buildList { | |
| repeat(10) { | |
| add(now().date.minusDays((it - 5).toLong())) | |
| } | |
| } | |
| } | |
| val locale = LocalLocale.current.platformLocale | |
| ListPicker( | |
| getValue = { value }, | |
| getValues = { list }, | |
| onValueChange = { value = it }, | |
| getIsError = { false }, | |
| onIsErrorChange = {}, | |
| wrapSelectorWheel = true, | |
| format = { format(formatStyle = FormatStyle.MEDIUM, locale = locale) }, | |
| textStyle = MaterialTheme.typography.labelLarge, | |
| verticalPadding = 8.dp, | |
| keyboardType = KeyboardType.Number, | |
| ) | |
| } | |
| } | |
| } | |
| @Preview(widthDp = 100) | |
| @Composable | |
| private fun Preview2() { | |
| MaterialTheme { | |
| Surface(color = MaterialTheme.colorScheme.tertiary) { | |
| var value by remember { mutableStateOf("5") } | |
| val values = remember { (1..10).map { it.toString() } } | |
| ListPicker( | |
| getValue = { value }, | |
| getValues = { values }, | |
| onValueChange = { value = it }, | |
| getIsError = { false }, | |
| onIsErrorChange = {}, | |
| beyondViewportPageCount = 2, | |
| textStyle = MaterialTheme.typography.labelLarge, | |
| verticalPadding = 8.dp, | |
| keyboardType = KeyboardType.Number, | |
| ) | |
| } | |
| } | |
| } | |
| @Preview | |
| @Composable | |
| private fun Preview3() { | |
| MaterialTheme { | |
| 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 = fullWidthModifier.padding(8.dp), | |
| style = MaterialTheme.typography.bodyMedium, | |
| ) | |
| } | |
| Spacer(modifier = Modifier.height(16.dp)) | |
| Surface { | |
| ListPicker( | |
| getValue = { value }, | |
| getValues = { list }, | |
| onValueChange = { value = it }, | |
| getIsError = { true }, | |
| onIsErrorChange = {}, | |
| parse = { | |
| try { | |
| takeIf { it.matches(integerOnlyRegex) }?.toInt() | |
| } catch (_: NumberFormatException) { | |
| null | |
| } | |
| }, | |
| beyondViewportPageCount = 2, | |
| textStyle = MaterialTheme.typography.labelLarge, | |
| verticalPadding = 8.dp, | |
| keyboardType = KeyboardType.Number, | |
| ) | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.inidamleader.ovtracker.util.compose | |
| import androidx.compose.runtime.Composable | |
| @Composable | |
| fun ComposeScope(content: @Composable () -> Unit) { | |
| content() | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @Stable | |
| fun Modifier.fadingEdge(brush: Brush, getIsEnabled: () -> Boolean = { true }): Modifier = graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithContent { | |
| drawContent() | |
| if (getIsEnabled()) drawRect(brush = brush, blendMode = BlendMode.DstIn) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey! I have issues with updating the ListPicker state on data loads. This is my UI component and the associated state.
I see log output for the initial value of 75.0 and the latest weight fetched with 161.0. But the UI never updates. It looks like ListPicker does not update on initialValue changes to me. Is that the case?
I think the recomposition chain works, just that the ListPicker doesn't update its state.