• トップ
  • ブログ一覧
  • 【Android】カレンダーを自作する
  • 【Android】カレンダーを自作する

    いまむー(エンジニア)いまむー(エンジニア)
    2024.06.04

    IT技術

    はじめに

    カレンダー機能を実装する場合、ライブラリを使用するとうまくデザインを実現できないことがあると思います
    そこで今回、RecyclerView と ViewPager2 を使ってカレンダーを自作してみました

    カレンダーの実装

    RecyclerView の GridLayoutManager を使用してカレンダーの見た目、ViewPager2 を使用してカレンダーの横スクロールを実装していきます

    View の作成

    メインとなる画面に、年月とボタンを表示する View と カレンダーを表示する ViewPager2 を配置し、 View を作成する
    また、年月とボタンの View に罫線を追加するため、drawable で枠線を作成し、backgound に設定します

    1// res/drawable/header_border.xml
    2<?xml version="1.0" encoding="utf-8"?>
    3<shape xmlns:android="http://schemas.android.com/apk/res/android">
    4    <stroke android:width="1dp" android:color="@color/gray" />
    5</shape>
    1// res/layout/activity_main.xml
    2<?xml version="1.0" encoding="utf-8"?>
    3<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    4    xmlns:app="http://schemas.android.com/apk/res-auto"
    5    xmlns:tools="http://schemas.android.com/tools"
    6    android:id="@+id/main"
    7    android:layout_width="match_parent"
    8    android:layout_height="match_parent"
    9    tools:context=".MainActivity"
    10    android:fitsSystemWindows="true">
    11
    12    <LinearLayout
    13        android:id="@+id/llHeader"
    14        android:layout_width="match_parent"
    15        android:layout_height="wrap_content"
    16        android:background="@drawable/header_border"
    17        android:baselineAligned="false"
    18        android:orientation="horizontal"
    19        app:layout_constraintEnd_toEndOf="parent"
    20        app:layout_constraintStart_toStartOf="parent"
    21        app:layout_constraintTop_toTopOf="parent">
    22
    23        <ImageButton
    24            android:id="@+id/btPreviousMonth"
    25            style="?android:attr/borderlessButtonStyle"
    26            android:layout_width="wrap_content"
    27            android:layout_height="wrap_content"
    28            android:contentDescription="@string/previous_month_button"
    29            android:src="@drawable/baseline_arrow_back_ios_new_24"
    30            app:tint="#ff000000" />
    31
    32        <TextView
    33            android:id="@+id/tvMonth"
    34            android:layout_width="0dp"
    35            android:layout_height="wrap_content"
    36            android:layout_gravity="center"
    37            android:layout_weight="1"
    38            android:textAlignment="center" />
    39
    40        <ImageButton
    41            android:id="@+id/btNextMonth"
    42            style="?android:attr/borderlessButtonStyle"
    43            android:layout_width="wrap_content"
    44            android:layout_height="wrap_content"
    45            android:contentDescription="@string/next_month_button"
    46            android:src="@drawable/baseline_arrow_forward_ios_24"
    47            app:tint="#ff000000" />
    48
    49    </LinearLayout>
    50
    51    <androidx.viewpager2.widget.ViewPager2
    52        android:id="@+id/calendarPager"
    53        android:layout_width="match_parent"
    54        android:layout_height="0dp"
    55        app:layout_constraintBottom_toBottomOf="parent"
    56        app:layout_constraintTop_toBottomOf="@+id/llHeader" />
    57
    58</androidx.constraintlayout.widget.ConstraintLayout>

     

    次に、カレンダーを表示するため、 RecyclerView を配置した View を作成する

    1// res/layout/fragment_calendar.xml
    2<?xml version="1.0" encoding="utf-8"?>
    3<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    4    xmlns:app="http://schemas.android.com/apk/res-auto"
    5    xmlns:tools="http://schemas.android.com/tools"
    6    android:id="@+id/main"
    7    android:layout_width="match_parent"
    8    android:layout_height="match_parent"
    9    tools:context=".MainActivity">
    10
    11    <androidx.recyclerview.widget.RecyclerView
    12        android:id="@+id/rvCalendar"
    13        android:layout_width="0dp"
    14        android:layout_height="0dp"
    15        app:layout_constraintBottom_toBottomOf="parent"
    16        app:layout_constraintEnd_toEndOf="parent"
    17        app:layout_constraintStart_toStartOf="parent"
    18        app:layout_constraintTop_toTopOf="parent" />
    19
    20</androidx.constraintlayout.widget.ConstraintLayout>

     

    最後に、日付を表示するための View を作成する
    また、View に罫線を追加するため、drawable で枠線を作成し、backgound に設定します

    1// res/drawable/day_item_border.xml
    2<?xml version="1.0" encoding="utf-8"?>
    3<shape xmlns:android="http://schemas.android.com/apk/res/android">
    4    <stroke android:width="0.5dp" android:color="@color/gray" />
    5</shape>
    1// res/layout/day_item.xml
    2<?xml version="1.0" encoding="utf-8"?>
    3<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    4    xmlns:app="http://schemas.android.com/apk/res-auto"
    5    xmlns:tools="http://schemas.android.com/tools"
    6    android:layout_width="match_parent"
    7    android:layout_height="match_parent"
    8    android:background="@drawable/day_item_border">
    9
    10    <TextView
    11        android:id="@+id/tvDay"
    12        android:layout_width="wrap_content"
    13        android:layout_height="wrap_content"
    14        android:layout_marginTop="8dp"
    15        app:layout_constraintEnd_toEndOf="parent"
    16        app:layout_constraintStart_toStartOf="parent"
    17        app:layout_constraintTop_toTopOf="parent" />
    18</androidx.constraintlayout.widget.ConstraintLayout>

     

    カレンダーの日付部を実装

    RecyclerView にカレンダーを表示するための処理を実装する

    1// kotlin+java/com.example.calendarapp/CalendarManager.kt
    2package com.example.calendarapp
    3
    4import android.icu.util.Calendar
    5
    6class CalendarManager(private val calendar: Calendar) {
    7    fun getDays(): IntArray {
    8        // 日付配列の作成用にカレンダーを複製
    9        val temporaryCalendar = calendar.clone() as Calendar
    10
    11        // 曜日の数
    12        val dayOfWeekCount = 7
    13        // カレンダーに表示する日付の数
    14        val calendarDaysCount = getWeeksCount() * dayOfWeekCount
    15        // カレンダーの日付を1日にする
    16        temporaryCalendar.set(Calendar.DATE, 1)
    17        // 曜日を取得(日曜日の1から土曜日の7までの数字)
    18        val dayOfWeek = temporaryCalendar.get(Calendar.DAY_OF_WEEK)
    19        // カレンダーに表示する最初の日付にする
    20        temporaryCalendar.add(Calendar.DATE, -dayOfWeek + 1)
    21
    22        // カレンダーの日付リストを作成
    23        val days: MutableList<Int> = mutableListOf()
    24        repeat(calendarDaysCount) {
    25            days.add(temporaryCalendar.get(Calendar.DATE))
    26            temporaryCalendar.add(Calendar.DATE, 1)
    27        }
    28
    29        return days.toIntArray()
    30    }
    31
    32    // カレンダーに表示される週の数
    33    fun getWeeksCount(): Int {
    34        return calendar.getActualMaximum(Calendar.WEEK_OF_MONTH)
    35    }
    36}

    このコードでは、カレンダーの日付配列を返す getDays() と カレンダーの週の数を返す getWeeksCount() を実装しています
    getDays() では、カレンダーに表示する最初の日付 と カレンダーに表示する日数から日付配列を作成します
    getWeeksCount() は、日付配列の作成と日付セルの高さの計算に使用します

     

    次に、RecyclerView に日付を表示する処理を実装する

    1// kotlin+java/com.example.calendarapp/CalendarFragment.kt
    2package com.example.calendarapp
    3
    4import android.icu.util.Calendar
    5import android.os.Build
    6import android.os.Bundle
    7import androidx.fragment.app.Fragment
    8import android.view.LayoutInflater
    9import android.view.View
    10import android.view.ViewGroup
    11import android.widget.TextView
    12import androidx.annotation.RequiresApi
    13import androidx.recyclerview.widget.GridLayoutManager
    14import androidx.recyclerview.widget.RecyclerView
    15import com.example.calendarapp.databinding.DayItemBinding
    16import com.example.calendarapp.databinding.FragmentCalendarBinding
    17
    18class CalendarFragment : Fragment() {
    19    private lateinit var fragmentCalendarBinding: FragmentCalendarBinding
    20    private lateinit var calendarManager: CalendarManager
    21
    22    companion object {
    23        private const val CALENDAR = "calendar"
    24
    25        // newInstanceメソッドで引数を設定
    26        fun newInstance(calendar: Calendar): CalendarFragment {
    27            val fragment = CalendarFragment()
    28            val args = Bundle()
    29            args.putSerializable(CALENDAR, calendar)
    30            fragment.arguments = args
    31            return fragment
    32        }
    33    }
    34
    35    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    36    override fun onCreateView(
    37        inflater: LayoutInflater, container: ViewGroup?,
    38        savedInstanceState: Bundle?
    39    ): View {
    40        arguments?.let {
    41            val calendar = it.getSerializable(CALENDAR, Calendar::class.java) as Calendar
    42            calendarManager = CalendarManager(calendar)
    43        }
    44
    45        fragmentCalendarBinding = FragmentCalendarBinding.inflate(inflater, container, false)
    46
    47        val rvCalendar = fragmentCalendarBinding.rvCalendar
    48        val numberOfColumns = 7
    49        val layout = GridLayoutManager(this@CalendarFragment.context, numberOfColumns)
    50        rvCalendar.layoutManager = layout
    51        val adapter = RecyclerListAdapter(calendarManager.getDays())
    52        rvCalendar.adapter = adapter
    53
    54        // RecyclerViewのスクロールをOFFにする
    55        rvCalendar.suppressLayout(true)
    56
    57        return fragmentCalendarBinding.root
    58    }
    59
    60    private inner class RecyclerListViewHolder(binding: DayItemBinding): RecyclerView.ViewHolder(binding.root) {
    61        var tvDay: TextView
    62
    63        init {
    64            val weeksCount = calendarManager.getWeeksCount()
    65            // 日付Viewの高さを指定
    66            itemView.layoutParams.height = fragmentCalendarBinding.rvCalendar.height / weeksCount
    67            tvDay = binding.tvDay
    68        }
    69    }
    70
    71    private inner class RecyclerListAdapter(private val listData: IntArray): RecyclerView.Adapter<RecyclerListViewHolder>() {
    72        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder {
    73            val binding = DayItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    74            return RecyclerListViewHolder(binding)
    75        }
    76
    77        override fun onBindViewHolder(holder: RecyclerListViewHolder, position: Int) {
    78            val day = listData[position].toString()
    79            holder.tvDay.text = day
    80        }
    81
    82        override fun getItemCount(): Int {
    83            return listData.size
    84        }
    85    }
    86}

    このコードでは、RecyclerView の ViewHolder と Adapter を定義し、RecyclerView に日付を表示するよう実装しています
    ViewHolder では、週の数から View の高さを計算して指定します
    Adapter では、ViewHolder の繋ぎ込みと RecyclerView のアイテム数を指定します
    RecyclerView には、カレンダーを表示するために GridLayoutManager を設定し、定義した Adapter を設定します

    ViewPager に日付部を表示

    ViewPager にカレンダーの日付部を表示する

    1// kotlin+java/com.example.calendarapp/MainActivity.kt
    2package com.example.calendarapp
    3
    4import android.icu.util.Calendar
    5import android.os.Bundle
    6import android.view.View
    7import androidx.activity.enableEdgeToEdge
    8import androidx.appcompat.app.AppCompatActivity
    9import androidx.fragment.app.Fragment
    10import androidx.fragment.app.FragmentActivity
    11import androidx.viewpager2.adapter.FragmentStateAdapter
    12import androidx.viewpager2.widget.ViewPager2
    13import com.example.calendarapp.databinding.ActivityMainBinding
    14import java.text.SimpleDateFormat
    15import java.util.Locale
    16
    17class MainActivity : AppCompatActivity() {
    18    companion object {
    19        // 今月から前後1年分のページ数を設定
    20        const val PAGE_COUNT = 25
    21    }
    22
    23    private val calendar = Calendar.getInstance()
    24
    25    private lateinit var activityMainBinding: ActivityMainBinding
    26    private lateinit var calendarPagerAdapter: CalendarPagerAdapter
    27
    28    override fun onCreate(savedInstanceState: Bundle?) {
    29        super.onCreate(savedInstanceState)
    30        enableEdgeToEdge()
    31        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
    32        val activityMainView = activityMainBinding.root
    33        setContentView(activityMainView)
    34
    35        calendarPagerAdapter = CalendarPagerAdapter(this@MainActivity, calendar)
    36        activityMainBinding.calendarPager.adapter = calendarPagerAdapter
    37
    38        // 初期表示をPagerの中央にする
    39        val centerPosition = (PAGE_COUNT - 1) / 2
    40        activityMainBinding.calendarPager.setCurrentItem(centerPosition, false)
    41    }
    42
    43    private inner class CalendarPagerAdapter(
    44        activity: FragmentActivity,
    45        private val calendar: Calendar
    46    ) : FragmentStateAdapter(activity) {
    47        override fun getItemCount(): Int {
    48            return PAGE_COUNT
    49        }
    50
    51        override fun createFragment(position: Int): Fragment {
    52            val calendar = calendar.clone() as Calendar
    53            // Pagerの中央に今月が表示されることを前提にamontの値を計算
    54            val calendarAddAmount = position - ((PAGE_COUNT - 1) / 2)
    55            calendar.add(Calendar.MONTH, calendarAddAmount)
    56            return CalendarFragment.newInstance(calendar)
    57        }
    58    }
    59}

    このコードでは、FragmentStateAdapter を定義し、ViewPager に日付部を表示しています
    FragmentStateAdapter では、今月から前後1年間分の25ページを指定し、今月が ViewPager の中央にくるよう設定します
    ViewPager には、作成した FragmentStateAdapter を設定し、初期表示が ViewPager の中央になるよう指定します

    カレンダーに年月を表示

    TextView にカレンダーの年月を表示する

    1// kotlin+java/com.example.calendarapp/MainActivity.kt
    2〜省略
    3
    4    override fun onCreate(savedInstanceState: Bundle?) {
    5        super.onCreate(savedInstanceState)
    6
    7        〜省略
    8
    9        val calendarOnPageChanger = CalendarOnPageChanger()
    10        activityMainBinding.calendarPager.registerOnPageChangeCallback(calendarOnPageChanger)
    11
    12        〜省略
    13    }
    14
    15
    16    // カレンダーのタイトル
    17    fun getTitle(position: Int): String {
    18        // タイトル作成用にカレンダーを複製
    19        val temporaryCalendar = calendar.clone() as Calendar
    20        // Pagerの中央に今月が表示されることを前提にamontの値を計算
    21        val calendarAddAmount = position - ((PAGE_COUNT - 1) / 2)
    22        temporaryCalendar.add(Calendar.MONTH, calendarAddAmount)
    23
    24        // 年月にフォーマットして返す
    25        val locale = Locale("ja", "JP", "JP")
    26        val dateFormat = SimpleDateFormat("yyyy年M月", locale)
    27        return dateFormat.format(temporaryCalendar.time)
    28    }    
    29
    30    private inner class CalendarOnPageChanger : ViewPager2.OnPageChangeCallback() {
    31        override fun onPageSelected(position: Int) {
    32            super.onPageSelected(position)
    33            activityMainBinding.tvMonth.text = getTitle(position)
    34        }
    35    }
    36
    37    〜省略

    このコードでは、ViewPager2.OnPageChangeCallback() を定義し、カレンダーに年月を表示しています
    ViewPager2.OnPageChangeCallback() では、onPageSelected の時に、そのページの年月を TextView に表示するようにします
    そして、定義した ViewPager2.OnPageChangeCallback() を ViewPager に設定します

    先月・翌月ボタンのハンドラー実装

    先月・翌月ボタンが押された時に、ViewPager がスクロールするようにする

    1// kotlin+java/com.example.calendarapp/MainActivity.kt
    2〜省略
    3
    4    override fun onCreate(savedInstanceState: Bundle?) {
    5        super.onCreate(savedInstanceState)
    6
    7        〜省略
    8
    9        val calendarHeaderListener = CalendarHeaderListener()
    10        activityMainBinding.btPreviousMonth.setOnClickListener(calendarHeaderListener)
    11        activityMainBinding.btNextMonth.setOnClickListener(calendarHeaderListener)
    12
    13        〜省略
    14    }
    15
    16    private inner class CalendarHeaderListener : View.OnClickListener {
    17        override fun onClick(v: View?) {
    18            when (v?.id) {
    19                // < ボタン
    20                activityMainBinding.btPreviousMonth.id -> {
    21                    val previousPosition = activityMainBinding.calendarPager.currentItem - 1
    22                    activityMainBinding.calendarPager.setCurrentItem(previousPosition, true)
    23                }
    24                // > ボタン
    25                activityMainBinding.btNextMonth.id -> {
    26                    val nextPosition = activityMainBinding.calendarPager.currentItem + 1
    27                    activityMainBinding.calendarPager.setCurrentItem(nextPosition, true)
    28                }
    29            }
    30        }
    31    }
    32
    33    〜省略

    このコードでは、View.OnClickListener を定義し、ボタンが押された時に ViewPager がスクロールするようにしています
    View.OnClickListener では、ViewPager の setCurrentItem を使って ViewPager をスクロールさせます
    そして、定義した View.OnClickListener を ViewPager に設定します

    無限スクロールの実装

    ViewPager を無限にスクロールできるようにする

    1// kotlin+java/com.example.calendarapp/MainActivity.kt
    2〜省略
    3
    4    private inner class CalendarOnPageChanger : ViewPager2.OnPageChangeCallback() {
    5
    6        〜省略
    7
    8        override fun onPageScrollStateChanged(state: Int) {
    9            super.onPageScrollStateChanged(state)
    10            // Page移動が完了した時
    11            if (ViewPager2.SCROLL_STATE_IDLE == state) {
    12                val currentPosition = activityMainBinding.calendarPager.currentItem
    13                val centerPosition = (PAGE_COUNT - 1) / 2
    14                when (currentPosition) {
    15                    // 表示がPagerの左端まできた時
    16                    0 -> {
    17                        calendar.add(Calendar.MONTH, -centerPosition)
    18                        activityMainBinding.calendarPager.setCurrentItem(centerPosition, false)
    19                    }
    20                    // 表示がPagerの右端まできた時
    21                    PAGE_COUNT - 1 -> {
    22                        calendar.add(Calendar.MONTH, centerPosition)
    23                        activityMainBinding.calendarPager.setCurrentItem(centerPosition, false)
    24                    }
    25                }
    26            }
    27        }
    28    }
    29
    30    〜省略

    このコードでは、onPageScrollStateChanged でPagerを無限にスクロールできるようにしています
    onPageScrollStateChanged では、最初と最後のページに移動が完了した時、そのページを ViewPager の中央に表示し、表示を中央に切り替えます

    おわりに

    実装は以上です
    ビルドすると以下のようなカレンダーが表示され、ボタンとスワイプでカレンダーを切り替えることができます

    まだ、日付の色を変えたり、日付の View に何か表示するなどはできていないので、これから機能を追加していこうと思います

    最後までご覧いただきありがとうございました
    こちらの記事が少しでも参考になれば幸いです

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background