Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-21 14:26:26 +07:00
parent a2f57e10be
commit 6df93c1d67
5 changed files with 607 additions and 12 deletions

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.snapshotFlow
import com.tangem.feature.wallet.presentation.wallet.ui.utils.LazyListItemData
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector
@ -19,7 +20,11 @@ internal fun WalletsListEffects(
onAutoScrollReset: () -> Unit,
) {
LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
snapshotFlow {
lazyListState.layoutInfo.visibleItemsInfo.map {
LazyListItemData(it.index, it.size, it.offset)
}
}
.collect(
collector = ScrollOffsetCollector(
selectedWalletIndex = selectedWalletIndex,

View file

@ -0,0 +1,405 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.withContext
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sign
@ExperimentalFoundationApi
class TangemSnapFlingBehavior(
private val snapLayoutInfoProvider: SnapLayoutInfoProvider,
private val lowVelocityAnimationSpec: AnimationSpec<Float>,
private val highVelocityAnimationSpec: DecayAnimationSpec<Float>,
private val snapAnimationSpec: AnimationSpec<Float>,
private val density: Density,
private val shortSnapVelocityThreshold: Dp = MinFlingVelocityDp,
) : FlingBehavior {
private val velocityThreshold = with(density) { shortSnapVelocityThreshold.toPx() }
private var motionScaleDuration = DefaultScrollMotionDurationScale
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
return performFling(initialVelocity) {}
}
/**
* Perform a snapping fling animation with given velocity and suspend until fling has
* finished. This will behave the same way as [performFling] except it will report on
* each remainingOffsetUpdate using the [onSettlingDistanceUpdated] lambda.
*
* @param initialVelocity velocity available for fling in the orientation specified in
* [androidx.compose.foundation.gestures.scrollable] that invoked this method.
*
* @param onSettlingDistanceUpdated a lambda that will be called anytime the
* distance to the settling offset is updated. The settling offset is the final offset where
* this fling will stop and may change depending on the snapping animation progression.
*
* @return remaining velocity after fling operation has ended
*/
private suspend fun ScrollScope.performFling(
initialVelocity: Float,
onSettlingDistanceUpdated: (Float) -> Unit,
): Float {
val (remainingOffset, remainingState) = fling(initialVelocity, onSettlingDistanceUpdated)
// No remaining offset means we've used everything, no need to propagate velocity. Otherwise
// we couldn't use everything (probably because we have hit the min/max bounds of the
// containing layout) we should propagate the offset.
return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
}
private suspend fun ScrollScope.fling(
initialVelocity: Float,
onRemainingScrollOffsetUpdate: (Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
// If snapping from scroll (short snap) or fling (long snap)
val result = withContext(motionScaleDuration) {
if (abs(initialVelocity) <= abs(velocityThreshold)) {
shortSnap(initialVelocity, onRemainingScrollOffsetUpdate)
} else {
longSnap(initialVelocity, onRemainingScrollOffsetUpdate)
}
}
onRemainingScrollOffsetUpdate(0f) // Animation finished or was cancelled
return result
}
private suspend fun ScrollScope.shortSnap(
velocity: Float,
onRemainingScrollOffsetUpdate: (Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val closestOffset = with(snapLayoutInfoProvider) {
density.calculateSnappingOffset(0f)
}
var remainingScrollOffset = closestOffset
val animationState = AnimationState(NoDistance, velocity)
return animateSnap(
closestOffset,
closestOffset,
animationState,
snapAnimationSpec,
) { delta ->
remainingScrollOffset -= delta
onRemainingScrollOffsetUpdate(remainingScrollOffset)
}
}
private suspend fun ScrollScope.longSnap(
initialVelocity: Float,
onAnimationStep: (remainingScrollOffset: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val initialOffset =
with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
abs(it) * sign(initialVelocity) // ensure offset sign is correct
}
var remainingScrollOffset = initialOffset
onAnimationStep(remainingScrollOffset) // First Scroll Offset
val (remainingOffset, animationState) = runApproach(
initialOffset,
initialVelocity,
) { delta ->
remainingScrollOffset -= delta
onAnimationStep(remainingScrollOffset)
}
remainingScrollOffset = remainingOffset
return animateSnap(
remainingOffset,
remainingOffset,
animationState.copy(value = 0f),
snapAnimationSpec,
) { delta ->
remainingScrollOffset -= delta
onAnimationStep(remainingScrollOffset)
}
}
private suspend fun ScrollScope.runApproach(
initialTargetOffset: Float,
initialVelocity: Float,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val animation =
if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
HighVelocityApproachAnimation(highVelocityAnimationSpec)
} else {
LowVelocityApproachAnimation(
lowVelocityAnimationSpec,
snapLayoutInfoProvider,
density,
)
}
return approach(
initialTargetOffset,
initialVelocity,
animation,
snapLayoutInfoProvider,
density,
onAnimationStep,
)
}
/**
* If we can approach the target and still have velocity left
*/
private fun isDecayApproachPossible(offset: Float, velocity: Float): Boolean {
val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() }
return decayOffset.absoluteValue >= offset.absoluteValue + snapStepSize
}
override fun equals(other: Any?): Boolean {
return if (other is TangemSnapFlingBehavior) {
other.snapAnimationSpec == this.snapAnimationSpec &&
other.highVelocityAnimationSpec == this.highVelocityAnimationSpec &&
other.lowVelocityAnimationSpec == this.lowVelocityAnimationSpec &&
other.snapLayoutInfoProvider == this.snapLayoutInfoProvider &&
other.density == this.density &&
other.shortSnapVelocityThreshold == this.shortSnapVelocityThreshold
} else {
false
}
}
override fun hashCode(): Int = 0
.let { 31 * it + snapAnimationSpec.hashCode() }
.let { 31 * it + highVelocityAnimationSpec.hashCode() }
.let { 31 * it + lowVelocityAnimationSpec.hashCode() }
.let { 31 * it + snapLayoutInfoProvider.hashCode() }
.let { 31 * it + density.hashCode() }
.let { 31 * it + shortSnapVelocityThreshold.hashCode() }
}
@Suppress("LongParameterList")
@OptIn(ExperimentalFoundationApi::class)
private suspend fun ScrollScope.approach(
initialTargetOffset: Float,
initialVelocity: Float,
animation: ApproachAnimation<Float, AnimationVector1D>,
snapLayoutInfoProvider: SnapLayoutInfoProvider,
density: Density,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val (_, currentAnimationState) = animation.approachAnimation(
this,
initialTargetOffset,
initialVelocity,
onAnimationStep,
)
val remainingOffset = with(snapLayoutInfoProvider) {
density.calculateSnappingOffset(currentAnimationState.velocity)
}
// will snap the remainder
return AnimationResult(remainingOffset, currentAnimationState)
}
/**
* Runs a [AnimationSpec] to snap the list into [targetOffset]. Uses [cancelOffset] to stop this
* animation before it reaches the target.
*
* @param targetOffset The final target of this animation
* @param cancelOffset If we'd like to finish the animation earlier we use this value
* @param animationState The current animation state for continuation purposes
* @param snapAnimationSpec The [AnimationSpec] that will drive this animation
* @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
*/
@Suppress("MagicNumber")
private suspend fun ScrollScope.animateSnap(
targetOffset: Float,
cancelOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
snapAnimationSpec: AnimationSpec<Float>,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
var consumedUpToNow = 0f
val initialVelocity = animationState.velocity
animationState.animateTo(
targetOffset,
animationSpec = snapAnimationSpec,
sequentialAnimation = animationState.velocity != 0f,
) {
val realValue = value.coerceToTarget(cancelOffset)
val delta = realValue - consumedUpToNow
val consumed = scrollBy(delta)
onAnimationStep(consumed)
// stop when unconsumed or when we reach the desired value
if (abs(delta - consumed) > 0.5f || realValue != value) {
cancelAnimation()
}
consumedUpToNow += consumed
}
// Always course correct velocity so they don't become too large.
val finalVelocity = animationState.velocity.coerceToTarget(initialVelocity)
return AnimationResult(
targetOffset - consumedUpToNow,
animationState.copy(velocity = finalVelocity),
)
}
private class HighVelocityApproachAnimation(
private val decayAnimationSpec: DecayAnimationSpec<Float>,
) : ApproachAnimation<Float, AnimationVector1D> {
override suspend fun approachAnimation(
scope: ScrollScope,
offset: Float,
velocity: Float,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
return with(scope) {
animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep)
}
}
}
private class LowVelocityApproachAnimation @OptIn(ExperimentalFoundationApi::class) constructor(
private val lowVelocityAnimationSpec: AnimationSpec<Float>,
private val layoutInfoProvider: SnapLayoutInfoProvider,
private val density: Density,
) : ApproachAnimation<Float, AnimationVector1D> {
@OptIn(ExperimentalFoundationApi::class)
override suspend fun approachAnimation(
scope: ScrollScope,
offset: Float,
velocity: Float,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
val targetOffset =
(abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
velocity,
)
return with(scope) {
animateSnap(
targetOffset = targetOffset,
cancelOffset = offset,
animationState = animationState,
snapAnimationSpec = lowVelocityAnimationSpec,
onAnimationStep = onAnimationStep,
)
}
}
}
@Suppress("MagicNumber")
private suspend fun ScrollScope.animateDecay(
targetOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
decayAnimationSpec: DecayAnimationSpec<Float>,
onAnimationStep: (delta: Float) -> Unit,
): AnimationResult<Float, AnimationVector1D> {
var previousValue = 0f
fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
val consumed = scrollBy(delta)
onAnimationStep(consumed)
if (abs(delta - consumed) > 0.5f) cancelAnimation()
}
animationState.animateDecay(
animationSpec = decayAnimationSpec,
sequentialAnimation = animationState.velocity != 0f,
) {
previousValue = if (abs(value) >= abs(targetOffset)) {
val finalValue = value.coerceToTarget(targetOffset)
val finalDelta = finalValue - previousValue
consumeDelta(finalDelta)
cancelAnimation()
finalValue
} else {
val delta = value - previousValue
consumeDelta(delta)
value
}
}
return AnimationResult(
targetOffset - previousValue,
animationState,
)
}
private interface ApproachAnimation<T, V : AnimationVector> {
suspend fun approachAnimation(
scope: ScrollScope,
offset: T,
velocity: T,
onAnimationStep: (delta: T) -> Unit,
): AnimationResult<T, V>
}
private fun Float.coerceToTarget(target: Float): Float {
if (target == 0f) return 0f
return if (target > 0) coerceAtMost(target) else coerceAtLeast(target)
}
private class AnimationResult<T, V : AnimationVector>(
val remainingOffset: T,
val currentAnimationState: AnimationState<T, V>,
) {
operator fun component1(): T = remainingOffset
operator fun component2(): AnimationState<T, V> = currentAnimationState
}
@Suppress("TopLevelPropertyNaming")
private const val DefaultScrollMotionDurationScaleFactor = 1f
@Suppress("TopLevelPropertyNaming")
val DefaultScrollMotionDurationScale = object : MotionDurationScale {
override val scaleFactor: Float
get() = DefaultScrollMotionDurationScaleFactor
}
@Suppress("TopLevelPropertyNaming")
internal val MinFlingVelocityDp = 400.dp
@Suppress("TopLevelPropertyNaming")
internal const val NoDistance = 0f
@Suppress("TopLevelPropertyNaming")
internal const val NoVelocity = 0f
@ExperimentalFoundationApi
interface SnapLayoutInfoProvider {
/**
* The minimum offset that snapping will use to animate.(e.g. an item size)
*/
fun Density.calculateSnapStepSize(): Float
/**
* Calculate the distance to navigate before settling into the next snapping bound.
*
* @param initialVelocity The current fling movement velocity. You can use this tho calculate a
* velocity based offset.
*/
fun Density.calculateApproachOffset(initialVelocity: Float): Float
/**
* Given a target placement in a layout, the snapping offset is the next snapping position
* this layout can be placed in. If this is a short snapping, [currentVelocity] is guaranteed
* to be 0.If it is a long snapping, this method will be called
* after [calculateApproachOffset].
*
* @param currentVelocity The current fling movement velocity. This may change throughout the
* fling animation.
*/
fun Density.calculateSnappingOffset(currentVelocity: Float): Float
}

View file

@ -0,0 +1,177 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.unit.Density
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sign
/**
* A [SnapLayoutInfoProvider] for LazyLists.
*
* @param lazyListState The [LazyListState] with information about the current state of the list
* @param positionInLayout The desired positioning of the snapped item within the main layout.
* This position should be considered with regard to the start edge of the item and the placement
* within the viewport.
*
* @return A [SnapLayoutInfoProvider] that can be used with [SnapFlingBehavior]
*/
@Suppress("FunctionNaming")
@ExperimentalFoundationApi
fun TangemSnapLayoutInfoProvider(
lazyListState: LazyListState,
positionInLayout: SnapPositionInLayout = SnapPositionInLayout.CenterToCenter,
): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {
private val layoutInfo: LazyListLayoutInfo
get() = lazyListState.layoutInfo
// Decayed page snapping is the default
override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
val offset =
decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
return if (finalDecayOffset == 0f) {
finalDecayOffset
} else {
finalDecayOffset * initialVelocity.sign
}
}
override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
var lowerBoundOffset = Float.NEGATIVE_INFINITY
var upperBoundOffset = Float.POSITIVE_INFINITY
layoutInfo.visibleItemsInfo.fastForEach { item ->
val offset =
calculateDistanceToDesiredSnapPosition(
mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
beforeContentPadding = layoutInfo.beforeContentPadding,
afterContentPadding = layoutInfo.afterContentPadding,
itemSize = item.size,
itemOffset = item.offset,
itemIndex = item.index,
snapPositionInLayout = positionInLayout,
)
// Find item that is closest to the center
if (offset <= 0 && offset > lowerBoundOffset) {
lowerBoundOffset = offset
}
// Find item that is closest to center, but after it
if (offset >= 0 && offset < upperBoundOffset) {
upperBoundOffset = offset
}
}
return calculateFinalOffset(currentVelocity, lowerBoundOffset, upperBoundOffset)
}
override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
if (visibleItemsInfo.isNotEmpty()) {
visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
} else {
0f
}
}
}
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastSumBy(selector: (T) -> Int): Int {
contract { callsInPlace(selector) }
var sum = 0
fastForEach { element ->
sum += selector(element)
}
return sum
}
internal fun calculateFinalOffset(velocity: Float, lowerBound: Float, upperBound: Float): Float {
fun Float.isValidDistance(): Boolean {
return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY
}
val finalDistance = when (sign(velocity)) {
0f -> {
if (abs(upperBound) <= abs(lowerBound)) {
upperBound
} else {
lowerBound
}
}
1f -> upperBound
-1f -> lowerBound
else -> NoDistance
}
return if (finalDistance.isValidDistance()) {
finalDistance
} else {
NoDistance
}
}
internal val LazyListLayoutInfo.singleAxisViewportSize: Int
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
contract { callsInPlace(action) }
for (index in indices) {
val item = get(index)
action(item)
}
}
@Suppress("LongParameterList")
@OptIn(ExperimentalFoundationApi::class)
internal fun Density.calculateDistanceToDesiredSnapPosition(
mainAxisViewPortSize: Int,
beforeContentPadding: Int,
afterContentPadding: Int,
itemSize: Int,
itemOffset: Int,
itemIndex: Int,
snapPositionInLayout: SnapPositionInLayout,
): Float {
val containerSize = mainAxisViewPortSize - beforeContentPadding - afterContentPadding
val desiredDistance = with(snapPositionInLayout) {
position(containerSize, itemSize, itemIndex)
}.toFloat()
return itemOffset - desiredDistance
}
@ExperimentalFoundationApi
fun interface SnapPositionInLayout {
/**
* Calculates an offset positioning between a container and an element within this container.
* The offset calculation is the necessary diff that should be applied to the item offset to
* align the item with a position within the container. As a base line, if we wanted to align
* the start of the container and the start of the item, we would return 0 in this function.
*/
fun Density.position(layoutSize: Int, itemSize: Int, itemIndex: Int): Int
companion object {
/**
* Aligns the center of the item with the center of the containing layout.
*/
val CenterToCenter =
SnapPositionInLayout { layoutSize, itemSize, _ -> layoutSize / 2 - itemSize / 2 }
}
}

View file

@ -4,8 +4,6 @@ import androidx.compose.animation.core.*
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -22,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
@ -30,6 +29,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
private const val SHORT_SNAP_ELEMENT_COUNT = 25
/**
* Wallets list component
*
@ -53,7 +54,7 @@ internal fun WalletsList(
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState),
flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth),
) {
items(
items = wallets,
@ -80,17 +81,19 @@ internal fun WalletsList(
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState): SnapFlingBehavior {
val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): TangemSnapFlingBehavior {
val snappingLayout = remember(lazyListState) { TangemSnapLayoutInfoProvider(lazyListState) }
val density = LocalDensity.current
val highVelocityApproachSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(key1 = snappingLayout, key2 = highVelocityApproachSpec, key3 = density) {
SnapFlingBehavior(
TangemSnapFlingBehavior(
snapLayoutInfoProvider = snappingLayout,
lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing),
highVelocityAnimationSpec = highVelocityApproachSpec,
snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow),
density = density,
shortSnapVelocityThreshold = itemWidth * SHORT_SNAP_ELEMENT_COUNT,
)
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.ui.utils
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import kotlinx.coroutines.flow.FlowCollector
import kotlin.math.abs
@ -20,14 +19,14 @@ internal class ScrollOffsetCollector(
selectedWalletIndex: Int,
private val lazyListState: LazyListState,
private val onWalletChange: (Int) -> Unit,
) : FlowCollector<List<LazyListItemInfo>> {
) : FlowCollector<List<LazyListItemData>> {
private val LazyListItemInfo.halfItemSize
private val LazyListItemData.halfItemSize
get() = size.div(other = 2)
private var currentIndex = selectedWalletIndex
override suspend fun emit(value: List<LazyListItemInfo>) {
override suspend fun emit(value: List<LazyListItemData>) {
if (!lazyListState.isScrollInProgress || value.size <= 1) return
val firstItem = value.firstOrNull() ?: return
@ -46,4 +45,10 @@ internal class ScrollOffsetCollector(
onWalletChange(newIndex)
}
}
}
}
internal data class LazyListItemData(
val index: Int,
val size: Int,
val offset: Int,
)