package com.prolificinteractive.materialcalendarview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.ArrayRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.prolificinteractive.materialcalendarview.format.ArrayWeekDayFormatter;
import com.prolificinteractive.materialcalendarview.format.DateFormatTitleFormatter;
import com.prolificinteractive.materialcalendarview.format.DayFormatter;
import com.prolificinteractive.materialcalendarview.format.MonthArrayTitleFormatter;
import com.prolificinteractive.materialcalendarview.format.TitleFormatter;
import com.prolificinteractive.materialcalendarview.format.WeekDayFormatter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* <p>
* This class is a calendar widget for displaying and selecting dates.
* The range of dates supported by this calendar is configurable.
* A user can select a date by taping on it and can page the calendar to a desired date.
* </p>
* <p>
* By default, the range of dates shown is from 200 years in the past to 200 years in the future.
* This can be extended or shortened by configuring the minimum and maximum dates.
* </p>
* <p>
* When selecting a date out of range, or when the range changes so the selection becomes outside,
* The date closest to the previous selection will become selected. This will also trigger the
* {@linkplain OnDateSelectedListener}
* </p>
* <p>
* <strong>Note:</strong> if this view's size isn't divisible by 7,
* the contents will be centered inside such that the days in the calendar are equally square.
* For example, 600px isn't divisible by 7, so a tile size of 85 is choosen, making the calendar
* 595px wide. The extra 5px are distributed left and right to get to 600px.
* </p>
*/
public class MaterialCalendarView extends ViewGroup {
public static final int INVALID_TILE_DIMENSION = -10;
/**
* {@linkplain IntDef} annotation for selection mode.
*
* @see #setSelectionMode(int)
* @see #getSelectionMode()
*/
@Retention(RetentionPolicy.RUNTIME)
@IntDef({SELECTION_MODE_NONE, SELECTION_MODE_SINGLE, SELECTION_MODE_MULTIPLE, SELECTION_MODE_RANGE})
public @interface SelectionMode {
}
/**
* Selection mode that disallows all selection.
* When changing to this mode, current selection will be cleared.
*/
public static final int SELECTION_MODE_NONE = 0;
/**
* Selection mode that allows one selected date at one time. This is the default mode.
* When switching from {@linkplain #SELECTION_MODE_MULTIPLE}, this will select the same date
* as from {@linkplain #getSelectedDate()}, which should be the last selected date
*/
public static final int SELECTION_MODE_SINGLE = 1;
/**
* Selection mode which allows more than one selected date at one time.
*/
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.
*
* @see #setShowOtherDates(int)
* @see #getShowOtherDates()
*/
@SuppressLint("UniqueConstants")
@Retention(RetentionPolicy.RUNTIME)
@IntDef(flag = true, value = {
SHOW_NONE, SHOW_ALL, SHOW_DEFAULTS,
SHOW_OUT_OF_RANGE, SHOW_OTHER_MONTHS, SHOW_DECORATED_DISABLED
})
public @interface ShowOtherDates {
}
/**
* Do not show any non-enabled dates
*/
public static final int SHOW_NONE = 0;
/**
* Show dates from the proceeding and successive months, in a disabled state.
* This flag also enables the {@link #SHOW_OUT_OF_RANGE} flag to prevent odd blank areas.
*/
public static final int SHOW_OTHER_MONTHS = 1;
/**
* Show dates that are outside of the min-max range.
* This will only show days from the current month unless {@link #SHOW_OTHER_MONTHS} is enabled.
*/
public static final int SHOW_OUT_OF_RANGE = 2;
/**
* Show days that are individually disabled with decorators.
* This will only show dates in the current month and inside the minimum and maximum date range.
*/
public static final int SHOW_DECORATED_DISABLED = 4;
/**
* The default flags for showing non-enabled dates. Currently only shows {@link #SHOW_DECORATED_DISABLED}
*/
public static final int SHOW_DEFAULTS = SHOW_DECORATED_DISABLED;
/**
* Show all the days
*/
public static final int SHOW_ALL = SHOW_OTHER_MONTHS | SHOW_OUT_OF_RANGE | SHOW_DECORATED_DISABLED;
/**
* Use this orientation to animate the title vertically
*/
public static final int VERTICAL = 0;
/**
* Use this orientation to animate the title horizontally
*/
public static final int HORIZONTAL = 1;
/**
* Default tile size in DIPs. This is used in cases where there is no tile size specificed and the view is set to {@linkplain ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT}
*/
public static final int DEFAULT_TILE_SIZE_DP = 44;
private static final int DEFAULT_DAYS_IN_WEEK = 7;
private static final int DEFAULT_MAX_WEEKS = 6;
private static final int DAY_NAMES_ROW = 1;
private static final TitleFormatter DEFAULT_TITLE_FORMATTER = new DateFormatTitleFormatter();
private final TitleChanger titleChanger;
private final TextView title;
private final DirectionButton buttonPast;
private final DirectionButton buttonFuture;
private final CalendarPager pager;
private CalendarPagerAdapter<?> adapter;
private CalendarDay currentMonth;
private LinearLayout topbar;
private CalendarMode calendarMode;
/**
* Used for the dynamic calendar height.
*/
private boolean mDynamicHeightEnabled;
private final ArrayList<DayViewDecorator> dayViewDecorators = new ArrayList<>();
private final OnClickListener onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (v == buttonFuture) {
pager.setCurrentItem(pager.getCurrentItem() + 1, true);
} else if (v == buttonPast) {
pager.setCurrentItem(pager.getCurrentItem() - 1, true);
}
}
};
private final ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageSelected(int position) {
titleChanger.setPreviousMonth(currentMonth);
currentMonth = adapter.getItem(position);
updateUi();
dispatchOnMonthChanged(currentMonth);
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
};
private CalendarDay minDate = null;
private CalendarDay maxDate = null;
private OnDateSelectedListener listener;
private OnMonthChangedListener monthListener;
private OnRangeSelectedListener rangeListener;
CharSequence calendarContentDescription;
private int accentColor = 0;
private int arrowColor = Color.BLACK;
private Drawable leftArrowMask;
private Drawable rightArrowMask;
private int tileHeight = INVALID_TILE_DIMENSION;
private int tileWidth = INVALID_TILE_DIMENSION;
@SelectionMode
private int selectionMode = SELECTION_MODE_SINGLE;
private boolean allowClickDaysOutsideCurrentMonth = true;
private int firstDayOfWeek;
private State state;
public MaterialCalendarView(Context context) {
this(context, null);
}
public MaterialCalendarView(Context context, AttributeSet attrs) {
super(context, attrs);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//If we're on good Android versions, turn off clipping for cool effects
setClipToPadding(false);
setClipChildren(false);
} else {
//Old Android does not like _not_ clipping view pagers, we need to clip
setClipChildren(true);
setClipToPadding(true);
}
buttonPast = new DirectionButton(getContext());
buttonPast.setContentDescription(getContext().getString(R.string.previous));
title = new TextView(getContext());
buttonFuture = new DirectionButton(getContext());
buttonFuture.setContentDescription(getContext().getString(R.string.next));
pager = new CalendarPager(getContext());
buttonPast.setOnClickListener(onClickListener);
buttonFuture.setOnClickListener(onClickListener);
titleChanger = new TitleChanger(title);
titleChanger.setTitleFormatter(DEFAULT_TITLE_FORMATTER);
pager.setOnPageChangeListener(pageChangeListener);
pager.setPageTransformer(false, new ViewPager.PageTransformer() {
@Override
public void transformPage(View page, float position) {
position = (float) Math.sqrt(1 - Math.abs(position));
page.setAlpha(position);
}
});
TypedArray a = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.MaterialCalendarView, 0, 0);
try {
int calendarModeIndex = a.getInteger(
R.styleable.MaterialCalendarView_mcv_calendarMode,
0
);
firstDayOfWeek = a.getInteger(
R.styleable.MaterialCalendarView_mcv_firstDayOfWeek,
-1
);
titleChanger.setOrientation(
a.getInteger(R.styleable.MaterialCalendarView_mcv_titleAnimationOrientation,
VERTICAL));
if (firstDayOfWeek < 0) {
//Allowing use of Calendar.getInstance() here as a performance optimization
firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
}
newState()
.setFirstDayOfWeek(firstDayOfWeek)
.setCalendarDisplayMode(CalendarMode.values()[calendarModeIndex])
.commit();
final int tileSize = a.getLayoutDimension(R.styleable.MaterialCalendarView_mcv_tileSize, INVALID_TILE_DIMENSION);
if (tileSize > INVALID_TILE_DIMENSION) {
setTileSize(tileSize);
}
final int tileWidth = a.getLayoutDimension(R.styleable.MaterialCalendarView_mcv_tileWidth, INVALID_TILE_DIMENSION);
if (tileWidth > INVALID_TILE_DIMENSION) {
setTileWidth(tileWidth);
}
final int tileHeight = a.getLayoutDimension(R.styleable.MaterialCalendarView_mcv_tileHeight, INVALID_TILE_DIMENSION);
if (tileHeight > INVALID_TILE_DIMENSION) {
setTileHeight(tileHeight);
}
setArrowColor(a.getColor(
R.styleable.MaterialCalendarView_mcv_arrowColor,
Color.BLACK
));
Drawable leftMask = a.getDrawable(
R.styleable.MaterialCalendarView_mcv_leftArrowMask
);
if (leftMask == null) {
leftMask = getResources().getDrawable(R.drawable.mcv_action_previous);
}
setLeftArrowMask(leftMask);
Drawable rightMask = a.getDrawable(
R.styleable.MaterialCalendarView_mcv_rightArrowMask
);
if (rightMask == null) {
rightMask = getResources().getDrawable(R.drawable.mcv_action_next);
}
setRightArrowMask(rightMask);
setSelectionColor(
a.getColor(
R.styleable.MaterialCalendarView_mcv_selectionColor,
getThemeAccentColor(context)
)
);
CharSequence[] array = a.getTextArray(R.styleable.MaterialCalendarView_mcv_weekDayLabels);
if (array != null) {
setWeekDayFormatter(new ArrayWeekDayFormatter(array));
}
array = a.getTextArray(R.styleable.MaterialCalendarView_mcv_monthLabels);
if (array != null) {
setTitleFormatter(new MonthArrayTitleFormatter(array));
}
setHeaderTextAppearance(a.getResourceId(
R.styleable.MaterialCalendarView_mcv_headerTextAppearance,
R.style.TextAppearance_MaterialCalendarWidget_Header
));
setWeekDayTextAppearance(a.getResourceId(
R.styleable.MaterialCalendarView_mcv_weekDayTextAppearance,
R.style.TextAppearance_MaterialCalendarWidget_WeekDay
));
setDateTextAppearance(a.getResourceId(
R.styleable.MaterialCalendarView_mcv_dateTextAppearance,
R.style.TextAppearance_MaterialCalendarWidget_Date
));
//noinspection ResourceType
setShowOtherDates(a.getInteger(
R.styleable.MaterialCalendarView_mcv_showOtherDates,
SHOW_DEFAULTS
));
setAllowClickDaysOutsideCurrentMonth(a.getBoolean(
R.styleable.MaterialCalendarView_mcv_allowClickDaysOutsideCurrentMonth,
true
));
} catch (Exception e) {
e.printStackTrace();
} finally {
a.recycle();
}
// Adapter is created while parsing the TypedArray attrs, so setup has to happen after
adapter.setTitleFormatter(DEFAULT_TITLE_FORMATTER);
setupChildren();
currentMonth = CalendarDay.today();
setCurrentDate(currentMonth);
if (isInEditMode()) {
removeView(pager);
MonthView monthView = new MonthView(this, currentMonth, getFirstDayOfWeek());
monthView.setSelectionColor(getSelectionColor());
monthView.setDateTextAppearance(adapter.getDateTextAppearance());
monthView.setWeekDayTextAppearance(adapter.getWeekDayTextAppearance());
monthView.setShowOtherDates(getShowOtherDates());
addView(monthView, new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW));
}
}
private void setupChildren() {
topbar = new LinearLayout(getContext());
topbar.setOrientation(LinearLayout.HORIZONTAL);
topbar.setClipChildren(false);
topbar.setClipToPadding(false);
addView(topbar, new LayoutParams(1));
buttonPast.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
topbar.addView(buttonPast, new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1));
title.setGravity(Gravity.CENTER);
topbar.addView(title, new LinearLayout.LayoutParams(
0, LayoutParams.MATCH_PARENT, DEFAULT_DAYS_IN_WEEK - 2
));
buttonFuture.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
topbar.addView(buttonFuture, new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1));
pager.setId(R.id.mcv_pager);
pager.setOffscreenPageLimit(1);
addView(pager, new LayoutParams(calendarMode.visibleWeeksCount + DAY_NAMES_ROW));
}
private void updateUi() {
titleChanger.change(currentMonth);
buttonPast.setEnabled(canGoBack());
buttonFuture.setEnabled(canGoForward());
}
/**
* 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},
* {@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_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<CalendarDay> dates = getSelectedDates();
if (!dates.isEmpty()) {
setSelectedDate(getSelectedDate());
}
}
break;
default:
case SELECTION_MODE_NONE:
this.selectionMode = SELECTION_MODE_NONE;
if (oldMode != SELECTION_MODE_NONE) {
//No selection! Clear out!
clearSelection();
}
break;
}
adapter.setSelectionEnabled(selectionMode != SELECTION_MODE_NONE);
}
/**
* Go to previous month or week without using the button {@link #buttonPast}. Should only go to
* previous if {@link #canGoBack()} is true, meaning it's possible to go to the previous month
* or week.
*/
public void goToPrevious() {
if (canGoBack()) {
pager.setCurrentItem(pager.getCurrentItem() - 1, true);
}
}
/**
* Go to next month or week without using the button {@link #buttonFuture}. Should only go to
* next if {@link #canGoForward()} is enabled, meaning it's possible to go to the next month or
* week.
*/
public void goToNext() {
if (canGoForward()) {
pager.setCurrentItem(pager.getCurrentItem() + 1, true);
}
}
/**
* Get the current selection mode. The default mode is {@linkplain #SELECTION_MODE_SINGLE}
*
* @return the current selection mode
* @see #setSelectionMode(int)
* @see #SELECTION_MODE_NONE
* @see #SELECTION_MODE_SINGLE
* @see #SELECTION_MODE_MULTIPLE
* @see #SELECTION_MODE_RANGE
*/
@SelectionMode
public int getSelectionMode() {
return selectionMode;
}
/**
* Use {@link #getTileWidth()} or {@link #getTileHeight()} instead. This method is deprecated
* and will just return the largest of the two sizes.
*
* @return tile height or width, whichever is larger
*/
@Deprecated
public int getTileSize() {
return Math.max(tileHeight, tileWidth);
}
/**
* Set the size of each tile that makes up the calendar.
* Each day is 1 tile, so the widget is 7 tiles wide and 7 or 8 tiles tall
* depending on the visibility of the {@link #topbar}.
*
* @param size the new size for each tile in pixels
*/
public void setTileSize(int size) {
this.tileWidth = size;
this.tileHeight = size;
requestLayout();
}
/**
* @param tileSizeDp the new size for each tile in dips
* @see #setTileSize(int)
*/
public void setTileSizeDp(int tileSizeDp) {
setTileSize(dpToPx(tileSizeDp));
}
/**
* @return the height of tiles in pixels
*/
public int getTileHeight() {
return tileHeight;
}
/**
* Set the height of each tile that makes up the calendar.
*
* @param height the new height for each tile in pixels
*/
public void setTileHeight(int height) {
this.tileHeight = height;
requestLayout();
}
/**
* @param tileHeightDp the new height for each tile in dips
* @see #setTileHeight(int)
*/
public void setTileHeightDp(int tileHeightDp) {
setTileHeight(dpToPx(tileHeightDp));
}
/**
* @return the width of tiles in pixels
*/
public int getTileWidth() {
return tileWidth;
}
/**
* Set the width of each tile that makes up the calendar.
*
* @param width the new width for each tile in pixels
*/
public void setTileWidth(int width) {
this.tileWidth = width;
requestLayout();
}
/**
* @param tileWidthDp the new width for each tile in dips
* @see #setTileWidth(int)
*/
public void setTileWidthDp(int tileWidthDp) {
setTileWidth(dpToPx(tileWidthDp));
}
private int dpToPx(int dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()
);
}
/**
* TODO should this be public?
*
* @return true if there is a future month that can be shown
*/
public boolean canGoForward() {
return pager.getCurrentItem() < (adapter.getCount() - 1);
}
/**
* Pass all touch events to the pager so scrolling works on the edges of the calendar view.
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return pager.dispatchTouchEvent(event);
}
/**
* TODO should this be public?
*
* @return true if there is a previous month that can be shown
*/
public boolean canGoBack() {
return pager.getCurrentItem() > 0;
}
/**
* @return the color used for the selection
*/
public int getSelectionColor() {
return accentColor;
}
/**
* @param color The selection color
*/
public void setSelectionColor(int color) {
if (color == 0) {
if (!isInEditMode()) {
return;
} else {
color = Color.GRAY;
}
}
accentColor = color;
adapter.setSelectionColor(color);
invalidate();
}
/**
* @return color used to draw arrows
*/
public int getArrowColor() {
return arrowColor;
}
/**
* @param color the new color for the paging arrows
*/
public void setArrowColor(int color) {
if (color == 0) {
return;
}
arrowColor = color;
buttonPast.setColor(color);
buttonFuture.setColor(color);
invalidate();
}
/**
* Set content description for button past
*
* @param description String to use as content description
*/
public void setContentDescriptionArrowPast(final CharSequence description) {
buttonPast.setContentDescription(description);
}
/**
* Set content description for button future
*
* @param description String to use as content description
*/
public void setContentDescriptionArrowFuture(final CharSequence description) {
buttonFuture.setContentDescription(description);
}
/**
* Set content description for calendar
*
* @param description String to use as content description
*/
public void setContentDescriptionCalendar(final CharSequence description) {
calendarContentDescription = description;
}
/**
* Get content description for calendar
*
* @return calendar's content description
*/
public CharSequence getCalendarContentDescription() {
return calendarContentDescription != null
? calendarContentDescription
: getContext().getString(R.string.calendar);
}
/**
* @return icon used for the left arrow
*/
public Drawable getLeftArrowMask() {
return leftArrowMask;
}
/**
* @param icon the new icon to use for the left paging arrow
*/
public void setLeftArrowMask(Drawable icon) {
leftArrowMask = icon;
buttonPast.setImageDrawable(icon);
}
/**
* @return icon used for the right arrow
*/
public Drawable getRightArrowMask() {
return rightArrowMask;
}
/**
* @param icon the new icon to use for the right paging arrow
*/
public void setRightArrowMask(Drawable icon) {
rightArrowMask = icon;
buttonFuture.setImageDrawable(icon);
}
/**
* @param resourceId The text appearance resource id.
*/
public void setHeaderTextAppearance(int resourceId) {
title.setTextAppearance(getContext(), resourceId);
}
/**
* @param resourceId The text appearance resource id.
*/
public void setDateTextAppearance(int resourceId) {
adapter.setDateTextAppearance(resourceId);
}
/**
* @param resourceId The text appearance resource id.
*/
public void setWeekDayTextAppearance(int resourceId) {
adapter.setWeekDayTextAppearance(resourceId);
}
/**
* @return the selected day, or null if no selection. If in multiple selection mode, this
* will return the last selected date
*/
public CalendarDay getSelectedDate() {
List<CalendarDay> dates = adapter.getSelectedDates();
if (dates.isEmpty()) {
return null;
} else {
return dates.get(dates.size() - 1);
}
}
/**
* @return all of the currently selected dates
*/
@NonNull
public List<CalendarDay> getSelectedDates() {
return adapter.getSelectedDates();
}
/**
* Clear the currently selected date(s)
*/
public void clearSelection() {
List<CalendarDay> dates = getSelectedDates();
adapter.clearSelections();
for (CalendarDay day : dates) {
dispatchOnDateSelected(day, false);
}
}
/**
* @param calendar a Calendar set to a day to select. Null to clear selection
*/
public void setSelectedDate(@Nullable Calendar calendar) {
setSelectedDate(CalendarDay.from(calendar));
}
/**
* @param date a Date to set as selected. Null to clear selection
*/
public void setSelectedDate(@Nullable Date date) {
setSelectedDate(CalendarDay.from(date));
}
/**
* @param date a Date to set as selected. Null to clear selection
*/
public void setSelectedDate(@Nullable CalendarDay date) {
clearSelection();
if (date != null) {
setDateSelected(date, true);
}
}
/**
* @param calendar a Calendar to change. Passing null does nothing
* @param selected true if day should be selected, false to deselect
*/
public void setDateSelected(@Nullable Calendar calendar, boolean selected) {
setDateSelected(CalendarDay.from(calendar), selected);
}
/**
* @param date a Date to change. Passing null does nothing
* @param selected true if day should be selected, false to deselect
*/
public void setDateSelected(@Nullable Date date, boolean selected) {
setDateSelected(CalendarDay.from(date), selected);
}
/**
* @param day a CalendarDay to change. Passing null does nothing
* @param selected true if day should be selected, false to deselect
*/
public void setDateSelected(@Nullable CalendarDay day, boolean selected) {
if (day == null) {
return;
}
adapter.setDateSelected(day, selected);
}
/**
* @param calendar a Calendar set to a day to focus the calendar on. Null will do nothing
*/
public void setCurrentDate(@Nullable Calendar calendar) {
setCurrentDate(CalendarDay.from(calendar));
}
/**
* @param date a Date to focus the calendar on. Null will do nothing
*/
public void setCurrentDate(@Nullable Date date) {
setCurrentDate(CalendarDay.from(date));
}
/**
* @return The current month shown, will be set to first day of the month
*/
public CalendarDay getCurrentDate() {
return adapter.getItem(pager.getCurrentItem());
}
/**
* @param day a CalendarDay to focus the calendar on. Null will do nothing
*/
public void setCurrentDate(@Nullable CalendarDay day) {
setCurrentDate(day, true);
}
/**
* @param day a CalendarDay to focus the calendar on. Null will do nothing
* @param useSmoothScroll use smooth scroll when changing months.
*/
public void setCurrentDate(@Nullable CalendarDay day, boolean useSmoothScroll) {
if (day == null) {
return;
}
int index = adapter.getIndexForDay(day);
pager.setCurrentItem(index, useSmoothScroll);
updateUi();
}
/**
* @return the minimum selectable date for the calendar, if any
*/
public CalendarDay getMinimumDate() {
return minDate;
}
/**
* @return the maximum selectable date for the calendar, if any
*/
public CalendarDay getMaximumDate() {
return 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.
*
* @param showOtherDates flags for showing non-enabled dates
* @see #SHOW_ALL
* @see #SHOW_NONE
* @see #SHOW_DEFAULTS
* @see #SHOW_OTHER_MONTHS
* @see #SHOW_OUT_OF_RANGE
* @see #SHOW_DECORATED_DISABLED
*/
public void setShowOtherDates(@ShowOtherDates int showOtherDates) {
adapter.setShowOtherDates(showOtherDates);
}
/**
* Allow the user to click on dates from other months that are not out of range. Go to next or
* previous month if a day outside the current month is clicked. The day still need to be
* enabled to be selected.
* Default value is true. Should be used with {@link #SHOW_OTHER_MONTHS}.
*
* @param enabled True to allow the user to click on a day outside current month displayed
*/
public void setAllowClickDaysOutsideCurrentMonth(final boolean enabled) {
this.allowClickDaysOutsideCurrentMonth = enabled;
}
/**
* Set a formatter for weekday labels.
*
* @param formatter the new formatter, null for default
*/
public void setWeekDayFormatter(WeekDayFormatter formatter) {
adapter.setWeekDayFormatter(formatter == null ? WeekDayFormatter.DEFAULT : formatter);
}
/**
* Set a formatter for day labels.
*
* @param formatter the new formatter, null for default
*/
public void setDayFormatter(DayFormatter formatter) {
adapter.setDayFormatter(formatter == null ? DayFormatter.DEFAULT : formatter);
}
/**
* Set a {@linkplain com.prolificinteractive.materialcalendarview.format.WeekDayFormatter}
* with the provided week day labels
*
* @param weekDayLabels Labels to use for the days of the week
* @see com.prolificinteractive.materialcalendarview.format.ArrayWeekDayFormatter
* @see #setWeekDayFormatter(com.prolificinteractive.materialcalendarview.format.WeekDayFormatter)
*/
public void setWeekDayLabels(CharSequence[] weekDayLabels) {
setWeekDayFormatter(new ArrayWeekDayFormatter(weekDayLabels));
}
/**
* Set a {@linkplain com.prolificinteractive.materialcalendarview.format.WeekDayFormatter}
* with the provided week day labels
*
* @param arrayRes String array resource of week day labels
* @see com.prolificinteractive.materialcalendarview.format.ArrayWeekDayFormatter
* @see #setWeekDayFormatter(com.prolificinteractive.materialcalendarview.format.WeekDayFormatter)
*/
public void setWeekDayLabels(@ArrayRes int arrayRes) {
setWeekDayLabels(getResources().getTextArray(arrayRes));
}
/**
* @return int of flags used for showing non-enabled dates
* @see #SHOW_ALL
* @see #SHOW_NONE
* @see #SHOW_DEFAULTS
* @see #SHOW_OTHER_MONTHS
* @see #SHOW_OUT_OF_RANGE
* @see #SHOW_DECORATED_DISABLED
*/
@ShowOtherDates
public int getShowOtherDates() {
return adapter.getShowOtherDates();
}
/**
* @return true if allow click on days outside current month displayed
*/
public boolean allowClickDaysOutsideCurrentMonth() {
return allowClickDaysOutsideCurrentMonth;
}
/**
* Set a custom formatter for the month/year title
*
* @param titleFormatter new formatter to use, null to use default formatter
*/
public void setTitleFormatter(TitleFormatter titleFormatter) {
if (titleFormatter == null) {
titleFormatter = DEFAULT_TITLE_FORMATTER;
}
titleChanger.setTitleFormatter(titleFormatter);
adapter.setTitleFormatter(titleFormatter);
updateUi();
}
/**
* Set a {@linkplain com.prolificinteractive.materialcalendarview.format.TitleFormatter}
* using the provided month labels
*
* @param monthLabels month labels to use
* @see com.prolificinteractive.materialcalendarview.format.MonthArrayTitleFormatter
* @see #setTitleFormatter(com.prolificinteractive.materialcalendarview.format.TitleFormatter)
*/
public void setTitleMonths(CharSequence[] monthLabels) {
setTitleFormatter(new MonthArrayTitleFormatter(monthLabels));
}
/**
* Set a {@linkplain com.prolificinteractive.materialcalendarview.format.TitleFormatter}
* using the provided month labels
*
* @param arrayRes String array resource of month labels to use
* @see com.prolificinteractive.materialcalendarview.format.MonthArrayTitleFormatter
* @see #setTitleFormatter(com.prolificinteractive.materialcalendarview.format.TitleFormatter)
*/
public void setTitleMonths(@ArrayRes int arrayRes) {
setTitleMonths(getResources().getTextArray(arrayRes));
}
/**
* Change the title animation orientation to have a different look and feel.
*
* @param orientation {@link MaterialCalendarView#VERTICAL} or {@link MaterialCalendarView#HORIZONTAL}
*/
public void setTitleAnimationOrientation(final int orientation) {
titleChanger.setOrientation(orientation);
}
/**
* Get the orientation of the animation of the title.
*
* @return Title animation orientation {@link MaterialCalendarView#VERTICAL} or {@link MaterialCalendarView#HORIZONTAL}
*/
public int getTitleAnimationOrientation() {
return titleChanger.getOrientation();
}
/**
* Sets the visibility {@link #topbar}, which contains
* the previous month button {@link #buttonPast}, next month button {@link #buttonFuture},
* and the month title {@link #title}.
*
* @param visible Boolean indicating if the topbar is visible
*/
public void setTopbarVisible(boolean visible) {
topbar.setVisibility(visible ? View.VISIBLE : View.GONE);
requestLayout();
}
/**
* @return true if the topbar is visible
*/
public boolean getTopbarVisible() {
return topbar.getVisibility() == View.VISIBLE;
}
@Override
protected Parcelable onSaveInstanceState() {
SavedState ss = new SavedState(super.onSaveInstanceState());
ss.color = getSelectionColor();
ss.dateTextAppearance = adapter.getDateTextAppearance();
ss.weekDayTextAppearance = adapter.getWeekDayTextAppearance();
ss.showOtherDates = getShowOtherDates();
ss.allowClickDaysOutsideCurrentMonth = allowClickDaysOutsideCurrentMonth();
ss.minDate = getMinimumDate();
ss.maxDate = getMaximumDate();
ss.selectedDates = getSelectedDates();
ss.firstDayOfWeek = getFirstDayOfWeek();
ss.orientation = getTitleAnimationOrientation();
ss.selectionMode = getSelectionMode();
ss.tileWidthPx = getTileWidth();
ss.tileHeightPx = getTileHeight();
ss.topbarVisible = getTopbarVisible();
ss.calendarMode = calendarMode;
ss.dynamicHeightEnabled = mDynamicHeightEnabled;
ss.currentMonth = currentMonth;
ss.cacheCurrentPosition = state.cacheCurrentPosition;
return ss;
}
@Override
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)
.isCacheCalendarPositionEnabled(ss.cacheCurrentPosition)
.commit();
setSelectionColor(ss.color);
setDateTextAppearance(ss.dateTextAppearance);
setWeekDayTextAppearance(ss.weekDayTextAppearance);
setShowOtherDates(ss.showOtherDates);
setAllowClickDaysOutsideCurrentMonth(ss.allowClickDaysOutsideCurrentMonth);
clearSelection();
for (CalendarDay calendarDay : ss.selectedDates) {
setDateSelected(calendarDay, true);
}
setTitleAnimationOrientation(ss.orientation);
setTileWidth(ss.tileWidthPx);
setTileHeight(ss.tileHeightPx);
setTopbarVisible(ss.topbarVisible);
setSelectionMode(ss.selectionMode);
setDynamicHeightEnabled(ss.dynamicHeightEnabled);
setCurrentDate(ss.currentMonth);
}
@Override
protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
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();
}
public static class SavedState extends BaseSavedState {
int color = 0;
int dateTextAppearance = 0;
int weekDayTextAppearance = 0;
int showOtherDates = SHOW_DEFAULTS;
boolean allowClickDaysOutsideCurrentMonth = true;
CalendarDay minDate = null;
CalendarDay maxDate = null;
List<CalendarDay> selectedDates = new ArrayList<>();
int firstDayOfWeek = Calendar.SUNDAY;
int orientation = 0;
int tileWidthPx = -1;
int tileHeightPx = -1;
boolean topbarVisible = true;
int selectionMode = SELECTION_MODE_SINGLE;
boolean dynamicHeightEnabled = false;
CalendarMode calendarMode = CalendarMode.MONTHS;
CalendarDay currentMonth = null;
boolean cacheCurrentPosition;
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(color);
out.writeInt(dateTextAppearance);
out.writeInt(weekDayTextAppearance);
out.writeInt(showOtherDates);
out.writeByte((byte) (allowClickDaysOutsideCurrentMonth ? 1 : 0));
out.writeParcelable(minDate, 0);
out.writeParcelable(maxDate, 0);
out.writeTypedList(selectedDates);
out.writeInt(firstDayOfWeek);
out.writeInt(orientation);
out.writeInt(tileWidthPx);
out.writeInt(tileHeightPx);
out.writeInt(topbarVisible ? 1 : 0);
out.writeInt(selectionMode);
out.writeInt(dynamicHeightEnabled ? 1 : 0);
out.writeInt(calendarMode == CalendarMode.WEEKS ? 1 : 0);
out.writeParcelable(currentMonth, 0);
out.writeByte((byte) (cacheCurrentPosition ? 1 : 0));
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private SavedState(Parcel in) {
super(in);
color = in.readInt();
dateTextAppearance = in.readInt();
weekDayTextAppearance = in.readInt();
showOtherDates = in.readInt();
allowClickDaysOutsideCurrentMonth = in.readByte() != 0;
ClassLoader loader = CalendarDay.class.getClassLoader();
minDate = in.readParcelable(loader);
maxDate = in.readParcelable(loader);
in.readTypedList(selectedDates, CalendarDay.CREATOR);
firstDayOfWeek = in.readInt();
orientation = in.readInt();
tileWidthPx = in.readInt();
tileHeightPx = in.readInt();
topbarVisible = in.readInt() == 1;
selectionMode = in.readInt();
dynamicHeightEnabled = in.readInt() == 1;
calendarMode = in.readInt() == 1 ? CalendarMode.WEEKS : CalendarMode.MONTHS;
currentMonth = in.readParcelable(loader);
cacheCurrentPosition = in.readByte() != 0;
}
}
private static int getThemeAccentColor(Context context) {
int colorAttr;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
colorAttr = android.R.attr.colorAccent;
} else {
//Get colorAccent defined for AppCompat
colorAttr = context.getResources().getIdentifier("colorAccent", "attr", context.getPackageName());
}
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(colorAttr, outValue, true);
return outValue.data;
}
/**
* @return The first day of the week as a {@linkplain Calendar} day constant.
*/
public int getFirstDayOfWeek() {
return firstDayOfWeek;
}
/**
* 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.
* <p>
* 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.
*
* @param useDynamicHeight true to have the view different heights based on the visible month
*/
public void setDynamicHeightEnabled(boolean useDynamicHeight) {
this.mDynamicHeightEnabled = useDynamicHeight;
}
/**
* @return the dynamic height state - true if enabled.
*/
public boolean isDynamicHeightEnabled() {
return mDynamicHeightEnabled;
}
/**
* Add a collection of day decorators
*
* @param decorators decorators to add
*/
public void addDecorators(Collection<? extends DayViewDecorator> decorators) {
if (decorators == null) {
return;
}
dayViewDecorators.addAll(decorators);
adapter.setDecorators(dayViewDecorators);
}
/**
* Add several day decorators
*
* @param decorators decorators to add
*/
public void addDecorators(DayViewDecorator... decorators) {
addDecorators(Arrays.asList(decorators));
}
/**
* Add a day decorator
*
* @param decorator decorator to add
*/
public void addDecorator(DayViewDecorator decorator) {
if (decorator == null) {
return;
}
dayViewDecorators.add(decorator);
adapter.setDecorators(dayViewDecorators);
}
/**
* Remove all decorators
*/
public void removeDecorators() {
dayViewDecorators.clear();
adapter.setDecorators(dayViewDecorators);
}
/**
* Remove a specific decorator instance. Same rules as {@linkplain List#remove(Object)}
*
* @param decorator decorator to remove
*/
public void removeDecorator(DayViewDecorator decorator) {
dayViewDecorators.remove(decorator);
adapter.setDecorators(dayViewDecorators);
}
/**
* Invalidate decorators after one has changed internally. That is, if a decorator mutates, you
* should call this method to update the widget.
*/
public void invalidateDecorators() {
adapter.invalidateDecorators();
}
/*
* Listener/Callback Code
*/
/**
* Sets the listener to be notified upon selected date changes.
*
* @param listener thing to be notified
*/
public void setOnDateChangedListener(OnDateSelectedListener listener) {
this.listener = listener;
}
/**
* Sets the listener to be notified upon month changes.
*
* @param listener thing to be notified
*/
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;
}
/**
* Add listener to the title or null to remove it.
*
* @param listener Listener to be notified.
*/
public void setOnTitleClickListener(final OnClickListener listener) {
title.setOnClickListener(listener);
}
/**
* Dispatch date change events to a listener, if set
*
* @param day the day that was selected
* @param selected true if the day is now currently selected, false otherwise
*/
protected void dispatchOnDateSelected(final CalendarDay day, final boolean selected) {
OnDateSelectedListener l = listener;
if (l != null) {
l.onDateSelected(MaterialCalendarView.this, day, selected);
}
}
/**
* 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<CalendarDay> 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
*
* @param day first day of the new month
*/
protected void dispatchOnMonthChanged(final CalendarDay day) {
OnMonthChangedListener l = monthListener;
if (l != null) {
l.onMonthChanged(MaterialCalendarView.this, day);
}
}
/**
* Call by {@link CalendarPagerView} to indicate that a day was clicked and we should handle it.
* This method will always process the click to the selected date.
*
* @param date date of the day that was clicked
* @param nowSelected true if the date is now selected, false otherwise
*/
protected void onDateClicked(@NonNull CalendarDay date, boolean nowSelected) {
switch (selectionMode) {
case SELECTION_MODE_MULTIPLE: {
adapter.setDateSelected(date, 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<CalendarDay> 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();
adapter.setDateSelected(date, true);
dispatchOnDateSelected(date, true);
}
break;
}
}
/**
* 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
*
* @param dayView
*/
protected void onDateClicked(final DayView dayView) {
final CalendarDay currentDate = getCurrentDate();
final CalendarDay selectedDate = dayView.getDate();
final int currentMonth = currentDate.getMonth();
final int selectedMonth = selectedDate.getMonth();
if (calendarMode == CalendarMode.MONTHS
&& allowClickDaysOutsideCurrentMonth
&& currentMonth != selectedMonth) {
if (currentDate.isAfter(selectedDate)) {
goToPrevious();
} else if (currentDate.isBefore(selectedDate)) {
goToNext();
}
}
onDateClicked(dayView.getDate(), !dayView.isChecked());
}
/**
* Called by the adapter for cases when changes in state result in dates being unselected
*
* @param date date that should be de-selected
*/
protected void onDateUnselected(CalendarDay date) {
dispatchOnDateSelected(date, false);
}
/*
* Show Other Dates Utils
*/
/**
* @param showOtherDates int flag for show other dates
* @return true if the other months flag is set
*/
public static boolean showOtherMonths(@ShowOtherDates int showOtherDates) {
return (showOtherDates & SHOW_OTHER_MONTHS) != 0;
}
/**
* @param showOtherDates int flag for show other dates
* @return true if the out of range flag is set
*/
public static boolean showOutOfRange(@ShowOtherDates int showOtherDates) {
return (showOtherDates & SHOW_OUT_OF_RANGE) != 0;
}
/**
* @param showOtherDates int flag for show other dates
* @return true if the decorated disabled flag is set
*/
public static boolean showDecoratedDisabled(@ShowOtherDates int showOtherDates) {
return (showOtherDates & SHOW_DECORATED_DISABLED) != 0;
}
/*
* Custom ViewGroup Code
*/
/**
* {@inheritDoc}
*/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(1);
}
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final int specWidthSize = MeasureSpec.getSize(widthMeasureSpec);
final int specWidthMode = MeasureSpec.getMode(widthMeasureSpec);
final int specHeightSize = MeasureSpec.getSize(heightMeasureSpec);
final int specHeightMode = MeasureSpec.getMode(heightMeasureSpec);
//We need to disregard padding for a while. This will be added back later
final int desiredWidth = specWidthSize - getPaddingLeft() - getPaddingRight();
final int desiredHeight = specHeightSize - getPaddingTop() - getPaddingBottom();
final int weekCount = getWeekCountBasedOnMode();
final int viewTileHeight = getTopbarVisible() ? (weekCount + 1) : weekCount;
//Calculate independent tile sizes for later
int desiredTileWidth = desiredWidth / DEFAULT_DAYS_IN_WEEK;
int desiredTileHeight = desiredHeight / viewTileHeight;
int measureTileSize = -1;
int measureTileWidth = -1;
int measureTileHeight = -1;
if (this.tileWidth != INVALID_TILE_DIMENSION || this.tileHeight != INVALID_TILE_DIMENSION) {
if (this.tileWidth > 0) {
//We have a tileWidth set, we should use that
measureTileWidth = this.tileWidth;
} else {
measureTileWidth = desiredTileWidth;
}
if (this.tileHeight > 0) {
//We have a tileHeight set, we should use that
measureTileHeight = this.tileHeight;
} else {
measureTileHeight = desiredTileHeight;
}
} else if (specWidthMode == MeasureSpec.EXACTLY || specWidthMode == MeasureSpec.AT_MOST) {
if (specHeightMode == MeasureSpec.EXACTLY) {
//Pick the smaller of the two explicit sizes
measureTileSize = Math.min(desiredTileWidth, desiredTileHeight);
} else {
//Be the width size the user wants
measureTileSize = desiredTileWidth;
}
} else if (specHeightMode == MeasureSpec.EXACTLY || specHeightMode == MeasureSpec.AT_MOST) {
//Be the height size the user wants
measureTileSize = desiredTileHeight;
}
if (measureTileSize > 0) {
//Use measureTileSize if set
measureTileHeight = measureTileSize;
measureTileWidth = measureTileSize;
} else if (measureTileSize <= 0) {
if (measureTileWidth <= 0) {
//Set width to default if no value were set
measureTileWidth = dpToPx(DEFAULT_TILE_SIZE_DP);
}
if (measureTileHeight <= 0) {
//Set height to default if no value were set
measureTileHeight = dpToPx(DEFAULT_TILE_SIZE_DP);
}
}
//Calculate our size based off our measured tile size
int measuredWidth = measureTileWidth * DEFAULT_DAYS_IN_WEEK;
int measuredHeight = measureTileHeight * viewTileHeight;
//Put padding back in from when we took it away
measuredWidth += getPaddingLeft() + getPaddingRight();
measuredHeight += getPaddingTop() + getPaddingBottom();
//Contract fulfilled, setting out measurements
setMeasuredDimension(
//We clamp inline because we want to use un-clamped versions on the children
clampSize(measuredWidth, widthMeasureSpec),
clampSize(measuredHeight, heightMeasureSpec)
);
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
LayoutParams p = (LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
DEFAULT_DAYS_IN_WEEK * measureTileWidth,
MeasureSpec.EXACTLY
);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
p.height * measureTileHeight,
MeasureSpec.EXACTLY
);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
private int getWeekCountBasedOnMode() {
int weekCount = calendarMode.visibleWeeksCount;
boolean isInMonthsMode = calendarMode.equals(CalendarMode.MONTHS);
if (isInMonthsMode && mDynamicHeightEnabled && adapter != null && pager != null) {
Calendar cal = (Calendar) adapter.getItem(pager.getCurrentItem()).getCalendar().clone();
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
//noinspection ResourceType
cal.setFirstDayOfWeek(getFirstDayOfWeek());
weekCount = cal.get(Calendar.WEEK_OF_MONTH);
}
return weekCount + DAY_NAMES_ROW;
}
/**
* Clamp the size to the measure spec.
*
* @param size Size we want to be
* @param spec Measure spec to clamp against
* @return the appropriate size to pass to {@linkplain View#setMeasuredDimension(int, int)}
*/
private static int clampSize(int size, int spec) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
switch (specMode) {
case MeasureSpec.EXACTLY: {
return specSize;
}
case MeasureSpec.AT_MOST: {
return Math.min(size, specSize);
}
case MeasureSpec.UNSPECIFIED:
default: {
return size;
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
final int parentLeft = getPaddingLeft();
final int parentWidth = right - left - parentLeft - getPaddingRight();
int childTop = getPaddingTop();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int delta = (parentWidth - width) / 2;
int childLeft = parentLeft + delta;
child.layout(childLeft, childTop, childLeft + width, childTop + height);
childTop += height;
}
}
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(1);
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(1);
}
@Override
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(MaterialCalendarView.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(MaterialCalendarView.class.getName());
}
/**
* Simple layout params for MaterialCalendarView. The only variation for layout is height.
*/
protected static class LayoutParams extends MarginLayoutParams {
/**
* Create a layout that matches parent width, and is X number of tiles high
*
* @param tileHeight view height in number of tiles
*/
public LayoutParams(int tileHeight) {
super(MATCH_PARENT, tileHeight);
}
}
/**
* Enable or disable the ability to swipe between months.
*
* @param pagingEnabled pass false to disable paging, true to enable (default)
*/
public void setPagingEnabled(boolean pagingEnabled) {
pager.setPagingEnabled(pagingEnabled);
updateUi();
}
/**
* @return true if swiping months is enabled, false if disabled. Default is true.
*/
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 {
private final CalendarMode calendarMode;
private final int firstDayOfWeek;
private final CalendarDay minDate;
private final CalendarDay maxDate;
private final boolean cacheCurrentPosition;
private State(final StateBuilder builder) {
calendarMode = builder.calendarMode;
firstDayOfWeek = builder.firstDayOfWeek;
minDate = builder.minDate;
maxDate = builder.maxDate;
cacheCurrentPosition = builder.cacheCurrentPosition;
}
/**
* 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();
private boolean cacheCurrentPosition = false;
private CalendarDay minDate = null;
private CalendarDay maxDate = null;
public StateBuilder() {
}
private StateBuilder(final State state) {
calendarMode = state.calendarMode;
firstDayOfWeek = state.firstDayOfWeek;
minDate = state.minDate;
maxDate = state.maxDate;
cacheCurrentPosition = state.cacheCurrentPosition;
}
/**
* Sets the first day of the week.
* <p>
* 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;
}
/**
* Use this method to enable saving the current position when switching
* between week and month mode. By default, the calendar update to the latest selected date
* or the current date. When set to true, the view will used the month that the calendar is
* currently on.
*
* @param cacheCurrentPosition Set to true to cache the current position, false otherwise.
*/
public StateBuilder isCacheCalendarPositionEnabled(final boolean cacheCurrentPosition) {
this.cacheCurrentPosition = cacheCurrentPosition;
return this;
}
public void commit() {
MaterialCalendarView.this.commit(new State(this));
}
}
private void commit(State state) {
// Use the calendarDayToShow to determine which date to focus on for the case of switching between month and week views
CalendarDay calendarDayToShow = null;
if (adapter != null && state.cacheCurrentPosition) {
calendarDayToShow = adapter.getItem(pager.getCurrentItem());
if (calendarMode != state.calendarMode) {
CalendarDay currentlySelectedDate = getSelectedDate();
if (calendarMode == CalendarMode.MONTHS && currentlySelectedDate != null) {
// Going from months to weeks
Calendar lastVisibleCalendar = calendarDayToShow.getCalendar();
lastVisibleCalendar.add(Calendar.MONTH, 1);
CalendarDay lastVisibleCalendarDay = CalendarDay.from(lastVisibleCalendar);
if (currentlySelectedDate.equals(calendarDayToShow) ||
(currentlySelectedDate.isAfter(calendarDayToShow) && currentlySelectedDate.isBefore(lastVisibleCalendarDay))) {
// Currently selected date is within view, so center on that
calendarDayToShow = currentlySelectedDate;
}
} else if (calendarMode == CalendarMode.WEEKS) {
// Going from weeks to months
Calendar lastVisibleCalendar = calendarDayToShow.getCalendar();
lastVisibleCalendar.add(Calendar.DAY_OF_WEEK, 6);
CalendarDay lastVisibleCalendarDay = CalendarDay.from(lastVisibleCalendar);
if (currentlySelectedDate != null &&
(currentlySelectedDate.equals(calendarDayToShow) || currentlySelectedDate.equals(lastVisibleCalendarDay) ||
(currentlySelectedDate.isAfter(calendarDayToShow) && currentlySelectedDate.isBefore(lastVisibleCalendarDay)))) {
// Currently selected date is within view, so center on that
calendarDayToShow = currentlySelectedDate;
} else {
calendarDayToShow = lastVisibleCalendarDay;
}
}
}
}
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());
if (calendarDayToShow != null) {
pager.setCurrentItem(adapter.getIndexForDay(calendarDayToShow));
}
invalidateDecorators();
updateUi();
}
}