How to make DatePicker stretch when CalendarView does not fit the screen?

In Android we have to deal with both big screen and small screens, tablets, watches, TVs and many other devices. In our case, we had a device with a 3.2″ display and other one with 5″ display. And on both of them we show a DatePicker dialog to choose a date. And guess what? Especially for the current month of October, there was a small difference. Can you guess it in the screenshot below?

One day is missing

I guess you already saw the difference. 31st of October is missing. If you don’t remember that this month is 31 days, you could have actually skipped it at all. The reason why we don’t see the 31st on the left side is because the DatePicker and dialogs overall are limited in size.

The issue

Dialogs and DialogFragments are limited in height. They usually do not take up all the available space in terms of height and width. They have some paddings and also some distance from the top and bottom to make them better looking. And in the old times, up until now, there is a lot of code in Android XML that uses the android:layout_weight property as you can see here. It can be problematic in some cases where you want certain content to be visible on the screen.

With the above case, it was really hard for me to understand why is CalendarView not scrollable and where the issue is. I tried reducing the size of the dates, the size of the title and the issue was kinda fixed but I couldn’t get a decent looking 31st on the screen. That’s why I decided to move to using a custom layout + AlertDialog rather than fighting this DatePickerFragment. And here is how it worked.

The solution

The solution was to try to stretch the AlertDialog height as much as possible. As you can see, there is a lot of space at the top and bottom of the dialog that can be used. And I started looking into a solution.

Using a custom view

I decided to use a custom layout with an alert dialog. Now that I wanted to stretch it manually, it just didn’t make sense to keep the DatePickerDialogFragment. And using an AlertDialog inside a DialogFragment is very very easy.

The custom view would contain:

  1. The CalendarView – it has a title that matches the one from the DatePickerDialog
  2. The buttons – Cancel and OK which are simple TextViews and I could style them by using the style=”@style/Widget.AppCompat.Button.ButtonBar.AlertDialog”. Job done.

Stretching the dialog in onLayoutChanged

Adding a listener when layout changes for the dialog is quite simple. And there, we can make sure that our dialog has MATCH_PARENT, MATCH_PARENT in terms of width and height. Something like this:

alertDialog.window?.decorView?.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->     
    dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 
        ViewGroup.LayoutParams.MATCH_PARENT)
}

Make sure to stretch the dialog only if space is not enough

I didn’t need to stretch the dialog for devices that are big and have enough screen space. That’s why I needed to detect whether a device has enough space or not. And what a bigger indicator than the CalendarView itself. If it matched or is higher than the AlertDialog height, this means the buttons are not visible and I need to make the dialog width and height – MATCH_PARENT. Otherwise, keep them as they are.

The result

Show me the code

import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.DatePicker
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import java.time.LocalDate
import java.time.ZoneOffset
@AndroidEntryPoint
class DatePickerDialogFragment private constructor() : DialogFragment(), DatePicker.OnDateChangedListener {
private val preselectedDate: LocalDate by lazy { (requireArguments().getSerializable(ARG_PRESELECTED_DATE) as? LocalDate) ?: LocalDate.now() }
private val minDate: LocalDate? by lazy { (requireArguments().getSerializable(ARG_MIN_DATE) as? LocalDate) }
private val maxDate: LocalDate? by lazy { (requireArguments().getSerializable(ARG_MAX_DATE) as? LocalDate) }
private val caller: String by lazy { requireNotNull(requireArguments().getString(ARG_REQUEST_KEY)) }
private val viewModel: DatePickerDialogViewModel by activityViewModels()
private var selectedDate: LocalDate? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialogView = View.inflate(requireContext(), R.layout.view_date_picker, null)
val datePicker = dialogView.findViewById<DatePicker>(R.id.date_picker)
val alertDialog = AlertDialog.Builder(requireContext()).create()
datePicker.init(preselectedDate.year, preselectedDate.legacyCalendarMonth, preselectedDate.dayOfMonth, this)
minDate?.let { datePicker.minDate = it.toEpochMilli(ZoneOffset.UTC) }
maxDate?.let { datePicker.maxDate = it.toEpochMilli(ZoneOffset.UTC) }
dialogView.findViewById<View>(R.id.btn_ok).setOnClickListener {
selectedDate?.let { viewModel.onDateSelected(requestKey = caller, date = it) }
alertDialog.dismiss()
}
dialogView.findViewById<View>(R.id.btn_cancel).setOnClickListener {
alertDialog.dismiss()
}
alertDialog.setView(dialogView)
alertDialog.window?.decorView?.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
if (doesDatePickerTakeAllScreenSpace(datePicker, dialogView)) {
dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
}
return alertDialog
}
private fun doesDatePickerTakeAllScreenSpace(datePicker: DatePicker, dialogView: View) = datePicker.height >= dialogView.height
override fun onDateChanged(view: DatePicker?, year: Int, monthOfYear: Int, dayOfMonth: Int) {
selectedDate = localDateOfCalendar(year, monthOfYear, dayOfMonth)
}
companion object {
private const val ARG_PRESELECTED_DATE = "preselectedDate"
private const val ARG_MIN_DATE = "minDate"
private const val ARG_MAX_DATE = "maxDate"
private const val ARG_REQUEST_KEY = "requestKey"
fun newInstance(preselectedDate: LocalDate?, requestKey: String, minDate: LocalDate? = null, maxDate: LocalDate? = null) =
DatePickerDialogFragment().apply {
arguments = bundleOf(
ARG_PRESELECTED_DATE to preselectedDate,
ARG_MIN_DATE to minDate,
ARG_MAX_DATE to maxDate,
ARG_REQUEST_KEY to requestKey,
)
}
}
}
view raw DatePicker.kt hosted with ❤ by GitHub
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<DatePicker
android:id="@+id/date_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="true"
android:spinnersShown="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/btn_ok"
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="@string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/date_picker" />
<TextView
android:id="@+id/btn_cancel"
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_ok"
app:layout_constraintTop_toBottomOf="@id/date_picker" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s