/*********************************************************************************** * The MIT License (MIT) * Copyright (c) 2014 Robin Chutaux * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ***********************************************************************************/ package com.marshalchen.common.uimodule.calendarlistview; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Typeface; import android.text.format.DateUtils; import android.text.format.Time; import android.view.MotionEvent; import android.view.View; import com.marshalchen.common.uimodule.R; import java.security.InvalidParameterException; import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.HashMap; import java.util.Locale; class SimpleMonthView extends View { public static final String VIEW_PARAMS_HEIGHT = "height"; public static final String VIEW_PARAMS_MONTH = "month"; public static final String VIEW_PARAMS_YEAR = "year"; public static final String VIEW_PARAMS_SELECTED_BEGIN_DAY = "selected_begin_day"; public static final String VIEW_PARAMS_SELECTED_LAST_DAY = "selected_last_day"; public static final String VIEW_PARAMS_SELECTED_BEGIN_MONTH = "selected_begin_month"; public static final String VIEW_PARAMS_SELECTED_LAST_MONTH = "selected_last_month"; public static final String VIEW_PARAMS_SELECTED_BEGIN_YEAR = "selected_begin_year"; public static final String VIEW_PARAMS_SELECTED_LAST_YEAR = "selected_last_year"; public static final String VIEW_PARAMS_WEEK_START = "week_start"; private static final int SELECTED_CIRCLE_ALPHA = 128; protected static int DEFAULT_HEIGHT = 32; protected static final int DEFAULT_NUM_ROWS = 6; protected static int DAY_SELECTED_CIRCLE_SIZE; protected static int DAY_SEPARATOR_WIDTH = 1; protected static int MINI_DAY_NUMBER_TEXT_SIZE; protected static int MIN_HEIGHT = 10; protected static int MONTH_DAY_LABEL_TEXT_SIZE; protected static int MONTH_HEADER_SIZE; protected static int MONTH_LABEL_TEXT_SIZE; protected int mPadding = 0; private String mDayOfWeekTypeface; private String mMonthTitleTypeface; protected Paint mMonthDayLabelPaint; protected Paint mMonthNumPaint; protected Paint mMonthTitleBGPaint; protected Paint mMonthTitlePaint; protected Paint mSelectedCirclePaint; protected int mCurrentDayTextColor; protected int mMonthTextColor; protected int mDayTextColor; protected int mDayNumColor; protected int mMonthTitleBGColor; protected int mPreviousDayColor; protected int mSelectedDaysColor; private final StringBuilder mStringBuilder; protected boolean mHasToday = false; protected boolean mIsPrev = false; protected int mSelectedBeginDay = -1; protected int mSelectedLastDay = -1; protected int mSelectedBeginMonth = -1; protected int mSelectedLastMonth = -1; protected int mSelectedBeginYear = -1; protected int mSelectedLastYear = -1; protected int mToday = -1; protected int mWeekStart = 1; protected int mNumDays = 7; protected int mNumCells = mNumDays; private int mDayOfWeekStart = 0; protected int mMonth; protected Boolean mDrawRect; protected int mRowHeight = DEFAULT_HEIGHT; protected int mWidth; protected int mYear; final Time today; private final Calendar mCalendar; private final Calendar mDayLabelCalendar; private final Boolean isPrevDayEnabled; private int mNumRows = DEFAULT_NUM_ROWS; private DateFormatSymbols mDateFormatSymbols = new DateFormatSymbols(); private OnDayClickListener mOnDayClickListener; public SimpleMonthView(Context context, TypedArray typedArray) { super(context); Resources resources = context.getResources(); mDayLabelCalendar = Calendar.getInstance(); mCalendar = Calendar.getInstance(); today = new Time(Time.getCurrentTimezone()); today.setToNow(); mDayOfWeekTypeface = "sans_serif"; mMonthTitleTypeface = "sans_serif"; mCurrentDayTextColor = typedArray.getColor(R.styleable.DayPickerView_colorCurrentDay, resources.getColor(R.color.normal_day)); mMonthTextColor = typedArray.getColor(R.styleable.DayPickerView_colorMonthName, resources.getColor(R.color.normal_day)); mDayTextColor = typedArray.getColor(R.styleable.DayPickerView_colorDayName, resources.getColor(R.color.normal_day)); mDayNumColor = typedArray.getColor(R.styleable.DayPickerView_colorNormalDay, resources.getColor(R.color.normal_day)); mPreviousDayColor = typedArray.getColor(R.styleable.DayPickerView_colorPreviousDay, resources.getColor(R.color.normal_day)); mSelectedDaysColor = typedArray.getColor(R.styleable.DayPickerView_colorSelectedDayBackground, resources.getColor(R.color.selected_day_background)); mMonthTitleBGColor = typedArray.getColor(R.styleable.DayPickerView_colorSelectedDayText, resources.getColor(R.color.selected_day_text)); mDrawRect = typedArray.getBoolean(R.styleable.DayPickerView_drawRoundRect, false); mStringBuilder = new StringBuilder(50); MINI_DAY_NUMBER_TEXT_SIZE = typedArray.getDimensionPixelSize(R.styleable.DayPickerView_textSizeDay, resources.getDimensionPixelSize(R.dimen.text_size_day)); MONTH_LABEL_TEXT_SIZE = typedArray.getDimensionPixelSize(R.styleable.DayPickerView_textSizeMonth, resources.getDimensionPixelSize(R.dimen.text_size_month)); MONTH_DAY_LABEL_TEXT_SIZE = typedArray.getDimensionPixelSize(R.styleable.DayPickerView_textSizeDayName, resources.getDimensionPixelSize(R.dimen.text_size_day_name)); MONTH_HEADER_SIZE = typedArray.getDimensionPixelOffset(R.styleable.DayPickerView_headerMonthHeight, resources.getDimensionPixelOffset(R.dimen.header_month_height)); DAY_SELECTED_CIRCLE_SIZE = typedArray.getDimensionPixelSize(R.styleable.DayPickerView_selectedDayRadius, resources.getDimensionPixelOffset(R.dimen.selected_day_radius)); mRowHeight = ((typedArray.getDimensionPixelSize(R.styleable.DayPickerView_calendarHeight, resources.getDimensionPixelOffset(R.dimen.calendar_height)) - MONTH_HEADER_SIZE) / 6); isPrevDayEnabled = typedArray.getBoolean(R.styleable.DayPickerView_enablePreviousDay, true); initView(); } private int calculateNumRows() { int offset = findDayOffset(); int dividend = (offset + mNumCells) / mNumDays; int remainder = (offset + mNumCells) % mNumDays; return (dividend + (remainder > 0 ? 1 : 0)); } private void drawMonthDayLabels(Canvas canvas) { int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2); int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); for (int i = 0; i < mNumDays; i++) { int calendarDay = (i + mWeekStart) % mNumDays; int x = (2 * i + 1) * dayWidthHalf + mPadding; mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); canvas.drawText(mDateFormatSymbols.getShortWeekdays()[mDayLabelCalendar.get(Calendar.DAY_OF_WEEK)].toUpperCase(Locale.getDefault()), x, y, mMonthDayLabelPaint); } } private void drawMonthTitle(Canvas canvas) { int x = (mWidth + 2 * mPadding) / 2; int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3); StringBuilder stringBuilder = new StringBuilder(getMonthAndYearString().toLowerCase()); stringBuilder.setCharAt(0, Character.toUpperCase(stringBuilder.charAt(0))); canvas.drawText(stringBuilder.toString(), x, y, mMonthTitlePaint); } private int findDayOffset() { return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) - mWeekStart; } private String getMonthAndYearString() { int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NO_MONTH_DAY; mStringBuilder.setLength(0); long millis = mCalendar.getTimeInMillis(); return DateUtils.formatDateRange(getContext(), millis, millis, flags); } private void onDayClick(SimpleMonthAdapter.CalendarDay calendarDay) { if (mOnDayClickListener != null && (isPrevDayEnabled || !((calendarDay.month == today.month) && (calendarDay.year == today.year) && calendarDay.day < today.monthDay))) { mOnDayClickListener.onDayClick(this, calendarDay); } } private boolean sameDay(int monthDay, Time time) { return (mYear == time.year) && (mMonth == time.month) && (monthDay == time.monthDay); } private boolean prevDay(int monthDay, Time time) { return ((mYear < time.year)) || (mYear == time.year && mMonth < time.month) || ( mMonth == time.month && monthDay < time.monthDay); } protected void drawMonthNums(Canvas canvas) { int y = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH + MONTH_HEADER_SIZE; int paddingDay = (mWidth - 2 * mPadding) / (2 * mNumDays); int dayOffset = findDayOffset(); int day = 1; while (day <= mNumCells) { int x = paddingDay * (1 + dayOffset * 2) + mPadding; if ((mMonth == mSelectedBeginMonth && mSelectedBeginDay == day && mSelectedBeginYear == mYear) || (mMonth == mSelectedLastMonth && mSelectedLastDay == day && mSelectedLastYear == mYear)) { if (mDrawRect) { RectF rectF = new RectF(x - DAY_SELECTED_CIRCLE_SIZE, (y - MINI_DAY_NUMBER_TEXT_SIZE / 3) - DAY_SELECTED_CIRCLE_SIZE, x + DAY_SELECTED_CIRCLE_SIZE, (y - MINI_DAY_NUMBER_TEXT_SIZE / 3) + DAY_SELECTED_CIRCLE_SIZE); canvas.drawRoundRect(rectF, 10.0f, 10.0f,mSelectedCirclePaint); } else canvas.drawCircle(x, y - MINI_DAY_NUMBER_TEXT_SIZE / 3, DAY_SELECTED_CIRCLE_SIZE, mSelectedCirclePaint); } if (mHasToday && (mToday == day)) { mMonthNumPaint.setColor(mCurrentDayTextColor); mMonthNumPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); } else { mMonthNumPaint.setColor(mDayNumColor); mMonthNumPaint.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); } if ((mMonth == mSelectedBeginMonth && mSelectedBeginDay == day && mSelectedBeginYear == mYear) || (mMonth == mSelectedLastMonth && mSelectedLastDay == day && mSelectedLastYear == mYear)) mMonthNumPaint.setColor(mMonthTitleBGColor); if ((mSelectedBeginDay != -1 && mSelectedLastDay != -1 && mSelectedBeginYear == mSelectedLastYear && mSelectedBeginMonth == mSelectedLastMonth && mSelectedBeginDay == mSelectedLastDay && day == mSelectedBeginDay && mMonth == mSelectedBeginMonth && mYear == mSelectedBeginYear)) mMonthNumPaint.setColor(mSelectedDaysColor); if ((mSelectedBeginDay != -1 && mSelectedLastDay != -1 && mSelectedBeginYear == mSelectedLastYear && mSelectedBeginYear == mYear) && (((mMonth == mSelectedBeginMonth && mSelectedLastMonth == mSelectedBeginMonth) && ((mSelectedBeginDay < mSelectedLastDay && day > mSelectedBeginDay && day < mSelectedLastDay) || (mSelectedBeginDay > mSelectedLastDay && day < mSelectedBeginDay && day > mSelectedLastDay))) || ((mSelectedBeginMonth < mSelectedLastMonth && mMonth == mSelectedBeginMonth && day > mSelectedBeginDay) || (mSelectedBeginMonth < mSelectedLastMonth && mMonth == mSelectedLastMonth && day < mSelectedLastDay)) || ((mSelectedBeginMonth > mSelectedLastMonth && mMonth == mSelectedBeginMonth && day < mSelectedBeginDay) || (mSelectedBeginMonth > mSelectedLastMonth && mMonth == mSelectedLastMonth && day > mSelectedLastDay)))) { mMonthNumPaint.setColor(mSelectedDaysColor); } if ((mSelectedBeginDay != -1 && mSelectedLastDay != -1 && mSelectedBeginYear != mSelectedLastYear && ((mSelectedBeginYear == mYear && mMonth == mSelectedBeginMonth) || (mSelectedLastYear == mYear && mMonth == mSelectedLastMonth)) && (((mSelectedBeginMonth < mSelectedLastMonth && mMonth == mSelectedBeginMonth && day < mSelectedBeginDay) || (mSelectedBeginMonth < mSelectedLastMonth && mMonth == mSelectedLastMonth && day > mSelectedLastDay)) || ((mSelectedBeginMonth > mSelectedLastMonth && mMonth == mSelectedBeginMonth && day > mSelectedBeginDay) || (mSelectedBeginMonth > mSelectedLastMonth && mMonth == mSelectedLastMonth && day < mSelectedLastDay))))) { mMonthNumPaint.setColor(mSelectedDaysColor); } if ((mSelectedBeginDay != -1 && mSelectedLastDay != -1 && mSelectedBeginYear == mSelectedLastYear && mYear == mSelectedBeginYear) && ((mMonth > mSelectedBeginMonth && mMonth < mSelectedLastMonth && mSelectedBeginMonth < mSelectedLastMonth) || (mMonth < mSelectedBeginMonth && mMonth > mSelectedLastMonth && mSelectedBeginMonth > mSelectedLastMonth))) { mMonthNumPaint.setColor(mSelectedDaysColor); } if ((mSelectedBeginDay != -1 && mSelectedLastDay != -1 && mSelectedBeginYear != mSelectedLastYear) && ((mSelectedBeginYear < mSelectedLastYear && ((mMonth > mSelectedBeginMonth && mYear == mSelectedBeginYear) || (mMonth < mSelectedLastMonth && mYear == mSelectedLastYear))) || (mSelectedBeginYear > mSelectedLastYear && ((mMonth < mSelectedBeginMonth && mYear == mSelectedBeginYear) || (mMonth > mSelectedLastMonth && mYear == mSelectedLastYear))))) { mMonthNumPaint.setColor(mSelectedDaysColor); } if (!isPrevDayEnabled && prevDay(day, today) && today.month == mMonth && today.year == mYear) { mMonthNumPaint.setColor(mPreviousDayColor); mMonthNumPaint.setTypeface(Typeface.defaultFromStyle(Typeface.ITALIC)); } canvas.drawText(String.format("%d", day), x, y, mMonthNumPaint); dayOffset++; if (dayOffset == mNumDays) { dayOffset = 0; y += mRowHeight; } day++; } } public SimpleMonthAdapter.CalendarDay getDayFromLocation(float x, float y) { int padding = mPadding; if ((x < padding) || (x > mWidth - mPadding)) { return null; } int yDay = (int) (y - MONTH_HEADER_SIZE) / mRowHeight; int day = 1 + ((int) ((x - padding) * mNumDays / (mWidth - padding - mPadding)) - findDayOffset()) + yDay * mNumDays; if (mMonth > 11 || mMonth < 0 || CalendarUtils.getDaysInMonth(mMonth, mYear) < day || day < 1) return null; return new SimpleMonthAdapter.CalendarDay(mYear, mMonth, day); } protected void initView() { mMonthTitlePaint = new Paint(); mMonthTitlePaint.setFakeBoldText(true); mMonthTitlePaint.setAntiAlias(true); mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); mMonthTitlePaint.setColor(mMonthTextColor); mMonthTitlePaint.setTextAlign(Align.CENTER); mMonthTitlePaint.setStyle(Style.FILL); mMonthTitleBGPaint = new Paint(); mMonthTitleBGPaint.setFakeBoldText(true); mMonthTitleBGPaint.setAntiAlias(true); mMonthTitleBGPaint.setColor(mMonthTitleBGColor); mMonthTitleBGPaint.setTextAlign(Align.CENTER); mMonthTitleBGPaint.setStyle(Style.FILL); mSelectedCirclePaint = new Paint(); mSelectedCirclePaint.setFakeBoldText(true); mSelectedCirclePaint.setAntiAlias(true); mSelectedCirclePaint.setColor(mSelectedDaysColor); mSelectedCirclePaint.setTextAlign(Align.CENTER); mSelectedCirclePaint.setStyle(Style.FILL); mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); mMonthDayLabelPaint = new Paint(); mMonthDayLabelPaint.setAntiAlias(true); mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); mMonthDayLabelPaint.setColor(mDayTextColor); mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); mMonthDayLabelPaint.setStyle(Style.FILL); mMonthDayLabelPaint.setTextAlign(Align.CENTER); mMonthDayLabelPaint.setFakeBoldText(true); mMonthNumPaint = new Paint(); mMonthNumPaint.setAntiAlias(true); mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); mMonthNumPaint.setStyle(Style.FILL); mMonthNumPaint.setTextAlign(Align.CENTER); mMonthNumPaint.setFakeBoldText(false); } protected void onDraw(Canvas canvas) { drawMonthTitle(canvas); drawMonthDayLabels(canvas); drawMonthNums(canvas); } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + MONTH_HEADER_SIZE); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { mWidth = w; } public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { SimpleMonthAdapter.CalendarDay calendarDay = getDayFromLocation(event.getX(), event.getY()); if (calendarDay != null) { onDayClick(calendarDay); } } return true; } public void reuse() { mNumRows = DEFAULT_NUM_ROWS; requestLayout(); } public void setMonthParams(HashMap<String, Integer> params) { if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { throw new InvalidParameterException("You must specify month and year for this view"); } setTag(params); if (params.containsKey(VIEW_PARAMS_HEIGHT)) { mRowHeight = params.get(VIEW_PARAMS_HEIGHT); if (mRowHeight < MIN_HEIGHT) { mRowHeight = MIN_HEIGHT; } } if (params.containsKey(VIEW_PARAMS_SELECTED_BEGIN_DAY)) { mSelectedBeginDay = params.get(VIEW_PARAMS_SELECTED_BEGIN_DAY); } if (params.containsKey(VIEW_PARAMS_SELECTED_LAST_DAY)) { mSelectedLastDay = params.get(VIEW_PARAMS_SELECTED_LAST_DAY); } if (params.containsKey(VIEW_PARAMS_SELECTED_BEGIN_MONTH)) { mSelectedBeginMonth = params.get(VIEW_PARAMS_SELECTED_BEGIN_MONTH); } if (params.containsKey(VIEW_PARAMS_SELECTED_LAST_MONTH)) { mSelectedLastMonth = params.get(VIEW_PARAMS_SELECTED_LAST_MONTH); } if (params.containsKey(VIEW_PARAMS_SELECTED_BEGIN_YEAR)) { mSelectedBeginYear = params.get(VIEW_PARAMS_SELECTED_BEGIN_YEAR); } if (params.containsKey(VIEW_PARAMS_SELECTED_LAST_YEAR)) { mSelectedLastYear = params.get(VIEW_PARAMS_SELECTED_LAST_YEAR); } mMonth = params.get(VIEW_PARAMS_MONTH); mYear = params.get(VIEW_PARAMS_YEAR); mHasToday = false; mToday = -1; mCalendar.set(Calendar.MONTH, mMonth); mCalendar.set(Calendar.YEAR, mYear); mCalendar.set(Calendar.DAY_OF_MONTH, 1); mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); if (params.containsKey(VIEW_PARAMS_WEEK_START)) { mWeekStart = params.get(VIEW_PARAMS_WEEK_START); } else { mWeekStart = mCalendar.getFirstDayOfWeek(); } mNumCells = CalendarUtils.getDaysInMonth(mMonth, mYear); for (int i = 0; i < mNumCells; i++) { final int day = i + 1; if (sameDay(day, today)) { mHasToday = true; mToday = day; } mIsPrev = prevDay(day, today); } mNumRows = calculateNumRows(); } public void setOnDayClickListener(OnDayClickListener onDayClickListener) { mOnDayClickListener = onDayClickListener; } public static abstract interface OnDayClickListener { public abstract void onDayClick(SimpleMonthView simpleMonthView, SimpleMonthAdapter.CalendarDay calendarDay); } }