Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Closes #3843: Ported the Month picker widget from Fennec
Browse files Browse the repository at this point in the history
  • Loading branch information
Amejia481 authored and pocmo committed Jul 23, 2019
1 parent cf82f8e commit 635a013
Show file tree
Hide file tree
Showing 13 changed files with 590 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe

val selectionType = when (format) {
"HH:mm" -> TimeSelection.Type.TIME
"yyyy-MM" -> TimeSelection.Type.MONTH
"yyyy-MM-dd'T'HH:mm" -> TimeSelection.Type.DATE_AND_TIME
else -> TimeSelection.Type.DATE
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ sealed class PromptRequest {
val onClear: () -> Unit
) : PromptRequest() {
enum class Type {
DATE, DATE_AND_TIME, TIME
DATE, DATE_AND_TIME, TIME, MONTH
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ class PromptFeature(
TimeSelection.Type.DATE -> TimePickerDialogFragment.SELECTION_TYPE_DATE
TimeSelection.Type.DATE_AND_TIME -> TimePickerDialogFragment.SELECTION_TYPE_DATE_AND_TIME
TimeSelection.Type.TIME -> TimePickerDialogFragment.SELECTION_TYPE_TIME
TimeSelection.Type.MONTH -> TimePickerDialogFragment.SELECTION_TYPE_MONTH
}

with(promptRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import android.widget.DatePicker
import android.widget.TimePicker
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import mozilla.components.feature.prompts.ext.day
import mozilla.components.feature.prompts.ext.hour
import mozilla.components.feature.prompts.ext.minute
import mozilla.components.feature.prompts.ext.month
import mozilla.components.feature.prompts.ext.toCalendar
import mozilla.components.feature.prompts.ext.year
import mozilla.components.feature.prompts.widget.MonthAndYearPicker
import java.util.Calendar
import java.util.Date

Expand All @@ -37,7 +44,8 @@ private const val KEY_SELECTION_TYPE = "KEY_SELECTION_TYPE"
@Suppress("TooManyFunctions")
internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnDateChangedListener,
TimePicker.OnTimeChangedListener, TimePickerDialog.OnTimeSetListener,
DatePickerDialog.OnDateSetListener, DialogInterface.OnClickListener {
DatePickerDialog.OnDateSetListener, DialogInterface.OnClickListener,
MonthAndYearPicker.OnDateSetListener {
private val initialDate: Date by lazy { safeArguments.getSerializable(KEY_INITIAL_DATE) as Date }
private val minimumDate: Date? by lazy { safeArguments.getSerializable((KEY_MIN_DATE)) as? Date }
private val maximumDate: Date? by lazy { safeArguments.getSerializable(KEY_MAX_DATE) as? Date }
Expand Down Expand Up @@ -78,6 +86,14 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD
it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this)
it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this)
}
SELECTION_TYPE_MONTH -> AlertDialog.Builder(context)
.setTitle(R.string.mozac_feature_prompts_set_month)
.setView(inflateDateMonthPicker())
.create()
.also {
it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this)
it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this)
}
else -> throw IllegalArgumentException()
}

Expand Down Expand Up @@ -112,6 +128,16 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD
return view
}

private fun inflateDateMonthPicker(): View {
return MonthAndYearPicker(
context = requireContext(),
selectedDate = initialDate.toCalendar(),
maxDate = maximumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMaxDate(),
minDate = minimumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMinDate(),
dateSetListener = this
)
}

@Suppress("DEPRECATION")
private fun initTimePicker(picker: TimePicker, cal: Calendar) {
if (Build.VERSION.SDK_INT >= M) {
Expand Down Expand Up @@ -145,6 +171,10 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD
onClick(null, BUTTON_POSITIVE)
}

override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) {
onDateChanged(null, year, month, 0)
}

override fun onTimeChanged(picker: TimePicker?, hourOfDay: Int, minute: Int) {
val calendar = selectedDate.toCalendar()
calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
Expand Down Expand Up @@ -203,18 +233,6 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD
const val SELECTION_TYPE_DATE = 1
const val SELECTION_TYPE_DATE_AND_TIME = 2
const val SELECTION_TYPE_TIME = 3
const val SELECTION_TYPE_MONTH = 4
}
}

internal fun Date.toCalendar() = Calendar.getInstance().also { it.time = this }

internal val Calendar.minute: Int
get() = get(Calendar.MINUTE)
internal val Calendar.hour: Int
get() = get(Calendar.HOUR_OF_DAY)
internal val Calendar.day: Int
get() = get(Calendar.DAY_OF_MONTH)
internal val Calendar.year: Int
get() = get(Calendar.YEAR)
internal val Calendar.month: Int
get() = get(Calendar.MONTH)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.prompts.ext

import java.util.Calendar
import java.util.Date

internal fun Date.toCalendar() = Calendar.getInstance().also { it.time = this }

internal val Calendar.minute: Int
get() = get(Calendar.MINUTE)
internal val Calendar.hour: Int
get() = get(Calendar.HOUR_OF_DAY)
internal var Calendar.day: Int
get() = get(Calendar.DAY_OF_MONTH)
set(value) {
set(Calendar.DAY_OF_MONTH, value)
}
internal var Calendar.year: Int
get() = get(Calendar.YEAR)
set(value) {
set(Calendar.YEAR, value)
}

internal var Calendar.month: Int
get() = get(Calendar.MONTH)
set(value) {
set(Calendar.MONTH, value)
}

internal fun Calendar.minMonth(): Int = getMinimum(Calendar.MONTH)
internal fun Calendar.maxMonth(): Int = getActualMaximum(Calendar.MONTH)
internal fun Calendar.minDay(): Int = getMinimum(Calendar.DAY_OF_MONTH)
internal fun Calendar.maxDay(): Int = getActualMaximum(Calendar.DAY_OF_MONTH)
internal fun Calendar.minYear(): Int = getMinimum(Calendar.YEAR)
internal fun Calendar.maxYear(): Int = getActualMaximum(Calendar.YEAR)
internal fun now() = Calendar.getInstance()
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.prompts.widget

import android.annotation.SuppressLint
import android.content.Context
import android.widget.NumberPicker
import android.widget.ScrollView
import androidx.annotation.VisibleForTesting
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.ext.month
import mozilla.components.feature.prompts.ext.now
import mozilla.components.feature.prompts.ext.year
import java.util.Calendar

/**
* UI widget that allows to select a month and a year.
*/
@SuppressLint("ViewConstructor") // This view is only instantiated in code
internal class MonthAndYearPicker @JvmOverloads constructor(
context: Context,
private val selectedDate: Calendar = now(),
private val maxDate: Calendar = getDefaultMaxDate(),
private val minDate: Calendar = getDefaultMinDate(),
internal var dateSetListener: OnDateSetListener? = null
) : ScrollView(context), NumberPicker.OnValueChangeListener {

@VisibleForTesting
internal val monthView: NumberPicker
@VisibleForTesting
internal val yearView: NumberPicker
private val monthsLabels: Array<out String>

init {
inflate(context, R.layout.mozac_feature_promps_widget_month_picker, this)

adjustMinMaxDateIfAreInIllogicalRange()
adjustIfSelectedDateIsInIllogicalRange()

monthsLabels = context.resources.getStringArray(R.array.mozac_feature_prompts_months)

monthView = findViewById(R.id.month_chooser)
yearView = findViewById(R.id.year_chooser)

iniMonthView()
iniYearView()
}

@Suppress("LongMethod")
override fun onValueChange(view: NumberPicker, oldVal: Int, newVal: Int) {
var month = 0
var year = 0
when (view.id) {
R.id.month_chooser -> {
month = newVal
// Wrapping months to update greater fields
if (oldVal == view.maxValue && newVal == view.minValue) {
yearView.value += 1
if (!yearView.value.isMinYear()) {
month = Calendar.JANUARY
}
} else if (oldVal == view.minValue && newVal == view.maxValue) {
yearView.value -= 1
if (!yearView.value.isMaxYear()) {
month = Calendar.DECEMBER
}
}
year = yearView.value
}
R.id.year_chooser -> {
month = monthView.value
year = newVal
}
}

selectedDate.month = month
selectedDate.year = year
updateMonthView(month)
dateSetListener?.onDateSet(this, month + 1, year) // Month is zero based
}

private fun Int.isMinYear() = minDate.year == this
private fun Int.isMaxYear() = maxDate.year == this

private fun iniMonthView() {
monthView.setOnValueChangedListener(this)
monthView.setOnLongPressUpdateInterval(SPEED_MONTH_SPINNER)
updateMonthView(selectedDate.month)
}

private fun iniYearView() {
val year = selectedDate.year
val max = maxDate.year
val min = minDate.year

yearView.init(year, min, max)
yearView.wrapSelectorWheel = false
yearView.setOnLongPressUpdateInterval(SPEED_YEAR_SPINNER)
}

private fun updateMonthView(month: Int) {
var min = Calendar.JANUARY
var max = Calendar.DECEMBER

if (selectedDate.year.isMinYear()) {
min = minDate.month
}

if (selectedDate.year.isMaxYear()) {
max = maxDate.month
}

monthView.apply {
displayedValues = null
minValue = min
maxValue = max
displayedValues = monthsLabels.copyOfRange(monthView.minValue, monthView.maxValue + 1)
value = month
wrapSelectorWheel = true
}
}

private fun adjustMinMaxDateIfAreInIllogicalRange() {
// If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the
// user to select any date). If we try to make any assumptions based on the illogical min/max date we could
// potentially prevent the user from selecting dates that are in the developers intended range, so it's best
// to allow anything.
if (maxDate.before(minDate)) {
minDate.timeInMillis = getDefaultMinDate().timeInMillis
maxDate.timeInMillis = getDefaultMaxDate().timeInMillis
}
}

private fun adjustIfSelectedDateIsInIllogicalRange() {
if (selectedDate.before(minDate) || selectedDate.after(maxDate)) {
selectedDate.timeInMillis = minDate.timeInMillis
}
}

private fun NumberPicker.init(currentValue: Int, min: Int, max: Int) {
minValue = min
maxValue = max
value = currentValue
setOnValueChangedListener(this@MonthAndYearPicker)
}

interface OnDateSetListener {
fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int)
}

companion object {

private const val SPEED_MONTH_SPINNER = 200L
private const val SPEED_YEAR_SPINNER = 100L
private const val DEFAULT_VALUE = -1

@VisibleForTesting
internal const val DEFAULT_MAX_YEAR = 9999

@VisibleForTesting
internal const val DEFAULT_MIN_YEAR = 1

internal fun getDefaultMinDate(): Calendar {
return now().apply {
month = Calendar.JANUARY
year = DEFAULT_MIN_YEAR
}
}

internal fun getDefaultMaxDate(): Calendar {
return now().apply {
month = Calendar.DECEMBER
year = DEFAULT_MAX_YEAR
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>

<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.ScrollView">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">

<android.widget.NumberPicker
android:id="@+id/month_chooser"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:focusable="true"
android:focusableInTouchMode="true" />

<android.widget.NumberPicker
android:id="@+id/year_chooser"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:focusable="true"
android:focusableInTouchMode="true" />

</LinearLayout>
</merge>


Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<!-- Months of the years, used on the month chooser dialog. -->
<string-array name="mozac_feature_prompts_months">
<item>@string/mozac_feature_prompts_jan</item>
<item>@string/mozac_feature_prompts_feb</item>
<item>@string/mozac_feature_prompts_mar</item>
<item>@string/mozac_feature_prompts_apr</item>
<item>@string/mozac_feature_prompts_may</item>
<item>@string/mozac_feature_prompts_jun</item>
<item>@string/mozac_feature_prompts_jul</item>
<item>@string/mozac_feature_prompts_aug</item>
<item>@string/mozac_feature_prompts_sep</item>
<item>@string/mozac_feature_prompts_oct</item>
<item>@string/mozac_feature_prompts_nov</item>
<item>@string/mozac_feature_prompts_dec</item>
</string-array>
</resources>
Loading

0 comments on commit 635a013

Please sign in to comment.