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