2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.widget;
19 import static android.os.Build.VERSION_CODES.N_MR1;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.content.res.TypedArray;
25 import android.graphics.Rect;
26 import android.icu.util.Calendar;
27 import android.util.AttributeSet;
28 import android.util.MathUtils;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.accessibility.AccessibilityManager;
34 import com.android.internal.R;
35 import com.android.internal.widget.ViewPager;
36 import com.android.internal.widget.ViewPager.OnPageChangeListener;
38 import libcore.icu.LocaleData;
40 import java.util.Locale;
42 class DayPickerView extends ViewGroup {
43 private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material;
44 private static final int DEFAULT_START_YEAR = 1900;
45 private static final int DEFAULT_END_YEAR = 2100;
47 private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor };
49 private final Calendar mSelectedDay = Calendar.getInstance();
50 private final Calendar mMinDate = Calendar.getInstance();
51 private final Calendar mMaxDate = Calendar.getInstance();
53 private final AccessibilityManager mAccessibilityManager;
55 private final ViewPager mViewPager;
56 private final ImageButton mPrevButton;
57 private final ImageButton mNextButton;
59 private final DayPickerPagerAdapter mAdapter;
61 /** Temporary calendar used for date calculations. */
62 private Calendar mTempCalendar;
64 private OnDaySelectedListener mOnDaySelectedListener;
66 public DayPickerView(Context context) {
70 public DayPickerView(Context context, @Nullable AttributeSet attrs) {
71 this(context, attrs, R.attr.calendarViewStyle);
74 public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
75 this(context, attrs, defStyleAttr, 0);
78 public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
80 super(context, attrs, defStyleAttr, defStyleRes);
82 mAccessibilityManager = (AccessibilityManager) context.getSystemService(
83 Context.ACCESSIBILITY_SERVICE);
85 final TypedArray a = context.obtainStyledAttributes(attrs,
86 R.styleable.CalendarView, defStyleAttr, defStyleRes);
88 final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
89 LocaleData.get(Locale.getDefault()).firstDayOfWeek);
91 final String minDate = a.getString(R.styleable.CalendarView_minDate);
92 final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
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);
104 final ColorStateList daySelectorColor = a.getColorStateList(
105 R.styleable.CalendarView_daySelectorColor);
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);
117 final LayoutInflater inflater = LayoutInflater.from(context);
118 final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false);
120 // Transfer all children from content to here.
121 while (content.getChildCount() > 0) {
122 final View child = content.getChildAt(0);
123 content.removeViewAt(0);
127 mPrevButton = findViewById(R.id.prev);
128 mPrevButton.setOnClickListener(mOnClickListener);
130 mNextButton = findViewById(R.id.next);
131 mNextButton.setOnClickListener(mOnClickListener);
133 mViewPager = findViewById(R.id.day_picker_view_pager);
134 mViewPager.setAdapter(mAdapter);
135 mViewPager.setOnPageChangeListener(mOnPageChangedListener);
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);
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);
154 final long minDateMillis = tempDate.getTimeInMillis();
156 if (!CalendarView.parseDate(maxDate, tempDate)) {
157 tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
159 final long maxDateMillis = tempDate.getTimeInMillis();
161 if (maxDateMillis < minDateMillis) {
162 throw new IllegalArgumentException("maxDate must be >= minDate");
165 final long setDateMillis = MathUtils.constrain(
166 System.currentTimeMillis(), minDateMillis, maxDateMillis);
168 setFirstDayOfWeek(firstDayOfWeek);
169 setMinDate(minDateMillis);
170 setMaxDate(maxDateMillis);
171 setDate(setDateMillis, false);
173 // Proxy selection callbacks to our own listener.
174 mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() {
176 public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) {
177 if (mOnDaySelectedListener != null) {
178 mOnDaySelectedListener.onDaySelected(DayPickerView.this, day);
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);
192 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
193 final ViewPager viewPager = mViewPager;
194 measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
196 final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
197 final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
198 setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
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);
209 public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
210 super.onRtlPropertiesChanged(layoutDirection);
216 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
217 final ImageButton leftButton;
218 final ImageButton rightButton;
220 leftButton = mNextButton;
221 rightButton = mPrevButton;
223 leftButton = mPrevButton;
224 rightButton = mNextButton;
227 final int width = right - left;
228 final int height = bottom - top;
229 mViewPager.layout(0, 0, width, height);
231 final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
232 final int monthHeight = monthView.getMonthHeight();
233 final int cellWidth = monthView.getCellWidth();
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);
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);
251 public void setDayOfWeekTextAppearance(int resId) {
252 mAdapter.setDayOfWeekTextAppearance(resId);
255 public int getDayOfWeekTextAppearance() {
256 return mAdapter.getDayOfWeekTextAppearance();
259 public void setDayTextAppearance(int resId) {
260 mAdapter.setDayTextAppearance(resId);
263 public int getDayTextAppearance() {
264 return mAdapter.getDayTextAppearance();
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)}.
272 * @param timeInMillis the target day in milliseconds
274 public void setDate(long timeInMillis) {
275 setDate(timeInMillis, false);
279 * Sets the currently selected date to the specified timestamp. Jumps
280 * immediately to the new date, optionally animating the transition.
282 * @param timeInMillis the target day in milliseconds
283 * @param animate whether to smooth scroll to the new position
285 public void setDate(long timeInMillis, boolean animate) {
286 setDate(timeInMillis, animate, true);
290 * Moves to the month containing the specified day, optionally setting the
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
297 * @throws IllegalArgumentException if the build version is greater than
298 * {@link android.os.Build.VERSION_CODES#N_MR1} and the provided timeInMillis is before
299 * the range start or after the range end.
301 private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
302 getTempCalendarForTime(timeInMillis);
304 final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
305 if (targetSdkVersion > N_MR1) {
306 if (mTempCalendar.before(mMinDate) || mTempCalendar.after(mMaxDate)) {
307 throw new IllegalArgumentException("timeInMillis must be between the values of "
308 + "getMinDate() and getMaxDate()");
313 mSelectedDay.setTimeInMillis(timeInMillis);
316 final int position = getPositionFromDay(timeInMillis);
317 if (position != mViewPager.getCurrentItem()) {
318 mViewPager.setCurrentItem(position, animate);
321 mAdapter.setSelectedDay(mTempCalendar);
324 public long getDate() {
325 return mSelectedDay.getTimeInMillis();
328 public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
329 final int position = getPositionFromDay(timeInMillis);
330 if (position != mViewPager.getCurrentItem()) {
334 mTempCalendar.setTimeInMillis(timeInMillis);
335 return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
338 public void setFirstDayOfWeek(int firstDayOfWeek) {
339 mAdapter.setFirstDayOfWeek(firstDayOfWeek);
342 public int getFirstDayOfWeek() {
343 return mAdapter.getFirstDayOfWeek();
346 public void setMinDate(long timeInMillis) {
347 mMinDate.setTimeInMillis(timeInMillis);
351 public long getMinDate() {
352 return mMinDate.getTimeInMillis();
355 public void setMaxDate(long timeInMillis) {
356 mMaxDate.setTimeInMillis(timeInMillis);
360 public long getMaxDate() {
361 return mMaxDate.getTimeInMillis();
365 * Handles changes to date range.
367 public void onRangeChanged() {
368 mAdapter.setRange(mMinDate, mMaxDate);
370 // Changing the min/max date changes the selection position since we
371 // don't really have stable IDs. Jumps immediately to the new position.
372 setDate(mSelectedDay.getTimeInMillis(), false, false);
374 updateButtonVisibility(mViewPager.getCurrentItem());
378 * Sets the listener to call when the user selects a day.
380 * @param listener The listener to call.
382 public void setOnDaySelectedListener(OnDaySelectedListener listener) {
383 mOnDaySelectedListener = listener;
386 private int getDiffMonths(Calendar start, Calendar end) {
387 final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
388 return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
391 private int getPositionFromDay(long timeInMillis) {
392 final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
393 final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
394 return MathUtils.constrain(diffMonth, 0, diffMonthMax);
397 private Calendar getTempCalendarForTime(long timeInMillis) {
398 if (mTempCalendar == null) {
399 mTempCalendar = Calendar.getInstance();
401 mTempCalendar.setTimeInMillis(timeInMillis);
402 return mTempCalendar;
406 * Gets the position of the view that is most prominently displayed within the list view.
408 public int getMostVisiblePosition() {
409 return mViewPager.getCurrentItem();
412 public void setPosition(int position) {
413 mViewPager.setCurrentItem(position, false);
416 private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
418 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
419 final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
420 mPrevButton.setAlpha(alpha);
421 mNextButton.setAlpha(alpha);
425 public void onPageScrollStateChanged(int state) {}
428 public void onPageSelected(int position) {
429 updateButtonVisibility(position);
433 private final OnClickListener mOnClickListener = new OnClickListener() {
435 public void onClick(View v) {
437 if (v == mPrevButton) {
439 } else if (v == mNextButton) {
445 // Animation is expensive for accessibility services since it sends
446 // lots of scroll and content change events.
447 final boolean animate = !mAccessibilityManager.isEnabled();
449 // ViewPager clamps input values, so we don't need to worry
450 // about passing invalid indices.
451 final int nextItem = mViewPager.getCurrentItem() + direction;
452 mViewPager.setCurrentItem(nextItem, animate);
456 public interface OnDaySelectedListener {
457 void onDaySelected(DayPickerView view, Calendar day);