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 
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 
updateButtonVisibility(int position)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