diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..58dddcf7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: android + +jdk: + - oraclejdk8 + +android: + components: + - tools + - platform-tools + - build-tools-23.0.3 + - android-23 + - extra-android-m2repository + - extra-google-m2repository + +script: + - ./gradlew clean check assemble -Dpre-dex=false + +notifications: + email: false + +sudo: false + +cache: + directories: + - $HOME/.gradle \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ce26043e..c4fb09ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ Change Log ========== +Version 1.4.0 *(TBD)* +---------------------------- +* New: Add select range functionality. Use `setSelectionMode(MaterialCalendarView.SELECTION_MODE_RANGE)` and `setOnRangeSelectedListener` +* Breaking Change: `setFirstDayOfWeek`, `setMin/MaxDate`, and `setCalendarDisplayMode` are moved to a `State` object. Call `mcv.state().edit()` to edit them and commit the changes with `commit`. See [CUSTOMIZATION_BUILDER](docs/CUSTOMIZATION_BUILDER.md) for usage details. +* Change: Updated documentation regarding 1.3.0 additions +* Fix: Current month and title pager updates correctly if minDate is set after the current month +* Fix: Week number difference calculation correctly accounts for DST offsets. Thanks Igor Levaja! +* Fix: Date formatter uses L instead of M for month (standalone instead of context sensitive) + Version 1.3.0 *(2016-05-16)* ---------------------------- @@ -69,7 +78,7 @@ where the view will try and take up as much space as necessary, but we base it on tile size instead of an aspect ratio. The exception being that if a `tileSize` is set, that will override everything and set the view to that size. -* Fix: Use more efficent method for indexing months +* Fix: Use more efficient method for indexing months Version 0.7.0 *(2015-07-09)* ---------------------------- diff --git a/README.md b/README.md index 9f6dd3ad..a94c4d1c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -Material Calendar View [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Material%20Calendar%20View-blue.svg?style=flat)](https://android-arsenal.com/details/1/1531) +Material Calendar View ====================== +[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Material%20Calendar%20View-blue.svg?style=flat)](https://android-arsenal.com/details/1/1531) [![Maven Central](https://img.shields.io/maven-central/v/com.prolificinteractive/material-calendarview.svg?maxAge=2592000)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22material-calendarview%22) [![Travis branch](https://img.shields.io/travis/prolificinteractive/material-calendarview.svg?maxAge=2592000)](https://travis-ci.org/prolificinteractive/material-calendarview) A Material design back port of Android's CalendarView. The goal is to have a Material look and feel, rather than 100% parity with the platform's implementation. @@ -11,7 +12,7 @@ and feel, rather than 100% parity with the platform's implementation. Usage ----- -1. Add `compile 'com.prolificinteractive:material-calendarview:1.3.0'` to your dependencies. +1. Add `compile 'com.prolificinteractive:material-calendarview:1.4.0'` to your dependencies. 2. Add `MaterialCalendarView` into your layouts or view hierarchy. 3. Set a `OnDateSelectedListener` or call `MaterialCalendarView.getSelectedDates()` when you need it. @@ -22,10 +23,10 @@ Example: ```xml @@ -35,6 +36,27 @@ Example: marked `@Experimental` are subject to change quickly and should not be used in production code. They are allowed for testing and feedback. +Major Change in 1.4.0 +--------------------- +* Breaking Change: `setFirstDayOfWeek`, `setMin/MaxDate`, and `setCalendarDisplayMode` are moved to a `State` object. This was necessary because it was unclear that these were not simple setters--individually, they were side effecting and triggered full adapter/date range recalculations. Typical usage of the view involves setting all these invariants up front during `onCreate` and it was unknown to the user that setting all 4 of these would create a lot of waste. Not to mention certain things were side effecting--some would reset the current day or selected date. As a result, the same 4 methods called in a different order could result in a different state, which is bad. + + For most cases you will simply need to replace setting those invariants with: + ```java + mcv.state().edit() + .setFirstDayOfWeek(Calendar.WEDNESDAY) + .setMinimumDate(CalendarDay.from(2016, 4, 3)) + .setMaximumDate(CalendarDay.from(2016, 5, 12)) + .setCalendarDisplayMode(CalendarMode.WEEKS) + .commit(); + ``` + + `mcv.state().edit()` will retain previously set values; `mcv.newState()` will create a new state using default values. Calling `commit` will trigger the rebuild of adapters and date ranges. It is recommended these state changes occur as the first modification to MCV (before configuring anything else like current date or selected date); we make no guarantee those modifications will be retained when the state is modified. + + See [CUSTOMIZATION_BUILDER](docs/CUSTOMIZATION_BUILDER.md) for usage details. +* New: `setSelectionMode(SELECTION_MODE_RANGE)` was added to allow 2 dates to be selected and have the entire range of dates selected. Much thanks to [papageorgiouk](https://github.com/papageorgiouk) for his work on this feature. + +See other changes in the [CHANGELOG](/CHANGELOG.md). + Major Change in 1.3.0 --------------------- * Breaking change: `getTileSize` is deprecated. Use `getTileWidth` or `getTileHeight`. @@ -42,8 +64,7 @@ Major Change in 1.3.0 * Allow users to click on dates outside of current month with `setAllowClickDaysOutsideCurrentMonth` * Set tile width/height separately rather than single tile size with `setTileWidth` and `setTileHeight` * Attributes: mcv_tileWidth, mcv_tileHeight, mcv_calendarMode - -See other changes in the [CHANGELOG](/CHANGELOG.md). +* TalkBack APIs: `setContentDescriptionArrowPast`, `ArrowFuture`, `Calendar` Major Change in 1.2.0 --------------------- @@ -70,6 +91,11 @@ but we base it on tile size instead of an aspect ratio. The exception being that if a `tileSize` is set, that will override everything and set the view to that size. +Documentation +------------- + +Make sure to check all the documentation available [here](docs/README.md). + Customization ------------- @@ -78,11 +104,12 @@ One of the aims of this library is to be customizable. The many options include: * [Define the view's width and height in terms of tile size](docs/CUSTOMIZATION.md#tile-size) * [Single or Multiple date selection, or disabling selection entirely](docs/CUSTOMIZATION.md#date-selection) * [Showing dates from other months or those out of range](docs/CUSTOMIZATION.md#showing-other-dates) -* [Setting the first day of the week](docs/CUSTOMIZATION.md#first-day-of-the-week) -* [Show only a range of dates](docs/CUSTOMIZATION.md#date-ranges) +* [Setting the first day of the week](docs/CUSTOMIZATION_BUILDER.md#first-day-of-the-week) +* [Show only a range of dates](docs/CUSTOMIZATION_BUILDER.md#date-ranges) * [Customize the top bar](docs/CUSTOMIZATION.md#topbar-options) * [Custom labels for the header, weekdays, or individual days](docs/CUSTOMIZATION.md#custom-labels) + ### Events, Highlighting, Custom Selectors, and More! All of this and more can be done via the decorator api. Please check out the [decorator documentation](docs/DECORATORS.md). diff --git a/docs/CUSTOMIZATION.md b/docs/CUSTOMIZATION.md index bda4995c..a6182362 100644 --- a/docs/CUSTOMIZATION.md +++ b/docs/CUSTOMIZATION.md @@ -3,10 +3,10 @@ Customization Options ```xml ``` @@ -34,15 +37,19 @@ If a tileSize is set, that will override the `layout_width` and `layout_height` The view is 7 tiles wide and 8 tiles high (with the top bar visible). +### Width and Height + +You also have the possibility to use `tileWidth` and `tileHeight` separately. I would recommend using either `tileSize` or, `tileWidth` and `tileHeight`. + ## Date Selection -We support three modes of selection: single, multiple, or none. The default is single selection. -The mode can be changed by calling `setSelectionMode()` and passing the appropriate constant (`SELECTION_MODE_NONE`, `SELECTION_MODE_SINGLE`, or `SELECTION_MODE_MULTIPLE`). +We support four modes of selection: single, multiple, range or none. The default is single selection. +The mode can be changed by calling `setSelectionMode()` and passing the appropriate constant (`SELECTION_MODE_NONE`, `SELECTION_MODE_SINGLE`, `SELECTION_MODE_RANGE` or `SELECTION_MODE_MULTIPLE`). If you change to single selection, all selected days except the last selected will be cleared. -If you change to none, all selected days will be cleared. +If you change to none or range, all selected days will be cleared. -You can set an `OnDateSelectedListener` to listen for selections, make sure to take into account multiple calls for the same date and state. +You can set an `OnDateSelectedListener` to listen for selections, make sure to take into account multiple calls for the same date and state. In case of range selection, use `OnRangeSelectedListener` which returns the list of date from the range including first and last. You can manually select or deselect dates by calling `setDateSelected()`. Use `setSelectedDate()` to clear the current selection(s) and select the provided date. @@ -76,20 +83,6 @@ You can provide a custom color by setting `mcv_selectionColor` in xml, or by cal If you want more control than just color, you can use the [decorator api](DECORATORS.md) to set a [custom selector](CUSTOM_SELECTORS.md). -## First Day Of The Week - -The default first day of the week is Sunday. You can set a custom day of the week by setting `mcv_firstDayOfWeek` in xml, or by calling `setFirstDayOfWeek()`. -The xml attribute is an enum of `sunday` through `saturday` and `setFirstDayOfWeek()` accepts values from `java.util.Calendar` such as `Calendar.MONDAY`. - - -## Date Ranges - -By default, the calendar displays months for 200 years before today and 200 years after. -You can specify different minimum and maximum dates by calling `setMinimumDate(CalendarDay)` and `setMaximumDate(CalendarDay)`. -Passing `null` will reset back to the default 200 years. -There are also convenience methods that accept a `Calendar` or a `Date` object and convert them to a `CalendarDay` using the relevant `CalendarDay.from()` factory method. - - ## Topbar Options ### Visibility diff --git a/docs/CUSTOMIZATION_BUILDER.md b/docs/CUSTOMIZATION_BUILDER.md new file mode 100644 index 00000000..01621101 --- /dev/null +++ b/docs/CUSTOMIZATION_BUILDER.md @@ -0,0 +1,50 @@ +State builder +============= + +Certain parameters are only modifiable using the state builder of the `MaterialCalendarView`. +Using the builder prevents from updating the view each time one of the setters is called. The view is updated when calling `Builder#commit()` and that improve performances. +Previously, the fields could be customize using casual setters. + +Here is a concrete example of how to use the builder: + +```java +mcv.state().edit() + .setFirstDayOfWeek(Calendar.WEDNESDAY) + .setMinimumDate(CalendarDay.from(2016, 4, 3)) + .setMaximumDate(CalendarDay.from(2016, 5, 12)) + .setCalendarDisplayMode(CalendarMode.WEEKS) + .commit(); +``` + +## state.edit() vs newState() + +Using `mcv.state().edit()` will preserve the current state of the `MaterialCalendarView` while `mcv.newState()` will initialize the builder with new parameters. +Only the fields that are modifiable using the builder can be reset or edit. Here is the list of the fields: + +- First Day Of Week +- Minimum Date +- Maximum Date +- Calendar Display Mode + +As an example, if you are setting `firstDayOfWeek` inside your xml, and you want to preserve the field when using the builder, you should use `state.edit()`. +However if you don't want to preserve any current parameters from the list above, use `newState()`. In most cases `state.edit()` should be the right method to use. + +### First Day Of The Week + +The default first day of the week is Sunday. You can set a custom day of the week by setting `mcv_firstDayOfWeek` in xml, or by calling `setFirstDayOfWeek()`. +The xml attribute is an enum of `sunday` through `saturday` and `setFirstDayOfWeek()` accepts values from `java.util.Calendar` such as `Calendar.MONDAY`. + + +### Date Ranges + +By default, the calendar displays months for 200 years before today and 200 years after. +You can specify different minimum and maximum dates by calling `setMinimumDate(CalendarDay)` and `setMaximumDate(CalendarDay)`. +Passing `null` will reset back to the default 200 years. +There are also convenience methods that accept a `Calendar` or a `Date` object and convert them to a `CalendarDay` using the relevant `CalendarDay.from()` factory method. + +### Calendar Display Mode + +`MaterialCalendarView` propose two display modes: weekly and monthly. You can set the display mode in your xml using the attribute `mcv_calendarMode` with `month` for monthly mode, or `week` for weekly mode. +You can also use the builder `setCalendarDisplayMode(CalendarMode)` parameter. + +It is **important** to note that the `CalendarMode.WEEKS` is still experimental. \ No newline at end of file diff --git a/docs/CUSTOM_SELECTORS.md b/docs/CUSTOM_SELECTORS.md index 679cc10a..fd0a1cab 100644 --- a/docs/CUSTOM_SELECTORS.md +++ b/docs/CUSTOM_SELECTORS.md @@ -73,7 +73,8 @@ Here's an example for a dark theme and a dark selector: - +``` +```xml diff --git a/docs/README.md b/docs/README.md index 91d4b309..ee4468d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,18 @@ This is where in depth documentation will be going. Check out most of the customization options [here](CUSTOMIZATION.md). +## Customization using state builder + +Some of the customization can be made using a builder. Using a builder for those parameters helps preventing bugs and improves performances. +Those parameters are: + +- [First Day Of Week](CUSTOMIZATION_BUILDER.md#first-day-of-the-week) +- [Minimum Date](CUSTOMIZATION_BUILDER.md#date-ranges) +- [Maximum Date](CUSTOMIZATION_BUILDER.md#date-ranges) +- [Calendar Display Mode](CUSTOMIZATION_BUILDER.md#calendar-display-mode) + +The documentation is available [here](CUSTOMIZATION_BUILDER.md). + ## Events, Highlighting, Custom Selectors, and More! All of this and more can be done via the decorator api. Please check out the [decorator documentation](DECORATORS.md). @@ -18,4 +30,6 @@ Check out the [documentation for custom states](CUSTOM_SELECTORS.md). ## TODO -Write and organize the documentation. Focus on customizability with real world scenarios. +- Write and organize the documentation. Focus on customization with real world scenarios. +- Improve performances. +- Add test cases. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 9481178d..5a024e4e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,8 +18,8 @@ # org.gradle.parallel=true GROUP=com.prolificinteractive -VERSION_NAME=1.3.0 -VERSION_CODE=14 +VERSION_NAME=1.4.0 +VERSION_CODE=15 POM_PACKAGING=aar POM_NAME=Material CalendarView diff --git a/library/build.gradle b/library/build.gradle index 2f3b9669..8ff0ada9 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'signing' android { compileSdkVersion 23 - buildToolsVersion "23.0.2" + buildToolsVersion "23.0.3" defaultConfig { minSdkVersion 14 diff --git a/library/src/main/java/com/prolificinteractive/materialcalendarview/MaterialCalendarView.java b/library/src/main/java/com/prolificinteractive/materialcalendarview/MaterialCalendarView.java index 81826f9b..9742b725 100644 --- a/library/src/main/java/com/prolificinteractive/materialcalendarview/MaterialCalendarView.java +++ b/library/src/main/java/com/prolificinteractive/materialcalendarview/MaterialCalendarView.java @@ -73,7 +73,7 @@ public class MaterialCalendarView extends ViewGroup { * @see #getSelectionMode() */ @Retention(RetentionPolicy.RUNTIME) - @IntDef({SELECTION_MODE_NONE, SELECTION_MODE_SINGLE, SELECTION_MODE_MULTIPLE}) + @IntDef({SELECTION_MODE_NONE, SELECTION_MODE_SINGLE, SELECTION_MODE_MULTIPLE, SELECTION_MODE_RANGE}) public @interface SelectionMode { } @@ -95,6 +95,11 @@ public class MaterialCalendarView extends ViewGroup { */ public static final int SELECTION_MODE_MULTIPLE = 2; + /** + * Selection mode which allows selection of a range between two dates + */ + public static final int SELECTION_MODE_RANGE = 3; + /** * {@linkplain IntDef} annotation for showOtherDates. * @@ -204,6 +209,8 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse private OnDateSelectedListener listener; private OnMonthChangedListener monthListener; + private OnRangeSelectedListener rangeListener; + CharSequence calendarContentDescription; private int accentColor = 0; @@ -217,6 +224,8 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse private boolean allowClickDaysOutsideCurrentMonth = true; private int firstDayOfWeek; + private State state; + public MaterialCalendarView(Context context) { this(context, null); } @@ -273,7 +282,11 @@ public void transformPage(View page, float position) { //Allowing use of Calendar.getInstance() here as a performance optimization firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek(); } - setCalendarDisplayMode(CalendarMode.values()[calendarModeIndex]); + + newState() + .setFirstDayOfWeek(firstDayOfWeek) + .setCalendarDisplayMode(CalendarMode.values()[calendarModeIndex]) + .commit(); final int tileSize = a.getDimensionPixelSize(R.styleable.MaterialCalendarView_mcv_tileSize, -1); if (tileSize > 0) { @@ -348,7 +361,6 @@ public void transformPage(View page, float position) { R.styleable.MaterialCalendarView_mcv_allowClickDaysOutsideCurrentMonth, true )); - } catch (Exception e) { e.printStackTrace(); } finally { @@ -408,40 +420,41 @@ private void updateUi() { * Change the selection mode of the calendar. The default mode is {@linkplain #SELECTION_MODE_SINGLE} * * @param mode the selection mode to change to. This must be one of - * {@linkplain #SELECTION_MODE_NONE}, {@linkplain #SELECTION_MODE_SINGLE}, or {@linkplain #SELECTION_MODE_MULTIPLE}. + * {@linkplain #SELECTION_MODE_NONE}, {@linkplain #SELECTION_MODE_SINGLE}, + * {@linkplain #SELECTION_MODE_RANGE} or {@linkplain #SELECTION_MODE_MULTIPLE}. * Unknown values will act as {@linkplain #SELECTION_MODE_SINGLE} * @see #getSelectionMode() * @see #SELECTION_MODE_NONE * @see #SELECTION_MODE_SINGLE * @see #SELECTION_MODE_MULTIPLE + * @see #SELECTION_MODE_RANGE */ public void setSelectionMode(final @SelectionMode int mode) { final @SelectionMode int oldMode = this.selectionMode; + this.selectionMode = mode; switch (mode) { - case SELECTION_MODE_MULTIPLE: { - this.selectionMode = SELECTION_MODE_MULTIPLE; - } - break; - default: - case SELECTION_MODE_SINGLE: { - this.selectionMode = SELECTION_MODE_SINGLE; - if (oldMode == SELECTION_MODE_MULTIPLE) { + case SELECTION_MODE_RANGE: + clearSelection(); + break; + case SELECTION_MODE_MULTIPLE: + break; + case SELECTION_MODE_SINGLE: + if (oldMode == SELECTION_MODE_MULTIPLE || oldMode == SELECTION_MODE_RANGE) { //We should only have one selection now, so we should pick one List dates = getSelectedDates(); if (!dates.isEmpty()) { setSelectedDate(getSelectedDate()); } } - } - break; - case SELECTION_MODE_NONE: { + break; + default: + case SELECTION_MODE_NONE: this.selectionMode = SELECTION_MODE_NONE; if (oldMode != SELECTION_MODE_NONE) { //No selection! Clear out! clearSelection(); } - } - break; + break; } adapter.setSelectionEnabled(selectionMode != SELECTION_MODE_NONE); @@ -469,50 +482,6 @@ public void goToNext() { } } - /** - * Set calendar display mode. The default mode is Months. - * When switching between modes will select todays date, or the selected date, - * if selection mode is single. - * - * @param mode - calendar mode - */ - @Experimental - public void setCalendarDisplayMode(CalendarMode mode) { - if (calendarMode != null && calendarMode.equals(mode)) { - return; - } - - CalendarPagerAdapter newAdapter; - switch (mode) { - case MONTHS: - newAdapter = new MonthPagerAdapter(this); - break; - case WEEKS: - newAdapter = new WeekPagerAdapter(this); - break; - default: - throw new IllegalArgumentException("Provided display mode which is not yet implemented"); - } - if (adapter == null) { - adapter = newAdapter; - } else { - adapter = adapter.migrateStateAndReturn(newAdapter); - } - pager.setAdapter(adapter); - setRangeDates(minDate, maxDate); - calendarMode = mode; - - // Reset height params after mode change - pager.setLayoutParams(new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW)); - - setCurrentDate( - selectionMode == SELECTION_MODE_SINGLE && !adapter.getSelectedDates().isEmpty() - ? adapter.getSelectedDates().get(0) - : CalendarDay.today()); - invalidateDecorators(); - updateUi(); - } - /** * Get the current selection mode. The default mode is {@linkplain #SELECTION_MODE_SINGLE} * @@ -521,6 +490,7 @@ public void setCalendarDisplayMode(CalendarMode mode) { * @see #SELECTION_MODE_NONE * @see #SELECTION_MODE_SINGLE * @see #SELECTION_MODE_MULTIPLE + * @see #SELECTION_MODE_RANGE */ @SelectionMode public int getSelectionMode() { @@ -907,28 +877,6 @@ public CalendarDay getMinimumDate() { return minDate; } - /** - * @param calendar set the minimum selectable date, null for no minimum - */ - public void setMinimumDate(@Nullable Calendar calendar) { - setMinimumDate(CalendarDay.from(calendar)); - } - - /** - * @param date set the minimum selectable date, null for no minimum - */ - public void setMinimumDate(@Nullable Date date) { - setMinimumDate(CalendarDay.from(date)); - } - - /** - * @param calendar set the minimum selectable date, null for no minimum - */ - public void setMinimumDate(@Nullable CalendarDay calendar) { - minDate = calendar; - setRangeDates(minDate, maxDate); - } - /** * @return the maximum selectable date for the calendar, if any */ @@ -936,28 +884,6 @@ public CalendarDay getMaximumDate() { return maxDate; } - /** - * @param calendar set the maximum selectable date, null for no maximum - */ - public void setMaximumDate(@Nullable Calendar calendar) { - setMaximumDate(CalendarDay.from(calendar)); - } - - /** - * @param date set the maximum selectable date, null for no maximum - */ - public void setMaximumDate(@Nullable Date date) { - setMaximumDate(CalendarDay.from(date)); - } - - /** - * @param calendar set the maximum selectable date, null for no maximum - */ - public void setMaximumDate(@Nullable CalendarDay calendar) { - maxDate = calendar; - setRangeDates(minDate, maxDate); - } - /** * The default value is {@link #SHOW_DEFAULTS}, which currently is just {@link #SHOW_DECORATED_DISABLED}. * This means that the default visible days are of the current month, in the min-max range. @@ -1131,25 +1057,27 @@ protected Parcelable onSaveInstanceState() { protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); + newState() + .setFirstDayOfWeek(ss.firstDayOfWeek) + .setCalendarDisplayMode(ss.calendarMode) + .setMinimumDate(ss.minDate) + .setMaximumDate(ss.maxDate) + .commit(); + setSelectionColor(ss.color); setDateTextAppearance(ss.dateTextAppearance); setWeekDayTextAppearance(ss.weekDayTextAppearance); setShowOtherDates(ss.showOtherDates); setAllowClickDaysOutsideCurrentMonth(ss.allowClickDaysOutsideCurrentMonth); - minDate = ss.minDate; - maxDate = ss.maxDate; - setRangeDates(ss.minDate, ss.maxDate); clearSelection(); for (CalendarDay calendarDay : ss.selectedDates) { setDateSelected(calendarDay, true); } - setFirstDayOfWeek(ss.firstDayOfWeek); setTileWidth(ss.tileWidthPx); setTileHeight(ss.tileHeightPx); setTopbarVisible(ss.topbarVisible); setSelectionMode(ss.selectionMode); setDynamicHeightEnabled(ss.dynamicHeightEnabled); - setCalendarDisplayMode(ss.calendarMode); setCurrentDate(ss.currentMonth); } @@ -1167,6 +1095,9 @@ private void setRangeDates(CalendarDay min, CalendarDay max) { CalendarDay c = currentMonth; adapter.setRangeDates(min, max); currentMonth = c; + if (min != null) { + currentMonth = min.isAfter(currentMonth) ? min : currentMonth; + } int position = adapter.getIndexForDay(c); pager.setCurrentItem(position, false); updateUi(); @@ -1262,46 +1193,6 @@ private static int getThemeAccentColor(Context context) { return outValue.data; } - /** - * Sets the first day of the week. - *

- * Uses the java.util.Calendar day constants. - * - * @param day The first day of the week as a java.util.Calendar day constant. - * @see java.util.Calendar - */ - public void setFirstDayOfWeek(int day) { - firstDayOfWeek = day; - // TODO: 5/12/16 consider a less nuclear means of resetting the adapter when setting a new - // first day of week and how regular notifyDataSetChanged doesn't work (may require updating - // getItemPosition to flag current object and the ones to the left/right as changed) - CalendarPagerAdapter newAdapter; - switch (calendarMode) { - case MONTHS: - newAdapter = new MonthPagerAdapter(this); - break; - case WEEKS: - newAdapter = new WeekPagerAdapter(this); - break; - default: - throw new IllegalArgumentException("Provided display mode which is not yet implemented"); - } - if (adapter == null) { - adapter = newAdapter; - } else { - adapter = adapter.migrateStateAndReturn(newAdapter); - } - pager.setAdapter(adapter); - setRangeDates(minDate, maxDate); - - setCurrentDate( - selectionMode == SELECTION_MODE_SINGLE && !adapter.getSelectedDates().isEmpty() - ? adapter.getSelectedDates().get(0) - : CalendarDay.today()); - invalidateDecorators(); - updateUi(); - } - /** * @return The first day of the week as a {@linkplain Calendar} day constant. */ @@ -1312,7 +1203,7 @@ public int getFirstDayOfWeek() { /** * By default, the calendar will take up all the space needed to show any month (6 rows). * By enabling dynamic height, the view will change height dependant on the visible month. - *

+ *

* This means months that only need 5 or 4 rows to show the entire month will only take up * that many rows, and will grow and shrink as necessary. * @@ -1414,6 +1305,15 @@ public void setOnMonthChangedListener(OnMonthChangedListener listener) { this.monthListener = listener; } + /** + * Sets the listener to be notified upon a range has been selected. + * + * @param listener thing to be notified + */ + public void setOnRangeSelectedListener(OnRangeSelectedListener listener) { + this.rangeListener = listener; + } + /** * Dispatch date change events to a listener, if set * @@ -1427,6 +1327,34 @@ protected void dispatchOnDateSelected(final CalendarDay day, final boolean selec } } + /** + * Dispatch a range of days to a listener, if set. First day must be before last Day. + * + * @param firstDay first day enclosing a range + * @param lastDay last day enclosing a range + */ + protected void dispatchOnRangeSelected(final CalendarDay firstDay, final CalendarDay lastDay) { + final OnRangeSelectedListener listener = rangeListener; + final List days = new ArrayList<>(); + + final Calendar counter = Calendar.getInstance(); + counter.setTime(firstDay.getDate()); // start from the first day and increment + + final Calendar end = Calendar.getInstance(); + end.setTime(lastDay.getDate()); // for comparison + + while (counter.before(end) || counter.equals(end)) { + final CalendarDay current = CalendarDay.from(counter); + adapter.setDateSelected(current, true); + days.add(current); + counter.add(Calendar.DATE, 1); + } + + if (listener != null) { + listener.onRangeSelected(MaterialCalendarView.this, days); + } + } + /** * Dispatch date change events to a listener, if set * @@ -1453,6 +1381,25 @@ protected void onDateClicked(@NonNull CalendarDay date, boolean nowSelected) { dispatchOnDateSelected(date, nowSelected); } break; + case SELECTION_MODE_RANGE: { + adapter.setDateSelected(date, nowSelected); + if (adapter.getSelectedDates().size() > 2) { + adapter.clearSelections(); + adapter.setDateSelected(date, nowSelected); // re-set because adapter has been cleared + dispatchOnDateSelected(date, nowSelected); + } else if (adapter.getSelectedDates().size() == 2) { + final List dates = adapter.getSelectedDates(); + if (dates.get(0).isAfter(dates.get(1))) { + dispatchOnRangeSelected(dates.get(1), dates.get(0)); + } else { + dispatchOnRangeSelected(dates.get(0), dates.get(1)); + } + } else { + adapter.setDateSelected(date, nowSelected); + dispatchOnDateSelected(date, nowSelected); + } + } + break; default: case SELECTION_MODE_SINGLE: { adapter.clearSelections(); @@ -1463,6 +1410,21 @@ protected void onDateClicked(@NonNull CalendarDay date, boolean nowSelected) { } } + /** + * Select a fresh range of date including first day and last day. + * + * @param firstDay first day of the range to select + * @param lastDay last day of the range to select + */ + public void selectRange(final CalendarDay firstDay, final CalendarDay lastDay) { + clearSelection(); + if (firstDay.isAfter(lastDay)) { + dispatchOnRangeSelected(lastDay, firstDay); + } else { + dispatchOnRangeSelected(firstDay, lastDay); + } + } + /** * Call by {@link CalendarPagerView} to indicate that a day was clicked and we should handle it * @@ -1772,4 +1734,174 @@ public void setPagingEnabled(boolean pagingEnabled) { public boolean isPagingEnabled() { return pager.isPagingEnabled(); } + + /** + * Preserve the current parameters of the Material Calendar View. + */ + public State state() { + return state; + } + + /** + * Initialize the parameters from scratch. + */ + public StateBuilder newState() { + return new StateBuilder(); + } + + public class State { + public final CalendarMode calendarMode; + public final int firstDayOfWeek; + public final CalendarDay minDate; + public final CalendarDay maxDate; + + public State(StateBuilder builder) { + calendarMode = builder.calendarMode; + firstDayOfWeek = builder.firstDayOfWeek; + minDate = builder.minDate; + maxDate = builder.maxDate; + } + + /** + * Modify parameters from current state. + */ + public StateBuilder edit() { + return new StateBuilder(this); + } + + } + + public class StateBuilder { + private CalendarMode calendarMode = CalendarMode.MONTHS; + private int firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek(); + public CalendarDay minDate = null; + public CalendarDay maxDate = null; + + public StateBuilder() { + } + + private StateBuilder(final State state) { + calendarMode = state.calendarMode; + firstDayOfWeek = state.firstDayOfWeek; + minDate = state.minDate; + maxDate = state.maxDate; + } + + /** + * Sets the first day of the week. + *

+ * Uses the java.util.Calendar day constants. + * + * @param day The first day of the week as a java.util.Calendar day constant. + * @see java.util.Calendar + */ + public StateBuilder setFirstDayOfWeek(int day) { + this.firstDayOfWeek = day; + return this; + } + + /** + * Set calendar display mode. The default mode is Months. + * When switching between modes will select todays date, or the selected date, + * if selection mode is single. + * + * @param mode - calendar mode + */ + public StateBuilder setCalendarDisplayMode(CalendarMode mode) { + this.calendarMode = mode; + return this; + } + + + /** + * @param calendar set the minimum selectable date, null for no minimum + */ + public StateBuilder setMinimumDate(@Nullable Calendar calendar) { + setMinimumDate(CalendarDay.from(calendar)); + return this; + } + + /** + * @param date set the minimum selectable date, null for no minimum + */ + public StateBuilder setMinimumDate(@Nullable Date date) { + setMinimumDate(CalendarDay.from(date)); + return this; + } + + /** + * @param calendar set the minimum selectable date, null for no minimum + */ + public StateBuilder setMinimumDate(@Nullable CalendarDay calendar) { + minDate = calendar; + return this; + } + + /** + * @param calendar set the maximum selectable date, null for no maximum + */ + public StateBuilder setMaximumDate(@Nullable Calendar calendar) { + setMaximumDate(CalendarDay.from(calendar)); + return this; + } + + /** + * @param date set the maximum selectable date, null for no maximum + */ + public StateBuilder setMaximumDate(@Nullable Date date) { + setMaximumDate(CalendarDay.from(date)); + return this; + } + + /** + * @param calendar set the maximum selectable date, null for no maximum + */ + public StateBuilder setMaximumDate(@Nullable CalendarDay calendar) { + maxDate = calendar; + return this; + } + + public void commit() { + MaterialCalendarView.this.commit(new State(this)); + } + } + + private void commit(State state) { + this.state = state; + // Save states parameters + calendarMode = state.calendarMode; + firstDayOfWeek = state.firstDayOfWeek; + minDate = state.minDate; + maxDate = state.maxDate; + + // Recreate adapter + final CalendarPagerAdapter newAdapter; + switch (calendarMode) { + case MONTHS: + newAdapter = new MonthPagerAdapter(this); + break; + case WEEKS: + newAdapter = new WeekPagerAdapter(this); + break; + default: + throw new IllegalArgumentException("Provided display mode which is not yet implemented"); + } + if (adapter == null) { + adapter = newAdapter; + } else { + adapter = adapter.migrateStateAndReturn(newAdapter); + } + pager.setAdapter(adapter); + setRangeDates(minDate, maxDate); + + // Reset height params after mode change + pager.setLayoutParams(new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW)); + + setCurrentDate( + selectionMode == SELECTION_MODE_SINGLE && !adapter.getSelectedDates().isEmpty() + ? adapter.getSelectedDates().get(0) + : CalendarDay.today()); + invalidateDecorators(); + updateUi(); + } } diff --git a/library/src/main/java/com/prolificinteractive/materialcalendarview/OnRangeSelectedListener.java b/library/src/main/java/com/prolificinteractive/materialcalendarview/OnRangeSelectedListener.java new file mode 100644 index 00000000..b157091a --- /dev/null +++ b/library/src/main/java/com/prolificinteractive/materialcalendarview/OnRangeSelectedListener.java @@ -0,0 +1,20 @@ +package com.prolificinteractive.materialcalendarview; + +import android.support.annotation.NonNull; + +import java.util.List; + +/** + * The callback used to indicate a range has been selected + */ +public interface OnRangeSelectedListener { + + /** + * Called when a user selects a range of days. + * There is no logic to prevent multiple calls for the same date and state. + * + * @param widget the view associated with this listener + * @param dates the dates in the range, in ascending order + */ + void onRangeSelected(@NonNull MaterialCalendarView widget, @NonNull List dates); +} diff --git a/library/src/main/java/com/prolificinteractive/materialcalendarview/WeekPagerAdapter.java b/library/src/main/java/com/prolificinteractive/materialcalendarview/WeekPagerAdapter.java index a1ab577b..62f77ebf 100644 --- a/library/src/main/java/com/prolificinteractive/materialcalendarview/WeekPagerAdapter.java +++ b/library/src/main/java/com/prolificinteractive/materialcalendarview/WeekPagerAdapter.java @@ -42,7 +42,7 @@ public static class Weekly implements DateRangeIndex { public Weekly(@NonNull CalendarDay min, @NonNull CalendarDay max, int firstDayOfWeek) { this.min = getFirstDayOfWeek(min, firstDayOfWeek); - this.count = weekNumberDifference(min, max) + 1; + this.count = weekNumberDifference(this.min, max) + 1; } @Override @@ -67,7 +67,11 @@ public CalendarDay getItem(int position) { private int weekNumberDifference(@NonNull CalendarDay min, @NonNull CalendarDay max) { long millisDiff = max.getDate().getTime() - min.getDate().getTime(); - long dayDiff = TimeUnit.DAYS.convert(millisDiff, TimeUnit.MILLISECONDS); + + int dstOffsetMax = max.getCalendar().get(Calendar.DST_OFFSET); + int dstOffsetMin = min.getCalendar().get(Calendar.DST_OFFSET); + + long dayDiff = TimeUnit.DAYS.convert(millisDiff + dstOffsetMax - dstOffsetMin, TimeUnit.MILLISECONDS); return (int) (dayDiff / DAYS_IN_WEEK); } diff --git a/library/src/main/java/com/prolificinteractive/materialcalendarview/format/DateFormatTitleFormatter.java b/library/src/main/java/com/prolificinteractive/materialcalendarview/format/DateFormatTitleFormatter.java index 3e1de558..b7eeae11 100644 --- a/library/src/main/java/com/prolificinteractive/materialcalendarview/format/DateFormatTitleFormatter.java +++ b/library/src/main/java/com/prolificinteractive/materialcalendarview/format/DateFormatTitleFormatter.java @@ -14,11 +14,11 @@ public class DateFormatTitleFormatter implements TitleFormatter { private final DateFormat dateFormat; /** - * Format using "MMMM yyyy" for formatting + * Format using "LLLL yyyy" for formatting */ public DateFormatTitleFormatter() { this.dateFormat = new SimpleDateFormat( - "MMMM yyyy", Locale.getDefault() + "LLLL yyyy", Locale.getDefault() ); } diff --git a/sample/build.gradle b/sample/build.gradle index 11d6b179..5d6be7e2 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion "23.0.2" + buildToolsVersion "23.0.3" defaultConfig { applicationId "com.prolificinteractive.materialcalendarview.sample" @@ -16,8 +16,8 @@ android { dependencies { // You should use the commented out line below in you're application. // We depend on the source directly here so that development is easier. -// compile project(':library') - compile 'com.prolificinteractive:material-calendarview:1.3.0' + compile project(':library') + //compile 'com.prolificinteractive:material-calendarview:1.4.0' compile 'com.android.support:appcompat-v7:23.4.0' compile 'com.android.support:recyclerview-v7:23.4.0' diff --git a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/BasicActivityDecorated.java b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/BasicActivityDecorated.java index 66e03b87..42f2eade 100644 --- a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/BasicActivityDecorated.java +++ b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/BasicActivityDecorated.java @@ -41,14 +41,19 @@ protected void onCreate(Bundle savedInstanceState) { widget.setOnDateChangedListener(this); widget.setShowOtherDates(MaterialCalendarView.SHOW_ALL); - Calendar calendar = Calendar.getInstance(); - widget.setSelectedDate(calendar.getTime()); + Calendar instance = Calendar.getInstance(); + widget.setSelectedDate(instance.getTime()); - calendar.set(calendar.get(Calendar.YEAR), Calendar.JANUARY, 1); - widget.setMinimumDate(calendar.getTime()); + Calendar instance1 = Calendar.getInstance(); + instance1.set(instance1.get(Calendar.YEAR), Calendar.JANUARY, 1); - calendar.set(calendar.get(Calendar.YEAR), Calendar.DECEMBER, 31); - widget.setMaximumDate(calendar.getTime()); + Calendar instance2 = Calendar.getInstance(); + instance2.set(instance2.get(Calendar.YEAR), Calendar.DECEMBER, 31); + + widget.state().edit() + .setMinimumDate(instance1.getTime()) + .setMaximumDate(instance2.getTime()) + .commit(); widget.addDecorators( new MySelectorDecorator(this), diff --git a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/CustomizeCodeActivity.java b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/CustomizeCodeActivity.java index 62588674..6e2e7b6d 100644 --- a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/CustomizeCodeActivity.java +++ b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/CustomizeCodeActivity.java @@ -42,10 +42,13 @@ protected void onCreate(Bundle savedInstanceState) { widget.setCurrentDate(today); widget.setSelectedDate(today); - widget.setFirstDayOfWeek(Calendar.WEDNESDAY); - widget.setMinimumDate(CalendarDay.from(2016, 4, 3)); - widget.setMaximumDate(CalendarDay.from(2016, 5, 12)); - widget.setCalendarDisplayMode(CalendarMode.WEEKS); + widget.state().edit() + .setFirstDayOfWeek(Calendar.WEDNESDAY) + .setMinimumDate(CalendarDay.from(2016, 4, 3)) + .setMaximumDate(CalendarDay.from(2016, 5, 12)) + .setCalendarDisplayMode(CalendarMode.WEEKS) + .commit(); + } } diff --git a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DisableDaysActivity.java b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DisableDaysActivity.java index d21e9fdd..5cd59589 100644 --- a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DisableDaysActivity.java +++ b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DisableDaysActivity.java @@ -36,11 +36,16 @@ protected void onCreate(Bundle savedInstanceState) { Calendar calendar = Calendar.getInstance(); widget.setSelectedDate(calendar.getTime()); - calendar.set(calendar.get(Calendar.YEAR), Calendar.JANUARY, 1); - widget.setMinimumDate(calendar.getTime()); + Calendar instance1 = Calendar.getInstance(); + instance1.set(instance1.get(Calendar.YEAR), Calendar.JANUARY, 1); - calendar.set(calendar.get(Calendar.YEAR) + 2, Calendar.OCTOBER, 31); - widget.setMaximumDate(calendar.getTime()); + Calendar instance2 = Calendar.getInstance(); + instance2.set(instance2.get(Calendar.YEAR) + 2, Calendar.OCTOBER, 31); + + widget.state().edit() + .setMinimumDate(instance1.getTime()) + .setMaximumDate(instance2.getTime()) + .commit(); } private static class PrimeDayDisableDecorator implements DayViewDecorator { diff --git a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DynamicSettersActivity.java b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DynamicSettersActivity.java index 06056825..cdb4d310 100644 --- a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DynamicSettersActivity.java +++ b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/DynamicSettersActivity.java @@ -123,7 +123,9 @@ void onMinClicked() { showDatePickerDialog(this, widget.getMinimumDate(), new DatePickerDialog.OnDateSetListener() { @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { - widget.setMinimumDate(CalendarDay.from(year, monthOfYear, dayOfMonth)); + widget.state().edit() + .setMinimumDate(CalendarDay.from(year, monthOfYear, dayOfMonth)) + .commit(); } }); } @@ -133,7 +135,9 @@ void onMaxClicked() { showDatePickerDialog(this, widget.getMaximumDate(), new DatePickerDialog.OnDateSetListener() { @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { - widget.setMaximumDate(CalendarDay.from(year, monthOfYear, dayOfMonth)); + widget.state().edit() + .setMaximumDate(CalendarDay.from(year, monthOfYear, dayOfMonth)) + .commit(); } }); } @@ -226,7 +230,8 @@ void onChangeSelectionMode() { CharSequence[] items = { "No Selection", "Single Date", - "Multiple Dates" + "Multiple Dates", + "Range of Dates" }; new AlertDialog.Builder(this) .setTitle("Selection Mode") @@ -253,17 +258,24 @@ public void onClick(DialogInterface dialog, int which) { @OnClick(R.id.button_set_first_day) void onFirstDayOfWeekClicked() { int index = random.nextInt(DAYS_OF_WEEK.length); - widget.setFirstDayOfWeek(DAYS_OF_WEEK[index]); + widget.state().edit() + .setFirstDayOfWeek(DAYS_OF_WEEK[index]) + .commit(); + } @OnClick(R.id.button_weeks) public void onSetWeekMode() { - widget.setCalendarDisplayMode(CalendarMode.WEEKS); + widget.state().edit() + .setCalendarDisplayMode(CalendarMode.WEEKS) + .commit(); } @OnClick(R.id.button_months) public void onSetMonthMode() { - widget.setCalendarDisplayMode(CalendarMode.MONTHS); + widget.state().edit() + .setCalendarDisplayMode(CalendarMode.MONTHS) + .commit(); } diff --git a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/SwappableBasicActivityDecorated.java b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/SwappableBasicActivityDecorated.java index b1a5e81f..8cd7517f 100644 --- a/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/SwappableBasicActivityDecorated.java +++ b/sample/src/main/java/com/prolificinteractive/materialcalendarview/sample/SwappableBasicActivityDecorated.java @@ -37,14 +37,20 @@ protected void onCreate(Bundle savedInstanceState) { widget.setOnDateChangedListener(this); widget.setShowOtherDates(MaterialCalendarView.SHOW_ALL); - Calendar calendar = Calendar.getInstance(); - widget.setSelectedDate(calendar.getTime()); - calendar.set(calendar.get(Calendar.YEAR), Calendar.JANUARY, 1); - widget.setMinimumDate(calendar.getTime()); + Calendar instance = Calendar.getInstance(); + widget.setSelectedDate(instance.getTime()); - calendar.set(calendar.get(Calendar.YEAR), Calendar.DECEMBER, 31); - widget.setMaximumDate(calendar.getTime()); + Calendar instance1 = Calendar.getInstance(); + instance1.set(instance1.get(Calendar.YEAR), Calendar.JANUARY, 1); + + Calendar instance2 = Calendar.getInstance(); + instance2.set(instance2.get(Calendar.YEAR), Calendar.DECEMBER, 31); + + widget.state().edit() + .setMinimumDate(instance1.getTime()) + .setMaximumDate(instance2.getTime()) + .commit(); widget.addDecorators( new MySelectorDecorator(this), @@ -62,11 +68,15 @@ public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull Calend @OnClick(R.id.button_weeks) public void onSetWeekMode() { - widget.setCalendarDisplayMode(CalendarMode.WEEKS); + widget.state().edit() + .setCalendarDisplayMode(CalendarMode.WEEKS) + .commit(); } @OnClick(R.id.button_months) public void onSetMonthMode() { - widget.setCalendarDisplayMode(CalendarMode.MONTHS); + widget.state().edit() + .setCalendarDisplayMode(CalendarMode.MONTHS) + .commit(); } }