Android(Kotlin) MaterialDatePicker を日本語化して期間選択
2020/9/23
はじめに
過去記事で DatePickerDialog に触れましたが、使いづらく、期間指定にも対応していませんでした。
そんな中、期間指定にも対応した MaterialDatePicker という新パッケージを知ったので試してみます。
実装内容
概要
- アクティビティから MaterialDatePicker を期間選択モードで呼び出し
- 呼び出し元アクティビティで選択した期間を取得
- 日本語化
- UTC ミリ秒ベースの API が使いづらいので、ローカル時間利用できるように拡張メソッドを用意
詳細
app/build.gradle
直接的に必要なパッケージは com.google.android.material:material
ですが、
ローカル時間指定に java.time.*
を使いたいので、前回記事と同様の差分も入れておきます。
@@ -22,6 +22,14 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+
+ compileOptions {
+ // Flag to enable support for the new language APIs
+ coreLibraryDesugaringEnabled true
+ // Sets Java compatibility to Java 8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
dependencies {
@@ -33,4 +41,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ implementation 'com.google.android.material:material:1.2.1'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
}
app/src/main/java/com/example/myapplication/MainActivity.kt
選択済み期間指定には setSelection()
メソッド、選択決定時における処理紐付けに addOnPositiveButtonClickListener()
メソッドを利用するわけですが、両方とも UTC ミリ秒(Long 型)をデータ形式として採用しているようです。ただ、都度変換するのは面倒なので、それぞれ同名で LocalDate 型に対応した拡張メソッドを用意してみました。
(ついでに、UTC ミリ秒変換処理も拡張メソッド化しています)
あと、Pair
が Kotlin のものではなく、androidx.core.util.Pair
であることには注意です。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import androidx.core.util.Pair
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import com.google.android.material.datepicker.MaterialDatePicker
import java.time.*
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var textViewStartDate: TextView
private lateinit var textViewEndDate: TextView
private var selectingStartDate: LocalDate? = null
private var selectingEndDate: LocalDate? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textViewStartDate = findViewById(R.id.textViewStartDate)
textViewEndDate = findViewById(R.id.textViewEndDate)
findViewById<Button>(R.id.buttonMaterialDatePicker).apply {
setOnClickListener {
showDatePickerDialog(selectingStartDate, selectingEndDate)
}
}
}
private fun showDatePickerDialog(initialStartDate: LocalDate?, initialEndDate: LocalDate?) {
val now = ZonedDateTime.now()
val localZoneId = now.zone
MaterialDatePicker.Builder.dateRangePicker()
.setSelection(initialStartDate, initialEndDate)
.build().apply {
addOnPositiveButtonClickListener(localZoneId) { startDate, endDate ->
selectingStartDate = startDate
selectingEndDate = endDate
textViewStartDate.text = selectingStartDate!!.toString()
textViewEndDate.text = selectingEndDate!!.toString()
}
}.show(supportFragmentManager, "MaterialDatePicker")
}
private fun MaterialDatePicker.Builder<Pair<Long, Long>>.setSelection(startDate: LocalDate?, endDate: LocalDate?) : MaterialDatePicker.Builder<Pair<Long, Long>> {
if (startDate != null && endDate != null) {
val utcZoneId = ZoneId.of("UTC")
val rangeDate = Pair(
startDate.getTimeInMillis(utcZoneId),
endDate.getTimeInMillis(utcZoneId)
)
setSelection(rangeDate)
}
return this
}
private fun MaterialDatePicker<Pair<Long, Long>>.addOnPositiveButtonClickListener(
localZoneId: ZoneId, onPositiveButtonClick: (LocalDate, LocalDate) -> Unit
): Boolean {
return addOnPositiveButtonClickListener { timeRange ->
val startDateTime = timeRange.first!!.fromUnixTimeInMillis(localZoneId)
val endDateTime = timeRange.second!!.fromUnixTimeInMillis(localZoneId)
onPositiveButtonClick(startDateTime.toLocalDate(), endDateTime.toLocalDate())
}
}
private fun Long.fromUnixTimeInMillis(zoneId: ZoneId): ZonedDateTime {
val instant = Instant.ofEpochSecond(this / 1000)
return ZonedDateTime.ofInstant(instant, zoneId)
}
private fun LocalDate.getTimeInMillis(zoneId: ZoneId): Long {
return ZonedDateTime
.of(this, LocalTime.of(0, 0, 0, 0), zoneId)
.toInstant()
.toEpochMilli()
}
}
app/src/main/res/layout/activity_main.xml
特筆すべき点はありません。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/textViewStartDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未選択" />
<TextView
android:id="@+id/textViewEndDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未選択" />
<Button
android:id="@+id/buttonMaterialDatePicker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ボタン" />
</LinearLayout>
app/src/main/res/values/strings.xml
日本語化対応です。既存文字列リソースを上書いています。
@@ -1,3 +1,16 @@
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">My Application</string>
+
+ <string name="mtrl_picker_range_header_title" tools:override="true">期間を選択</string>
+ <string name="mtrl_picker_range_header_unselected" tools:override="true">開始日 – 終了日</string>
+ <string name="mtrl_picker_range_header_only_start_selected" tools:override="true">%1$s – 終了日</string>
+ <string name="mtrl_picker_range_header_only_end_selected" tools:override="true">開始日 – %1$s</string>
+ <string name="mtrl_picker_save" tools:override="true">OK</string>
+ <string name="mtrl_picker_text_input_date_range_start_hint" tools:override="true">開始日</string>
+ <string name="mtrl_picker_text_input_date_range_end_hint" tools:override="true">終了日</string>
+ <string name="mtrl_picker_invalid_range" tools:override="true">期間が正しくありません。</string>
+ <string name="mtrl_picker_out_of_range" tools:override="true">期間が正しくありません。</string>
+ <string name="mtrl_picker_invalid_format" tools:override="true">入力形式が正しくありません。</string>
+ <string name="mtrl_picker_invalid_format_use" tools:override="true">正しい形式: %1$s</string>
+ <string name="mtrl_picker_invalid_format_example" tools:override="true">例: %1$s</string>
</resources>
app/src/main/res/values/styles.xml
java.lang.IllegalArgumentException: com.google.android.material.datepicker.MaterialDatePicker requires a value for the com.example.myapplication:attr/materialCalendarFullscreenTheme attribute to be set in your app theme. You can either set the attribute in your theme or update your theme to inherit from Theme.MaterialComponents (or a descendant).
特定のアトリビュートが足りないとエラーが出たので、対応しています。
@@ -1,6 +1,6 @@
<resources>
<!-- Base application theme. -->
- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
おわりに
今回は MaterialDatePicker
を使ってみました。全体的に DatePickerDialog
を利用するよりもシンプルに記述できて良いですね。
情報受け渡しが UTC ミリ秒(Long 型)ベースになっていて、使いづらくなったとも言えますが、今回のように変換処理を共通化してあげれば大した問題ではないと思います。