https://kotlinlang.org logo
Title
k

K Merle

05/22/2023, 2:09 PM
I have 2 components,
LazyColumn
and
LazyRow
. They need to be synced, meaning, first item needs to be the same. I am having issues syncing their scrolls when vertical items have different height and am unable to figure out why. Anyone have an idea? 🧵
1.gif
@Composable
fun SyncedScrollScreen() {
    val dates = generateDates()
    val verticalScrollState = rememberLazyListState()
    val horizontalScrollState = rememberLazyListState()
    var scrollSource by remember { mutableStateOf<ScrollSource?>(null) }


    val density = LocalDensity.current

    val calendarItemHeight by remember {
        derivedStateOf {
            val firstVisibleColumnItemIndex = verticalScrollState.firstVisibleItemIndex
            val firstVisibleColumnItem = verticalScrollState.layoutInfo.visibleItemsInfo
                .firstOrNull { it.index == firstVisibleColumnItemIndex }
            val firstVisibleColumnItemWidth = firstVisibleColumnItem?.size ?: 0
            with(density) { firstVisibleColumnItemWidth.toDp() }
        }
    }

    val timelineItemWidth by remember {
        derivedStateOf {
            val firstVisibleRowItemIndex = horizontalScrollState.firstVisibleItemIndex
            val firstVisibleRowItem = horizontalScrollState.layoutInfo.visibleItemsInfo
                .firstOrNull { it.index == firstVisibleRowItemIndex }
            val firstVisibleRowItemWidth = firstVisibleRowItem?.size ?: 0
            with(density) { firstVisibleRowItemWidth.toDp() }
        }
    }

    LaunchedEffect(horizontalScrollState, scrollSource) {
        if(scrollSource == ScrollSource.TIMELINE) {
            snapshotFlow { horizontalScrollState.firstVisibleItemScrollOffset }
                .collect { scrollOffset ->
                    val verticalScrollOffset = (scrollOffset * (calendarItemHeight / timelineItemWidth)).toInt()
                    verticalScrollState.scrollToItem(
                        horizontalScrollState.firstVisibleItemIndex,
                        verticalScrollOffset
                    )
                }
        }
    }

    LaunchedEffect(verticalScrollState, scrollSource) {
        if(scrollSource == ScrollSource.CALENDAR) {
            snapshotFlow { verticalScrollState.firstVisibleItemScrollOffset }
                .collect { scrollOffset ->
                    val horizontalScrollOffset = (scrollOffset * (timelineItemWidth / calendarItemHeight)).toInt()
                    horizontalScrollState.scrollToItem(
                        verticalScrollState.firstVisibleItemIndex,
                        horizontalScrollOffset
                    )
                }
        }
    }

    LaunchedEffect(verticalScrollState.isScrollInProgress) {
        if(scrollSource == null) {
            scrollSource = ScrollSource.CALENDAR
        } else if(!verticalScrollState.isScrollInProgress) {
            scrollSource = null
        }
    }
    LaunchedEffect(horizontalScrollState.isScrollInProgress) {
        if(scrollSource == null) {
            scrollSource = ScrollSource.TIMELINE
        } else if(!horizontalScrollState.isScrollInProgress) {
            scrollSource = null
        }
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(top = 50.dp)
    ) {
        LazyRow(
            state = horizontalScrollState,
        ) {
            itemsIndexed(dates, key= {index, key -> key}) { index, date  ->
                Box(modifier = Modifier
                    .width(300.dp)
                    .height(50.dp)) {
                    Text(date)
                }
            }
        }
        LazyColumn(
            state = verticalScrollState
        ) {
            itemsIndexed(dates, key= {index, key -> key}) { index, date ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(if (index % 2 == 0) 90.dp else 200.dp)
                        .background(if (index % 2 == 0) Color.Gray else Color.White)
                ) {
                    Text(date)
                }
            }
        }
    }
}



fun generateDates(): List<String> {
    val format = DateTimeFormatter.ofPattern("yyyy-MM-dd")
    val startDate = LocalDate.now()
    return List(30) { index ->
        startDate.plusDays(index.toLong()).format(format)
    }
}