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