🎨

【Android】ライブラリCalendarのカスタマイズ

2023/10/22に公開

はじめに

前回試した Calendar の見た目や挙動を調整した際のものになります。
今回カスタマイズしたBefore → Afterは以下になります。

  • Before
    image1.png

  • After
    image2.gif

環境構築や準備

各バージョンや環境

- Android Studio Giraffe | 2022.3.1 Patch 1
- com.kizitonwose.calendar:compose: 2.4.0-beta01
$ sw_vers
ProductName: macOS
ProductVersion: 13.4
BuildVersion: 22F66

ベースとなる実装

@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())
    }
}

前回実装した最低限の実装になります。動作としては↓の様に左右スワイプで月変更ができるだけのものです。

image3.gif

実装

1. 曜日を表示する

1-1. MonthHeader を追加

fun DayOfWeek.displayText(uppercase: Boolean = false): String {
    return getDisplayName(TextStyle.SHORT, Locale.getDefault()).let { value ->
        if (uppercase) value.uppercase(Locale.getDefault()) else value
    }
}

@Composable
private fun MonthHeader(daysOfWeek: List<DayOfWeek>) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .testTag("MonthHeader"),
    ) {
        for (dayOfWeek in daysOfWeek) {
            Text(
                modifier = Modifier.weight(1f),
                textAlign = TextAlign.Center,
                fontSize = 15.sp,
                text = dayOfWeek.displayText(),
                fontWeight = FontWeight.Medium,
            )
        }
    }
}

1-2. HorizontalCalendar に反映する

Column(modifier = Modifier.fillMaxSize()) {
        HorizontalCalendar(
            state = state,
            dayContent = { Day(it) },
            monthHeader = { month ->
                val daysOfWeek = month.weekDays.first().map { it.date.dayOfWeek }
                MonthHeader(daysOfWeek = daysOfWeek)
            }
        )
    }

image4.png

2. 表示月に含まれない日のTextColorを変更する

Day Composable で day.position による判定を追加します。

@Composable
private fun Day(day: CalendarDay) {
    val textColor = when (day.position) {
        DayPosition.MonthDate -> Color.Unspecified
        DayPosition.InDate, DayPosition.OutDate -> Color.LightGray
    }
    Box(
        modifier = Modifier
            .aspectRatio(1f),
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString(), color = textColor)
    }
}

image5.png

3. body部分の背景色を変更する

HorizontalCalendarmonthBody を追加

monthBody = { _, content ->
                Box(
                    modifier = Modifier.background(
                        brush = Brush.verticalGradient(
                            colors = listOf(
                                MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
                                MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f)
                            )
                        )
                    )
                ) {
                    content()
                }
            },

image6.png

4. 日付に枠線を追加する

HorizontalCalendar(
    ...
    monthBody = { _, content ->
        Box(
            modifier = Modifier
                .background(
                    brush = Brush.verticalGradient(
                        colors = listOf(
                            MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), // TODO: 着せ替え考慮
                            MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f) // TODO: 着せ替え考慮
                        )
                    )
                )
                .border(width = 0.5.dp, color = Color.LightGray) // 追加
        ) {
            content()
        }
    },
)

@Composable
private fun Day(day: CalendarDay) {
    val textColor = when (day.position) {
        DayPosition.MonthDate -> Color.Unspecified
        DayPosition.InDate, DayPosition.OutDate -> Color.LightGray
    }
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .border(width = 0.5.dp, color = Color.LightGray) // 追加
            .padding(1.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(text = day.date.dayOfMonth.toString(), color = textColor)
    }
}

image7.png

5. 日付を左上に寄せて土日の色を変える

@Composable
private fun Day(day: CalendarDay) {
    val textColor = when (day.position) {
        DayPosition.MonthDate -> when (day.date.dayOfWeek) {
            DayOfWeek.SATURDAY -> Color.Blue
            DayOfWeek.SUNDAY -> Color.Red
            else -> Color.Unspecified
        }

        DayPosition.InDate, DayPosition.OutDate -> Color.LightGray
    }
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .border(width = 0.5.dp, color = Color.LightGray)
            .padding(1.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            modifier = Modifier
                .align(Alignment.TopStart)
                .padding(top = 3.dp, start = 4.dp),
            text = day.date.dayOfMonth.toString(), color = textColor
        )
    }
}

image8.png

6. 選択できるようにする

まずは選択された日付を保持する以下変数を追加します。

var selection by remember { mutableStateOf<CalendarDay?>(null) }

次に Day Composableがクリックできるように Modifierclickable を追します。

@Composable
private fun Day(day: CalendarDay, onClick: (CalendarDay) -> Unit = {}) {
    ...
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .border(width = 0.5.dp, color = Color.LightGray)
            .padding(1.dp)
            .clickable { // ←追加
                onClick(day)
            },
        contentAlignment = Alignment.Center
    ) {
       ...
    }
}

この状態でクリックされた時にログが出る様にしてみます。

HorizontalCalendar(
    state = state,
    dayContent = {
        Day(it) { clickDay ->
            Timber.d("clickDay: $clickDay")
        }
    },

クリックした日付がログに出力されています↓

image9.gif

また、表示している月以外の月の日付をクリックしても反応しないようにするには clickableenabled に以下を設定します。

.clickable(enabled = day.position == DayPosition.MonthDate) { // ←追加
    onClick(day)
},

image10.gif

最後に選択日付を反映します。

Day(it, isSelected = selection == it) { clickDay ->
    selection = clickDay
}

...

@Composable
private fun Day(
    day: CalendarDay,
    isSelected: Boolean = false, // ← 追加
    onClick: (CalendarDay) -> Unit = {}
) {
    ...
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .background(color = if (isSelected) Color.Cyan else Color.Transparent) // ← 追加
            .border(width = 0.5.dp, color = Color.LightGray)
            .padding(1.dp)
            .clickable(enabled = day.position == DayPosition.MonthDate) {
                onClick(day)
            },
        contentAlignment = Alignment.Center
    ) {
       ...
    }
}

image11.gif

以上になります。

Discussion