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    private void updateButtonVisibility(int position) {
182        final boolean hasPrev = position > 0;
183        final boolean hasNext = position < (mAdapter.getCount() - 1);
184        mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE);
185        mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE);
186    }
187
188    @Override
189    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
190        final ViewPager viewPager = mViewPager;
191        measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
192
193        final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
194        final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
195        setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
196
197        final int pagerWidth = viewPager.getMeasuredWidth();
198        final int pagerHeight = viewPager.getMeasuredHeight();
199        final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
200        final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
201        mPrevButton.measure(buttonWidthSpec, buttonHeightSpec);
202        mNextButton.measure(buttonWidthSpec, buttonHeightSpec);
203    }
204
205    @Override
206    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
207        super.onRtlPropertiesChanged(layoutDirection);
208
209        requestLayout();
210    }
211
212    @Override
213    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
214        final ImageButton leftButton;
215        final ImageButton rightButton;
216        if (isLayoutRtl()) {
217            leftButton = mNextButton;
218            rightButton = mPrevButton;
219        } else {
220            leftButton = mPrevButton;
221            rightButton = mNextButton;
222        }
223
224        final int width = right - left;
225        final int height = bottom - top;
226        mViewPager.layout(0, 0, width, height);
227
228        final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
229        final int monthHeight = monthView.getMonthHeight();
230        final int cellWidth = monthView.getCellWidth();
231
232        // Vertically center the previous/next buttons within the month
233        // header, horizontally center within the day cell.
234        final int leftDW = leftButton.getMeasuredWidth();
235        final int leftDH = leftButton.getMeasuredHeight();
236        final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
237        final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2;
238        leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
239
240        final int rightDW = rightButton.getMeasuredWidth();
241        final int rightDH = rightButton.getMeasuredHeight();
242        final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
243        final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2;
244        rightButton.layout(rightIconRight - rightDW, rightIconTop,
245                rightIconRight, rightIconTop + rightDH);
246    }
247
248    public void setDayOfWeekTextAppearance(int resId) {
249        mAdapter.setDayOfWeekTextAppearance(resId);
250    }
251
252    public int getDayOfWeekTextAppearance() {
253        return mAdapter.getDayOfWeekTextAppearance();
254    }
255
256    public void setDayTextAppearance(int resId) {
257        mAdapter.setDayTextAppearance(resId);
258    }
259
260    public int getDayTextAppearance() {
261        return mAdapter.getDayTextAppearance();
262    }
263
264    /**
265     * Sets the currently selected date to the specified timestamp. Jumps
266     * immediately to the new date. To animate to the new date, use
267     * {@link #setDate(long, boolean)}.
268     *
269     * @param timeInMillis the target day in milliseconds
270     */
271    public void setDate(long timeInMillis) {
272        setDate(timeInMillis, false);
273    }
274
275    /**
276     * Sets the currently selected date to the specified timestamp. Jumps
277     * immediately to the new date, optionally animating the transition.
278     *
279     * @param timeInMillis the target day in milliseconds
280     * @param animate whether to smooth scroll to the new position
281     */
282    public void setDate(long timeInMillis, boolean animate) {
283        setDate(timeInMillis, animate, true);
284    }
285
286    /**
287     * Moves to the month containing the specified day, optionally setting the
288     * day as selected.
289     *
290     * @param timeInMillis the target day in milliseconds
291     * @param animate whether to smooth scroll to the new position
292     * @param setSelected whether to set the specified day as selected
293     */
294    private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
295        if (setSelected) {
296            mSelectedDay.setTimeInMillis(timeInMillis);
297        }
298
299        final int position = getPositionFromDay(timeInMillis);
300        if (position != mViewPager.getCurrentItem()) {
301            mViewPager.setCurrentItem(position, animate);
302        }
303
304        mTempCalendar.setTimeInMillis(timeInMillis);
305        mAdapter.setSelectedDay(mTempCalendar);
306    }
307
308    public long getDate() {
309        return mSelectedDay.getTimeInMillis();
310    }
311
312    public void setFirstDayOfWeek(int firstDayOfWeek) {
313        mAdapter.setFirstDayOfWeek(firstDayOfWeek);
314    }
315
316    public int getFirstDayOfWeek() {
317        return mAdapter.getFirstDayOfWeek();
318    }
319
320    public void setMinDate(long timeInMillis) {
321        mMinDate.setTimeInMillis(timeInMillis);
322        onRangeChanged();
323    }
324
325    public long getMinDate() {
326        return mMinDate.getTimeInMillis();
327    }
328
329    public void setMaxDate(long timeInMillis) {
330        mMaxDate.setTimeInMillis(timeInMillis);
331        onRangeChanged();
332    }
333
334    public long getMaxDate() {
335        return mMaxDate.getTimeInMillis();
336    }
337
338    /**
339     * Handles changes to date range.
340     */
341    public void onRangeChanged() {
342        mAdapter.setRange(mMinDate, mMaxDate);
343
344        // Changing the min/max date changes the selection position since we
345        // don't really have stable IDs. Jumps immediately to the new position.
346        setDate(mSelectedDay.getTimeInMillis(), false, false);
347
348        updateButtonVisibility(mViewPager.getCurrentItem());
349    }
350
351    /**
352     * Sets the listener to call when the user selects a day.
353     *
354     * @param listener The listener to call.
355     */
356    public void setOnDaySelectedListener(OnDaySelectedListener listener) {
357        mOnDaySelectedListener = listener;
358    }
359
360    private int getDiffMonths(Calendar start, Calendar end) {
361        final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
362        return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
363    }
364
365    private int getPositionFromDay(long timeInMillis) {
366        final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
367        final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
368        return MathUtils.constrain(diffMonth, 0, diffMonthMax);
369    }
370
371    private Calendar getTempCalendarForTime(long timeInMillis) {
372        if (mTempCalendar == null) {
373            mTempCalendar = Calendar.getInstance();
374        }
375        mTempCalendar.setTimeInMillis(timeInMillis);
376        return mTempCalendar;
377    }
378
379    /**
380     * Gets the position of the view that is most prominently displayed within the list view.
381     */
382    public int getMostVisiblePosition() {
383        return mViewPager.getCurrentItem();
384    }
385
386    public void setPosition(int position) {
387        mViewPager.setCurrentItem(position, false);
388    }
389
390    private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
391        @Override
392        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
393            final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
394            mPrevButton.setAlpha(alpha);
395            mNextButton.setAlpha(alpha);
396        }
397
398        @Override
399        public void onPageScrollStateChanged(int state) {}
400
401        @Override
402        public void onPageSelected(int position) {
403            updateButtonVisibility(position);
404        }
405    };
406
407    private final OnClickListener mOnClickListener = new OnClickListener() {
408        @Override
409        public void onClick(View v) {
410            final int direction;
411            if (v == mPrevButton) {
412                direction = -1;
413            } else if (v == mNextButton) {
414                direction = 1;
415            } else {
416                return;
417            }
418
419            // Animation is expensive for accessibility services since it sends
420            // lots of scroll and content change events.
421            final boolean animate = !mAccessibilityManager.isEnabled();
422
423            // ViewPager clamps input values, so we don't need to worry
424            // about passing invalid indices.
425            final int nextItem = mViewPager.getCurrentItem() + direction;
426            mViewPager.setCurrentItem(nextItem, animate);
427        }
428    };
429
430    public interface OnDaySelectedListener {
431        void onDaySelected(DayPickerView view, Calendar day);
432    }
433}
434