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