DayPickerView.java revision cf5a420ead56916cb3e8886574e1447cf17c8e19
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 android.annotation.Nullable; 20import android.content.Context; 21import android.content.res.ColorStateList; 22import android.content.res.TypedArray; 23import android.graphics.Rect; 24import android.icu.util.Calendar; 25import android.util.AttributeSet; 26import android.util.MathUtils; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.accessibility.AccessibilityManager; 31 32import com.android.internal.R; 33import com.android.internal.widget.ViewPager; 34import com.android.internal.widget.ViewPager.OnPageChangeListener; 35 36import libcore.icu.LocaleData; 37 38import java.util.Locale; 39 40class 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 64 public DayPickerView(Context context) { 65 this(context, null); 66 } 67 68 public DayPickerView(Context context, @Nullable AttributeSet attrs) { 69 this(context, attrs, R.attr.calendarViewStyle); 70 } 71 72 public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 73 this(context, attrs, defStyleAttr, 0); 74 } 75 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 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 getTempCalendarForTime(timeInMillis); 297 298 if (setSelected) { 299 mSelectedDay.setTimeInMillis(timeInMillis); 300 } 301 302 final int position = getPositionFromDay(timeInMillis); 303 if (position != mViewPager.getCurrentItem()) { 304 mViewPager.setCurrentItem(position, animate); 305 } 306 307 mAdapter.setSelectedDay(mTempCalendar); 308 } 309 310 public long getDate() { 311 return mSelectedDay.getTimeInMillis(); 312 } 313 314 public boolean getBoundsForDate(long timeInMillis, Rect outBounds) { 315 final int position = getPositionFromDay(timeInMillis); 316 if (position != mViewPager.getCurrentItem()) { 317 return false; 318 } 319 320 mTempCalendar.setTimeInMillis(timeInMillis); 321 return mAdapter.getBoundsForDate(mTempCalendar, outBounds); 322 } 323 324 public void setFirstDayOfWeek(int firstDayOfWeek) { 325 mAdapter.setFirstDayOfWeek(firstDayOfWeek); 326 } 327 328 public int getFirstDayOfWeek() { 329 return mAdapter.getFirstDayOfWeek(); 330 } 331 332 public void setMinDate(long timeInMillis) { 333 mMinDate.setTimeInMillis(timeInMillis); 334 onRangeChanged(); 335 } 336 337 public long getMinDate() { 338 return mMinDate.getTimeInMillis(); 339 } 340 341 public void setMaxDate(long timeInMillis) { 342 mMaxDate.setTimeInMillis(timeInMillis); 343 onRangeChanged(); 344 } 345 346 public long getMaxDate() { 347 return mMaxDate.getTimeInMillis(); 348 } 349 350 /** 351 * Handles changes to date range. 352 */ 353 public void onRangeChanged() { 354 mAdapter.setRange(mMinDate, mMaxDate); 355 356 // Clamp the selected day to the new min/max. 357 if (mSelectedDay.before(mMinDate)) { 358 mSelectedDay.setTimeInMillis(mMinDate.getTimeInMillis()); 359 } else if (mSelectedDay.after(mMaxDate)) { 360 mSelectedDay.setTimeInMillis(mMaxDate.getTimeInMillis()); 361 } 362 363 // Changing the min/max date changes the selection position since we 364 // don't really have stable IDs. Jumps immediately to the new position. 365 setDate(mSelectedDay.getTimeInMillis(), false, false); 366 367 updateButtonVisibility(mViewPager.getCurrentItem()); 368 } 369 370 /** 371 * Sets the listener to call when the user selects a day. 372 * 373 * @param listener The listener to call. 374 */ 375 public void setOnDaySelectedListener(OnDaySelectedListener listener) { 376 mOnDaySelectedListener = listener; 377 } 378 379 private int getDiffMonths(Calendar start, Calendar end) { 380 final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR); 381 return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears; 382 } 383 384 private int getPositionFromDay(long timeInMillis) { 385 final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate); 386 final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis)); 387 return MathUtils.constrain(diffMonth, 0, diffMonthMax); 388 } 389 390 private Calendar getTempCalendarForTime(long timeInMillis) { 391 if (mTempCalendar == null) { 392 mTempCalendar = Calendar.getInstance(); 393 } 394 mTempCalendar.setTimeInMillis(timeInMillis); 395 return mTempCalendar; 396 } 397 398 /** 399 * Gets the position of the view that is most prominently displayed within the list view. 400 */ 401 public int getMostVisiblePosition() { 402 return mViewPager.getCurrentItem(); 403 } 404 405 public void setPosition(int position) { 406 mViewPager.setCurrentItem(position, false); 407 } 408 409 private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() { 410 @Override 411 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 412 final float alpha = Math.abs(0.5f - positionOffset) * 2.0f; 413 mPrevButton.setAlpha(alpha); 414 mNextButton.setAlpha(alpha); 415 } 416 417 @Override 418 public void onPageScrollStateChanged(int state) {} 419 420 @Override 421 public void onPageSelected(int position) { 422 updateButtonVisibility(position); 423 } 424 }; 425 426 private final OnClickListener mOnClickListener = new OnClickListener() { 427 @Override 428 public void onClick(View v) { 429 final int direction; 430 if (v == mPrevButton) { 431 direction = -1; 432 } else if (v == mNextButton) { 433 direction = 1; 434 } else { 435 return; 436 } 437 438 // Animation is expensive for accessibility services since it sends 439 // lots of scroll and content change events. 440 final boolean animate = !mAccessibilityManager.isEnabled(); 441 442 // ViewPager clamps input values, so we don't need to worry 443 // about passing invalid indices. 444 final int nextItem = mViewPager.getCurrentItem() + direction; 445 mViewPager.setCurrentItem(nextItem, animate); 446 } 447 }; 448 449 public interface OnDaySelectedListener { 450 void onDaySelected(DayPickerView view, Calendar day); 451 } 452} 453