【Jetpack Compose】カレンダー　水平にスクロール&表示されている月を取得

# カレンダーをスクロール

## 何をやったか(結論)
家計簿アプリを作っていて、カレンダーをUIに入れたかった。カレンダーを簡単に作れるライブラリがあったので使おうとした。しかし、横にスクロールして前後の月のカレンダーを表示することもデフォルトで可能だったが、現在表示されている月を取得する方法がわからなかった。そこで、HorizontalPagerを使って解決した。
 
https://github.com/kizitonwose/Calendar

### 環境
- OS:Windows 11 Home
- Android Studio Koala Feature Drop | 2024.1.2 Patch 1
Build #AI-241.19072.14.2412.12360217, built on September 13, 2024
Runtime version: 17.0.11+0--11852314 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.

### 手順
#### ライブラリのimport
***koalaの場合、kizitonwoseのREADME.mdに表示されているjitpack.ioの書き方だとうまくいかなかった。*** 次のように書かないといけない。
```settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()

        //ここに自分で追加する
        maven {
            url = uri("https://jitpack.io")
        }
    }
}
```

参考 : [Adding maven { url "https://jitpack.io" }](https://stackoverflow.com/questions/76888801/adding-maven-url-https-jitpack-io)

build.gradle.ktsのdependencies内には次を追加する
```build.gradle.kts
dependencies {
    val calendar_version="2.6.0"
    // The compose calendar library for Android
    implementation("com.kizitonwose.calendar:compose:${calendar_version}")
    ..
    ..
}
```

#### カレンダーの表示
<details><summary>元々のコード</summary>

```kotlin
@Composable
fun CalendarDisplay() {
    // 現在の年月
    val currentMonth = remember { YearMonth.now() }
    // 現在より前の年月
    val startMonth = remember { currentMonth.minusMonths(100) }
    // 現在より後の年月
    val endMonth = remember { currentMonth.plusMonths(100) }
    // 曜日
    val daysOfWeek = remember { daysOfWeek() }
    // カレンダーの状態を持つ
    val state = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = currentMonth,
        firstDayOfWeek = daysOfWeek.first(),
        outDateStyle = OutDateStyle.EndOfGrid
    )

    // 横スクロールのカレンダーを作成するためのComposable関数
    // 縦スクロールのVerticalなどもある
    HorizontalCalendar(
        state = state,
        // 日付を表示する部分
        dayContent = {Day(it)},
        // カレンダーのヘッダー
        monthHeader = {DaysOfWeekTitle(daysOfWeek = daysOfWeek)}
    )
}

@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
fun Day(day: CalendarDay) {
    Box(
        modifier = Modifier
            .aspectRatio(1f),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = day.date.dayOfMonth.toString(),
            // ここで今月でないものの日付をグレーアウトさせている
            color = if (day.position == DayPosition.MonthDate) Color.Black else Color.Gray
        )
    }
}
```
</details>

#### カレンダーの修正
yearとmonthを渡すことで特定の月のカレンダーを作るように変更した。また横にスクロールするのはPagerを使って行うので、startMonthとendMonthを一致させた。
```kotlin
@Composable
fun CalendarDisplay(calendarYear:Int, calendarMonth:Int ) {
    // 現在の年月
    val calendarYearMonth = YearMonth.of(calendarYear,calendarMonth)
    // 現在より前の年月
    val startMonth =  calendarYearMonth.minusMonths(0)
    // 現在より後の年月
    val endMonth = calendarYearMonth.plusMonths(0)
    // 曜日
    val daysOfWeek =  daysOfWeek()
    // カレンダーの状態を持つ
    val state = rememberCalendarState(
        startMonth = startMonth,
        endMonth = endMonth,
        firstVisibleMonth = calendarYearMonth,
        firstDayOfWeek = daysOfWeek.first(),
        outDateStyle = OutDateStyle.EndOfGrid
    )

    // 横スクロールのカレンダーを作成するためのComposable関数
    // 縦スクロールのVerticalなどもある
    HorizontalCalendar(
        state = state,
        // 日付を表示する部分
        dayContent = {Day(it)},
        // カレンダーのヘッダー
        monthHeader = {DaysOfWeekTitle(daysOfWeek = daysOfWeek)},
        //ユーザーのスクロール
        userScrollEnabled = false
    )
}
```


#### HorizontalPagerの基本的な使い方
参考 : [Composeでのページャー](https://developer.android.com/develop/ui/compose/layouts/pager?hl=ja)
<details>
<summary>サンプルコード</summary>

```kotlin
// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}
```
</details>
<br>
- 特にページの状態が変更したとき(通知を受け取る)<br>
<details>
<summary>サンプルコード</summary>

```kotlin
val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}
```
</details>


#### HorizontalPagerを使ってCalendarをスクロールする
viewModelの中身は以下のようになっていて、calendarDateを保存している。
```ExpenseViewModel.kt
class ExpenseViewModel():ViewModel() {
    private val calendarDate = mutableStateOf(LocalDate.now())

    fun getCalendarYear(): Int {
        return calendarDate.value.year
    }

    fun getCalendarMonth():Int{
        return calendarDate.value.monthValue
    }

    fun incrementMonth(){
        calendarDate.value = calendarDate.value.plusMonths(1)
    }

    fun decrementMonth(){
        calendarDate.value = calendarDate.value.minusMonths(1)
    }

    //MainViewもLazyColumnに表示する
    fun getMonthExpenses():List<Expense>{
        return DummyExpenses.expensesList.filter {
            it.datetime.year == calendarDate.value.year &&
                    it.datetime.month == calendarDate.value.month
        }
    }
}
```
<br>

***スクロール部分の実装***
```MainView.kt
    //カレンダー横スクロールのため
    val calendarPagerState= rememberPagerState(initialPage = 1){ 3 }
    var previousCalendarPage by remember { mutableStateOf(calendarPagerState.currentPage) }
....
....

            Row(
                modifier = Modifier.fillMaxWidth(),
            ){
                HorizontalPager(
                    state = calendarPagerState,
                    modifier = Modifier.weight(1f)
                ) {
                    //viewModelからCalendarの年と月を渡す
                    CalendarDisplay(viewModel.getCalendarYear(),viewModel.getCalendarMonth())
                }
            }

            /**************************************************/
            /*カレンダーをスクロールしたときにviewModel内の日付を変更する*/
            /**************************************************/
            LaunchedEffect(calendarPagerState) {
                snapshotFlow { calendarPagerState.currentPage }
                    .distinctUntilChanged()
                    .collect{
                    currentPage->
                        if(currentPage>previousCalendarPage){
                            //右にスクロールしたとき
                            //incrementすることによって次の月に切り替える
                            viewModel.incrementMonth()
                        }else if (currentPage<previousCalendarPage) {
                            //左にスクロールしたとき
                            //decrementすることによって前の月に切り替える
                            viewModel.decrementMonth()
                        }
                        previousCalendarPage=currentPage
                }
            }
....
....
```
これでviewModel内のcalendarDateと表示されているカレンダーの年月が一致するようになった。
質問あればお願いしますm(__)m

## 参考文献
https://qiita.com/a-Mana/items/32dbd0c5abf36d4faeff