📆

【Android】ライブラリCalendarを試す

2023/10/08に公開

Calendar

Android用の高度にカスタマイズ可能なカレンダー・ライブラリで、ビュー・システムにはRecyclerView、合成にはLazyRow/LazyColumnが採用されている。

Androidアプリ内でカレンダーを扱う際に便利な Calendar ライブラリを軽く試した際のメモになります。

動作確認環境

$ sw_vers
ProductName: macOS
ProductVersion: 13.4
BuildVersion: 22F66

Android Studio Giraffe | 2022.3.1 Patch 1

Calendarライブラリの機能

  • 単一選択、複数選択、範囲選択 - お好みの方法で日付選択を行うことができる
  • 週または月モード - 週ベースのカレンダー、または通常の月カレンダーを表示
  • 一部の日付を無効にして選択できないようにする
  • カレンダーの日付範囲を制限する
  • Custom date view/composable
  • Custom calendar view/composable
  • 任意の日を週の初日として使用する
  • 水平または垂直スクロールカレンダー

パッケージインストール

こちらに記載がある通り、ComposeUIのVersionに合わせてCalendarのVersionも指定する。

今回 compose bom2022.10.00 を使っているのでこちら参照するとComposeUIのVersionは 1.3.0 なので今回は 2.2.0 を使います。

libs.versions.toml に以下を追加

[versions]
calendar-view = "2.2.0"

[libraries]
calendar-view = { module = "com.kizitonwose.calendar:compose", version.ref = "calendar-view" }

app/build.gradle.kts に以下を追加

dependencies {
  implementation(libs.calendar.view)
}

ドキュメント

Calendar/docs/Compose.md at main · kizitonwose/Calendar

↑を参考にすすめていきます。

主な4つのComposable

  1. HorizontalCalendar
    • 横スクロールの月単位のカレンダー
  2. VerticalCalendar
    • 縦スクロールの月単位のカレンダー
  3. WeekCalendar
    • 横スクロールの週単位のカレンダー
  4. HeatMapCalendar
    • GitHub の草のあれ

まずは HorizontalCalendar で最低限の実装を試してみる

@Composable
fun CalendarCompose() {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(100) } // Adjust as needed
    val endMonth = remember { currentMonth.plusMonths(100) } // Adjust as needed
    val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library

    val state = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = firstDayOfWeek
    )

    Column(modifier = Modifier.fillMaxSize()) {
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) }
        )
    }
}

@Composable
private fun Day(day: CalendarDay) {
    Box(
        modifier = Modifier
            .aspectRatio(1f), // This is important for square sizing!
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString())
    }
}

エミュレータで動作してみたものが↓になります。

image1.gif

VerticalCalendar, WeekCalendar, HeatMapCalendar の挙動

次に各カレンダーを使ってみたいと思います。

  • VerticalCalendar
    HorizontalCalendarをVerticalCalendarに変更しただけのもの↓
    image2.gif

  • WeekCalendar

    @Composable
    fun WeekCalendarSimple() {
        val currentDate = remember { LocalDate.now() }
        val currentMonth = remember { YearMonth.now() }
        val startDate = remember { currentMonth.atStartOfMonth() } // Adjust as needed
        val endDate = remember { currentMonth.atEndOfMonth() } // Adjust as needed
        val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library
    
        val state = rememberWeekCalendarState(
            startDate = startDate,
            endDate = endDate,
            firstVisibleWeekDate = currentDate,
            firstDayOfWeek = firstDayOfWeek
        )
    
        Column(modifier = Modifier.fillMaxSize()) {
            WeekCalendar(
                state = state,
                dayContent = { Day(it) }
            )
        }
    }
    
    @Composable
    private fun Day(day: WeekDay) {
        Box(
            modifier = Modifier
                .aspectRatio(1f), // This is important for square sizing!
            contentAlignment = Alignment.Center
        ) {
            Text(text = day.date.dayOfMonth.toString())
        }
    }
    

    image3.gif

  • HeatMapCalendar

    @Composable
    fun HeatMapCalendarSimple() {
        val currentMonth = remember { YearMonth.now() }
        val endDate = remember { LocalDate.now() }
        val startDate = remember { endDate.minusMonths(12) }
    
        val state = rememberHeatMapCalendarState(
            startMonth = startDate.yearMonth,
            endMonth = endDate.yearMonth,
            firstVisibleMonth = endDate.yearMonth,
            firstDayOfWeek = firstDayOfWeekFromLocale(),
        )
    
        Column(modifier = Modifier.fillMaxSize()) {
            HeatMapCalendar(
                state = state,
                dayContent = { day, week -> Day(day, startDate, endDate, week) }
            )
        }
    }
    
    @Composable
    private fun Day(
        day: CalendarDay,
        startDate: LocalDate,
        endDate: LocalDate,
        week: HeatMapWeek,
    ) {
        val weekDates = week.days.map { it.date }
        if (day.date in startDate..endDate) {
            LevelBox(Color.Green)
        } else if (weekDates.contains(startDate)) {
            LevelBox(Color.Transparent)
        }
    }
    
    @Composable
    private fun LevelBox(color: Color) {
        Box(
            modifier = Modifier
                .size(18.dp) // Must set a size on the day.
                .padding(2.dp)
                .clip(RoundedCornerShape(2.dp))
                .background(color = color)
        )
    }
    

    image4.gif

WeekTitleをつけてみる

ドキュメントはこちら

@Composable
fun HorizontalCalendarWeekTitle() {
    val currentMonth = remember { YearMonth.now() }
    val startMonth = remember { currentMonth.minusMonths(100) } // Adjust as needed
    val endMonth = remember { currentMonth.plusMonths(100) } // Adjust as needed
    val daysOfWeek = remember { daysOfWeek() }

    val state = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = daysOfWeek.first()
    )

    Column(modifier = Modifier.fillMaxSize()) {
        DaysOfWeekTitle(daysOfWeek = daysOfWeek)
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) }
        )
    }
}

@Composable
fun DaysOfWeekTitle(daysOfWeek: List<DayOfWeek>) {
    Row(modifier = Modifier.fillMaxWidth()) {
        for (dayOfWeek in daysOfWeek) {
            Text(
                modifier = Modifier.weight(1f),
                textAlign = TextAlign.Center,
                text = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
            )
        }
    }
}

@Composable
private fun Day(day: CalendarDay) {
    Box(
        modifier = Modifier
            .aspectRatio(1f), // This is important for square sizing!
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString())
    }
}

エミュレータで動作してみたものが↓になります。

image5.gif

感想

軽く試しただけですが、シンプルなベースのカレンダーを元に、
ドキュメント通り結構カスタマイズ性が高いライブラリだと感じました。
また深掘りした際は別途記事を書こうと思います。

Discussion