Document TextPicker Composable and it's util classes, functions

This commit is contained in:
Gergely Hegedus 2022-06-15 12:40:59 +03:00
parent be18d9859d
commit ef15aca31f
15 changed files with 311 additions and 45 deletions

View file

@ -46,8 +46,8 @@ fun NumberPickerScope.LinedInnerTextPicker(modifier: Modifier = Modifier) {
modifier = modifier,
textForIndex = textForIndex,
itemCount = itemCount,
selected = selectedIndex,
onSelectedChange = onSelectedIndexChange,
selectedIndex = selectedIndex,
onSelectedIndexChange = onSelectedIndexChange,
onIndexDifferenceChanging = onIndexDifferenceChanging,
state = state
)

View file

@ -11,17 +11,45 @@ import org.fknives.android.compose.picker.text.util.TextPickerDefaults
import org.fknives.android.compose.picker.text.util.rememberTextPickerAnimator
import org.fknives.android.compose.picker.text.util.rememberTextPickerState
/**
* Customised version of TextPicker.
* Adds additional Dividers by [textPickerContent] and places them by customized [textPickerMeasurePolicy]
* The Default Dividers can be customised by giving parameters to [LinedTextPickerContent] as [textPickerContent].
*
* If no selection changes, snaps back to the selected element.
* Longer drags and letting go, animates scrolling over elements.
* Remembers the previously selected element and when selecting programmatically, scrolls to it..
* (Selected by UI action makes it previously selected before sending back the event, aka no double animation)
*
* @param modifier Standard Modifier for the Layout
* @param textForIndex Used to display Text for the given index. Indexes are between `0 until [itemCount]`.
* @param itemCount Number of Elements that can be selected
* @param selectedIndex Currently selected index. Must be between `0 until [itemCount]`
* @param onSelectedIndexChange Notified after animation, what the User selected through dragging of the UI.
* @param textStyle Default textStyle provided for the Text Composables inside the Layout.
* @param roundAround Behavioural change:
* Setting this to True, means the elements behave like a wheel, and the last element is above the first.
* Setting this to False, means the first element has no elements above it, and the last element has no element below it.
* @param onIndexDifferenceChanging Signals the animation changes, how much the current dragging is away from [selectedIndex].
* Negative values mean the index were decreased, Positive means it was increased.
* @param state Animation State for the TextPicker
* @param animator Uses state to Animate the Composable elements. Handles continous drag, calculating fling and snapping to an index.
* @param textPickerContent The actual Composables inside the TextPicker, by default it is the 4 Texts and the 2 Dividers
* @param textPickerMeasurePolicy The MeasurePolicy of the Layout, by default Translates the 4 Texts in [textPickerContent]
* according to [state] changed by [animator]
* @param pickerItem The Expected Text Composables in [textPickerContent].
*/
@Composable
fun LinedTextPicker(
modifier: Modifier = Modifier,
textForIndex: (Int) -> String,
itemCount: Int,
selected: Int,
onSelectedChange: (Int) -> Unit,
selectedIndex: Int,
onSelectedIndexChange: (Int) -> Unit,
textStyle: TextStyle = LocalTextStyle.current,
roundAround: Boolean = TextPickerDefaults.roundAround,
onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging,
state: TextPickerState = rememberTextPickerState(selected = selected, itemCount = itemCount),
state: TextPickerState = rememberTextPickerState(selected = selectedIndex, itemCount = itemCount),
animator: TextPickerAnimator = rememberTextPickerAnimator(roundAround = roundAround, onIndexDifferenceChanging = onIndexDifferenceChanging),
textPickerContent: TextPickerContent = LinedTextPickerContent(),
textPickerMeasurePolicy: MeasurePolicy = remember(state) { LinedTextPickerMeasurePolicy(state) },
@ -31,8 +59,8 @@ fun LinedTextPicker(
modifier = modifier,
textForIndex = textForIndex,
itemCount = itemCount,
selectedIndex = selected,
onSelectedIndexChange = onSelectedChange,
selectedIndex = selectedIndex,
onSelectedIndexChange = onSelectedIndexChange,
textStyle = textStyle,
roundAround = roundAround,
onIndexDifferenceChanging = onIndexDifferenceChanging,

View file

@ -15,13 +15,48 @@ import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.text.TextStyle
import org.fknives.android.compose.picker.text.content.DefaultTextPickerContent
import org.fknives.android.compose.picker.text.content.DefaultTextPickerMeasurePolicy
import org.fknives.android.compose.picker.text.content.TextPickerContent
import org.fknives.android.compose.picker.text.content.defaultNumberPickerTextAlphaModifier
import org.fknives.android.compose.picker.text.util.TextPickerDefaults
import org.fknives.android.compose.picker.text.util.rememberDefaultMoveUnsafeToProperIndex
import org.fknives.android.compose.picker.text.util.rememberTextPickerAnimator
import org.fknives.android.compose.picker.text.util.rememberTextPickerState
import org.fknives.android.compose.picker.text.util.rememberWrappedTextForIndex
import org.fknives.android.compose.picker.text.util.rememberSafeTextForIndex
/**
* NumberPicker Custom Layout Composable which shows given text on each index.
* Shows 3 selections options (while animating may show 4).
* Scrolling the Composable moves between elements and at the end of the movement is selects the most middle element.
*
* If no selection changes, snaps back to the selected element.
* Longer drags and letting go, animates scrolling over elements.
* Remembers the previously selected element and when selecting programmatically, scrolls to it..
* (Selected by UI action makes it previously selected before sending back the event, aka no double animation)
*
* @param modifier Standard Modifier for the Layout
* @param textForIndex Used to display Text for the given index. Indexes are between `0 until [itemCount]`.
* @param itemCount Number of Elements that can be selected
* @param selectedIndex Currently selected index. Must be between `0 until [itemCount]`
* @param onSelectedIndexChange Notified after animation, what the User selected through dragging of the UI.
* @param textStyle Default textStyle provided for the Text Composables inside the Layout.
* @param roundAround Behavioural change:
* Setting this to True, means the elements behave like a wheel, and the last element is above the first.
* Setting this to False, means the first element has no elements above it, and the last element has no element below it.
* @param onIndexDifferenceChanging Signals the animation changes, how much the current dragging is away from [selectedIndex].
* Negative values mean the index were decreased, Positive means it was increased.
* @param state Animation State for the TextPicker
* @param animator Uses state to Animate the Composable elements. Handles continous drag, calculating fling and snapping to an index.
* @param textPickerContent The actual Composables inside the TextPicker, by default it is the 4 Texts
* @param textPickerMeasurePolicy The MeasurePolicy of the Layout, by default Translates the 4 Texts in [textPickerContent]
* according to [state] changed by [animator]
* @param pickerItem The Expected Text Composables in [textPickerContent].
* text is the given Text for the Item.
* translation is the movement of the element:
* - 1.0 -> selected
* - 0.5 -> unselected
* - 0.0 -> completely outside of boundaries.
* - movement between translation are expected to be proper (such as setting alpha based on the translation linearly).
*/
@Composable
fun TextPicker(
modifier: Modifier = Modifier,
@ -40,7 +75,12 @@ fun TextPicker(
) {
val moveUnsafeToProperIndex: (Int) -> Int = rememberDefaultMoveUnsafeToProperIndex(itemCount = itemCount, roundAround = roundAround)
val rememberTextForIndex = rememberWrappedTextForIndex(itemCount = itemCount, roundAround = roundAround, textForIndex = textForIndex)
val rememberTextForIndex = rememberSafeTextForIndex(
itemCount = itemCount,
roundAround = roundAround,
textForIndex = textForIndex,
moveUnsafeToProperIndex = moveUnsafeToProperIndex
)
Layout(
modifier = modifier
@ -70,7 +110,6 @@ fun TextPicker(
textPickerContent.Content(
textForIndex = rememberTextForIndex,
item = pickerItem,
moveUnsafeToProperIndex = moveUnsafeToProperIndex,
selected = moveUnsafeToProperIndex(selectedIndex + state.indexOffset),
before1TranslatePercent = state.before1TranslatePercent,
itemTranslatePercent = state.itemTranslatePercent,
@ -83,16 +122,3 @@ fun TextPicker(
)
}
fun interface TextPickerContent {
@Composable
fun Content(
textForIndex: (Int) -> String,
selected: Int,
moveUnsafeToProperIndex: (Int) -> Int,
before1TranslatePercent: Float,
itemTranslatePercent: Float,
after1TranslatePercent: Float,
after2TranslatePercent: Float,
item: @Composable (text: String, translation: Float) -> Unit
)
}

View file

@ -16,6 +16,21 @@ import org.fknives.android.compose.picker.text.util.TextPickerDefaults
import kotlin.math.abs
import kotlin.math.roundToInt
/**
* Animator for [TextPicker]
*
* Handles 3 actions:
* - Continuous Dragging via [onDeltaY]
* - Scrolling Fling Animation via [flingAndSnap]
* - Scrolling newly selected item via [snapToIndex]
*
* @param flingDecaySpec AnimationSpec calculating expected scrolling by the given velocity. Used for the Fling Animation.
* @param flingAnimationSpec AnimationSpec used to do the actual scrolling animation, used in both animations.
* @param velocityMultiplier Controls Velocity to Slow or Fasten fling calculation, Raw Velocity is multiplied with this parameter.
* @param offsetLimiter Limits the offset animation. This can be used, to not let over scrolling happen after a given index or similar.
* @param onIndexDifferenceChanging Index Difference updates as animations and dragging happens.
* Negative values mean the index were decreased, Positive means it was increased.
*/
class TextPickerAnimator(
private val flingDecaySpec: DecayAnimationSpec<Float>,
private val flingAnimationSpec: AnimationSpec<Float> = spring(0.75f),

View file

@ -6,6 +6,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
/**
* Internal State for [TextPicker]
*
* Holds the Scrolling [offset] changes and Text [itemSize]-s.
* From this the Translations and index changes are Derived.
*/
class TextPickerState(
itemSizeState: MutableState<Float>,
previousSelectedState: MutableState<Int>,

View file

@ -1,24 +1,33 @@
package org.fknives.android.compose.picker.text.content
import androidx.compose.runtime.Composable
import org.fknives.android.compose.picker.text.TextPickerContent
/**
* Default [TextPickerContent] for [TextPicker][org.fknives.android.compose.picker.text.TextPicker].
*/
class DefaultTextPickerContent : TextPickerContent {
/**
* Creates the actual content.
*
* Calculates the indexes around [selected], gets each item's text via [textForIndex].
* From that the [item]s are placed with their respective text and given translation.
*
* The items are expected to be places by [DefaultTextPickerContent]
*/
@Composable
override fun Content(
textForIndex: (Int) -> String,
selected: Int,
moveUnsafeToProperIndex: (Int) -> Int,
before1TranslatePercent: Float,
itemTranslatePercent: Float,
after1TranslatePercent: Float,
after2TranslatePercent: Float,
item: @Composable (text: String, translation: Float) -> Unit
) {
val before1 = moveUnsafeToProperIndex(selected - 1)
val after1 = moveUnsafeToProperIndex(selected + 1)
val after2 = moveUnsafeToProperIndex(selected + 2)
val before1 = selected - 1
val after1 = selected + 1
val after2 = selected + 2
item(
text = textForIndex(before1),
translation = before1TranslatePercent

View file

@ -7,6 +7,9 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.style.TextAlign
import org.fknives.android.compose.picker.text.util.TextPickerDefaults
/**
* Default Text Label inside a [TextPicker][org.fknives.android.compose.picker.text.TextPicker]
*/
@Composable
fun DefaultNumberPickerText(
text: String,
@ -19,6 +22,28 @@ fun DefaultNumberPickerText(
)
}
/**
* Wraps [numberPickerLabel], to calculate the corresponding alpha from the given Translation.
*
* @param unselectedAlpha The alpha value an unselected element in [TextPicker][org.fknives.android.compose.picker.text.TextPicker] should have.
* @param selectedAlpha The alpha value a selected element in [TextPicker][org.fknives.android.compose.picker.text.TextPicker] should have.
* @param numberPickerLabel the actual label Composable.
* @param modifier additional [Modifier] for [numberPickerLabel]
*
* As described in [TextPicker][org.fknives.android.compose.picker.text.TextPicker].
* translation is the movement of the element:
* - 1 -> selected
* - 0.5 -> unselected
* - 0 -> completely outside of boundaries.
* - movement between translation are expected to be proper (such as setting alpha based on the translation linearly).
*
* This mediator, calculates the linear alpha in the following way:
* - 1.0 -> [selectedAlpha] is shown
* - 0.75 -> middle way linearly between [selectedAlpha] and [unselectedAlpha]
* - 0.5f -> [unselectedAlpha]
* - 0.25 -> middle way linearly between [unselectedAlpha] and 0 Alpha.
* - 0.0 -> completely gone 0 Alpha.
*/
fun defaultNumberPickerTextAlphaModifier(
modifier: Modifier = Modifier,
unselectedAlpha: Float = TextPickerDefaults.unselectedAlpha,

View file

@ -8,6 +8,16 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import org.fknives.android.compose.picker.text.TextPickerState
/**
* Default TextMeasurePolicy of [TextPicker][org.fknives.android.compose.picker.text.TextPicker].
*
* Intended to be used together with [TextPickerContent] and [DefaultTextPickerContent].
*
* The expected [numberOfItemsToPlace] items are places one below the other, translated by [state].
* Only [numberOfHeightMeasuringItems] items are counted in height, because elements are expected to be translated in and out of boundaries.
*
* Can be extended via [placementAfterItems] to place additional Items if necessary.
*/
open class DefaultTextPickerMeasurePolicy(private val state: TextPickerState) : MeasurePolicy {
open val numberOfItemsToPlace = 4

View file

@ -7,10 +7,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Dp
import org.fknives.android.compose.picker.text.TextPickerContent
import org.fknives.android.compose.picker.text.util.TextPickerDefaults
import org.fknives.android.compose.picker.text.TextPickerState
/**
* Default [TextPickerContent] for [LinedTextPicker][org.fknives.android.compose.picker.text.LinedTextPicker].
*/
class LinedTextPickerContent(
private val dividerModifier: Modifier = Modifier,
private val dividerColor: @Composable () -> Color = { MaterialTheme.colors.onSurface.copy(alpha = TextPickerDefaults.dividerAlpha) },
@ -18,20 +20,28 @@ class LinedTextPickerContent(
private val dividerStartIndent: Dp = TextPickerDefaults.dividerStartIndent
) : TextPickerContent {
/**
* Creates the actual content.
*
* Calculates the indexes around [selected], gets each item's text via [textForIndex].
* From that the [item]s are placed with their respective text and given translation.
*
* Also adds 2 Divider configured by constructor at the end.
* The Dividers are expected to be places by [LinedTextPickerMeasurePolicy].
*/
@Composable
override fun Content(
textForIndex: (Int) -> String,
selected: Int,
moveUnsafeToProperIndex: (Int) -> Int,
before1TranslatePercent: Float,
itemTranslatePercent: Float,
after1TranslatePercent: Float,
after2TranslatePercent: Float,
item: @Composable (text: String, translation: Float) -> Unit
) {
val before1 = moveUnsafeToProperIndex(selected - 1)
val after1 = moveUnsafeToProperIndex(selected + 1)
val after2 = moveUnsafeToProperIndex(selected + 2)
val before1 = selected - 1
val after1 = selected + 1
val after2 = selected + 2
item(
text = textForIndex(before1),
translation = before1TranslatePercent
@ -63,6 +73,12 @@ class LinedTextPickerContent(
}
}
/**
* Same as [DefaultTextPickerMeasurePolicy], with additiona Divider positioning.
*
* Expected to be used together with [LinedTextPickerContent] providing the Dividers,
* which this MeasurePolicy places around the selected Item's usual place.
*/
class LinedTextPickerMeasurePolicy(state: TextPickerState) : DefaultTextPickerMeasurePolicy(state = state) {
override fun Placeable.PlacementScope.placementAfterItems(placeables: List<Placeable>, state: TextPickerState) {

View file

@ -0,0 +1,16 @@
package org.fknives.android.compose.picker.text.content
import androidx.compose.runtime.Composable
fun interface TextPickerContent {
@Composable
fun Content(
textForIndex: (Int) -> String,
selected: Int,
before1TranslatePercent: Float,
itemTranslatePercent: Float,
after1TranslatePercent: Float,
after2TranslatePercent: Float,
item: @Composable (text: String, translation: Float) -> Unit
)
}

View file

@ -3,10 +3,56 @@ package org.fknives.android.compose.picker.text.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
/**
* Remembers the lapCounterByIndexOfDifference function with fixed [selectedIndex] and [itemCount].
*/
@Composable
fun rememberLapsCounterByOnIndexDifference(selectedIndex: Int, itemCount: Int) = remember(selectedIndex, itemCount) {
createLapsCounterByOnIndexDifference(selectedIndex = selectedIndex, itemCount = itemCount)
}
/**
* Curry function for [lapCounterByIndexOfDifference]
* Keeps [itemCount] and [selectedIndex] for scope.
*
* @return Returned lambda expectes the indexDifference for the [lapCounterByIndexOfDifference] calculation.
*/
fun createLapsCounterByOnIndexDifference(itemCount: Int, selectedIndex: Int): (Int) -> Int = { indexDifference ->
lapCounterByIndexOfDifference(itemCount = itemCount, indexDifference = indexDifference, selectedIndex = selectedIndex)
}
/**
* Helper function intended to be used with onIndexDifferenceChanging of [TextPicker][org.fknives.android.compose.picker.text.TextPicker].
*
* Calculates how many times the TextPicker were scrolled over (first element -to-> last element by index decrease or vice versa)
* Useful when OverScrolling needs some reaction.
*
* @param itemCount Number of items
* @param selectedIndex the currently selected index, must be between `0 until [itemCount]`
* @param indexDifference the current index difference from [selectedIndex].
*
* @return The Round Around Scrolling.
*
* Example1:
* `itemCount = 3, selectedIndex = 0, indexDifference = -1.`
* This results in -1, meaning the currently shown element is index 2. 1 overscroll happened from index 0 to index 2.
*
* Example2:
* `itemCount = 3, selectedIndex = 0, indexDifference = -3.`
* This results in -1, meaning the currently shown element is index 0. 1 overscroll happened from index 0 to index 2.
*
* Example3:
* `itemCount = 3, selectedIndex = 0, indexDifference = -4.`
* This results in -2, meaning the currently shown element is index 2. 2 overscroll happened from index 0 to index 2.
*
* Example3:
* `itemCount = 3, selectedIndex = 0, indexDifference = 3.`
* This results in 1, meaning the currently shown element is index 0. 1 overscroll happened from index 2 to index 0.
*
* Example3:
* `itemCount = 3, selectedIndex = 0, indexDifference = 5.`
* This results in 1, meaning the currently shown element is index 2. 1 overscroll happened from index 2 to index 0.
*/
fun lapCounterByIndexOfDifference(
itemCount: Int,
selectedIndex: Int,
@ -28,9 +74,4 @@ fun lapCounterByIndexOfDifference(
}
return counter
}
@Composable
fun rememberLapsCounterByOnIndexDifference(selectedIndex: Int, itemCount: Int) = remember(selectedIndex, itemCount) {
createLapsCounterByOnIndexDifference(selectedIndex = selectedIndex, itemCount = itemCount)
}

View file

@ -4,10 +4,24 @@ import org.fknives.android.compose.picker.text.TextPickerState
import kotlin.math.max
import kotlin.math.min
/**
* Scrolling Offset Limiter intended for [TextPickerAnimator][org.fknives.android.compose.picker.text.TextPickerAnimator].
*
* Provides to [default]s:
* - [NoLimit] Used when scrolling should not be limited
* (like RoundAround ([TextPicker][org.fknives.android.compose.picker.text.TextPicker])
* - [MinMaxLimit] Used when scrolling should be limited to maximum scroll of up to First Element and down to LastElement.
*/
fun interface OffsetLimiter {
/**
* Hard limit on offset, to ensure indexes are within range.
*/
fun limit(offset: Float, state: TextPickerState): Float
/**
* Soft limit on offset, to enable overshooting for animations.
*/
fun overshootLimit(offset: Float, state: TextPickerState): Float =
limit(offset, state)

View file

@ -2,12 +2,19 @@ package org.fknives.android.compose.picker.text.util
import androidx.compose.ui.unit.dp
/**
* Default values for [TextPicker][org.fknives.android.compose.picker.text.TextPicker]
*/
object TextPickerDefaults {
const val selected = 0
const val velocityMultiplier = 0.3f
const val unselectedAlpha = 0.6f
const val selectedAlpha = 1f
/**
* [TextPickerAnimator][org.fknives.android.compose.picker.text.TextPickerAnimator] checks against this to not emit unnecessary updates.
*/
internal val onIndexDifferenceChanging: (Int) -> Unit = {}
const val roundAround = true
const val dividerAlpha = 0.12f
val dividerThickness = 2.dp

View file

@ -9,6 +9,9 @@ import androidx.compose.ui.unit.Density
import org.fknives.android.compose.picker.text.TextPickerAnimator
import org.fknives.android.compose.picker.text.TextPickerState
/**
* Helper function caching [TextPickerState]
*/
@Composable
fun rememberTextPickerState(selected: Int, itemCount: Int): TextPickerState {
require(selected >= 0) { "Selected value ($selected) is less than 0!" }
@ -26,6 +29,9 @@ fun rememberTextPickerState(selected: Int, itemCount: Int): TextPickerState {
}
}
/**
* Helper function caching [TextPickerAnimator]
*/
@Composable
fun rememberTextPickerAnimator(
roundAround: Boolean,
@ -43,6 +49,9 @@ fun rememberTextPickerAnimator(
)
}
/**
* Helper function caching [TextPickerAnimator]
*/
@Composable
fun rememberTextPickerAnimator(
offsetLimiter: OffsetLimiter,
@ -61,6 +70,12 @@ fun rememberTextPickerAnimator(
}
}
/**
* Creates a Lambda changing index based on [roundAround].
*
* If roundAround is False, means the index is not touched, since it should not behave like a wheel.
* IF roundAround is True, means the index is feed into [moveUnsafeToProperIndex] to behave like a wheel.
*/
@Composable
fun rememberDefaultMoveUnsafeToProperIndex(itemCount: Int, roundAround: Boolean) =
if (roundAround) {
@ -69,12 +84,21 @@ fun rememberDefaultMoveUnsafeToProperIndex(itemCount: Int, roundAround: Boolean)
remember { { index: Int -> index } }
}
/**
* Curry function for [moveUnsafeToProperIndex] which stores the [itemCount]
*
* @return lambda expecting the unsafeIndex to call [moveUnsafeToProperIndex]
*/
fun createMoveUnsafeToProperIndex(itemCount: Int) = moveUnsafeToProperIndex@{ unsafeIndex: Int ->
moveUnsafeToProperIndex(unsafeIndex, itemCount)
}
fun moveUnsafeToProperIndex(unsafeIndex: Int, itemCount: Int) : Int {
/**
* Moves a given [unsafeIndex] between `0 until [itemCount]` like it is a continuous wheel.
* Meaning that `unsafeIndex=-1` becomes `itemCount-1` while `unsafeIndex=itemCount` becomes 0.
*
*/
fun moveUnsafeToProperIndex(unsafeIndex: Int, itemCount: Int): Int {
var index = unsafeIndex
while (index < 0) {
index += itemCount
@ -85,13 +109,42 @@ fun moveUnsafeToProperIndex(unsafeIndex: Int, itemCount: Int) : Int {
return index
}
/**
* Caches function accessing text for given index safely.
* [roundAround] True means the Index will be modified if it needs to be to fall between the safe range for [textForIndex]
* [roundAround] False means the if the Index is falls outside of safeZone, [textForUnsafeIndex] will be used as fallback.
* Note: unsafeIndex means the index NOT between `0 until itemCount`.
*/
@Composable
fun rememberWrappedTextForIndex(itemCount: Int, roundAround: Boolean, textForIndex: (Int) -> String): (Int) -> String =
fun rememberSafeTextForIndex(
textForUnsafeIndex: String = "",
itemCount: Int,
roundAround: Boolean,
textForIndex: (Int) -> String,
moveUnsafeToProperIndex: (Int) -> Int
): (Int) -> String =
if (roundAround) {
textForIndex
remember(moveUnsafeToProperIndex, textForIndex) {
createSafeRoundAroundTextForIndex(moveUnsafeToProperIndex = moveUnsafeToProperIndex, textForIndex = textForIndex)
}
} else {
remember(itemCount, textForIndex) {
{ index -> if (index < 0 || index >= itemCount) "" else textForIndex(index) }
createSafeOrDefaultTextForIndex(itemCount = itemCount, textForIndex = textForIndex, textForUnsafeIndex = textForUnsafeIndex)
}
}
/**
* Returns lambda, which ensures the index is moved to into safe position by [moveUnsafeToProperIndex] before accessing [textForIndex]
*/
fun createSafeRoundAroundTextForIndex(moveUnsafeToProperIndex: (Int) -> Int, textForIndex: (Int) -> String): (Int) -> String =
{ index: Int ->
textForIndex(moveUnsafeToProperIndex(index))
}
/**
* Returns lambda, which ensures the index is between `0 until itemCount` before accessing [textForIndex], fallbacks [textForUnsafeIndex]
*/
fun createSafeOrDefaultTextForIndex(textForUnsafeIndex: String = "", itemCount: Int, textForIndex: (Int) -> String): (Int) -> String =
{ index: Int ->
if (index in 0 until itemCount) textForIndex(index) else textForUnsafeIndex
}