package com.philliphsu.bottomsheetpickers.date; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.support.annotation.ColorInt; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.philliphsu.bottomsheetpickers.R; import com.philliphsu.bottomsheetpickers.Utils; import java.text.DateFormatSymbols; import java.util.Calendar; import static android.support.v4.content.ContextCompat.getColor; import static com.philliphsu.bottomsheetpickers.date.PagingDayPickerView.MONTH_NAVIGATION_BAR_SIZE; /** * Grid view of selectable months. */ final class MonthPickerView extends View { private static final String TAG = "MonthPickerView"; private static final int NUM_COLUMNS = 3; private static final int NUM_ROWS = 4; private static final int MONTH_SEPARATOR_WIDTH = 1; private static int MONTH_LABEL_TEXT_SIZE; private static int MONTH_SELECTED_CIRCLE_SIZE; // affects the padding on the sides of this view private int mEdgePadding = 0; private Paint mMonthLabelPaint; private Paint mSelectedCirclePaint; // Quick reference to the width of this view, matches parent private int mWidth; private int mRowHeight; private CalendarDay mSelectedDay; // The year represented in this view. May or may not be the same as the selected year. private int mYear; // The month of the current date private final int mCurrentMonth; // The year of the current date private final int mCurrentYear; private final String[] mShortMonthLabels; @Nullable // Created only when a DatePickerController is set. private DateRangeHelper mDateRangeHelper; @Nullable private OnMonthClickListener mOnMonthClickListener; private int mNormalTextColor; private int mCurrentMonthTextColor; private int mDisabledMonthTextColor; private int mSelectedMonthTextColor; public MonthPickerView(Context context) { this(context, null); } public MonthPickerView(Context context, AttributeSet attrs) { super(context, attrs); Resources res = context.getResources(); mShortMonthLabels = new DateFormatSymbols().getShortMonths(); mNormalTextColor = getColor(context, R.color.bsp_text_color_primary_light); // Same as background color mSelectedMonthTextColor = getColor(context, R.color.bsp_date_picker_view_animator); mCurrentMonthTextColor = Utils.getThemeAccentColor(context); mDisabledMonthTextColor = getColor(context, R.color.bsp_text_color_disabled_light); Calendar now = Calendar.getInstance(); mCurrentMonth = now.get(Calendar.MONTH); mCurrentYear = now.get(Calendar.YEAR); MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.bsp_month_picker_month_label_size); MONTH_SELECTED_CIRCLE_SIZE = res.getDimensionPixelSize(R.dimen.bsp_month_select_circle_radius); mRowHeight = (res.getDimensionPixelOffset(R.dimen.bsp_date_picker_view_animator_height) - MONTH_NAVIGATION_BAR_SIZE) / NUM_ROWS; mEdgePadding = res.getDimensionPixelSize(R.dimen.bsp_month_view_edge_padding); // TODO: Set up accessibility components. // Sets up any standard paints that will be used initView(); } /** * Sets all the parameters for displaying the months. * @param selectedDay the selected date *@param year the year to be represented in this view */ void setDisplayParams(CalendarDay selectedDay, int year) { mSelectedDay = selectedDay; mYear = year; // Invalidate cached accessibility information. // mTouchHelper.invalidateRoot(); } /** * If the newly selected month in {@link #mYear} does not contain the currently selected day number, * change the selected day number to the last day of the selected month or year. * e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 * e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 */ private void adjustDayInMonthIfNeeded(int month) { int daysInMonth = Utils.getDaysInMonth(month, mYear); if (mSelectedDay.day > daysInMonth) { mSelectedDay.day = daysInMonth; } } private int constrainDayInMonth(int month, int defaultDay) { int daysInMonth = Utils.getDaysInMonth(month, mYear); return Math.min(defaultDay, daysInMonth); } public void setDatePickerController(DatePickerController controller) { mDateRangeHelper = new DateRangeHelper(controller); } public void setOnMonthClickListener(@Nullable OnMonthClickListener onMonthClickListener) { mOnMonthClickListener = onMonthClickListener; } void setTheme(Context context, boolean themeDark) { if (themeDark) { mNormalTextColor = getColor(context, R.color.bsp_text_color_primary_dark); mSelectedMonthTextColor = getColor(context, R.color.bsp_dark_gray); mDisabledMonthTextColor = getColor(context, R.color.bsp_text_color_disabled_dark); initView(); } } /** * Set the text color that will highlight the current (i.e. today's) month. * <p> * Make sure to call {@link #initView()} after calling this so that our Paints can be updated. * </p> */ void setCurrentMonthTextColor(@ColorInt int color) { mCurrentMonthTextColor = color; } /** * Set the color of the selection circle. This is equivalent to {@link #setCurrentMonthTextColor(int)}. * <p> * Make sure to call {@link #initView()} after calling this so that our Paints can be updated. * </p> */ void setSelectedCirclePaintColor(@ColorInt int color) { mSelectedCirclePaint.setColor(color); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_UP: final int month = getMonthFromLocation(event.getX(), event.getY()); if (month >= 0) { onMonthClick(month); } break; } return true; } /** * Sets up the text and style properties for painting. Override this if you * want to use a different paint. */ protected void initView() { mMonthLabelPaint = new Paint(); mMonthLabelPaint.setAntiAlias(true); mMonthLabelPaint.setTextSize(MONTH_LABEL_TEXT_SIZE); mMonthLabelPaint.setStyle(Style.FILL); mMonthLabelPaint.setTextAlign(Align.CENTER); mMonthLabelPaint.setFakeBoldText(false); mSelectedCirclePaint = new Paint(); mSelectedCirclePaint.setFakeBoldText(true); mSelectedCirclePaint.setAntiAlias(true); mSelectedCirclePaint.setColor(mCurrentMonthTextColor); mSelectedCirclePaint.setTextAlign(Align.CENTER); mSelectedCirclePaint.setStyle(Style.FILL); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * NUM_ROWS + MONTH_NAVIGATION_BAR_SIZE); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mWidth = w; // Invalidate cached accessibility information. // mTouchHelper.invalidateRoot(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawMonthLabels(canvas); } private void drawMonthLabels(Canvas canvas) { int y = (((mRowHeight + MONTH_LABEL_TEXT_SIZE) / 2) - MONTH_SEPARATOR_WIDTH); final float monthWidthHalf = (mWidth - mEdgePadding * 2) / (NUM_COLUMNS * 2.0f); int col = 0; for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; month++) { final int x = (int)((2 * col + 1) * monthWidthHalf + mEdgePadding); // This is what the current value of the selected day would resolve to in // this month-year combination. // Each of the months will be drawn enabled/disabled based on whether // the full date constrained with this day is within range. int constrainedDay = constrainDayInMonth(month, mSelectedDay.day); drawMonthLabel(canvas, mYear, month, constrainedDay, x, y); col++; if (col == NUM_COLUMNS) { col = 0; y += mRowHeight; } } } private void drawMonthLabel(Canvas canvas, int year, int month, int day, int x, int y) { final int selectedYear = mSelectedDay.year; final int selectedMonth = mSelectedDay.month; boolean drawCircle = selectedYear == year && selectedMonth == month; if (drawCircle) { canvas.drawCircle(x , y - (MONTH_LABEL_TEXT_SIZE / 3), MONTH_SELECTED_CIRCLE_SIZE, mSelectedCirclePaint); } // If we have a mindate or maxdate, gray out the month if it's outside the range. // If the date range helper has not been created, just let the runtime throw an NPE. if (mDateRangeHelper != null && mDateRangeHelper.isOutOfRange(year, month, day)) { mMonthLabelPaint.setFakeBoldText(false); mMonthLabelPaint.setColor(mDisabledMonthTextColor); } else { boolean currentMonthYear = mCurrentYear == year && mCurrentMonth == month; mMonthLabelPaint.setFakeBoldText(currentMonthYear || drawCircle); mMonthLabelPaint.setColor(drawCircle ? mSelectedMonthTextColor : (currentMonthYear ? mCurrentMonthTextColor : mNormalTextColor)); } canvas.drawText(mShortMonthLabels[month], x, y, mMonthLabelPaint); } /** * Calculates the month that the given x position is in. * Returns the month or -1 if the position wasn't in a month. * * @param x The x position of the touch event * @return The month number, or -1 if the position wasn't in a month */ public int getMonthFromLocation(float x, float y) { final int month = getInternalMonthFromLocation(x, y); if (month < Calendar.JANUARY || month > Calendar.DECEMBER) { return -1; } return month; } /** * Calculates the month that the given x position is in, accounting for week * number. * * @param x The x position of the touch event * @return The month number */ protected int getInternalMonthFromLocation(float x, float y) { int monthStart = mEdgePadding; if (x < monthStart || x > mWidth - mEdgePadding) { // Out of edge boundaries return -1; } int row = (int) (y / mRowHeight); // Selection is (x - start) / (pixels/month) == (x -s) * month / pixels int column = (int) ((x - monthStart) * NUM_COLUMNS / (mWidth - mEdgePadding * 2)); int month = column; month += row * NUM_COLUMNS; return month; } /** * Called when the user clicks on a month. Handles callbacks to the * {@link MonthPickerView.OnMonthClickListener} if one is set. * <p/> * If the month is out of the range set by minDate and/or maxDate, this is a no-op. * * @param month The month that was clicked */ private void onMonthClick(int month) { adjustDayInMonthIfNeeded(month); // The day may not exist in the new month // If the min / max date are set, only process the click if it's a valid selection. if (mDateRangeHelper != null && mDateRangeHelper.isOutOfRange(mYear, month, mSelectedDay.day)) { return; } if (mOnMonthClickListener != null) { mOnMonthClickListener.onMonthClick(this, month, mYear); } // This is a no-op if accessibility is turned off. // mTouchHelper.sendEventForVirtualView(month, AccessibilityEvent.TYPE_VIEW_CLICKED); } interface OnMonthClickListener { void onMonthClick(MonthPickerView view, int month, int year); } }