SimpleMonthView.java revision 51da77ac265fc6e46403bc6f8d3cca57e57427d7
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.Typeface;
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    private String mDayOfWeekTypeface;
114    private String mMonthTitleTypeface;
115
116    protected Paint mMonthNumPaint;
117    protected Paint mMonthTitlePaint;
118    protected Paint mMonthTitleBGPaint;
119    protected Paint mSelectedCirclePaint;
120    protected Paint mMonthDayLabelPaint;
121
122    // The Julian day of the first day displayed by this item
123    protected int mFirstJulianDay = -1;
124    // The month of the first day in this week
125    protected int mFirstMonth = -1;
126    // The month of the last day in this week
127    protected int mLastMonth = -1;
128
129    protected int mMonth;
130
131    protected int mYear;
132    // Quick reference to the width of this view, matches parent
133    protected int mWidth;
134    // The height this view should draw at in pixels, set by height param
135    protected int mRowHeight = DEFAULT_HEIGHT;
136    // If this view contains the today
137    protected boolean mHasToday = false;
138    // Which day is selected [0-6] or -1 if no day is selected
139    protected int mSelectedDay = -1;
140    // Which day is today [0-6] or -1 if no day is today
141    protected int mToday = DEFAULT_SELECTED_DAY;
142    // Which day of the week to start on [0-6]
143    protected int mWeekStart = DEFAULT_WEEK_START;
144    // How many days to display
145    protected int mNumDays = DEFAULT_NUM_DAYS;
146    // The number of days + a spot for week number if it is displayed
147    protected int mNumCells = mNumDays;
148    // The left edge of the selected day
149    protected int mSelectedLeft = -1;
150    // The right edge of the selected day
151    protected int mSelectedRight = -1;
152
153    private final Calendar mCalendar;
154    private final Calendar mDayLabelCalendar;
155
156    private int mNumRows = DEFAULT_NUM_ROWS;
157
158    protected int mDayTextColor;
159    protected int mTodayNumberColor;
160    protected int mMonthTitleColor;
161    protected int mMonthTitleBGColor;
162
163    public SimpleMonthView(Context context) {
164        super(context);
165
166        Resources res = context.getResources();
167
168        mDayLabelCalendar = Calendar.getInstance();
169        mCalendar = Calendar.getInstance();
170
171        mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
172        mMonthTitleTypeface = res.getString(R.string.sans_serif);
173
174        mDayTextColor = res.getColor(R.color.date_picker_text_normal);
175        mTodayNumberColor = res.getColor(R.color.blue);
176        mMonthTitleColor = res.getColor(R.color.white);
177        mMonthTitleBGColor = res.getColor(R.color.circle_background);
178
179        MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
180        MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
181        MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
182        MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
183        DAY_SELECTED_CIRCLE_SIZE = res
184                .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
185
186        mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
187                - MONTH_HEADER_SIZE) / MAX_NUM_ROWS;
188        // Sets up any standard paints that will be used
189        initView();
190    }
191
192    /**
193     * Sets up the text and style properties for painting. Override this if you
194     * want to use a different paint.
195     */
196    protected void initView() {
197        mMonthTitlePaint = new Paint();
198        mMonthTitlePaint.setFakeBoldText(true);
199        mMonthTitlePaint.setAntiAlias(true);
200        mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
201        mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
202        mMonthTitlePaint.setColor(mDayTextColor);
203        mMonthTitlePaint.setTextAlign(Align.CENTER);
204        mMonthTitlePaint.setStyle(Style.FILL);
205
206        mMonthTitleBGPaint = new Paint();
207        mMonthTitleBGPaint.setFakeBoldText(true);
208        mMonthTitleBGPaint.setAntiAlias(true);
209        mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
210        mMonthTitleBGPaint.setTextAlign(Align.CENTER);
211        mMonthTitleBGPaint.setStyle(Style.FILL);
212
213        mSelectedCirclePaint = new Paint();
214        mSelectedCirclePaint.setFakeBoldText(true);
215        mSelectedCirclePaint.setAntiAlias(true);
216        mSelectedCirclePaint.setColor(mTodayNumberColor);
217        mSelectedCirclePaint.setTextAlign(Align.CENTER);
218        mSelectedCirclePaint.setStyle(Style.FILL);
219        mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
220
221        mMonthDayLabelPaint = new Paint();
222        mMonthDayLabelPaint.setAntiAlias(true);
223        mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
224        mMonthDayLabelPaint.setColor(mDayTextColor);
225        mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
226        mMonthDayLabelPaint.setStyle(Style.FILL);
227        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
228        mMonthDayLabelPaint.setFakeBoldText(true);
229
230        mMonthNumPaint = new Paint();
231        mMonthNumPaint.setAntiAlias(true);
232        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
233        mMonthNumPaint.setStyle(Style.FILL);
234        mMonthNumPaint.setTextAlign(Align.CENTER);
235        mMonthNumPaint.setFakeBoldText(false);
236    }
237
238    @Override
239    protected void onDraw(Canvas canvas) {
240        drawMonthTitle(canvas);
241        drawMonthDayLabels(canvas);
242        drawMonthNums(canvas);
243    }
244
245    private int mDayOfWeekStart = 0;
246
247    /**
248     * Sets all the parameters for displaying this week. The only required
249     * parameter is the week number. Other parameters have a default value and
250     * will only update if a new value is included, except for focus month,
251     * which will always default to no focus month if no value is passed in. See
252     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
253     *
254     * @param params A map of the new parameters, see
255     *            {@link #VIEW_PARAMS_HEIGHT}
256     * @param tz The time zone this view should reference times in
257     */
258    public void setMonthParams(HashMap<String, Integer> params) {
259        if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
260            throw new InvalidParameterException("You must specify the month and year for this view");
261        }
262        setTag(params);
263        // We keep the current value for any params not present
264        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
265            mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
266            if (mRowHeight < MIN_HEIGHT) {
267                mRowHeight = MIN_HEIGHT;
268            }
269        }
270        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
271            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
272        }
273
274        // Allocate space for caching the day numbers and focus values
275        mMonth = params.get(VIEW_PARAMS_MONTH);
276        mYear = params.get(VIEW_PARAMS_YEAR);
277
278        // Figure out what day today is
279        final Time today = new Time(Time.getCurrentTimezone());
280        today.setToNow();
281        mHasToday = false;
282        mToday = -1;
283
284        mCalendar.set(Calendar.MONTH, mMonth);
285        mCalendar.set(Calendar.YEAR, mYear);
286        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
287        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
288
289        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
290            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
291        } else {
292            mWeekStart = mCalendar.getFirstDayOfWeek();
293        }
294
295        mNumCells = Utils.getDaysInMonth(mMonth, mYear);
296        for (int i = 0; i < mNumCells; i++) {
297            final int day = i + 1;
298            if (sameDay(day, today)) {
299                mHasToday = true;
300                mToday = day;
301            }
302        }
303        mNumRows = calculateNumRows();
304    }
305
306    public void reuse() {
307        mNumRows = DEFAULT_NUM_ROWS;
308        requestLayout();
309    }
310
311    private int calculateNumRows() {
312        int offset = findDayOffset();
313        int dividend = (offset + mNumCells) / mNumDays;
314        int remainder = (offset + mNumCells) % mNumDays;
315        return (dividend + (remainder > 0 ? 1 : 0));
316    }
317
318    private boolean sameDay(int day, Time today) {
319        return mYear == today.year &&
320                mMonth == today.month &&
321                day == today.monthDay;
322    }
323
324    @Override
325    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
326        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
327                + MONTH_HEADER_SIZE);
328    }
329
330    @Override
331    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
332        mWidth = w;
333    }
334
335    private void drawMonthTitle(Canvas canvas) {
336        int x = (mWidth + 2 * mPadding) / 2;
337        int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2;
338        StringBuffer sbuf = new StringBuffer();
339        sbuf.append(mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG,
340                Locale.getDefault()));
341        sbuf.append(" ");
342        sbuf.append(mYear);
343        canvas.drawText(sbuf.toString(), x, y, mMonthTitlePaint);
344    }
345
346    private void drawMonthDayLabels(Canvas canvas) {
347        int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
348        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
349
350        for (int i = 0; i < mNumDays; i++) {
351            int calendarDay = (i + mWeekStart) % mNumDays;
352            int x = (2 * i + 1) * dayWidthHalf + mPadding;
353            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
354            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
355                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
356                    mMonthDayLabelPaint);
357        }
358    }
359
360    /**
361     * Draws the week and month day numbers for this week. Override this method
362     * if you need different placement.
363     *
364     * @param canvas The canvas to draw on
365     */
366    protected void drawMonthNums(Canvas canvas) {
367        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
368                + MONTH_HEADER_SIZE;
369        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
370        int j = findDayOffset();
371        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
372            int x = (2 * j + 1) * dayWidthHalf + mPadding;
373            if (mSelectedDay == dayNumber) {
374                canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE,
375                        mSelectedCirclePaint);
376            }
377
378            if (mHasToday && mToday == dayNumber) {
379                mMonthNumPaint.setColor(mTodayNumberColor);
380            } else {
381                mMonthNumPaint.setColor(mDayTextColor);
382            }
383            canvas.drawText(Integer.valueOf(dayNumber).toString(), x, y, mMonthNumPaint);
384
385            j++;
386            if (j == mNumDays) {
387                j = 0;
388                y += mRowHeight;
389            }
390        }
391    }
392
393    private int findDayOffset() {
394        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
395                - mWeekStart;
396    }
397
398
399    /**
400     * Calculates the day that the given x position is in, accounting for week
401     * number. Returns a Time referencing that day or null if
402     *
403     * @param x The x position of the touch event
404     * @return A time object for the tapped day or null if the position wasn't
405     *         in a day
406     */
407    public CalendarDay getDayFromLocation(float x, float y) {
408        int dayStart = mPadding;
409        if (x < dayStart || x > mWidth - mPadding) {
410            return null;
411        }
412        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
413        int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
414        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
415
416        int day = column - findDayOffset() + 1;
417        day += row * mNumDays;
418        if (day < 1 || day > mNumCells) {
419            return null;
420        }
421        return new CalendarDay(mYear, mMonth, day);
422    }
423
424}
425