DayPickerView.java revision cf5a420ead56916cb3e8886574e1447cf17c8e19
1/*
2 * Copyright (C) 2015 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 android.widget;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.TypedArray;
23import android.graphics.Rect;
24import android.icu.util.Calendar;
25import android.util.AttributeSet;
26import android.util.MathUtils;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.accessibility.AccessibilityManager;
31
32import com.android.internal.R;
33import com.android.internal.widget.ViewPager;
34import com.android.internal.widget.ViewPager.OnPageChangeListener;
35
36import libcore.icu.LocaleData;
37
38import java.util.Locale;
39
40class DayPickerView extends ViewGroup {
41    private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material;
42    private static final int DEFAULT_START_YEAR = 1900;
43    private static final int DEFAULT_END_YEAR = 2100;
44
45    private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor };
46
47    private final Calendar mSelectedDay = Calendar.getInstance();
48    private final Calendar mMinDate = Calendar.getInstance();
49    private final Calendar mMaxDate = Calendar.getInstance();
50
51    private final AccessibilityManager mAccessibilityManager;
52
53    private final ViewPager mViewPager;
54    private final ImageButton mPrevButton;
55    private final ImageButton mNextButton;
56
57    private final DayPickerPagerAdapter mAdapter;
58
59    /** Temporary calendar used for date calculations. */
60    private Calendar mTempCalendar;
61
62    private OnDaySelectedListener mOnDaySelectedListener;
63
64    public DayPickerView(Context context) {
65        this(context, null);
66    }
67
68    public DayPickerView(Context context, @Nullable AttributeSet attrs) {
69        this(context, attrs, R.attr.calendarViewStyle);
70    }
71
72    public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
73        this(context, attrs, defStyleAttr, 0);
74    }
75
76    public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
77            int defStyleRes) {
78        super(context, attrs, defStyleAttr, defStyleRes);
79
80        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
81                Context.ACCESSIBILITY_SERVICE);
82
83        final TypedArray a = context.obtainStyledAttributes(attrs,
84                R.styleable.CalendarView, defStyleAttr, defStyleRes);
85
86        final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
87                LocaleData.get(Locale.getDefault()).firstDayOfWeek);
88
89        final String minDate = a.getString(R.styleable.CalendarView_minDate);
90        final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
91
92        final int monthTextAppearanceResId = a.getResourceId(
93                R.styleable.CalendarView_monthTextAppearance,
94                R.style.TextAppearance_Material_Widget_Calendar_Month);
95        final int dayOfWeekTextAppearanceResId = a.getResourceId(
96                R.styleable.CalendarView_weekDayTextAppearance,
97                R.style.TextAppearance_Material_Widget_Calendar_DayOfWeek);
98        final int dayTextAppearanceResId = a.getResourceId(
99                R.styleable.CalendarView_dateTextAppearance,
100                R.style.TextAppearance_Material_Widget_Calendar_Day);
101
102        final ColorStateList daySelectorColor = a.getColorStateList(
103                R.styleable.CalendarView_daySelectorColor);
104
105        a.recycle();
106
107        // Set up adapter.
108        mAdapter = new DayPickerPagerAdapter(context,
109                R.layout.date_picker_month_item_material, R.id.month_view);
110        mAdapter.setMonthTextAppearance(monthTextAppearanceResId);
111        mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId);
112        mAdapter.setDayTextAppearance(dayTextAppearanceResId);
113        mAdapter.setDaySelectorColor(daySelectorColor);
114
115        final LayoutInflater inflater = LayoutInflater.from(context);
116        final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false);
117
118        // Transfer all children from content to here.
119        while (content.getChildCount() > 0) {
120            final View child = content.getChildAt(0);
121            content.removeViewAt(0);
122            addView(child);
123        }
124
125        mPrevButton = findViewById(R.id.prev);
126        mPrevButton.setOnClickListener(mOnClickListener);
127
128        mNextButton = findViewById(R.id.next);
129        mNextButton.setOnClickListener(mOnClickListener);
130
131        mViewPager = findViewById(R.id.day_picker_view_pager);
132        mViewPager.setAdapter(mAdapter);
133        mViewPager.setOnPageChangeListener(mOnPageChangedListener);
134
135        // Proxy the month text color into the previous and next buttons.
136        if (monthTextAppearanceResId != 0) {
137            final TypedArray ta = mContext.obtainStyledAttributes(null,
138                    ATTRS_TEXT_COLOR, 0, monthTextAppearanceResId);
139            final ColorStateList monthColor = ta.getColorStateList(0);
140            if (monthColor != null) {
141                mPrevButton.setImageTintList(monthColor);
142                mNextButton.setImageTintList(monthColor);
143            }
144            ta.recycle();
145        }
146
147        // Set up min and max dates.
148        final Calendar tempDate = Calendar.getInstance();
149        if (!CalendarView.parseDate(minDate, tempDate)) {
150            tempDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
151        }
152        final long minDateMillis = tempDate.getTimeInMillis();
153
154        if (!CalendarView.parseDate(maxDate, tempDate)) {
155            tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
156        }
157        final long maxDateMillis = tempDate.getTimeInMillis();
158
159        if (maxDateMillis < minDateMillis) {
160            throw new IllegalArgumentException("maxDate must be >= minDate");
161        }
162
163        final long setDateMillis = MathUtils.constrain(
164                System.currentTimeMillis(), minDateMillis, maxDateMillis);
165
166        setFirstDayOfWeek(firstDayOfWeek);
167        setMinDate(minDateMillis);
168        setMaxDate(maxDateMillis);
169        setDate(setDateMillis, false);
170
171        // Proxy selection callbacks to our own listener.
172        mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() {
173            @Override
174            public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) {
175                if (mOnDaySelectedListener != null) {
176                    mOnDaySelectedListener.onDaySelected(DayPickerView.this, day);
177                }
178            }
179        });
180    }
181
182    private void updateButtonVisibility(int position) {
183        final boolean hasPrev = position > 0;
184        final boolean hasNext = position < (mAdapter.getCount() - 1);
185        mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE);
186        mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE);
187    }
188
189    @Override
190    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
191        final ViewPager viewPager = mViewPager;
192        measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
193
194        final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
195        final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
196        setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
197
198        final int pagerWidth = viewPager.getMeasuredWidth();
199        final int pagerHeight = viewPager.getMeasuredHeight();
200        final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
201        final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
202        mPrevButton.measure(buttonWidthSpec, buttonHeightSpec);
203        mNextButton.measure(buttonWidthSpec, buttonHeightSpec);
204    }
205
206    @Override
207    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
208        super.onRtlPropertiesChanged(layoutDirection);
209
210        requestLayout();
211    }
212
213    @Override
214    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
215        final ImageButton leftButton;
216        final ImageButton rightButton;
217        if (isLayoutRtl()) {
218            leftButton = mNextButton;
219            rightButton = mPrevButton;
220        } else {
221            leftButton = mPrevButton;
222            rightButton = mNextButton;
223        }
224
225        final int width = right - left;
226        final int height = bottom - top;
227        mViewPager.layout(0, 0, width, height);
228
229        final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
230        final int monthHeight = monthView.getMonthHeight();
231        final int cellWidth = monthView.getCellWidth();
232
233        // Vertically center the previous/next buttons within the month
234        // header, horizontally center within the day cell.
235        final int leftDW = leftButton.getMeasuredWidth();
236        final int leftDH = leftButton.getMeasuredHeight();
237        final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
238        final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2;
239        leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
240
241        final int rightDW = rightButton.getMeasuredWidth();
242        final int rightDH = rightButton.getMeasuredHeight();
243        final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
244        final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2;
245        rightButton.layout(rightIconRight - rightDW, rightIconTop,
246                rightIconRight, rightIconTop + rightDH);
247    }
248
249    public void setDayOfWeekTextAppearance(int resId) {
250        mAdapter.setDayOfWeekTextAppearance(resId);
251    }
252
253    public int getDayOfWeekTextAppearance() {
254        return mAdapter.getDayOfWeekTextAppearance();
255    }
256
257    public void setDayTextAppearance(int resId) {
258        mAdapter.setDayTextAppearance(resId);
259    }
260
261    public int getDayTextAppearance() {
262        return mAdapter.getDayTextAppearance();
263    }
264
265    /**
266     * Sets the currently selected date to the specified timestamp. Jumps
267     * immediately to the new date. To animate to the new date, use
268     * {@link #setDate(long, boolean)}.
269     *
270     * @param timeInMillis the target day in milliseconds
271     */
272    public void setDate(long timeInMillis) {
273        setDate(timeInMillis, false);
274    }
275
276    /**
277     * Sets the currently selected date to the specified timestamp. Jumps
278     * immediately to the new date, optionally animating the transition.
279     *
280     * @param timeInMillis the target day in milliseconds
281     * @param animate whether to smooth scroll to the new position
282     */
283    public void setDate(long timeInMillis, boolean animate) {
284        setDate(timeInMillis, animate, true);
285    }
286
287    /**
288     * Moves to the month containing the specified day, optionally setting the
289     * day as selected.
290     *
291     * @param timeInMillis the target day in milliseconds
292     * @param animate whether to smooth scroll to the new position
293     * @param setSelected whether to set the specified day as selected
294     */
295    private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
296        getTempCalendarForTime(timeInMillis);
297
298        if (setSelected) {
299            mSelectedDay.setTimeInMillis(timeInMillis);
300        }
301
302        final int position = getPositionFromDay(timeInMillis);
303        if (position != mViewPager.getCurrentItem()) {
304            mViewPager.setCurrentItem(position, animate);
305        }
306
307        mAdapter.setSelectedDay(mTempCalendar);
308    }
309
310    public long getDate() {
311        return mSelectedDay.getTimeInMillis();
312    }
313
314    public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
315        final int position = getPositionFromDay(timeInMillis);
316        if (position != mViewPager.getCurrentItem()) {
317            return false;
318        }
319
320        mTempCalendar.setTimeInMillis(timeInMillis);
321        return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
322    }
323
324    public void setFirstDayOfWeek(int firstDayOfWeek) {
325        mAdapter.setFirstDayOfWeek(firstDayOfWeek);
326    }
327
328    public int getFirstDayOfWeek() {
329        return mAdapter.getFirstDayOfWeek();
330    }
331
332    public void setMinDate(long timeInMillis) {
333        mMinDate.setTimeInMillis(timeInMillis);
334        onRangeChanged();
335    }
336
337    public long getMinDate() {
338        return mMinDate.getTimeInMillis();
339    }
340
341    public void setMaxDate(long timeInMillis) {
342        mMaxDate.setTimeInMillis(timeInMillis);
343        onRangeChanged();
344    }
345
346    public long getMaxDate() {
347        return mMaxDate.getTimeInMillis();
348    }
349
350    /**
351     * Handles changes to date range.
352     */
353    public void onRangeChanged() {
354        mAdapter.setRange(mMinDate, mMaxDate);
355
356        // Clamp the selected day to the new min/max.
357        if (mSelectedDay.before(mMinDate)) {
358            mSelectedDay.setTimeInMillis(mMinDate.getTimeInMillis());
359        } else if (mSelectedDay.after(mMaxDate)) {
360            mSelectedDay.setTimeInMillis(mMaxDate.getTimeInMillis());
361        }
362
363        // Changing the min/max date changes the selection position since we
364        // don't really have stable IDs. Jumps immediately to the new position.
365        setDate(mSelectedDay.getTimeInMillis(), false, false);
366
367        updateButtonVisibility(mViewPager.getCurrentItem());
368    }
369
370    /**
371     * Sets the listener to call when the user selects a day.
372     *
373     * @param listener The listener to call.
374     */
375    public void setOnDaySelectedListener(OnDaySelectedListener listener) {
376        mOnDaySelectedListener = listener;
377    }
378
379    private int getDiffMonths(Calendar start, Calendar end) {
380        final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
381        return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
382    }
383
384    private int getPositionFromDay(long timeInMillis) {
385        final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
386        final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
387        return MathUtils.constrain(diffMonth, 0, diffMonthMax);
388    }
389
390    private Calendar getTempCalendarForTime(long timeInMillis) {
391        if (mTempCalendar == null) {
392            mTempCalendar = Calendar.getInstance();
393        }
394        mTempCalendar.setTimeInMillis(timeInMillis);
395        return mTempCalendar;
396    }
397
398    /**
399     * Gets the position of the view that is most prominently displayed within the list view.
400     */
401    public int getMostVisiblePosition() {
402        return mViewPager.getCurrentItem();
403    }
404
405    public void setPosition(int position) {
406        mViewPager.setCurrentItem(position, false);
407    }
408
409    private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
410        @Override
411        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
412            final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
413            mPrevButton.setAlpha(alpha);
414            mNextButton.setAlpha(alpha);
415        }
416
417        @Override
418        public void onPageScrollStateChanged(int state) {}
419
420        @Override
421        public void onPageSelected(int position) {
422            updateButtonVisibility(position);
423        }
424    };
425
426    private final OnClickListener mOnClickListener = new OnClickListener() {
427        @Override
428        public void onClick(View v) {
429            final int direction;
430            if (v == mPrevButton) {
431                direction = -1;
432            } else if (v == mNextButton) {
433                direction = 1;
434            } else {
435                return;
436            }
437
438            // Animation is expensive for accessibility services since it sends
439            // lots of scroll and content change events.
440            final boolean animate = !mAccessibilityManager.isEnabled();
441
442            // ViewPager clamps input values, so we don't need to worry
443            // about passing invalid indices.
444            final int nextItem = mViewPager.getCurrentItem() + direction;
445            mViewPager.setCurrentItem(nextItem, animate);
446        }
447    };
448
449    public interface OnDaySelectedListener {
450        void onDaySelected(DayPickerView view, Calendar day);
451    }
452}
453