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