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