Java tutorial
/* * Copyright (c) 2015 Kuloud * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.kuloud.android.calendarview; import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.ArrayRes; import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; 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.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.kuloud.android.calendarview.format.ArrayWeekDayFormatter; import com.kuloud.android.calendarview.format.DateFormatTitleFormatter; import com.kuloud.android.calendarview.format.DayFormatter; import com.kuloud.android.calendarview.format.MonthArrayTitleFormatter; import com.kuloud.android.calendarview.format.TitleFormatter; import com.kuloud.android.calendarview.format.WeekDayFormatter; import com.kuloud.android.common.util.CalendarUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import static com.kuloud.android.common.util.UIUtils.dp2px; import static com.kuloud.android.common.util.UIUtils.getThemeAccentColor; /** * <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 OnDateChangedListener} * </p> */ public class CalendarView extends FrameLayout { /** * Default tile size in DIPs */ public static final int DEFAULT_TILE_SIZE_DP = 44; private static final TitleFormatter DEFAULT_TITLE_FORMATTER = new DateFormatTitleFormatter(); private final TitleChanger titleChanger; private View topbar; private TextView titleMonth; private TextView titlePercent; private final ViewPager pager; private final MonthPagerAdapter adapter; private final ArrayList<DayViewDecorator> dayViewDecorators = new ArrayList<>(); private CalendarDay currentMonth; private CalendarDay minDate = null; private CalendarDay maxDate = null; private OnDateChangedListener listener; private final MonthView.Callbacks monthViewCallbacks = new MonthView.Callbacks() { @Override public void onDateChanged(CalendarDay date) { setSelectedDate(date); if (listener != null) { listener.onDateChanged(CalendarView.this, date); } } }; private OnMonthChangedListener monthListener; private final ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { titleChanger.setPreviousMonth(currentMonth); currentMonth = adapter.getItem(position); updateUi(); if (monthListener != null) { monthListener.onMonthChanged(CalendarView.this, currentMonth); } } @Override public void onPageScrollStateChanged(int state) { } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } }; private int accentColor = 0; private LinearLayout root; public CalendarView(Context context) { this(context, null); } public CalendarView(Context context, AttributeSet attrs) { super(context, attrs); setClipChildren(false); setClipToPadding(false); pager = new ViewPager(getContext()); setupChildren(); titleChanger = new TitleChanger(topbar); titleChanger.setTitleFormatter(DEFAULT_TITLE_FORMATTER); adapter = new MonthPagerAdapter(this); pager.setAdapter(adapter); 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); } }); adapter.setCallbacks(monthViewCallbacks); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CalendarView, 0, 0); try { int tileSize = a.getDimensionPixelSize(R.styleable.CalendarView_mcv_tileSize, -1); if (tileSize > 0) { setTileSize(tileSize); } setSelectionColor( a.getColor(R.styleable.CalendarView_mcv_selectionColor, getThemeAccentColor(context))); CharSequence[] array = a.getTextArray(R.styleable.CalendarView_mcv_weekDayLabels); if (array != null) { setWeekDayFormatter(new ArrayWeekDayFormatter(array)); } array = a.getTextArray(R.styleable.CalendarView_mcv_monthLabels); if (array != null) { setTitleFormatter(new MonthArrayTitleFormatter(array)); } setShowOtherDates(a.getBoolean(R.styleable.CalendarView_mcv_showOtherDates, false)); int firstDayOfWeek = a.getInt(R.styleable.CalendarView_mcv_firstDayOfWeek, -1); if (firstDayOfWeek < 0) { firstDayOfWeek = CalendarUtils.getInstance().getFirstDayOfWeek(); } setFirstDayOfWeek(firstDayOfWeek); } catch (Exception e) { e.printStackTrace(); } finally { if (a != null) { a.recycle(); } } currentMonth = CalendarDay.today(); setCurrentDate(currentMonth); } private void setupChildren() { int tileSize = (int) dp2px(getContext(), DEFAULT_TILE_SIZE_DP); root = new LinearLayout(getContext()); root.setOrientation(LinearLayout.VERTICAL); root.setClipChildren(false); root.setClipToPadding(false); LayoutParams p = new LayoutParams(tileSize * MonthView.DEFAULT_DAYS_IN_WEEK, tileSize * (MonthView.DEFAULT_MONTH_TILE_HEIGHT + 1)); p.gravity = Gravity.CENTER; addView(root, p); topbar = LayoutInflater.from(getContext()).inflate(R.layout.calendar_toolbar, root, false); root.addView(topbar, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0, 1)); titleMonth = (TextView) topbar.findViewById(R.id.tv_calendar_toolbar_month); titlePercent = (TextView) topbar.findViewById(R.id.tv_calendar_toolbar_percent); pager.setId(R.id.mcv_pager); pager.setOffscreenPageLimit(1); root.addView(pager, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0, MonthView.DEFAULT_MONTH_TILE_HEIGHT)); } /** * Sets the listener to be notified upon selected date changes. * * @param listener thing to be notified */ public void setOnDateChangedListener(OnDateChangedListener 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; } private void updateUi() { titleChanger.change(currentMonth); } /** * @return the size of tiles in pixels */ public int getTileSize() { return root.getLayoutParams().width / MonthView.DEFAULT_DAYS_IN_WEEK; } /** * 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) { LayoutParams p = new LayoutParams(size * MonthView.DEFAULT_DAYS_IN_WEEK, size * (MonthView.DEFAULT_MONTH_TILE_HEIGHT + 1)); p.gravity = Gravity.CENTER; root.setLayoutParams(p); } /** * @param tileSizeDp the new size for each tile in dips * @see #setTileSize(int) */ public void setTileSizeDp(int tileSizeDp) { setTileSize((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tileSizeDp, getResources().getDisplayMetrics())); } /** * TODO should this be public? * * @return true if there is a future month that can be shown */ private boolean canGoForward() { return pager.getCurrentItem() < (adapter.getCount() - 1); } /** * TODO should this be public? * * @return true if there is a previous month that can be shown */ private 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) { return; } accentColor = color; adapter.setSelectionColor(color); invalidate(); } /** * @return the currently selected day, or null if no selection */ public CalendarDay getSelectedDate() { return adapter.getSelectedDate(); } /** * @param day a CalendarDay to set as selected. Null to clear selection */ public void setSelectedDate(@Nullable CalendarDay day) { adapter.setSelectedDate(day); setCurrentDate(day); } /** * Clear the current selection */ public void clearSelection() { setSelectedDate((CalendarDay) null); } /** * @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 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; } /** * @param calendar set the minimum selectable date, null for no minimum */ public void setMinimumDate(@Nullable CalendarDay calendar) { minDate = calendar; setRangeDates(minDate, maxDate); } /** * @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)); } /** * @return the maximum selectable date for the calendar, if any */ public CalendarDay getMaximumDate() { return maxDate; } /** * @param calendar set the maximum selectable date, null for no maximum */ public void setMaximumDate(@Nullable CalendarDay calendar) { maxDate = calendar; setRangeDates(minDate, 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)); } /** * 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 WeekDayFormatter} * with the provided week day labels * * @param weekDayLabels Labels to use for the days of the week * @see ArrayWeekDayFormatter * @see #setWeekDayFormatter(WeekDayFormatter) */ public void setWeekDayLabels(CharSequence[] weekDayLabels) { setWeekDayFormatter(new ArrayWeekDayFormatter(weekDayLabels)); } /** * @return true if days from previous or next months are shown, otherwise false. */ public boolean getShowOtherDates() { return adapter.getShowOtherDates(); } /** * By default, only days of one month are shown. If this is set true, * then days from the previous and next months are used to fill the empty space. * This also controls showing dates outside of the min-max range. * * @param showOtherDates show other days, default is false */ public void setShowOtherDates(boolean showOtherDates) { adapter.setShowOtherDates(showOtherDates); } /** * 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) { titleChanger.setTitleFormatter(titleFormatter == null ? DEFAULT_TITLE_FORMATTER : titleFormatter); updateUi(); } /** * Set a {@linkplain TitleFormatter} * using the provided month labels * * @param monthLabels month labels to use * @see MonthArrayTitleFormatter * @see #setTitleFormatter(TitleFormatter) */ public void setTitleMonths(CharSequence[] monthLabels) { setTitleFormatter(new MonthArrayTitleFormatter(monthLabels)); } /** * Set a {@linkplain TitleFormatter} * using the provided month labels * * @param arrayRes String array resource of month labels to use * @see MonthArrayTitleFormatter * @see #setTitleFormatter(TitleFormatter) */ public void setTitleMonths(@ArrayRes int arrayRes) { setTitleMonths(getResources().getTextArray(arrayRes)); } @Override protected Parcelable onSaveInstanceState() { SavedState ss = new SavedState(super.onSaveInstanceState()); ss.color = getSelectionColor(); ss.showOtherDates = getShowOtherDates(); ss.minDate = getMinimumDate(); ss.maxDate = getMaximumDate(); ss.selectedDate = getSelectedDate(); ss.firstDayOfWeek = getFirstDayOfWeek(); ss.tileSizePx = getTileSize(); return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setSelectionColor(ss.color); setShowOtherDates(ss.showOtherDates); setRangeDates(ss.minDate, ss.maxDate); setSelectedDate(ss.selectedDate); setFirstDayOfWeek(ss.firstDayOfWeek); setTileSize(ss.tileSizePx); } @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { //super.dispatchSaveInstanceState(container); super.dispatchFreezeSelfOnly(container); } @Override protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { //super.dispatchRestoreInstanceState(container); super.dispatchThawSelfOnly(container); } private void setRangeDates(CalendarDay min, CalendarDay max) { CalendarDay c = currentMonth; adapter.setRangeDates(min, max); currentMonth = c; int position = adapter.getIndexForDay(c); pager.setCurrentItem(position, false); } /** * @return The first day of the week as a {@linkplain Calendar} day constant. */ public int getFirstDayOfWeek() { return adapter.getFirstDayOfWeek(); } /** * 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 void setFirstDayOfWeek(int day) { adapter.setFirstDayOfWeek(day); } /** * 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(); } public static class SavedState extends BaseSavedState { 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]; } }; int color = 0; boolean showOtherDates = false; CalendarDay minDate = null; CalendarDay maxDate = null; CalendarDay selectedDate = null; int firstDayOfWeek = Calendar.SUNDAY; int tileSizePx = 0; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); color = in.readInt(); showOtherDates = in.readInt() == 1; ClassLoader loader = CalendarDay.class.getClassLoader(); minDate = in.readParcelable(loader); maxDate = in.readParcelable(loader); selectedDate = in.readParcelable(loader); firstDayOfWeek = in.readInt(); tileSizePx = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(color); out.writeInt(showOtherDates ? 1 : 0); out.writeParcelable(minDate, 0); out.writeParcelable(maxDate, 0); out.writeParcelable(selectedDate, 0); out.writeInt(firstDayOfWeek); out.writeInt(tileSizePx); } } private static class MonthPagerAdapter extends PagerAdapter { private final CalendarView view; private final LinkedList<MonthView> currentViews; private final ArrayList<CalendarDay> months; private MonthView.Callbacks callbacks = null; private Integer color = null; private Boolean showOtherDates = null; private CalendarDay minDate = null; private CalendarDay maxDate = null; private CalendarDay selectedDate = null; private WeekDayFormatter weekDayFormatter = WeekDayFormatter.DEFAULT; private DayFormatter dayFormatter = DayFormatter.DEFAULT; private List<DayViewDecorator> decorators = new ArrayList<>(); private List<DecoratorResult> decoratorResults = null; private int firstDayOfTheWeek = Calendar.SUNDAY; private MonthPagerAdapter(CalendarView view) { this.view = view; currentViews = new LinkedList<>(); months = new ArrayList<>(); setRangeDates(null, null); } public void setDecorators(List<DayViewDecorator> decorators) { this.decorators = decorators; invalidateDecorators(); } public void invalidateDecorators() { decoratorResults = new ArrayList<>(); for (DayViewDecorator decorator : decorators) { DayViewFacade facade = new DayViewFacade(); decorator.decorate(facade); if (facade.isDecorated()) { decoratorResults.add(new DecoratorResult(decorator, facade)); } } for (MonthView monthView : currentViews) { monthView.setDayViewDecorators(decoratorResults); } } @Override public int getCount() { return months.size(); } public int getIndexForDay(CalendarDay day) { if (day == null) { return getCount() / 2; } if (minDate != null && day.isBefore(minDate)) { return 0; } if (maxDate != null && day.isAfter(maxDate)) { return getCount() - 1; } for (int i = 0; i < months.size(); i++) { CalendarDay month = months.get(i); if (day.getYear() == month.getYear() && day.getMonth() == month.getMonth()) { return i; } } return getCount() / 2; } @Override public int getItemPosition(Object object) { if (!(object instanceof MonthView)) { return POSITION_NONE; } MonthView monthView = (MonthView) object; CalendarDay month = monthView.getMonth(); if (month == null) { return POSITION_NONE; } int index = months.indexOf(month); if (index < 0) { return POSITION_NONE; } return index; } @Override public Object instantiateItem(ViewGroup container, int position) { CalendarDay month = months.get(position); MonthView monthView = new MonthView(container.getContext(), month, firstDayOfTheWeek); monthView.setWeekDayFormatter(weekDayFormatter); monthView.setDayFormatter(dayFormatter); monthView.setCallbacks(callbacks); if (color != null) { monthView.setSelectionColor(color); } if (showOtherDates != null) { monthView.setShowOtherDates(showOtherDates); } monthView.setMinimumDate(minDate); monthView.setMaximumDate(maxDate); monthView.setSelectedDate(selectedDate); container.addView(monthView); currentViews.add(monthView); monthView.setDayViewDecorators(decoratorResults); return monthView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { MonthView monthView = (MonthView) object; currentViews.remove(monthView); container.removeView(monthView); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } public void setCallbacks(MonthView.Callbacks callbacks) { this.callbacks = callbacks; for (MonthView monthView : currentViews) { monthView.setCallbacks(callbacks); } } public void setSelectionColor(int color) { this.color = color; for (MonthView monthView : currentViews) { monthView.setSelectionColor(color); } } public void setWeekDayFormatter(WeekDayFormatter formatter) { this.weekDayFormatter = formatter; for (MonthView monthView : currentViews) { monthView.setWeekDayFormatter(formatter); } } public void setDayFormatter(DayFormatter formatter) { this.dayFormatter = formatter; for (MonthView monthView : currentViews) { monthView.setDayFormatter(formatter); } } public boolean getShowOtherDates() { return showOtherDates; } public void setShowOtherDates(boolean show) { this.showOtherDates = show; for (MonthView monthView : currentViews) { monthView.setShowOtherDates(show); } } public void setRangeDates(CalendarDay min, CalendarDay max) { this.minDate = min; this.maxDate = max; for (MonthView monthView : currentViews) { monthView.setMinimumDate(min); monthView.setMaximumDate(max); } if (min == null) { Calendar worker = CalendarUtils.getInstance(); worker.add(Calendar.YEAR, -200); min = CalendarDay.from(worker); } if (max == null) { Calendar worker = CalendarUtils.getInstance(); worker.add(Calendar.YEAR, 200); max = CalendarDay.from(worker); } months.clear(); Calendar worker = CalendarUtils.getInstance(); min.copyToMonthOnly(worker); CalendarDay workingMonth = CalendarDay.from(worker); while (!max.isBefore(workingMonth)) { months.add(CalendarDay.from(worker)); worker.add(Calendar.MONTH, 1); worker.set(Calendar.DAY_OF_MONTH, 1); workingMonth = CalendarDay.from(worker); } CalendarDay prevDate = selectedDate; notifyDataSetChanged(); setSelectedDate(prevDate); if (prevDate != null) { if (!prevDate.equals(selectedDate)) { callbacks.onDateChanged(selectedDate); } } } private CalendarDay getValidSelectedDate(CalendarDay date) { if (date == null) { return null; } if (minDate != null && minDate.isAfter(date)) { return minDate; } if (maxDate != null && maxDate.isBefore(date)) { return maxDate; } return date; } public CalendarDay getItem(int position) { return months.get(position); } public CalendarDay getSelectedDate() { return selectedDate; } public void setSelectedDate(@Nullable CalendarDay date) { CalendarDay prevDate = selectedDate; this.selectedDate = getValidSelectedDate(date); for (MonthView monthView : currentViews) { monthView.setSelectedDate(selectedDate); } if (date == null && prevDate != null) { callbacks.onDateChanged(null); } } public int getFirstDayOfWeek() { return firstDayOfTheWeek; } public void setFirstDayOfWeek(int day) { firstDayOfTheWeek = day; for (MonthView monthView : currentViews) { monthView.setFirstDayOfWeek(firstDayOfTheWeek); } } } }