SimpleMonthView.java revision 3e9818e0267619fecebd55095ab26c53eff92e93
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.datetimepicker.date;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Paint.Align;
24import android.graphics.Paint.Style;
25import android.graphics.Rect;
26import android.text.format.Time;
27import android.view.View;
28
29import com.android.datetimepicker.R;
30import com.android.datetimepicker.Utils;
31import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay;
32
33import java.security.InvalidParameterException;
34import java.util.Calendar;
35import java.util.HashMap;
36import java.util.Locale;
37
38/**
39 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
40 * within the specified month.
41 */
42public class SimpleMonthView extends View {
43
44    /**
45     * These params can be passed into the view to control how it appears.
46     * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
47     * values are unlikely to fit most layouts correctly.
48     */
49    /**
50     * This sets the height of this week in pixels
51     */
52    public static final String VIEW_PARAMS_HEIGHT = "height";
53    /**
54     * This specifies the position (or weeks since the epoch) of this week,
55     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
56     */
57    public static final String VIEW_PARAMS_MONTH = "month";
58    /**
59     * This specifies the position (or weeks since the epoch) of this week,
60     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
61     */
62    public static final String VIEW_PARAMS_YEAR = "year";
63    /**
64     * This sets one of the days in this view as selected {@link Time#SUNDAY}
65     * through {@link Time#SATURDAY}.
66     */
67    public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
68    /**
69     * Which day the week should start on. {@link Time#SUNDAY} through
70     * {@link Time#SATURDAY}.
71     */
72    public static final String VIEW_PARAMS_WEEK_START = "week_start";
73    /**
74     * How many days to display at a time. Days will be displayed starting with
75     * {@link #mWeekStart}.
76     */
77    public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
78    /**
79     * Which month is currently in focus, as defined by {@link Time#month}
80     * [0-11].
81     */
82    public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
83    /**
84     * If this month should display week numbers. false if 0, true otherwise.
85     */
86    public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
87
88    protected static int DEFAULT_HEIGHT = 32;
89    protected static int MIN_HEIGHT = 10;
90    protected static final int DEFAULT_SELECTED_DAY = -1;
91    protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
92    protected static final int DEFAULT_NUM_DAYS = 7;
93    protected static final int DEFAULT_SHOW_WK_NUM = 0;
94    protected static final int DEFAULT_FOCUS_MONTH = -1;
95    protected static final int DEFAULT_NUM_ROWS = 6;
96    protected static final int MAX_NUM_ROWS = 6;
97
98    private static final int SELECTED_CIRCLE_ALPHA = 60;
99
100    protected static int DAY_SEPARATOR_WIDTH = 1;
101    protected static int MINI_DAY_NUMBER_TEXT_SIZE;
102    protected static int MONTH_LABEL_TEXT_SIZE;
103    protected static int MONTH_DAY_LABEL_TEXT_SIZE;
104    protected static int MONTH_HEADER_SIZE;
105    protected static int DAY_SELECTED_CIRCLE_SIZE;
106
107    // used for scaling to the device density
108    protected static float mScale = 0;
109
110    // affects the padding on the sides of this view
111    protected int mPadding = 0;
112
113    protected Rect r = new Rect();
114    protected Paint mMonthNumPaint;
115    protected Paint mMonthTitlePaint;
116    protected Paint mSelectedCirclePaint;
117    protected Paint mMonthDayLabelPaint;
118
119    // The Julian day of the first day displayed by this item
120    protected int mFirstJulianDay = -1;
121    // The month of the first day in this week
122    protected int mFirstMonth = -1;
123    // The month of the last day in this week
124    protected int mLastMonth = -1;
125
126    protected int mMonth;
127
128    protected int mYear;
129    // Quick reference to the width of this view, matches parent
130    protected int mWidth;
131    // The height this view should draw at in pixels, set by height param
132    protected int mRowHeight = DEFAULT_HEIGHT;
133    // If this view contains the today
134    protected boolean mHasToday = false;
135    // Which day is selected [0-6] or -1 if no day is selected
136    protected int mSelectedDay = -1;
137    // Which day is today [0-6] or -1 if no day is today
138    protected int mToday = DEFAULT_SELECTED_DAY;
139    // Which day of the week to start on [0-6]
140    protected int mWeekStart = DEFAULT_WEEK_START;
141    // How many days to display
142    protected int mNumDays = DEFAULT_NUM_DAYS;
143    // The number of days + a spot for week number if it is displayed
144    protected int mNumCells = mNumDays;
145    // The left edge of the selected day
146    protected int mSelectedLeft = -1;
147    // The right edge of the selected day
148    protected int mSelectedRight = -1;
149
150    private final Calendar mCalendar;
151    private final Calendar mDayLabelCalendar;
152
153    private int mNumRows = DEFAULT_NUM_ROWS;
154
155    protected int mBGColor;
156    protected int mFocusMonthColor;
157    protected int mOtherMonthColor;
158    protected int mDaySeparatorColor;
159    protected int mTodayOutlineColor;
160    protected int mWeekNumColor;
161
162    protected int mDayTextColor;
163    protected int mMonthTitleTextColor;
164
165    public SimpleMonthView(Context context) {
166        super(context);
167
168        Resources res = context.getResources();
169
170        mDayLabelCalendar = Calendar.getInstance();
171        mCalendar = Calendar.getInstance();
172        mBGColor = res.getColor(R.color.month_bgcolor);
173        mFocusMonthColor = res.getColor(R.color.month_mini_day_number);
174        mOtherMonthColor = res.getColor(R.color.month_other_month_day_number);
175        mDaySeparatorColor = res.getColor(R.color.month_grid_lines);
176        mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color);
177        mDayTextColor = res.getColor(R.color.calendar_day_number);
178        mMonthTitleTextColor = res.getColor(R.color.selected_text);
179        mWeekNumColor = res.getColor(R.color.month_week_num_color);
180
181        MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
182        MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
183        MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
184        MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
185        DAY_SELECTED_CIRCLE_SIZE = res
186                .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
187
188        mRowHeight = (res.getDimensionPixelOffset(R.dimen.pager_height) - MONTH_HEADER_SIZE)
189                / MAX_NUM_ROWS;
190        // Sets up any standard paints that will be used
191        initView();
192    }
193
194    /**
195     * Sets up the text and style properties for painting. Override this if you
196     * want to use a different paint.
197     */
198    protected void initView() {
199        mMonthTitlePaint = new Paint();
200        mMonthTitlePaint.setFakeBoldText(true);
201        mMonthTitlePaint.setAntiAlias(true);
202        mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
203        mMonthTitlePaint.setColor(mMonthTitleTextColor);
204        mMonthTitlePaint.setTextAlign(Align.CENTER);
205        mMonthTitlePaint.setStyle(Style.FILL);
206        mMonthTitlePaint.setFakeBoldText(true);
207
208        mSelectedCirclePaint = new Paint();
209        mSelectedCirclePaint.setFakeBoldText(true);
210        mSelectedCirclePaint.setAntiAlias(true);
211        mSelectedCirclePaint.setColor(mMonthTitleTextColor);
212        mSelectedCirclePaint.setTextAlign(Align.CENTER);
213        mSelectedCirclePaint.setStyle(Style.FILL);
214        mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
215
216        mMonthDayLabelPaint = new Paint();
217        mMonthDayLabelPaint.setAntiAlias(true);
218        mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
219        mMonthDayLabelPaint.setColor(mDayTextColor);
220        mMonthDayLabelPaint.setStyle(Style.FILL);
221        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
222        mMonthDayLabelPaint.setFakeBoldText(true);
223
224        mMonthNumPaint = new Paint();
225        mMonthNumPaint.setAntiAlias(true);
226        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
227        mMonthNumPaint.setStyle(Style.FILL);
228        mMonthNumPaint.setTextAlign(Align.CENTER);
229        mMonthNumPaint.setFakeBoldText(false);
230    }
231
232    @Override
233    protected void onDraw(Canvas canvas) {
234        drawMonthTitle(canvas);
235        drawMonthDayLabels(canvas);
236        drawMonthNums(canvas);
237    }
238
239    private int mDayOfWeekStart = 0;
240
241    /**
242     * Sets all the parameters for displaying this week. The only required
243     * parameter is the week number. Other parameters have a default value and
244     * will only update if a new value is included, except for focus month,
245     * which will always default to no focus month if no value is passed in. See
246     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
247     *
248     * @param params A map of the new parameters, see
249     *            {@link #VIEW_PARAMS_HEIGHT}
250     * @param tz The time zone this view should reference times in
251     */
252    public void setMonthParams(HashMap<String, Integer> params) {
253        if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
254            throw new InvalidParameterException("You must specify the month and year for this view");
255        }
256        setTag(params);
257        // We keep the current value for any params not present
258        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
259            mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
260            if (mRowHeight < MIN_HEIGHT) {
261                mRowHeight = MIN_HEIGHT;
262            }
263        }
264        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
265            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
266        }
267
268        // Allocate space for caching the day numbers and focus values
269        mMonth = params.get(VIEW_PARAMS_MONTH);
270        mYear = params.get(VIEW_PARAMS_YEAR);
271
272        // Figure out what day today is
273        final Time today = new Time(Time.getCurrentTimezone());
274        today.setToNow();
275        mHasToday = false;
276        mToday = -1;
277
278        mCalendar.set(Calendar.MONTH, mMonth);
279        mCalendar.set(Calendar.YEAR, mYear);
280        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
281        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
282
283        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
284            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
285        } else {
286            mWeekStart = mCalendar.getFirstDayOfWeek();
287        }
288
289        mNumCells = Utils.getDaysInMonth(mMonth, mYear);
290        for (int i = 0; i < mNumCells; i++) {
291            final int day = i + 1;
292            if (sameDay(day, today)) {
293                mHasToday = true;
294                mToday = day;
295            }
296        }
297        mNumRows = calculateNumRows();
298    }
299
300    public void reuse() {
301        mNumRows = DEFAULT_NUM_ROWS;
302        requestLayout();
303    }
304
305    private int calculateNumRows() {
306        int offset = findDayOffset();
307        int dividend = (offset + mNumCells) / mNumDays;
308        int remainder = (offset + mNumCells) % mNumDays;
309        return (dividend + (remainder > 0 ? 1 : 0));
310    }
311
312    private boolean sameDay(int day, Time today) {
313        return mYear == today.year &&
314                mMonth == today.month &&
315                day == today.monthDay;
316    }
317
318    @Override
319    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
320        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
321                + MONTH_HEADER_SIZE);
322    }
323
324    @Override
325    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
326        mWidth = w;
327    }
328
329    private void drawMonthTitle(Canvas canvas) {
330        int x = (mWidth + 2 * mPadding) / 2;
331        int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2;
332        StringBuffer sbuf = new StringBuffer();
333        sbuf.append(mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG,
334                Locale.getDefault()));
335        sbuf.append(" ");
336        sbuf.append(mYear);
337        canvas.drawText(sbuf.toString(), x, y, mMonthTitlePaint);
338    }
339
340    private void drawMonthDayLabels(Canvas canvas) {
341        int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
342        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
343
344        for (int i = 0; i < mNumDays; i++) {
345            int calendarDay = (i + mWeekStart) % mNumDays;
346            int x = (2 * i + 1) * dayWidthHalf + mPadding;
347            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
348            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
349                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
350                    mMonthDayLabelPaint);
351        }
352    }
353
354    /**
355     * Draws the week and month day numbers for this week. Override this method
356     * if you need different placement.
357     *
358     * @param canvas The canvas to draw on
359     */
360    protected void drawMonthNums(Canvas canvas) {
361        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
362                + MONTH_HEADER_SIZE;
363        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
364        int j = findDayOffset();
365        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
366            int x = (2 * j + 1) * dayWidthHalf + mPadding;
367            if (mSelectedDay == dayNumber) {
368                canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE,
369                        mSelectedCirclePaint);
370            }
371
372            if (mHasToday && mToday == dayNumber) {
373                mMonthNumPaint.setColor(mMonthTitleTextColor);
374            } else {
375                mMonthNumPaint.setColor(mDayTextColor);
376            }
377            canvas.drawText(Integer.valueOf(dayNumber).toString(), x, y, mMonthNumPaint);
378
379            j++;
380            if (j == mNumDays) {
381                j = 0;
382                y += mRowHeight;
383            }
384        }
385    }
386
387    private int findDayOffset() {
388        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
389                - mWeekStart;
390    }
391
392
393    /**
394     * Calculates the day that the given x position is in, accounting for week
395     * number. Returns a Time referencing that day or null if
396     *
397     * @param x The x position of the touch event
398     * @return A time object for the tapped day or null if the position wasn't
399     *         in a day
400     */
401    public CalendarDay getDayFromLocation(float x, float y) {
402        int dayStart = mPadding;
403        if (x < dayStart || x > mWidth - mPadding) {
404            return null;
405        }
406        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
407        int row = (int) (y - mRowHeight) / mRowHeight;
408        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
409
410        int day = column - findDayOffset() + 1;
411        day += row * mNumDays;
412        if (day < 1 || day > mNumCells) {
413            return null;
414        }
415        return new CalendarDay(mYear, mMonth, day);
416    }
417
418}
419