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