Skip to content

Commit 60db282

Browse files
authored
Merge pull request #900 from tunjid/tj/home-tab-glitches
Fix home tab glitches
2 parents 6406909 + 69f518e commit 60db282

File tree

1 file changed

+63
-24
lines changed
  • ui/core/src/commonMain/kotlin/com/tunjid/heron/ui

1 file changed

+63
-24
lines changed

ui/core/src/commonMain/kotlin/com/tunjid/heron/ui/Tabs.kt

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.BoxScope
2323
import androidx.compose.foundation.layout.height
2424
import androidx.compose.foundation.layout.offset
2525
import androidx.compose.foundation.layout.width
26+
import androidx.compose.foundation.lazy.LazyListItemInfo
2627
import androidx.compose.foundation.lazy.LazyListState
2728
import androidx.compose.foundation.lazy.LazyRow
2829
import androidx.compose.foundation.lazy.items
@@ -38,6 +39,7 @@ import androidx.compose.runtime.LaunchedEffect
3839
import androidx.compose.runtime.Stable
3940
import androidx.compose.runtime.derivedStateOf
4041
import androidx.compose.runtime.getValue
42+
import androidx.compose.runtime.mutableLongStateOf
4143
import androidx.compose.runtime.mutableStateListOf
4244
import androidx.compose.runtime.mutableStateOf
4345
import androidx.compose.runtime.remember
@@ -48,8 +50,13 @@ import androidx.compose.ui.Modifier
4850
import androidx.compose.ui.platform.LocalDensity
4951
import androidx.compose.ui.unit.IntOffset
5052
import androidx.compose.ui.unit.dp
53+
import androidx.compose.ui.util.fastRoundToInt
5154
import 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
5258
import com.tunjid.heron.ui.TabsState.Companion.TabBackgroundColor
59+
import kotlin.jvm.JvmInline
5360
import kotlin.math.ceil
5461
import kotlin.math.floor
5562
import 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+
256295
private val TabShape = RoundedCornerShape(16.dp)
257296

258297
val PagerState.tabIndex get() = currentPage + currentPageOffsetFraction

0 commit comments

Comments
 (0)