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        boolean dateClamped = false;
297        // Clamp the target day in milliseconds to the min or max if outside the range.
298        if (timeInMillis < mMinDate.getTimeInMillis()) {
299            timeInMillis = mMinDate.getTimeInMillis();
300            dateClamped = true;
301        } else if (timeInMillis > mMaxDate.getTimeInMillis()) {
302            timeInMillis = mMaxDate.getTimeInMillis();
303            dateClamped = true;
304        }
305
306        getTempCalendarForTime(timeInMillis);
307
308        if (setSelected || dateClamped) {
309            mSelectedDay.setTimeInMillis(timeInMillis);
310        }
311
312        final int position = getPositionFromDay(timeInMillis);
313        if (position != mViewPager.getCurrentItem()) {
314            mViewPager.setCurrentItem(position, animate);
315        }
316
317        mAdapter.setSelectedDay(mTempCalendar);
318    }
319
320    public long getDate() {
321        return mSelectedDay.getTimeInMillis();
322    }
323
324    public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
325        final int position = getPositionFromDay(timeInMillis);
326        if (position != mViewPager.getCurrentItem()) {
327            return false;
328        }
329
330        mTempCalendar.setTimeInMillis(timeInMillis);
331        return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
332    }
333
334    public void setFirstDayOfWeek(int firstDayOfWeek) {
335        mAdapter.setFirstDayOfWeek(firstDayOfWeek);
336    }
337
338    public int getFirstDayOfWeek() {
339        return mAdapter.getFirstDayOfWeek();
340    }
341
342    public void setMinDate(long timeInMillis) {
343        mMinDate.setTimeInMillis(timeInMillis);
344        onRangeChanged();
345    }
346
347    public long getMinDate() {
348        return mMinDate.getTimeInMillis();
349    }
350
351    public void setMaxDate(long timeInMillis) {
352        mMaxDate.setTimeInMillis(timeInMillis);
353        onRangeChanged();
354    }
355
356    public long getMaxDate() {
357        return mMaxDate.getTimeInMillis();
358    }
359
360    /**
361     * Handles changes to date range.
362     */
363    public void onRangeChanged() {
364        mAdapter.setRange(mMinDate, mMaxDate);
365
366        // Changing the min/max date changes the selection position since we
367        // don't really have stable IDs. Jumps immediately to the new position.
368        setDate(mSelectedDay.getTimeInMillis(), false, false);
369
370        updateButtonVisibility(mViewPager.getCurrentItem());
371    }
372
373    /**
374     * Sets the listener to call when the user selects a day.
375     *
376     * @param listener The listener to call.
377     */
378    public void setOnDaySelectedListener(OnDaySelectedListener listener) {
379        mOnDaySelectedListener = listener;
380    }
381
382    private int getDiffMonths(Calendar start, Calendar end) {
383        final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
384        return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
385    }
386
387    private int getPositionFromDay(long timeInMillis) {
388        final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
389        final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
390        return MathUtils.constrain(diffMonth, 0, diffMonthMax);
391    }
392
393    private Calendar getTempCalendarForTime(long timeInMillis) {
394        if (mTempCalendar == null) {
395            mTempCalendar = Calendar.getInstance();
396        }
397        mTempCalendar.setTimeInMillis(timeInMillis);
398        return mTempCalendar;
399    }
400
401    /**
402     * Gets the position of the view that is most prominently displayed within the list view.
403     */
404    public int getMostVisiblePosition() {
405        return mViewPager.getCurrentItem();
406    }
407
408    public void setPosition(int position) {
409        mViewPager.setCurrentItem(position, false);
410    }
411
412    private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
413        @Override
414        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
415            final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
416            mPrevButton.setAlpha(alpha);
417            mNextButton.setAlpha(alpha);
418        }
419
420        @Override
421        public void onPageScrollStateChanged(int state) {}
422
423        @Override
424        public void onPageSelected(int position) {
425            updateButtonVisibility(position);
426        }
427    };
428
429    private final OnClickListener mOnClickListener = new OnClickListener() {
430        @Override
431        public void onClick(View v) {
432            final int direction;
433            if (v == mPrevButton) {
434                direction = -1;
435            } else if (v == mNextButton) {
436                direction = 1;
437            } else {
438                return;
439            }
440
441            // Animation is expensive for accessibility services since it sends
442            // lots of scroll and content change events.
443            final boolean animate = !mAccessibilityManager.isEnabled();
444
445            // ViewPager clamps input values, so we don't need to worry
446            // about passing invalid indices.
447            final int nextItem = mViewPager.getCurrentItem() + direction;
448            mViewPager.setCurrentItem(nextItem, animate);
449        }
450    };
451
452    public interface OnDaySelectedListener {
453        void onDaySelected(DayPickerView view, Calendar day);
454    }
455}
456