Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compose wrapper #331

Closed
jakoss opened this issue Aug 31, 2021 · 11 comments
Closed

Compose wrapper #331

jakoss opened this issue Aug 31, 2021 · 11 comments

Comments

@jakoss
Copy link

jakoss commented Aug 31, 2021

Have you considered creating a compose wrapper over CalendarView? I'm not talking about total rewrite, but about a separate artifact with CalendarView as composable function. It would be nothing more then a wrapper using AndroidView, but until some new fully compose calendar will stabilize - this should work just fine.

If you're fine with that i think i'll try and find some time to create a PR with that

@kizitonwose
Copy link
Owner

That seems like a great idea, I have not had the time to work with compose, so feel free to take it up.

@jakoss
Copy link
Author

jakoss commented Sep 22, 2021

Initial code:

@Composable
fun CalendarView(
    modifier: Modifier = Modifier,
    currentMonth: YearMonth,
    firstMonth: YearMonth,
    lastMonth: YearMonth,
    firstDayOfWeek: DayOfWeek,
    daySize: Size = CalendarView.SIZE_SQUARE,
    scrollMode: ScrollMode = ScrollMode.CONTINUOUS,
    orientation: Orientation = Orientation.VERTICAL,
    maxRowCount: Int = 6,
    inDateStyle: InDateStyle = InDateStyle.ALL_MONTHS,
    outDateStyle: OutDateStyle = OutDateStyle.END_OF_ROW,
    hasBoundaries: Boolean = true,
    wrappedPageHeightAnimationDuration: Int = 200,
    calendarViewState: CalendarViewState = rememberCalendarViewState(),
    monthScrollEvent: ((CalendarMonth) -> Unit)? = null,
    monthHeaderContent: (@Composable (CalendarMonth) -> Unit)? = null,
    monthFooterContent: (@Composable (CalendarMonth) -> Unit)? = null,
    dayContent: @Composable (CalendarDay) -> Unit,
) {
    var firstMonthState by remember {
        mutableStateOf(firstMonth)
    }
    var lastMonthState by remember {
        mutableStateOf(lastMonth)
    }
    AndroidView(
        modifier = modifier,
        factory = { context ->
            CalendarView(context).apply {
                calendarViewState.calendarView = WeakReference(this)
                dayViewResource = R.layout.compose_view_layout
                dayBinder = object : DayBinder<ComposeViewContainer> {
                    override fun create(view: View) = ComposeViewContainer(view)
                    override fun bind(container: ComposeViewContainer, day: CalendarDay) {
                        container.composeView.setContent {
                            dayContent(day)
                        }
                    }
                }
                if (monthHeaderContent != null) {
                    monthHeaderResource = R.layout.compose_view_layout
                    monthHeaderBinder = object : MonthHeaderFooterBinder<ComposeViewContainer> {
                        override fun create(view: View) = ComposeViewContainer(view)
                        override fun bind(container: ComposeViewContainer, month: CalendarMonth) {
                            container.composeView.setContent {
                                monthHeaderContent(month)
                            }
                        }
                    }
                }
                if (monthFooterContent != null) {
                    monthFooterResource = R.layout.compose_view_layout
                    monthFooterBinder = object : MonthHeaderFooterBinder<ComposeViewContainer> {
                        override fun create(view: View) = ComposeViewContainer(view)
                        override fun bind(container: ComposeViewContainer, month: CalendarMonth) {
                            container.composeView.setContent {
                                monthFooterContent(month)
                            }
                        }
                    }
                }

                this.daySize = daySize
                this.orientation = when (orientation) {
                    Orientation.VERTICAL -> RecyclerView.VERTICAL
                    Orientation.HORIZONTAL -> RecyclerView.HORIZONTAL
                }
                this.scrollMode = scrollMode
                this.maxRowCount = maxRowCount
                this.inDateStyle = inDateStyle
                this.outDateStyle = outDateStyle
                this.hasBoundaries = hasBoundaries
                this.wrappedPageHeightAnimationDuration = wrappedPageHeightAnimationDuration
                monthScrollListener = monthScrollEvent

                setup(firstMonth, lastMonth, firstDayOfWeek)
                scrollToMonth(currentMonth)
            }
        },
        update = { calendarView ->
            calendarView.daySize = daySize
            calendarView.orientation = when (orientation) {
                Orientation.VERTICAL -> RecyclerView.VERTICAL
                Orientation.HORIZONTAL -> RecyclerView.HORIZONTAL
            }
            calendarView.scrollMode = scrollMode
            calendarView.maxRowCount = maxRowCount
            calendarView.inDateStyle = inDateStyle
            calendarView.outDateStyle = outDateStyle
            calendarView.hasBoundaries = hasBoundaries
            if (firstMonthState != firstMonth || lastMonthState != lastMonth) {
                firstMonthState = firstMonth
                lastMonthState = lastMonth
                calendarView.updateMonthRange(firstMonth, lastMonth)
            }
        })
}

class ComposeViewContainer(view: View) : ViewContainer(view) {
    val composeView: ComposeView = view.findViewById(R.id.composeView)
}

enum class Orientation {
    VERTICAL,
    HORIZONTAL,
}

@Composable
fun rememberCalendarViewState(): CalendarViewState {
    return remember {
        CalendarViewState()
    }
}

class CalendarViewState internal constructor(){
    internal var calendarView: WeakReference<CalendarView>? = null

    fun scrollToMonth(month: YearMonth) {
        calendarView?.get()?.scrollToMonth(month)
    }

    fun smoothScrollToMonth(month: YearMonth) {
        calendarView?.get()?.smoothScrollToMonth(month)
    }

    fun scrollToDay(day: CalendarDay) {
        calendarView?.get()?.scrollToDay(day)
    }

    fun scrollToDate(date: LocalDate, owner: DayOwner = DayOwner.THIS_MONTH) {
        calendarView?.get()?.scrollToDate(date, owner)
    }

    fun smoothScrollToDay(day: CalendarDay) {
        calendarView?.get()?.smoothScrollToDay(day)
    }

    fun smoothScrollToDate(date: LocalDate, owner: DayOwner = DayOwner.THIS_MONTH) {
        calendarView?.get()?.smoothScrollToDate(date, owner)
    }
}

And sample usage:

@Composable
fun MainScreen() {
    var selectedDay by remember {
        mutableStateOf<LocalDate?>(null)
    }
    val calendarViewState = rememberCalendarViewState()
    CalendarView(
        modifier = Modifier.fillMaxSize(),
        currentMonth = YearMonth.now(),
        firstMonth = YearMonth.now().minusMonths(10),
        lastMonth = YearMonth.now().plusMonths(10),
        firstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
        scrollMode = ScrollMode.PAGED,
        orientation = Orientation.HORIZONTAL,
        calendarViewState = calendarViewState,
        dayContent = { day ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .clickable {
                        if (selectedDay != day.date) {
                            selectedDay = day.date
                        } else if (selectedDay == day.date) {
                            selectedDay = null
                        }
                    }, contentAlignment = Alignment.Center
            ) {
                val color = when {
                    selectedDay == day.date -> Color.Red
                    day.owner == DayOwner.THIS_MONTH -> Color.Black
                    else -> Color.Gray
                }
                Text(text = day.day.toString(), color = color)
            }
        },
        monthHeaderContent = { calendarMonth ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Button(onClick = {
                    calendarViewState.smoothScrollToMonth(
                        calendarMonth.yearMonth.minusMonths(1)
                    )
                }) {
                    Text(text = "Previous")
                }

                Text(text = "Month: ${calendarMonth.month}")

                Button(onClick = {
                    calendarViewState.smoothScrollToMonth(
                        calendarMonth.yearMonth.plusMonths(1)
                    )
                }) {
                    Text(text = "Next")
                }
            }
        },
    )
}

I'm not sure about the notification of view about the changes. This code seems to work fine and all of the views gets updated just because the dayContent composable is listening to selectedDate state. So i need to do some more testing.

I could use some helping hand in testing this in a wild!

@kizitonwose
Copy link
Owner

Looks good from my point of view but I have not used compose intensively to say for sure. However, it does look like the calendar properties cannot be updated individually without recreating the entire thing (this includes internal month generation etc), is this efficient?

@jakoss
Copy link
Author

jakoss commented Oct 4, 2021

I saw that in CalendarView you are using pattern

set(value) {
            if (field != value) {
                field = value
                updateAdapterViewConfig()
            }
        }

So for those properties this should make no effect really. For now i see no performance problems, but that's why i could use some helping hand here. Maybe somebody have some heavy use that i'm missing here

@kizitonwose
Copy link
Owner

kizitonwose commented Oct 31, 2021

Yep, you are right that it will not trigger new updates. I'll get on testing this a bit later (anyone can also test if interested), then we'll see how we can move forward with the feature. Thanks a lot for contributing.

@ammargitham
Copy link

@jakoss There are some more pointers here.

This has to be added too:

override fun onViewRecycled(holder: MyComposeViewHolder) {
    // Dispose of the underlying Composition of the ComposeView
    // when RecyclerView has recycled this ViewHolder
    holder.composeView.disposeComposition()
}

But currently the binders do not have the corresponding recycle method, so this can't be called. The API has to be updated.

Also:

init {
    composeView.setViewCompositionStrategy(
        ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
    )
}

@jakoss
Copy link
Author

jakoss commented Nov 5, 2021

@ammargitham Thanks, i indeed have to take this into account. The issue is that DayBinder interface does not expose anything but create and bind functions. @kizitonwose What can we do about that?

@kizitonwose
Copy link
Owner

@jakoss But DayBinder is not responsible for the cells (it is not the item view holder). It is more like an "inner view holder" in that it exists within the items (for the days only). So if this is true for the DayBinder, then the month header and footer binders will need the same. The internal view holder can call disposeComposition() on all three.

@jakoss
Copy link
Author

jakoss commented Jan 24, 2022

@kizitonwose I'm not 100% sure if i understand correctly, but the outcome for me is the same - to properly handle composable here we need some kind of callback on onViewRecycled event to call disposeComposition. Right now there is no way for me to know when to call that

@jakoss
Copy link
Author

jakoss commented Apr 6, 2022

To update the thread - we found that performance of my solution was not acceptable, every click had it's delay and we couldn't really find reason why it was happening. Fully compose alternative showed up fortunately: https://github.com/boguszpawlowski/ComposeCalendar . So far it's working for us just fine

@jakoss jakoss closed this as completed Apr 6, 2022
@kizitonwose
Copy link
Owner

kizitonwose commented Oct 15, 2022

Hi @jakoss Compose support is now available in version 2.0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants