DayPickerView.java revision fc9ea846803aa6184d440a3f0726da40b77d6ca0
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 17package android.widget; 18 19import static android.os.Build.VERSION_CODES.N_MR1; 20 21import android.annotation.Nullable; 22import android.content.Context; 23import android.content.res.ColorStateList; 24import android.content.res.TypedArray; 25import android.graphics.Rect; 26import android.icu.util.Calendar; 27import android.util.AttributeSet; 28import android.util.MathUtils; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.accessibility.AccessibilityManager; 33 34import com.android.internal.R; 35import com.android.internal.widget.ViewPager; 36import com.android.internal.widget.ViewPager.OnPageChangeListener; 37 38import libcore.icu.LocaleData; 39 40import java.util.Locale; 41 42class 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; 46 47 private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor }; 48 49 private final Calendar mSelectedDay = Calendar.getInstance(); 50 private final Calendar mMinDate = Calendar.getInstance(); 51 private final Calendar mMaxDate = Calendar.getInstance(); 52 53 private final AccessibilityManager mAccessibilityManager; 54 55 private final ViewPager mViewPager; 56 private final ImageButton mPrevButton; 57 private final ImageButton mNextButton; 58 59 private final DayPickerPagerAdapter mAdapter; 60 61 /** Temporary calendar used for date calculations. */ 62 private Calendar mTempCalendar; 63 64 private OnDaySelectedListener mOnDaySelectedListener; 65 66 public DayPickerView(Context context) { 67 this(context, null); 68 } 69 70 public DayPickerView(Context context, @Nullable AttributeSet attrs) { 71 this(context, attrs, R.attr.calendarViewStyle); 72 } 73 74 public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 75 this(context, attrs, defStyleAttr, 0); 76 } 77 78 public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 79 int defStyleRes) { 80 super(context, attrs, defStyleAttr, defStyleRes); 81 82 mAccessibilityManager = (AccessibilityManager) context.getSystemService( 83 Context.ACCESSIBILITY_SERVICE); 84 85 final TypedArray a = context.obtainStyledAttributes(attrs, 86 R.styleable.CalendarView, 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 = (ImageButton) findViewById(R.id.prev); 128 mPrevButton.setOnClickListener(mOnClickListener); 129 130 mNextButton = (ImageButton) findViewById(R.id.next); 131 mNextButton.setOnClickListener(mOnClickListener); 132 133 mViewPager = (ViewPager) 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 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 * @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. 300 */ 301 private void setDate(long timeInMillis, boolean animate, boolean setSelected) { 302 getTempCalendarForTime(timeInMillis); 303 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()"); 309 } 310 } 311 312 if (setSelected) { 313 mSelectedDay.setTimeInMillis(timeInMillis); 314 } 315 316 final int position = getPositionFromDay(timeInMillis); 317 if (position != mViewPager.getCurrentItem()) { 318 mViewPager.setCurrentItem(position, animate); 319 } 320 321 mAdapter.setSelectedDay(mTempCalendar); 322 } 323 324 public long getDate() { 325 return mSelectedDay.getTimeInMillis(); 326 } 327 328 public boolean getBoundsForDate(long timeInMillis, Rect outBounds) { 329 final int position = getPositionFromDay(timeInMillis); 330 if (position != mViewPager.getCurrentItem()) { 331 return false; 332 } 333 334 mTempCalendar.setTimeInMillis(timeInMillis); 335 return mAdapter.getBoundsForDate(mTempCalendar, outBounds); 336 } 337 338 public void setFirstDayOfWeek(int firstDayOfWeek) { 339 mAdapter.setFirstDayOfWeek(firstDayOfWeek); 340 } 341 342 public int getFirstDayOfWeek() { 343 return mAdapter.getFirstDayOfWeek(); 344 } 345 346 public void setMinDate(long timeInMillis) { 347 mMinDate.setTimeInMillis(timeInMillis); 348 onRangeChanged(); 349 } 350 351 public long getMinDate() { 352 return mMinDate.getTimeInMillis(); 353 } 354 355 public void setMaxDate(long timeInMillis) { 356 mMaxDate.setTimeInMillis(timeInMillis); 357 onRangeChanged(); 358 } 359 360 public long getMaxDate() { 361 return mMaxDate.getTimeInMillis(); 362 } 363 364 /** 365 * Handles changes to date range. 366 */ 367 public void onRangeChanged() { 368 mAdapter.setRange(mMinDate, mMaxDate); 369 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); 373 374 updateButtonVisibility(mViewPager.getCurrentItem()); 375 } 376 377 /** 378 * Sets the listener to call when the user selects a day. 379 * 380 * @param listener The listener to call. 381 */ 382 public void setOnDaySelectedListener(OnDaySelectedListener listener) { 383 mOnDaySelectedListener = listener; 384 } 385 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; 389 } 390 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); 395 } 396 397 private Calendar getTempCalendarForTime(long timeInMillis) { 398 if (mTempCalendar == null) { 399 mTempCalendar = Calendar.getInstance(); 400 } 401 mTempCalendar.setTimeInMillis(timeInMillis); 402 return mTempCalendar; 403 } 404 405 /** 406 * Gets the position of the view that is most prominently displayed within the list view. 407 */ 408 public int getMostVisiblePosition() { 409 return mViewPager.getCurrentItem(); 410 } 411 412 public void setPosition(int position) { 413 mViewPager.setCurrentItem(position, false); 414 } 415 416 private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() { 417 @Override 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); 422 } 423 424 @Override 425 public void onPageScrollStateChanged(int state) {} 426 427 @Override 428 public void onPageSelected(int position) { 429 updateButtonVisibility(position); 430 } 431 }; 432 433 private final OnClickListener mOnClickListener = new OnClickListener() { 434 @Override 435 public void onClick(View v) { 436 final int direction; 437 if (v == mPrevButton) { 438 direction = -1; 439 } else if (v == mNextButton) { 440 direction = 1; 441 } else { 442 return; 443 } 444 445 // Animation is expensive for accessibility services since it sends 446 // lots of scroll and content change events. 447 final boolean animate = !mAccessibilityManager.isEnabled(); 448 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); 453 } 454 }; 455 456 public interface OnDaySelectedListener { 457 void onDaySelected(DayPickerView view, Calendar day); 458 } 459} 460