Skip to content

Commit

Permalink
Merge pull request #570 from kizitonwose/year_calendar_view
Browse files Browse the repository at this point in the history
Add `YearCalendarView` class to the view module
  • Loading branch information
kizitonwose authored Aug 3, 2024
2 parents 6e4bc22 + b1d6c4f commit 16391af
Show file tree
Hide file tree
Showing 40 changed files with 2,633 additions and 209 deletions.
29 changes: 29 additions & 0 deletions docs/Compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ fun MainScreen() {
}
```

`HorizontalYearCalendar` and `VerticalYearCalendar`:

```kotlin
@Composable
fun MainScreen() {
val currentYear = remember { Year.now() }
val startYear = remember { currentYear.minusYears(100) } // Adjust as needed
val endYear = remember { currentYear.plusYears(100) } // Adjust as needed
val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library

val state = rememberYearCalendarState(
startYear = startYear,
endYear = endYear,
firstVisibleYear = currentYear,
firstDayOfWeek = firstDayOfWeek,
)
HorizontalYearCalendar(
state = state,
dayContent = { Day(it) },
)

// If you need a vertical year calendar.
// VerticalYearCalendar(
// state = state,
// dayContent = { Day(it) }
// )
}
```

Your `Day` composable in its simplest form would be:

```kotlin
Expand Down
161 changes: 136 additions & 25 deletions docs/View.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,12 @@ class CalendarViewTest {

@Test
fun findVisibleDaysAndMonthsWorksOnVerticalOrientation() {
openExampleAt(1)
openExampleAt(7)

val calendarView = getView<CalendarView>(R.id.exTwoCalendar)
val calendarView = getView<CalendarView>(R.id.exEightCalendar)

runOnMain {
calendarView.orientation = RecyclerView.VERTICAL
// Scroll to a random date
calendarView.scrollToDate(LocalDate.now().plusDays(120))
}
Expand All @@ -266,11 +267,12 @@ class CalendarViewTest {

@Test
fun findVisibleDaysAndMonthsWorksOnHorizontalOrientation() {
openExampleAt(0)
openExampleAt(7)

val calendarView = getView<CalendarView>(R.id.exOneCalendar)
val calendarView = getView<CalendarView>(R.id.exEightCalendar)

runOnMain {
calendarView.orientation = RecyclerView.HORIZONTAL
// Scroll to a random date
calendarView.scrollToDate(LocalDate.now().plusDays(120))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.kizitonwose.calendar.sample.view

import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import com.kizitonwose.calendar.sample.R

interface HasToolbar {
Expand All @@ -18,6 +22,13 @@ abstract class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes) {
val activityToolbar: Toolbar
get() = (requireActivity() as CalendarViewActivity).binding.activityToolbar

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (this is MenuProvider) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.CREATED)
}
}

override fun onStart() {
super.onStart()
if (this is HasToolbar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ class CalendarViewOptionsAdapter(val onClick: (ExampleItem) -> Unit) :
R.string.example_8_subtitle,
horizontal,
) { Example8Fragment() },
ExampleItem(
R.string.example_9_title,
R.string.example_9_subtitle,
horizontal,
) { Example9Fragment() },
ExampleItem(
R.string.example_10_title,
R.string.example_10_subtitle,
horizontal,
) { Example10Fragment() },
)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionsViewHolder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.kizitonwose.calendar.sample.view

import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuProvider
import androidx.core.view.children
import androidx.core.view.updatePaddingRelative
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
import com.kizitonwose.calendar.sample.R
import com.kizitonwose.calendar.sample.databinding.Example10FragmentBinding
import com.kizitonwose.calendar.sample.databinding.Example9CalendarDayBinding
import com.kizitonwose.calendar.sample.databinding.Example9CalendarMonthHeaderBinding
import com.kizitonwose.calendar.sample.shared.displayText
import com.kizitonwose.calendar.view.MarginValues
import com.kizitonwose.calendar.view.MonthDayBinder
import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
import com.kizitonwose.calendar.view.ViewContainer
import java.time.LocalDate
import java.time.Year

class Example10Fragment : BaseFragment(R.layout.example_10_fragment), HasToolbar, HasBackButton, MenuProvider {
override val toolbar: Toolbar
get() = binding.exTenToolbar

override val titleRes: Int = R.string.example_10_title

private lateinit var binding: Example10FragmentBinding

private var selectedDate: LocalDate? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = Example10FragmentBinding.bind(view)
val config = requireContext().resources.configuration
val isTablet = config.smallestScreenWidthDp >= 600

configureBinders(isTablet)

binding.exTenToolbar.updatePaddingRelative(end = dpToPx(if (isTablet) 42 else 6, requireContext()))

binding.exTenCalendar.apply {
val currentYear = Year.now()
monthVerticalSpacing = dpToPx(20, requireContext())
monthHorizontalSpacing = dpToPx(if (isTablet) 52 else 10, requireContext())
yearMargins = MarginValues(
vertical = dpToPx(if (isTablet) 20 else 6, requireContext()),
horizontal = dpToPx(if (isTablet) 52 else 14, requireContext()),
)
yearScrollListener = { year ->
binding.exTenToolbar.title = year.year.value.toString()
}
setup(
currentYear.minusYears(100),
currentYear.plusYears(100),
firstDayOfWeekFromLocale(),
)
scrollToYear(currentYear)
}
}

private fun configureBinders(isTablet: Boolean) {
val calendarView = binding.exTenCalendar

class DayViewContainer(view: View) : ViewContainer(view) {
// Will be set when this container is bound. See the dayBinder.
lateinit var day: CalendarDay
val textView = Example9CalendarDayBinding.bind(view).exNineDayText.apply {
textSize = if (isTablet) 10f else 9f
}

init {
textView.setOnClickListener {
if (day.position == DayPosition.MonthDate) {
if (selectedDate == day.date) {
selectedDate = null
calendarView.notifyDayChanged(day)
} else {
val oldDate = selectedDate
selectedDate = day.date
calendarView.notifyDateChanged(day.date)
oldDate?.let { calendarView.notifyDateChanged(oldDate) }
}
}
}
}
}

calendarView.dayBinder = object : MonthDayBinder<DayViewContainer> {
override fun create(view: View) = DayViewContainer(view)
override fun bind(container: DayViewContainer, data: CalendarDay) {
container.day = data
val textView = container.textView
textView.text = data.date.dayOfMonth.toString()

if (data.position == DayPosition.MonthDate) {
textView.makeVisible()
when (data.date) {
selectedDate -> {
textView.setTextColorRes(R.color.example_2_white)
textView.setBackgroundResource(R.drawable.example_2_selected_bg)
}

else -> {
textView.setTextColorRes(R.color.example_2_black)
textView.background = null
}
}
} else {
textView.makeInVisible()
}
}
}

val monthNameTypeFace = Typeface.semiBold(requireContext())

class MonthViewContainer(view: View) : ViewContainer(view) {
val bind = Example9CalendarMonthHeaderBinding.bind(view)
val textView = bind.exNineMonthHeaderText.apply {
setTypeface(monthNameTypeFace)
textSize = if (isTablet) 16f else 14f
updatePaddingRelative(start = dpToPx(if (isTablet) 10 else 6, requireContext()))
}
val legendLayout = bind.legendLayout.root
}

val legendTypeface = Typeface.medium(requireContext())

calendarView.monthHeaderBinder =
object : MonthHeaderFooterBinder<MonthViewContainer> {
override fun create(view: View) = MonthViewContainer(view)
override fun bind(container: MonthViewContainer, data: CalendarMonth) {
container.textView.text = data.yearMonth.month.displayText(short = false)
// Setup each header day text if we have not done that already.
if (container.legendLayout.tag == null) {
container.legendLayout.tag = true
val daysOfWeek = data.weekDays.first().map { it.date.dayOfWeek }
container.legendLayout.children.map { it as TextView }
.forEachIndexed { index, tv ->
tv.text = daysOfWeek[index].displayText(uppercase = true, narrow = true)
tv.setTextColorRes(R.color.example_3_black)
tv.textSize = if (isTablet) 14f else 11f
tv.setTypeface(legendTypeface)
}
}
}
}
}

override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.example_10_menu, menu)
}

override fun onMenuItemSelected(item: MenuItem): Boolean = with(binding.exTenCalendar) {
return when (item.itemId) {
R.id.menuItemPrevious -> {
findFirstVisibleYear()?.year?.let { visibleYear ->
smoothScrollToYear(visibleYear.minusYears(1))
}
true
}

R.id.menuItemNext -> {
findFirstVisibleYear()?.year?.let { visibleYear ->
smoothScrollToYear(visibleYear.plusYears(1))
}
true
}

else -> false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuProvider
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import com.kizitonwose.calendar.core.CalendarDay
Expand All @@ -25,7 +26,7 @@ import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter

class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar, HasBackButton {
class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar, HasBackButton, MenuProvider {
override val toolbar: Toolbar
get() = binding.exTwoToolbar

Expand All @@ -38,7 +39,6 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
binding = Example2FragmentBinding.bind(view)
val daysOfWeek = daysOfWeek()
binding.legendLayout.root.children.forEachIndexed { index, child ->
Expand All @@ -57,22 +57,20 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,
}

private lateinit var menuItem: MenuItem
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.example_2_menu, menu)
menuItem = menu.getItem(0)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuItemDone) {
val date = selectedDate ?: return false
menuItem = menu.findItem(R.id.menuItemDone)
menuItem.setOnMenuItemClickListener click@{
val date = selectedDate ?: return@click true
val text = "Selected: ${DateTimeFormatter.ofPattern("d MMMM yyyy").format(date)}"
Snackbar.make(requireView(), text, Snackbar.LENGTH_SHORT).show()
parentFragmentManager.popBackStack()
return true
return@click true
}
return super.onOptionsItemSelected(item)
}

override fun onMenuItemSelected(item: MenuItem): Boolean = true

private fun configureBinders() {
val calendarView = binding.exTwoCalendar

Expand Down Expand Up @@ -113,10 +111,12 @@ class Example2Fragment : BaseFragment(R.layout.example_2_fragment), HasToolbar,
textView.setTextColorRes(R.color.example_2_white)
textView.setBackgroundResource(R.drawable.example_2_selected_bg)
}

today -> {
textView.setTextColorRes(R.color.example_2_red)
textView.background = null
}

else -> {
textView.setTextColorRes(R.color.example_2_black)
textView.background = null
Expand Down
Loading

0 comments on commit 16391af

Please sign in to comment.