// Copyright 2012 Square, Inc. package com.marshalchen.common.uimodule.timessquare; 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.marshalchen.common.uimodule.R; import com.marshalchen.common.commonUtils.logUtils.Logs; import com.marshalchen.common.uimodule.timessquare.MonthCellDescriptor.RangeState; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import static java.util.Calendar.*; /** * Android component to allow picking a date from a calendar view (a list of months). Must be * initialized after inflation with {@link #init(java.util.Date, java.util.Date)} and can be customized with any of the * {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.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: * <ul> * <li>Have a range selected and select another date (even if it's in the current range).</li> * <li>Have one date selected and then select an earlier date.</li> * </ul> */ RANGE } private final MonthAdapter adapter; private final List<List<List<MonthCellDescriptor>>> cells = new ArrayList<List<List<MonthCellDescriptor>>>(); final MonthView.Listener listener = new CellClickedListener(); final List<MonthDescriptor> months = new ArrayList<MonthDescriptor>(); final List<MonthCellDescriptor> selectedCells = new ArrayList<MonthCellDescriptor>(); final List<MonthCellDescriptor> highlightedCells = new ArrayList<MonthCellDescriptor>(); final List<Calendar> selectedCals = new ArrayList<Calendar>(); final List<Calendar> highlightedCals = new ArrayList<Calendar>(); private Locale locale; private DateFormat monthNameFormat; private DateFormat weekdayNameFormat; private DateFormat fullDateFormat; private Calendar minCal; private Calendar maxCal; private Calendar monthCounter; private boolean displayOnly; SelectionMode selectionMode; Calendar today; private int dividerColor; private int dayBackgroundResId; private int dayTextColorResId; private int titleTextColor; private int headerTextColor; private OnDateSelectedListener dateListener; private DateSelectableFilter dateConfiguredListener; private OnInvalidDateSelectedListener invalidDateListener = new DefaultOnInvalidDateSelectedListener(); public CalendarPickerView(Context context, AttributeSet attrs) { super(context, attrs); Resources res = context.getResources(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CalendarPickerView); final int bg = a.getColor(R.styleable.CalendarPickerView_android_background, res.getColor(R.color.calendar_bg)); dividerColor = a.getColor(R.styleable.CalendarPickerView_dividerColor, res.getColor(R.color.calendar_divider)); dayBackgroundResId = a.getResourceId(R.styleable.CalendarPickerView_dayBackground, R.drawable.calendar_bg_selector); dayTextColorResId = a.getResourceId(R.styleable.CalendarPickerView_dayTextColor, R.color.calendar_text_selector); titleTextColor = a.getColor(R.styleable.CalendarPickerView_titleTextColor, R.color.calendar_text_active); headerTextColor = a.getColor(R.styleable.CalendarPickerView_headerTextColor, R.color.calendar_text_active); a.recycle(); adapter = new MonthAdapter(); setDivider(null); setDividerHeight(0); setBackgroundColor(bg); setCacheColorHint(bg); locale = Locale.getDefault(); today = Calendar.getInstance(locale); minCal = Calendar.getInstance(locale); maxCal = Calendar.getInstance(locale); monthCounter = Calendar.getInstance(locale); monthNameFormat = new SimpleDateFormat(context.getString(R.string.month_name_format), locale); weekdayNameFormat = new SimpleDateFormat(context.getString(R.string.day_name_format), locale); fullDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); if (isInEditMode()) { Calendar nextYear = Calendar.getInstance(locale); nextYear.add(Calendar.YEAR, 1); init(new Date(), nextYear.getTime()) // .withSelectedDate(new Date()); } } /** * Both date parameters must be non-null and their {@link java.util.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). * <p/> * This will implicitly set the {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode} to {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#SINGLE}. If you * want a different selection mode, use {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.FluentInitializer#inMode(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode)} on the * {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.FluentInitializer} this method returns. * <p/> * 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(); highlightedCals.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)); Logs.d("Adding month %s" + month); months.add(month); monthCounter.add(MONTH, 1); } validateAndUpdate(); return new FluentInitializer(); } /** * Both date parameters must be non-null and their {@link java.util.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). * <p/> * This will implicitly set the {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode} to {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#SINGLE}. If you * want a different selection mode, use {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.FluentInitializer#inMode(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode)} on the * {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.FluentInitializer} this method returns. * <p/> * 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 com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode} from the default ({@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.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(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode)}. */ public FluentInitializer withSelectedDates(Collection<Date> selectedDates) { if (selectionMode == SelectionMode.SINGLE && selectedDates.size() > 1) { throw new IllegalArgumentException("SINGLE mode can't be used with multiple selectedDates"); } if (selectedDates != null) { for (Date date : selectedDates) { selectDate(date); } } scrollToSelectedDates(); validateAndUpdate(); return this; } public FluentInitializer withHighlightedDates(Collection<Date> dates) { highlightDates(dates); return this; } public FluentInitializer withHighlightedDate(Date date) { return withHighlightedDates(Arrays.asList(date)); } public FluentInitializer displayOnly() { displayOnly = true; return this; } } private void validateAndUpdate() { if (getAdapter() == null) { setAdapter(adapter); } adapter.notifyDataSetChanged(); } private void scrollToSelectedMonth(final int selectedIndex) { scrollToSelectedMonth(selectedIndex, false); } private void scrollToSelectedMonth(final int selectedIndex, final boolean smoothScroll) { post(new Runnable() { @Override public void run() { Logs.d("Scrolling to position %d" + selectedIndex); if (smoothScroll) { smoothScrollToPosition(selectedIndex); } else { setSelection(selectedIndex); } } }); } private void scrollToSelectedDates() { Integer selectedIndex = null; Integer todayIndex = null; Calendar today = Calendar.getInstance(locale); for (int c = 0; c < months.size(); c++) { MonthDescriptor month = months.get(c); if (selectedIndex == null) { for (Calendar selectedCal : selectedCals) { if (sameMonth(selectedCal, month)) { selectedIndex = c; break; } } if (selectedIndex == null && todayIndex == null && sameMonth(today, month)) { todayIndex = c; } } } if (selectedIndex != null) { scrollToSelectedMonth(selectedIndex); } else if (todayIndex != null) { scrollToSelectedMonth(todayIndex); } } /** * This method should only be called if the calendar is contained in a dialog, and it should only * be called once, right after the dialog is shown (using * {@link android.content.DialogInterface.OnShowListener} or * {@link android.app.DialogFragment#onStart()}). */ public void fixDialogDimens() { Logs.d("Fixing dimensions to h = %d / w = %d"+getMeasuredHeight()+getMeasuredWidth()); // Fix the layout height/width after the dialog has been shown. getLayoutParams().height = getMeasuredHeight(); getLayoutParams().width = getMeasuredWidth(); // Post this runnable so it runs _after_ the dimen changes have been applied/re-measured. post(new Runnable() { @Override public void run() { Logs.d("Dimens are fixed: now scroll to the selected date"); scrollToSelectedDates(); } }); } /** * This method should only be called if the calendar is contained in a dialog, and it should only * be called when the screen has been rotated and the dialog should be re-measured. */ public void unfixDialogDimens() { Logs.d("Reset the fixed dimensions to allow for re-measurement"); // Fix the layout height/width after the dialog has been shown. getLayoutParams().height = LayoutParams.MATCH_PARENT; getLayoutParams().width = LayoutParams.MATCH_PARENT; requestLayout(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (months.isEmpty()) { throw new IllegalStateException( "Must have at least one month to display. Did you forget to call init()?"); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public Date getSelectedDate() { return (selectedCals.size() > 0 ? selectedCals.get(0).getTime() : null); } public List<Date> getSelectedDates() { List<Date> selectedDates = new ArrayList<Date>(); for (MonthCellDescriptor cal : selectedCells) { selectedDates.add(cal.getDate()); } Collections.sort(selectedDates); return selectedDates; } /** * Returns a string summarizing what the client sent us for init() params. */ private static String dbg(Date minDate, Date maxDate) { return "minDate: " + minDate + "\nmaxDate: " + maxDate; } /** * Clears out the hours/minutes/seconds/millis of a Calendar. */ static void setMidnight(Calendar cal) { cal.set(HOUR_OF_DAY, 0); cal.set(MINUTE, 0); cal.set(SECOND, 0); cal.set(MILLISECOND, 0); } private class CellClickedListener implements MonthView.Listener { @Override public void handleClick(MonthCellDescriptor cell) { Date clickedDate = cell.getDate(); if (!betweenDates(clickedDate, minCal, maxCal) || !isDateSelectable(clickedDate)) { if (invalidDateListener != null) { invalidDateListener.onInvalidDateSelected(clickedDate); } } else { boolean wasSelected = doSelectDate(clickedDate, cell); if (dateListener != null) { if (wasSelected) { dateListener.onDateSelected(clickedDate); } else { dateListener.onDateUnselected(clickedDate); } } } } } /** * Select a new date. Respects the {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode} this CalendarPickerView is configured * with: if you are in {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#SINGLE}, the previously selected date will be * un-selected. In {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#MULTIPLE}, the new date will be added to the list of * selected dates. * <p/> * 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) { return selectDate(date, false); } /** * Select a new date. Respects the {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode} this CalendarPickerView is configured * with: if you are in {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#SINGLE}, the previously selected date will be * un-selected. In {@link com.marshalchen.common.uimodule.timessquare.CalendarPickerView.SelectionMode#MULTIPLE}, the new date will be added to the list of * selected dates. * <p/> * 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, boolean smoothScroll) { validateDate(date); MonthCellWithMonthIndex monthCellWithMonthIndex = getMonthCellWithIndexByDate(date); if (monthCellWithMonthIndex == null || !isDateSelectable(date)) { return false; } boolean wasSelected = doSelectDate(date, monthCellWithMonthIndex.cell); if (wasSelected) { scrollToSelectedMonth(monthCellWithMonthIndex.monthIndex, smoothScroll); } 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<List<MonthCellDescriptor>> month : cells) { for (List<MonthCellDescriptor> week : month) { for (MonthCellDescriptor singleCell : week) { if (singleCell.getDate().after(start) && singleCell.getDate().before(end) && singleCell.isSelectable()) { singleCell.setSelected(true); singleCell.setRangeState(MonthCellDescriptor.RangeState.MIDDLE); selectedCells.add(singleCell); } } } } } } // Update the adapter. validateAndUpdate(); return date != null; } private void clearOldSelections() { for (MonthCellDescriptor selectedCell : selectedCells) { // De-select the currently-selected cell. selectedCell.setSelected(false); } selectedCells.clear(); selectedCals.clear(); } private Date applyMultiSelect(Date date, Calendar selectedCal) { for (MonthCellDescriptor selectedCell : selectedCells) { if (selectedCell.getDate().equals(date)) { // De-select the currently-selected cell. selectedCell.setSelected(false); selectedCells.remove(selectedCell); date = null; break; } } for (Calendar cal : selectedCals) { if (sameDate(cal, selectedCal)) { selectedCals.remove(cal); break; } } return date; } public void highlightDates(Collection<Date> dates) { for (Date date : dates) { validateDate(date); MonthCellWithMonthIndex monthCellWithMonthIndex = getMonthCellWithIndexByDate(date); if (monthCellWithMonthIndex != null) { Calendar newlyHighlightedCal = Calendar.getInstance(); newlyHighlightedCal.setTime(date); MonthCellDescriptor cell = monthCellWithMonthIndex.cell; highlightedCells.add(cell); highlightedCals.add(newlyHighlightedCal); cell.setHighlighted(true); } } adapter.notifyDataSetChanged(); setAdapter(adapter); } /** * Hold a cell with a month-index. */ private static class MonthCellWithMonthIndex { public MonthCellDescriptor cell; public int monthIndex; public MonthCellWithMonthIndex(MonthCellDescriptor cell, int monthIndex) { this.cell = cell; this.monthIndex = monthIndex; } } /** * Return cell and month-index (for scrolling) for a given Date. */ private MonthCellWithMonthIndex getMonthCellWithIndexByDate(Date date) { int index = 0; Calendar searchCal = Calendar.getInstance(locale); searchCal.setTime(date); Calendar actCal = Calendar.getInstance(locale); for (List<List<MonthCellDescriptor>> monthCells : cells) { for (List<MonthCellDescriptor> weekCells : monthCells) { for (MonthCellDescriptor actCell : weekCells) { actCal.setTime(actCell.getDate()); if (sameDate(actCal, searchCal) && actCell.isSelectable()) { return new MonthCellWithMonthIndex(actCell, index); } } } index++; } return null; } private class MonthAdapter extends BaseAdapter { private final LayoutInflater inflater; private MonthAdapter() { inflater = LayoutInflater.from(getContext()); } @Override public boolean isEnabled(int position) { // Disable selectability: each cell will handle that itself. return false; } @Override public int getCount() { return months.size(); } @Override public Object getItem(int position) { return months.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { MonthView monthView = (MonthView) convertView; if (monthView == null) { monthView = MonthView.create(parent, inflater, weekdayNameFormat, listener, today, dividerColor, dayBackgroundResId, dayTextColorResId, titleTextColor, headerTextColor); } monthView.init(months.get(position), cells.get(position), displayOnly); return monthView; } } List<List<MonthCellDescriptor>> getMonthCells(MonthDescriptor month, Calendar startCal) { Calendar cal = Calendar.getInstance(locale); cal.setTime(startCal.getTime()); List<List<MonthCellDescriptor>> cells = new ArrayList<List<MonthCellDescriptor>>(); 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()) { Logs.d("Building week row starting at %s"+cal.getTime()); List<MonthCellDescriptor> weekCells = new ArrayList<MonthCellDescriptor>(); cells.add(weekCells); for (int c = 0; c < 7; c++) { Date date = cal.getTime(); boolean isCurrentMonth = cal.get(MONTH) == month.getMonth(); boolean isSelected = isCurrentMonth && containsDate(selectedCals, cal); boolean isSelectable = isCurrentMonth && betweenDates(cal, minCal, maxCal) && isDateSelectable(date); boolean isToday = sameDate(cal, today); boolean isHighlighted = containsDate(highlightedCals, cal); int value = cal.get(DAY_OF_MONTH); MonthCellDescriptor.RangeState rangeState = MonthCellDescriptor.RangeState.NONE; if (selectedCals.size() > 1) { if (sameDate(minSelectedCal, cal)) { rangeState = MonthCellDescriptor.RangeState.FIRST; } else if (sameDate(maxDate(selectedCals), cal)) { rangeState = MonthCellDescriptor.RangeState.LAST; } else if (betweenDates(cal, minSelectedCal, maxSelectedCal)) { rangeState = MonthCellDescriptor.RangeState.MIDDLE; } } weekCells.add( new MonthCellDescriptor(date, isCurrentMonth, isSelectable, isSelected, isToday, isHighlighted, value, rangeState) ); cal.add(DATE, 1); } } return cells; } private static boolean containsDate(List<Calendar> selectedCals, Calendar cal) { for (Calendar selectedCal : selectedCals) { if (sameDate(cal, selectedCal)) { return true; } } return false; } private static Calendar minDate(List<Calendar> selectedCals) { if (selectedCals == null || selectedCals.size() == 0) { return null; } Collections.sort(selectedCals); return selectedCals.get(0); } private static Calendar maxDate(List<Calendar> selectedCals) { if (selectedCals == null || selectedCals.size() == 0) { return null; } Collections.sort(selectedCals); return selectedCals.get(selectedCals.size() - 1); } private static boolean sameDate(Calendar cal, Calendar selectedDate) { return cal.get(MONTH) == selectedDate.get(MONTH) && cal.get(YEAR) == selectedDate.get(YEAR) && cal.get(DAY_OF_MONTH) == selectedDate.get(DAY_OF_MONTH); } private static boolean betweenDates(Calendar cal, Calendar minCal, Calendar maxCal) { final Date date = cal.getTime(); return betweenDates(date, minCal, maxCal); } static boolean betweenDates(Date date, Calendar minCal, Calendar maxCal) { final Date min = minCal.getTime(); return (date.equals(min) || date.after(min)) // >= minCal && date.before(maxCal.getTime()); // && < maxCal } private static boolean sameMonth(Calendar cal, MonthDescriptor month) { return (cal.get(MONTH) == month.getMonth() && cal.get(YEAR) == month.getYear()); } private boolean isDateSelectable(Date date) { return dateConfiguredListener == null || dateConfiguredListener.isDateSelectable(date); } public void setOnDateSelectedListener(OnDateSelectedListener listener) { dateListener = listener; } /** * Set a listener to react to user selection of a disabled date. * * @param listener the listener to set, or null for no reaction */ public void setOnInvalidDateSelectedListener(OnInvalidDateSelectedListener listener) { invalidDateListener = listener; } /** * Set a listener used to discriminate between selectable and unselectable dates. Set this to * disable arbitrary dates as they are rendered. * <p/> * Important: set this before you call {@link #init(java.util.Date, java.util.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(java.util.Date)} this * listener will not be notified. * * @see #setOnDateSelectedListener(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.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(java.util.Date)} this * listener will not be notified. * * @see #setOnInvalidDateSelectedListener(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.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(com.marshalchen.common.uimodule.timessquare.CalendarPickerView.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(); } } }