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 com.android.internal.R;
20 import com.android.internal.widget.ViewPager;
21 import com.android.internal.widget.ViewPager.OnPageChangeListener;
22 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.TypedArray;
27 import android.icu.util.Calendar;
28 import android.util.AttributeSet;
29 import android.util.MathUtils;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.accessibility.AccessibilityManager;
34 
35 import java.util.Locale;
36 
37 import libcore.icu.LocaleData;
38 
39 class 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 
DayPickerView(Context context)63     public DayPickerView(Context context) {
64         this(context, null);
65     }
66 
DayPickerView(Context context, @Nullable AttributeSet attrs)67     public DayPickerView(Context context, @Nullable AttributeSet attrs) {
68         this(context, attrs, R.attr.calendarViewStyle);
69     }
70 
DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)71     public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
72         this(context, attrs, defStyleAttr, 0);
73     }
74 
DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)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 
updateButtonVisibility(int position)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