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