gantonious / MaterialDayPicker

An elegant day of the week picker inspired by Google's clock app
MIT License
59 stars 10 forks source link

Add day range selection #48

Open SapuSeven opened 4 years ago

SapuSeven commented 4 years ago

I want to allow a user to input a day range. Using this library, it is possible to select every day independently, however for my application I only want to allow selection of a range of continuous days.

You can see this on the Material Design Pickers page as Mobile Date Range Picker.

I imagine it looking like this, only for a single week:

Is there any possibility for this to get added?

gantonious commented 4 years ago

This is a great idea but I don't have the bandwidth right now to build it. You are more than welcome to add this functionality and submit a pull request.

gantonious commented 4 years ago

If you're okay with not having the background between the start and end days you can implement a custom SelectionMode to achieve the range behaviour. I drafted one below. You can update it to get the user experience you want:

class RangeSelectionMode(
    private val materialDayPicker: MaterialDayPicker
) : SelectionMode {

    override fun getSelectionStateAfterSelecting(lastSelectionState: SelectionState, dayToSelect: MaterialDayPicker.Weekday): SelectionState {
        return createRangedSelectionState(
            lastSelectionState = lastSelectionState,
            dayPressed = dayToSelect
        )
    }

    override fun getSelectionStateAfterDeselecting(lastSelectionState: SelectionState, dayToDeselect: MaterialDayPicker.Weekday): SelectionState {
        return createRangedSelectionState(
            lastSelectionState = lastSelectionState,
            dayPressed = dayToDeselect
        )
    }

    private fun createRangedSelectionState(lastSelectionState: SelectionState, dayPressed: MaterialDayPicker.Weekday): SelectionState {
        val previouslySelectedDays = lastSelectionState.selectedDays
        val orderedWeekdays = getWeekdaysOrderedForCurrentLocale()
        val ordinalsOfPreviouslySelectedDays = previouslySelectedDays.map { orderedWeekdays.indexOf(it) }

        val ordinalOfFirstDayInPreviousRange = ordinalsOfPreviouslySelectedDays.min()
        val ordinalOfLastDayInPreviousRange = ordinalsOfPreviouslySelectedDays.max()
        val ordinalOfSelectedDay = orderedWeekdays.indexOf(dayPressed)

        return when {
            ordinalOfFirstDayInPreviousRange == null || ordinalOfLastDayInPreviousRange == null -> {
                // We had no previous selection so just return the day pressed as the selection.
                SelectionState.withSingleDay(dayPressed)
            }
            ordinalOfFirstDayInPreviousRange == ordinalOfLastDayInPreviousRange && ordinalOfFirstDayInPreviousRange == ordinalOfSelectedDay -> {
                // User pressed the only day in the range selection. Return an empty selection.
                SelectionState()
            }
            ordinalOfSelectedDay == ordinalOfFirstDayInPreviousRange || ordinalOfSelectedDay == ordinalOfLastDayInPreviousRange -> {
                // User pressed the first or last item in range. Just deselect that item.
                lastSelectionState.withDayDeselected(dayPressed)
            }
            ordinalOfSelectedDay < ordinalOfFirstDayInPreviousRange -> {
                // User pressed a day on the left of the previous date range. Grow the starting point of the range to that.
                SelectionState(selectedDays = orderedWeekdays.subList(ordinalOfSelectedDay, ordinalOfLastDayInPreviousRange + 1))
            }
            else -> {
                // User pressed a day on the right of the start of the date range. Update the ending point to that position.
                SelectionState(selectedDays = orderedWeekdays.subList(ordinalOfFirstDayInPreviousRange, ordinalOfSelectedDay + 1))
            }
        }
    }

    private fun getWeekdaysOrderedForCurrentLocale(): List<MaterialDayPicker.Weekday> {
        return MaterialDayPicker.Weekday.getOrderedDaysOfWeek(materialDayPicker.locale)
    }
}

You can use it by doing:

materialDayPicker.selectionMode = RangeSelectionMode(materialDayPicker)

Sample usage: range_selections

SapuSeven commented 4 years ago

Awesome, thank you very much! I'll use your suggested approach for now, maybe I'll implement the background thing and submit a pull request later.