1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package android.support.v17.leanback.widget.picker; 16 17 import android.content.Context; 18 import android.content.res.TypedArray; 19 import android.support.v17.leanback.R; 20 import android.text.TextUtils; 21 import android.util.AttributeSet; 22 import android.util.Log; 23 24 import java.text.DateFormat; 25 import java.text.ParseException; 26 import java.text.SimpleDateFormat; 27 import java.util.ArrayList; 28 import java.util.Calendar; 29 import java.util.Locale; 30 import java.util.TimeZone; 31 32 /** 33 * {@link DatePicker} is a directly subclass of {@link Picker}. 34 * This class is a widget for selecting a date. The date can be selected by a 35 * year, month, and day Columns. The "minDate" and "maxDate" from which dates to be selected 36 * can be customized. The columns can be customized by attribute "datePickerFormat" or 37 * {@link #setDatePickerFormat(String)}. 38 * 39 * @attr ref R.styleable#lbDatePicker_android_maxDate 40 * @attr ref R.styleable#lbDatePicker_android_minDate 41 * @attr ref R.styleable#lbDatePicker_datePickerFormat 42 * @hide 43 */ 44 public class DatePicker extends Picker { 45 46 static final String LOG_TAG = "DatePicker"; 47 48 private String mDatePickerFormat; 49 PickerColumn mMonthColumn; 50 PickerColumn mDayColumn; 51 PickerColumn mYearColumn; 52 int mColMonthIndex; 53 int mColDayIndex; 54 int mColYearIndex; 55 56 final static String DATE_FORMAT = "MM/dd/yyyy"; 57 final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); 58 PickerConstant mConstant; 59 60 Calendar mMinDate; 61 Calendar mMaxDate; 62 Calendar mCurrentDate; 63 Calendar mTempDate; 64 DatePicker(Context context, AttributeSet attrs)65 public DatePicker(Context context, AttributeSet attrs) { 66 this(context, attrs, 0); 67 } 68 DatePicker(Context context, AttributeSet attrs, int defStyleAttr)69 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { 70 super(context, attrs, defStyleAttr); 71 72 updateCurrentLocale(); 73 setSeparator(mConstant.dateSeparator); 74 75 final TypedArray attributesArray = context.obtainStyledAttributes(attrs, 76 R.styleable.lbDatePicker); 77 String minDate = attributesArray.getString(R.styleable.lbDatePicker_android_minDate); 78 String maxDate = attributesArray.getString(R.styleable.lbDatePicker_android_maxDate); 79 mTempDate.clear(); 80 if (!TextUtils.isEmpty(minDate)) { 81 if (!parseDate(minDate, mTempDate)) { 82 mTempDate.set(1900, 0, 1); 83 } 84 } else { 85 mTempDate.set(1900, 0, 1); 86 } 87 mMinDate.setTimeInMillis(mTempDate.getTimeInMillis()); 88 89 mTempDate.clear(); 90 if (!TextUtils.isEmpty(maxDate)) { 91 if (!parseDate(maxDate, mTempDate)) { 92 mTempDate.set(2100, 0, 1); 93 } 94 } else { 95 mTempDate.set(2100, 0, 1); 96 } 97 mMaxDate.setTimeInMillis(mTempDate.getTimeInMillis()); 98 99 String datePickerFormat = attributesArray 100 .getString(R.styleable.lbDatePicker_datePickerFormat); 101 if (TextUtils.isEmpty(datePickerFormat)) { 102 datePickerFormat = new String( 103 android.text.format.DateFormat.getDateFormatOrder(context)); 104 } 105 setDatePickerFormat(datePickerFormat); 106 } 107 parseDate(String date, Calendar outDate)108 private boolean parseDate(String date, Calendar outDate) { 109 try { 110 outDate.setTime(mDateFormat.parse(date)); 111 return true; 112 } catch (ParseException e) { 113 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); 114 return false; 115 } 116 } 117 118 /** 119 * Changes format of showing dates. For example "YMD". 120 * @param datePickerFormat Format of showing dates. 121 */ setDatePickerFormat(String datePickerFormat)122 public void setDatePickerFormat(String datePickerFormat) { 123 if (TextUtils.isEmpty(datePickerFormat)) { 124 datePickerFormat = new String( 125 android.text.format.DateFormat.getDateFormatOrder(getContext())); 126 } 127 datePickerFormat = datePickerFormat.toUpperCase(); 128 if (TextUtils.equals(mDatePickerFormat, datePickerFormat)) { 129 return; 130 } 131 mDatePickerFormat = datePickerFormat; 132 mYearColumn = mMonthColumn = mDayColumn = null; 133 mColYearIndex = mColDayIndex = mColMonthIndex = -1; 134 ArrayList<PickerColumn> columns = new ArrayList<PickerColumn>(3); 135 for (int i = 0; i < datePickerFormat.length(); i++) { 136 switch (datePickerFormat.charAt(i)) { 137 case 'Y': 138 if (mYearColumn != null) { 139 throw new IllegalArgumentException("datePicker format error"); 140 } 141 columns.add(mYearColumn = new PickerColumn()); 142 mColYearIndex = i; 143 mYearColumn.setLabelFormat("%d"); 144 break; 145 case 'M': 146 if (mMonthColumn != null) { 147 throw new IllegalArgumentException("datePicker format error"); 148 } 149 columns.add(mMonthColumn = new PickerColumn()); 150 mMonthColumn.setStaticLabels(mConstant.months); 151 mColMonthIndex = i; 152 break; 153 case 'D': 154 if (mDayColumn != null) { 155 throw new IllegalArgumentException("datePicker format error"); 156 } 157 columns.add(mDayColumn = new PickerColumn()); 158 mDayColumn.setLabelFormat("%02d"); 159 mColDayIndex = i; 160 break; 161 default: 162 throw new IllegalArgumentException("datePicker format error"); 163 } 164 } 165 setColumns(columns); 166 updateSpinners(false); 167 } 168 169 /** 170 * Get format of showing dates. For example "YMD". Default value is from 171 * {@link android.text.format.DateFormat#getDateFormatOrder(Context)}. 172 * @return Format of showing dates. 173 */ getDatePickerFormat()174 public String getDatePickerFormat() { 175 return mDatePickerFormat; 176 } 177 getCalendarForLocale(Calendar oldCalendar, Locale locale)178 private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { 179 if (oldCalendar == null) { 180 return Calendar.getInstance(locale); 181 } else { 182 final long currentTimeMillis = oldCalendar.getTimeInMillis(); 183 Calendar newCalendar = Calendar.getInstance(locale); 184 newCalendar.setTimeInMillis(currentTimeMillis); 185 return newCalendar; 186 } 187 } 188 updateCurrentLocale()189 private void updateCurrentLocale() { 190 mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources()); 191 mTempDate = getCalendarForLocale(mTempDate, mConstant.locale); 192 mMinDate = getCalendarForLocale(mMinDate, mConstant.locale); 193 mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale); 194 mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale); 195 196 if (mMonthColumn != null) { 197 mMonthColumn.setStaticLabels(mConstant.months); 198 setColumnAt(mColMonthIndex, mMonthColumn); 199 } 200 } 201 202 @Override onColumnValueChanged(int column, int newVal)203 public final void onColumnValueChanged(int column, int newVal) { 204 mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); 205 // take care of wrapping of days and months to update greater fields 206 int oldVal = getColumnAt(column).getCurrentValue(); 207 if (column == mColDayIndex) { 208 mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); 209 } else if (column == mColMonthIndex) { 210 mTempDate.add(Calendar.MONTH, newVal - oldVal); 211 } else if (column == mColYearIndex) { 212 mTempDate.add(Calendar.YEAR, newVal - oldVal); 213 } else { 214 throw new IllegalArgumentException(); 215 } 216 setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), 217 mTempDate.get(Calendar.DAY_OF_MONTH)); 218 updateSpinners(false); 219 } 220 221 222 /** 223 * Sets the minimal date supported by this {@link DatePicker} in 224 * milliseconds since January 1, 1970 00:00:00 in 225 * {@link TimeZone#getDefault()} time zone. 226 * 227 * @param minDate The minimal supported date. 228 */ setMinDate(long minDate)229 public void setMinDate(long minDate) { 230 mTempDate.setTimeInMillis(minDate); 231 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 232 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { 233 return; 234 } 235 mMinDate.setTimeInMillis(minDate); 236 if (mCurrentDate.before(mMinDate)) { 237 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 238 } 239 updateSpinners(false); 240 } 241 242 243 /** 244 * Gets the minimal date supported by this {@link DatePicker} in 245 * milliseconds since January 1, 1970 00:00:00 in 246 * {@link TimeZone#getDefault()} time zone. 247 * <p> 248 * Note: The default minimal date is 01/01/1900. 249 * <p> 250 * 251 * @return The minimal supported date. 252 */ getMinDate()253 public long getMinDate() { 254 return mMinDate.getTimeInMillis(); 255 } 256 257 /** 258 * Sets the maximal date supported by this {@link DatePicker} in 259 * milliseconds since January 1, 1970 00:00:00 in 260 * {@link TimeZone#getDefault()} time zone. 261 * 262 * @param maxDate The maximal supported date. 263 */ setMaxDate(long maxDate)264 public void setMaxDate(long maxDate) { 265 mTempDate.setTimeInMillis(maxDate); 266 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 267 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { 268 return; 269 } 270 mMaxDate.setTimeInMillis(maxDate); 271 if (mCurrentDate.after(mMaxDate)) { 272 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 273 } 274 updateSpinners(false); 275 } 276 277 /** 278 * Gets the maximal date supported by this {@link DatePicker} in 279 * milliseconds since January 1, 1970 00:00:00 in 280 * {@link TimeZone#getDefault()} time zone. 281 * <p> 282 * Note: The default maximal date is 12/31/2100. 283 * <p> 284 * 285 * @return The maximal supported date. 286 */ getMaxDate()287 public long getMaxDate() { 288 return mMaxDate.getTimeInMillis(); 289 } 290 291 /** 292 * Gets current date value in milliseconds since January 1, 1970 00:00:00 in 293 * {@link TimeZone#getDefault()} time zone. 294 * 295 * @return Current date values. 296 */ getDate()297 public long getDate() { 298 return mCurrentDate.getTimeInMillis(); 299 } 300 setDate(int year, int month, int dayOfMonth)301 private void setDate(int year, int month, int dayOfMonth) { 302 mCurrentDate.set(year, month, dayOfMonth); 303 if (mCurrentDate.before(mMinDate)) { 304 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 305 } else if (mCurrentDate.after(mMaxDate)) { 306 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 307 } 308 } 309 310 /** 311 * Update the current date. 312 * 313 * @param year The year. 314 * @param month The month which is <strong>starting from zero</strong>. 315 * @param dayOfMonth The day of the month. 316 * @param animation True to run animation to scroll the column. 317 */ updateDate(int year, int month, int dayOfMonth, boolean animation)318 public void updateDate(int year, int month, int dayOfMonth, boolean animation) { 319 if (!isNewDate(year, month, dayOfMonth)) { 320 return; 321 } 322 setDate(year, month, dayOfMonth); 323 updateSpinners(animation); 324 } 325 isNewDate(int year, int month, int dayOfMonth)326 private boolean isNewDate(int year, int month, int dayOfMonth) { 327 return (mCurrentDate.get(Calendar.YEAR) != year 328 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth 329 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); 330 } 331 updateMin(PickerColumn column, int value)332 private static boolean updateMin(PickerColumn column, int value) { 333 if (value != column.getMinValue()) { 334 column.setMinValue(value); 335 return true; 336 } 337 return false; 338 } 339 updateMax(PickerColumn column, int value)340 private static boolean updateMax(PickerColumn column, int value) { 341 if (value != column.getMaxValue()) { 342 column.setMaxValue(value); 343 return true; 344 } 345 return false; 346 } 347 348 private static int[] DATE_FIELDS = {Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR}; 349 350 // Following implementation always keeps up-to-date date ranges (min & max values) no matter 351 // what the currently selected date is. This prevents the constant updating of date values while 352 // scrolling vertically and thus fixes the animation jumps that used to happen when we reached 353 // the endpoint date field values since the adapter values do not change while scrolling up 354 // & down across a single field. updateSpinnersImpl(boolean animation)355 private void updateSpinnersImpl(boolean animation) { 356 // set the spinner ranges respecting the min and max dates 357 int dateFieldIndices[] = {mColDayIndex, mColMonthIndex, mColYearIndex}; 358 359 boolean allLargerDateFieldsHaveBeenEqualToMinDate = true; 360 boolean allLargerDateFieldsHaveBeenEqualToMaxDate = true; 361 for(int i = DATE_FIELDS.length - 1; i >= 0; i--) { 362 boolean dateFieldChanged = false; 363 if (dateFieldIndices[i] < 0) 364 continue; 365 366 int currField = DATE_FIELDS[i]; 367 PickerColumn currPickerColumn = getColumnAt(dateFieldIndices[i]); 368 369 if (allLargerDateFieldsHaveBeenEqualToMinDate) { 370 dateFieldChanged |= updateMin(currPickerColumn, 371 mMinDate.get(currField)); 372 } else { 373 dateFieldChanged |= updateMin(currPickerColumn, 374 mCurrentDate.getActualMinimum(currField)); 375 } 376 377 if (allLargerDateFieldsHaveBeenEqualToMaxDate) { 378 dateFieldChanged |= updateMax(currPickerColumn, 379 mMaxDate.get(currField)); 380 } else { 381 dateFieldChanged |= updateMax(currPickerColumn, 382 mCurrentDate.getActualMaximum(currField)); 383 } 384 385 allLargerDateFieldsHaveBeenEqualToMinDate &= 386 (mCurrentDate.get(currField) == mMinDate.get(currField)); 387 allLargerDateFieldsHaveBeenEqualToMaxDate &= 388 (mCurrentDate.get(currField) == mMaxDate.get(currField)); 389 390 if (dateFieldChanged) { 391 setColumnAt(dateFieldIndices[i], currPickerColumn); 392 } 393 setColumnValue(dateFieldIndices[i], mCurrentDate.get(currField), animation); 394 } 395 } 396 updateSpinners(final boolean animation)397 private void updateSpinners(final boolean animation) { 398 // update range in a post call. The reason is that RV does not allow notifyDataSetChange() 399 // in scroll pass. UpdateSpinner can be called in a scroll pass, UpdateSpinner() may 400 // notifyDataSetChange to update the range. 401 post(new Runnable() { 402 @Override 403 public void run() { 404 updateSpinnersImpl(animation); 405 } 406 }); 407 } 408 }