kizitonwose / Calendar

A highly customizable calendar view and compose library for Android and Kotlin Multiplatform.
MIT License
4.63k stars 503 forks source link

Week Calendar scroll to past skips not scrolls #567

Open akrulec opened 1 month ago

akrulec commented 1 month ago

Library information:

Describe the bug**

I recently migrated from Views to Compose, so I just went with the newest library (but I also tried 2.5.2 which is the last stable version). When I try to scroll, it only scrolls forwards, but not backwards. Is that a setting that I accidentally set somewhere, or is that a bug? Thank you

Expected behavior (if applicable)

Ability to scroll forwards and backwards.

Screenshots? (if applicable)

Attached screen recording.

https://github.com/user-attachments/assets/c3a475f8-2ff0-4886-8837-32dd42a4cced

Additional information

    val state = rememberWeekCalendarState(
        startDate = startDate,
        endDate = endDate,
        firstVisibleWeekDate = currentDate,
        firstDayOfWeek = firstDayOfWeek)

    var firstDateOfWeek = remember { state.firstVisibleWeek.days.first().date }

    LaunchedEffect(currentDate) {
        state.animateScrollToWeek(currentDate)
    }

    LaunchedEffect(state) {
        snapshotFlow { state.isScrollInProgress }.distinctUntilChanged().collectLatest {
            val firstVisibleDateOfTheWeek = state.firstVisibleWeek.days.first().date

            if (firstDateOfWeek != firstVisibleDateOfTheWeek) {
                // Week changed, make sure to refresh data.
                firstDateOfWeek = firstVisibleDateOfTheWeek
                onCalendarScroll(firstVisibleDateOfTheWeek, state.firstVisibleWeek.days.last().date)

                // Because Sunday is the first day, we need to % number of days.
                // Otherwise it doesn't work correctly if the user selected a Sunday.
                val dayOfWeek = (currentDate.dayOfWeek.value % NUM_DAYS_TO_SHOW)
                // Scroll to the same day in the previous/next week.
                onDateSelected(state.firstVisibleWeek.days[dayOfWeek].date)
            }
        }
    }

    WeekCalendar(
        state = state,
        dayContent = { day ->
            // In order to trigger re-composition of the DayCard, we need to use [derivedStateOf].
            val streakStateForDay by remember(habitDayList) {
                derivedStateOf { habitDayList?.find { it.date == day.date }?.state }
            }
            DayCard(
                date = day.date,
                selectedDate = currentDate,
                dayPosition = null,
                streakState = streakStateForDay,
                onDayClick = { clickedDate ->
                    onDateSelected(clickedDate)
                }
            )
        }
    )
kizitonwose commented 1 month ago

Hi, sorry I do not fully understand. In the video, I see both forward and backward scrolls. Can you explain further?

Some pointers:

val state = rememberWeekCalendarState(
    startDate = startDate,
    endDate = endDate,
    firstVisibleWeekDate = currentDate, // Do you really need to update this every time it changes? 
    firstDayOfWeek = firstDayOfWeek
)

// Why are you using the `currentDate` here?
 val dayOfWeek = (currentDate.dayOfWeek.value % NUM_DAYS_TO_SHOW)

It is difficult to tell what is incorrect since the code is incomplete. You can check if this issue is happening in the sample project or try to reproduce it there, then I can easily have a look.

akrulec commented 1 month ago

At second 6 the backwards scroll appears, and if you look at a date, it changes from July 10th to July 3rd (at second 7), but the calendar doesn't scroll, it just skips and the icons change. Then more scrolling was supposed to happen but i just skips to June 26th, June 19th, June 12th... Thank you

kizitonwose commented 1 month ago

I am able to scroll fine in the sample project, so it is hard to tell what is intercepting the scroll behaviour in your app. Could it be the drawer?

https://github.com/user-attachments/assets/6ab23a18-7252-4600-b88f-8854baf4c39f

akrulec commented 1 month ago

How would I figure that out? I am still using the old Android Views for the app, and this is just a ComposeView that is part of the today screen's fragment.

kizitonwose commented 1 month ago

It's hard to tell since I do not have access to the project. I'd suggest isolating the calendar composable, then create and run a preview of the composable to see the issue happens there.

akrulec commented 1 month ago

I get an error when trying to run it on device (composable not found -- probably because I have modules, and maybe because my app is still mostly set up for views). However, the interactive mode, seems to work fine. If this is a symptom of the Composable being inflated inside of a good ol' fragment, and the navigation drawer messing things up, what are my options for workarounds? It's going to be a while before everything is rewritten in compose.

akrulec commented 1 month ago

I did find this in the main activity, but removing it didn't help the symptom: binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)

kizitonwose commented 4 weeks ago

Not sure how else I can help here, unfortunately.

kizitonwose commented 4 weeks ago

Could you maybe try removing all calls to scrollToWeek or animateScrollToWeek in your code and initialise the calendar with static dates like below to see if the scroll issue is from your code?

    val currentDate = remember { LocalDate.now() }
    val currentMonth = remember { YearMonth.now() }
    val startDate = remember { currentMonth.minusMonths(100).atStartOfMonth() }
    val endDate = remember { currentMonth.plusMonths(100).atEndOfMonth() }
    val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } 

    val state = rememberWeekCalendarState(
        startDate = startDate,
        endDate = endDate,
        firstVisibleWeekDate = currentDate,
        firstDayOfWeek = firstDayOfWeek
    )
akrulec commented 2 weeks ago

I think we have hit the nail on the head. Do you have a recommended flow for the following:

Thank you

akrulec commented 2 weeks ago

I have realized that I'm missing the part where we only take into account the ! scrolling actions like here: .filter { scrolling -> !scrolling }

That mostly solves the problem of the odd 'snap behavior' when scrolling into the past. There is still times when I try to scroll more like a fling behavior, and then it still behaves rather oddly.

akrulec commented 2 weeks ago

After digging in a little more, this is what I came up with. I admit, the previous solution was mostly just a direct translation from view's based view.

@Composable
fun WeekCalendarCard(
    currentDate: LocalDate,
    onDateSelected: (LocalDate) -> Unit,
    onCalendarScroll: (LocalDate, LocalDate) -> Unit,
) {
    val currentMonth = remember { YearMonth.of(currentDate.year, currentDate.month) }
    val startDate = remember { currentMonth.minusMonths(24).atStartOfMonth() }
    val endDate = remember { currentMonth.plusMonths(2).atEndOfMonth() }
    val firstDayOfWeek = remember { DayOfWeek.SUNDAY }

    val state = rememberWeekCalendarState(
        startDate = startDate,
        endDate = endDate,
        firstVisibleWeekDate = currentDate,
        firstDayOfWeek = firstDayOfWeek)
    val visibleWeek = rememberFirstVisibleWeekAfterScroll(state)

    LaunchedEffect(currentDate) {
        // Update the state of the calendar if the date changes (by user clicking on a different
        // date in the week, or month calendar).
        state.animateScrollToWeek(currentDate)
    }

    LaunchedEffect(visibleWeek) {
        visibleWeek.days.firstOrNull()?.date?.let { firstDateAfterScroll ->
            if (firstDateAfterScroll != currentDate) {
                // Week changed, make sure to refresh data.
                visibleWeek.days.lastOrNull()?.date?.let { lastDate ->
                    onCalendarScroll(firstDateAfterScroll, lastDate)
                }

                // Select the same day in the new week, by updating the date.
                val dayOfWeek = (currentDate.dayOfWeek.value % NUM_DAYS_TO_SHOW)
                visibleWeek.days.getOrNull(dayOfWeek)?.date?.let { newDate ->
                    onDateSelected(newDate)
                }
            }
        }
    }

I've also noticed that setting firstDayOfWeek to firstDayOfWeekFromLocale caused issues for international users when they were selecting a new date to show from a full screen calendar (which is still written in Views). So for now, we just have all of our calendar set to have the first day of the week on Sunday.

Thank you!

Joshuacobarrubia commented 1 week ago

Wakanda

DanielRendox commented 10 hours ago

I had the same problem with monthly calendar. It was because I passed a dynamically changing currentMonth to initialMonth property, but this property should be constant.