// Copyright 2012 Square, Inc. package com.squareup.timessquare; import static java.util.Calendar.DATE; import static java.util.Calendar.DAY_OF_MONTH; import static java.util.Calendar.DAY_OF_WEEK; import static java.util.Calendar.HOUR_OF_DAY; import static java.util.Calendar.MILLISECOND; import static java.util.Calendar.MINUTE; import static java.util.Calendar.MONTH; import static java.util.Calendar.SECOND; import static java.util.Calendar.YEAR; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.Toast; import com.ectrip.trips.check.R; import com.squareup.timessquare.MonthCellDescriptor.RangeState; /** * Android component to allow picking a date from a calendar view (a list of * months). Must be initialized after inflation with {@link #init(Date, Date)} * and can be customized with any of the {@link FluentInitializer} methods * returned. The currently selected date can be retrieved with * {@link #getSelectedDate()}. */ public class CalendarPickerView extends ListView { public enum SelectionMode { /** * Only one date will be selectable. If there is already a selected date * and you select a new one, the old date will be unselected. */ SINGLE, /** * Multiple dates will be selectable. Selecting an already-selected date * will un-select it. */ MULTIPLE, /** * Allows you to select a date range. Previous selections are cleared * when you either: *
* This will implicitly set the {@link SelectionMode} to * {@link SelectionMode#SINGLE}. If you want a different selection mode, use * {@link FluentInitializer#inMode(SelectionMode)} on the * {@link FluentInitializer} this method returns. *
* The calendar will be constructed using the given locale. This means that * all names (months, days) will be in the language of the locale and the * weeks start with the day specified by the locale. * * @param minDate * Earliest selectable date, inclusive. Must be earlier than * {@code maxDate}. * @param maxDate * Latest selectable date, exclusive. Must be later than * {@code minDate}. */ public FluentInitializer init(Date minDate, Date maxDate, Locale locale) { if (minDate == null || maxDate == null) { throw new IllegalArgumentException( "minDate and maxDate must be non-null. " + dbg(minDate, maxDate)); } if (minDate.after(maxDate)) { throw new IllegalArgumentException( "minDate must be before maxDate. " + dbg(minDate, maxDate)); } if (minDate.getTime() == 0 || maxDate.getTime() == 0) { throw new IllegalArgumentException( "minDate and maxDate must be non-zero. " + dbg(minDate, maxDate)); } if (locale == null) { throw new IllegalArgumentException("Locale is null."); } // Make sure that all calendar instances use the same locale. this.locale = locale; today = Calendar.getInstance(locale); minCal = Calendar.getInstance(locale); maxCal = Calendar.getInstance(locale); monthCounter = Calendar.getInstance(locale); monthNameFormat = new SimpleDateFormat(getContext().getString( R.string.month_name_format), locale); for (MonthDescriptor month : months) { month.setLabel(monthNameFormat.format(month.getDate())); } weekdayNameFormat = new SimpleDateFormat(getContext().getString( R.string.day_name_format), locale); fullDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); this.selectionMode = SelectionMode.SINGLE; // Clear out any previously-selected dates/cells. selectedCals.clear(); selectedCells.clear(); highlightedCells.clear(); // Clear previous state. cells.clear(); months.clear(); minCal.setTime(minDate); maxCal.setTime(maxDate); setMidnight(minCal); setMidnight(maxCal); displayOnly = false; // maxDate is exclusive: bump back to the previous day so if maxDate is // the first of a month, // we don't accidentally include that month in the view. maxCal.add(MINUTE, -1); // Now iterate between minCal and maxCal and build up our list of months // to show. monthCounter.setTime(minCal.getTime()); final int maxMonth = maxCal.get(MONTH); final int maxYear = maxCal.get(YEAR); while ((monthCounter.get(MONTH) <= maxMonth // Up to, including the // month. || monthCounter.get(YEAR) < maxYear) // Up to the year. && monthCounter.get(YEAR) < maxYear + 1) { // But not > next yr. Date date = monthCounter.getTime(); MonthDescriptor month = new MonthDescriptor( monthCounter.get(MONTH), monthCounter.get(YEAR), date, monthNameFormat.format(date)); cells.add(getMonthCells(month, monthCounter)); months.add(month); monthCounter.add(MONTH, 1); } validateAndUpdate(); return new FluentInitializer(); } /** * Both date parameters must be non-null and their {@link Date#getTime()} * must not return 0. Time of day will be ignored. For instance, if you pass * in {@code minDate} as 11/16/2012 5:15pm and {@code maxDate} as 11/16/2013 * 4:30am, 11/16/2012 will be the first selectable date and 11/15/2013 will * be the last selectable date ({@code maxDate} is exclusive). *
* This will implicitly set the {@link SelectionMode} to * {@link SelectionMode#SINGLE}. If you want a different selection mode, use * {@link FluentInitializer#inMode(SelectionMode)} on the * {@link FluentInitializer} this method returns. *
* The calendar will be constructed using the default locale as returned by
* {@link java.util.Locale#getDefault()}. If you wish the calendar to be
* constructed using a different locale, use
* {@link #init(java.util.Date, java.util.Date, java.util.Locale)}.
*
* @param minDate
* Earliest selectable date, inclusive. Must be earlier than
* {@code maxDate}.
* @param maxDate
* Latest selectable date, exclusive. Must be later than
* {@code minDate}.
*/
public FluentInitializer init(Date minDate, Date maxDate) {
return init(minDate, maxDate, Locale.getDefault());
}
public class FluentInitializer {
/**
* Override the {@link SelectionMode} from the default (
* {@link SelectionMode#SINGLE}).
*/
public FluentInitializer inMode(SelectionMode mode) {
selectionMode = mode;
validateAndUpdate();
return this;
}
/**
* Set an initially-selected date. The calendar will scroll to that date
* if it's not already visible.
*/
public FluentInitializer withSelectedDate(Date selectedDates) {
return withSelectedDates(Arrays.asList(selectedDates));
}
/**
* Set multiple selected dates. This will throw an
* {@link IllegalArgumentException} if you pass in multiple dates and
* haven't already called {@link #inMode(SelectionMode)}.
*/
public FluentInitializer withSelectedDates(
Collection
* If the selection was made (selectable date, in range), the view will
* scroll to the newly selected date if it's not already visible.
*
* @return - whether we were able to set the date
*/
public boolean selectDate(Date date) {
validateDate(date);
MonthCellWithMonthIndex monthCellWithMonthIndex = getMonthCellWithIndexByDate(date);
if (monthCellWithMonthIndex == null || !isDateSelectable(date)) {
return false;
}
boolean wasSelected = doSelectDate(date, monthCellWithMonthIndex.cell);
if (wasSelected) {
scrollToSelectedMonth(monthCellWithMonthIndex.monthIndex);
}
return wasSelected;
}
private void validateDate(Date date) {
if (date == null) {
throw new IllegalArgumentException(
"Selected date must be non-null.");
}
if (date.getTime() == 0) {
throw new IllegalArgumentException(
"Selected date must be non-zero. " + date);
}
if (date.before(minCal.getTime()) || date.after(maxCal.getTime())) {
throw new IllegalArgumentException(String.format(
"SelectedDate must be between minDate and maxDate."
+ "%nminDate: %s%nmaxDate: %s%nselectedDate: %s",
minCal.getTime(), maxCal.getTime(), date));
}
}
private boolean doSelectDate(Date date, MonthCellDescriptor cell) {
Calendar newlySelectedCal = Calendar.getInstance(locale);
newlySelectedCal.setTime(date);
// Sanitize input: clear out the hours/minutes/seconds/millis.
setMidnight(newlySelectedCal);
// Clear any remaining range state.
for (MonthCellDescriptor selectedCell : selectedCells) {
selectedCell.setRangeState(RangeState.NONE);
}
switch (selectionMode) {
case RANGE:
if (selectedCals.size() > 1) {
// We've already got a range selected: clear the old one.
clearOldSelections();
} else if (selectedCals.size() == 1
&& newlySelectedCal.before(selectedCals.get(0))) {
// We're moving the start of the range back in time: clear the
// old start date.
clearOldSelections();
}
break;
case MULTIPLE:
date = applyMultiSelect(date, newlySelectedCal);
break;
case SINGLE:
clearOldSelections();
break;
default:
throw new IllegalStateException("Unknown selectionMode "
+ selectionMode);
}
if (date != null) {
// Select a new cell.
if (selectedCells.size() == 0 || !selectedCells.get(0).equals(cell)) {
selectedCells.add(cell);
cell.setSelected(true);
}
selectedCals.add(newlySelectedCal);
if (selectionMode == SelectionMode.RANGE
&& selectedCells.size() > 1) {
// Select all days in between start and end.
Date start = selectedCells.get(0).getDate();
Date end = selectedCells.get(1).getDate();
selectedCells.get(0).setRangeState(
MonthCellDescriptor.RangeState.FIRST);
selectedCells.get(1).setRangeState(
MonthCellDescriptor.RangeState.LAST);
for (List
* Important: set this before you call {@link #init(Date, Date)} methods. If
* called afterwards, it will not be consistently applied.
*/
public void setDateSelectableFilter(DateSelectableFilter listener) {
dateConfiguredListener = listener;
}
/**
* Interface to be notified when a new date is selected or unselected. This
* will only be called when the user initiates the date selection. If you
* call {@link #selectDate(Date)} this listener will not be notified.
*
* @see #setOnDateSelectedListener(OnDateSelectedListener)
*/
public interface OnDateSelectedListener {
void onDateSelected(Date date);
void onDateUnselected(Date date);
}
/**
* Interface to be notified when an invalid date is selected by the user.
* This will only be called when the user initiates the date selection. If
* you call {@link #selectDate(Date)} this listener will not be notified.
*
* @see #setOnInvalidDateSelectedListener(OnInvalidDateSelectedListener)
*/
public interface OnInvalidDateSelectedListener {
void onInvalidDateSelected(Date date);
}
/**
* Interface used for determining the selectability of a date cell when it
* is configured for display on the calendar.
*
* @see #setDateSelectableFilter(DateSelectableFilter)
*/
public interface DateSelectableFilter {
boolean isDateSelectable(Date date);
}
private class DefaultOnInvalidDateSelectedListener implements
OnInvalidDateSelectedListener {
@Override
public void onInvalidDateSelected(Date date) {
String errMessage = getResources().getString(R.string.invalid_date,
fullDateFormat.format(minCal.getTime()),
fullDateFormat.format(maxCal.getTime()));
Toast.makeText(getContext(), errMessage, Toast.LENGTH_SHORT).show();
}
}
}
> month : cells) {
for (List
> monthCells : cells) {
for (List
> getMonthCells(MonthDescriptor month,
Calendar startCal) {
Calendar cal = Calendar.getInstance(locale);
cal.setTime(startCal.getTime());
List
> cells = new ArrayList
>();
cal.set(DAY_OF_MONTH, 1);
int firstDayOfWeek = cal.get(DAY_OF_WEEK);
int offset = cal.getFirstDayOfWeek() - firstDayOfWeek;
if (offset > 0) {
offset -= 7;
}
cal.add(Calendar.DATE, offset);
Calendar minSelectedCal = minDate(selectedCals);
Calendar maxSelectedCal = maxDate(selectedCals);
while ((cal.get(MONTH) < month.getMonth() + 1 || cal.get(YEAR) < month
.getYear()) //
&& cal.get(YEAR) <= month.getYear()) {
List