@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.BoxScope
2323import androidx.compose.foundation.layout.height
2424import androidx.compose.foundation.layout.offset
2525import androidx.compose.foundation.layout.width
26+ import androidx.compose.foundation.lazy.LazyListItemInfo
2627import androidx.compose.foundation.lazy.LazyListState
2728import androidx.compose.foundation.lazy.LazyRow
2829import androidx.compose.foundation.lazy.items
@@ -38,6 +39,7 @@ import androidx.compose.runtime.LaunchedEffect
3839import androidx.compose.runtime.Stable
3940import androidx.compose.runtime.derivedStateOf
4041import androidx.compose.runtime.getValue
42+ import androidx.compose.runtime.mutableLongStateOf
4143import androidx.compose.runtime.mutableStateListOf
4244import androidx.compose.runtime.mutableStateOf
4345import androidx.compose.runtime.remember
@@ -48,8 +50,13 @@ import androidx.compose.ui.Modifier
4850import androidx.compose.ui.platform.LocalDensity
4951import androidx.compose.ui.unit.IntOffset
5052import androidx.compose.ui.unit.dp
53+ import androidx.compose.ui.util.fastRoundToInt
5154import androidx.compose.ui.util.lerp
55+ import androidx.compose.ui.util.packInts
56+ import androidx.compose.ui.util.unpackInt1
57+ import androidx.compose.ui.util.unpackInt2
5258import com.tunjid.heron.ui.TabsState.Companion.TabBackgroundColor
59+ import kotlin.jvm.JvmInline
5360import kotlin.math.ceil
5461import kotlin.math.floor
5562import kotlin.math.roundToInt
@@ -183,76 +190,108 @@ private fun BoxScope.Indicator(
183190 lazyListState : LazyListState ,
184191 selectedTabIndex : () -> Float ,
185192) {
186- var interpolatedOffset by remember { mutableStateOf(IntOffset .Zero ) }
193+ var packedSizeAndPosition by remember {
194+ mutableLongStateOf(
195+ TabSizeAndPosition (
196+ size = 0 ,
197+ position = 0 ,
198+ ).packed,
199+ )
200+ }
201+ val sizeAndPosition = TabSizeAndPosition (packedSizeAndPosition)
187202
188203 // Keep selected tab on screen
189204 LaunchedEffect (lazyListState) {
190205 snapshotFlow {
206+ val roundedIndex = selectedTabIndex().fastRoundToInt()
207+
191208 val layoutInfo = lazyListState.layoutInfo
192- val roundedIndex = selectedTabIndex().roundToInt()
209+ val visibleItemsInfo = layoutInfo.visibleItemsInfo
193210
194- if (roundedIndex == layoutInfo.totalItemsCount - 1 )
195- return @snapshotFlow layoutInfo.totalItemsCount - 1
211+ val visibleIndex = visibleItemsInfo.binarySearch { it.index - roundedIndex }
212+ if (visibleIndex < 0 ) return @snapshotFlow roundedIndex
196213
197- val index = layoutInfo.visibleItemsInfo.binarySearch {
198- it.index - roundedIndex
199- }
200- if (index < 0 ) return @snapshotFlow roundedIndex
201- val item = layoutInfo.visibleItemsInfo[index]
214+ val item = visibleItemsInfo[visibleIndex]
202215
203- if (item.offset + item.size > layoutInfo.viewportEndOffset)
204- lazyListState.firstVisibleItemIndex + 1
205- else lazyListState.firstVisibleItemIndex
216+ val isFullyVisible = item.offset >= 0 &&
217+ (item.offset + item.size) <= layoutInfo.viewportEndOffset
218+
219+ if (isFullyVisible) null else roundedIndex
206220 }
207- .collect { lazyListState.animateScrollToItem(it) }
221+ .collect { index ->
222+ if (index != null ) lazyListState.animateScrollToItem(index)
223+ }
208224 }
209225
210226 // Interpolated highlighted tab position
211227 LaunchedEffect (lazyListState) {
212228 snapshotFlow {
213229 val currentIndex = selectedTabIndex()
214- val flooredIndex = floor(currentIndex).roundToInt ()
215- val roundedIndex = ceil(currentIndex).roundToInt ()
230+ val flooredIndex = floor(currentIndex).fastRoundToInt ()
231+ val roundedIndex = ceil(currentIndex).fastRoundToInt ()
216232 val fraction = currentIndex - flooredIndex
217233
218- val flooredPosition = lazyListState.layoutInfo.visibleItemsInfo.binarySearch {
234+ val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo
235+
236+ val flooredPosition = visibleItemsInfo.binarySearch {
219237 it.index - flooredIndex
220238 }
221- if (flooredPosition < 0 ) return @snapshotFlow IntOffset .Zero
239+ if (flooredPosition < 0 ) return @snapshotFlow visibleItemsInfo.firstOrNull()
240+ .tabSizeAndPosition()
222241
223242 val roundedPosition = lazyListState.layoutInfo.visibleItemsInfo.binarySearch {
224243 it.index - roundedIndex
225244 }
226- if (roundedPosition < 0 ) return @snapshotFlow IntOffset .Zero
245+ if (roundedPosition < 0 ) return @snapshotFlow visibleItemsInfo.firstOrNull()
246+ .tabSizeAndPosition()
227247
228248 val floored = lazyListState.layoutInfo.visibleItemsInfo[flooredPosition]
229249 val rounded = lazyListState.layoutInfo.visibleItemsInfo[roundedPosition]
230250
231- IntOffset (
232- lerp(floored.size, rounded.size, fraction),
233- lerp(floored.offset, rounded.offset, fraction),
251+ TabSizeAndPosition (
252+ size = lerp(floored.size, rounded.size, fraction),
253+ position = lerp(floored.offset, rounded.offset, fraction),
234254 )
235255 }
236256 .collect {
237- interpolatedOffset = it
257+ packedSizeAndPosition = it.packed
238258 }
239259 }
240260
241261 val density = LocalDensity .current
242262 Box (
243263 Modifier
244264 .align(Alignment .CenterStart )
245- .offset { IntOffset (x = interpolatedOffset.y , y = 0 ) }
265+ .offset { IntOffset (x = sizeAndPosition.position , y = 0 ) }
246266 .height(32 .dp)
247267 .matchParentSize()
248- .width(with (density) { interpolatedOffset.x .toDp() })
268+ .width(with (density) { sizeAndPosition.size .toDp() })
249269 .background(
250270 color = TabBackgroundColor ,
251271 shape = TabShape ,
252272 ),
253273 )
254274}
255275
276+ private fun LazyListItemInfo?.tabSizeAndPosition () =
277+ if (this != null ) TabSizeAndPosition (size = size, position = offset)
278+ else TabSizeAndPosition (0 , 0 )
279+
280+ @JvmInline
281+ value class TabSizeAndPosition (
282+ val packed : Long ,
283+ ) {
284+ constructor (
285+ size: Int ,
286+ position: Int ,
287+ ) : this (
288+ packInts(size, position),
289+ )
290+
291+ val size: Int get() = unpackInt1(packed)
292+ val position: Int get() = unpackInt2(packed)
293+ }
294+
256295private val TabShape = RoundedCornerShape (16 .dp)
257296
258297val PagerState .tabIndex get() = currentPage + currentPageOffsetFraction
0 commit comments