1 /* 2 * Copyright (C) 2012 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 com.android.test.hwui; 18 19 import android.annotation.Widget; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.text.format.DateFormat; 25 import android.util.AttributeSet; 26 import android.util.SparseArray; 27 import android.view.ContextThemeWrapper; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.widget.CheckBox; 31 import android.widget.CompoundButton; 32 import android.widget.FrameLayout; 33 import android.widget.LinearLayout; 34 import android.widget.NumberPicker; 35 36 import java.text.DateFormatSymbols; 37 import java.text.SimpleDateFormat; 38 import java.util.Calendar; 39 40 /** 41 * A view for selecting a month / year / day based on a calendar like layout. 42 * 43 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date Picker 44 * tutorial</a>.</p> 45 * 46 * For a dialog using this view, see {@link android.app.DatePickerDialog}. 47 */ 48 @Widget 49 public class DatePicker extends FrameLayout { 50 51 private static final int DEFAULT_START_YEAR = 1900; 52 private static final int DEFAULT_END_YEAR = 2100; 53 54 /* UI Components */ 55 private final CheckBox mYearToggle; 56 private final NumberPicker mDayPicker; 57 private final NumberPicker mMonthPicker; 58 private final NumberPicker mYearPicker; 59 60 /** 61 * How we notify users the date has changed. 62 */ 63 private OnDateChangedListener mOnDateChangedListener; 64 65 private int mDay; 66 private int mMonth; 67 private int mYear; 68 private boolean mYearOptional = true; 69 private boolean mHasYear; 70 71 /** 72 * The callback used to indicate the user changes the date. 73 */ 74 public interface OnDateChangedListener { 75 76 /** 77 * @param view The view associated with this listener. 78 * @param year The year that was set. 79 * @param monthOfYear The month that was set (0-11) for compatibility 80 * with {@link java.util.Calendar}. 81 * @param dayOfMonth The day of the month that was set. 82 */ 83 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); 84 } 85 86 public DatePicker(Context context) { 87 this(context, null); 88 } 89 90 public DatePicker(Context context, AttributeSet attrs) { 91 this(context, attrs, 0); 92 } 93 94 @SuppressWarnings("deprecation") 95 public DatePicker(Context context, AttributeSet attrs, int defStyle) { 96 super(context, attrs, defStyle); 97 98 ContextThemeWrapper themed = new ContextThemeWrapper(context, 99 com.android.internal.R.style.Theme_Holo_Light_Dialog_Alert); 100 LayoutInflater inflater = (LayoutInflater) themed.getSystemService( 101 Context.LAYOUT_INFLATER_SERVICE); 102 inflater.inflate(R.layout.date_picker, this, true); 103 104 mDayPicker = (NumberPicker) findViewById(R.id.day); 105 mDayPicker.setFormatter(NumberPicker.getTwoDigitFormatter()); 106 mDayPicker.setOnLongPressUpdateInterval(100); 107 mDayPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 108 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 109 mDay = newVal; 110 notifyDateChanged(); 111 } 112 }); 113 mMonthPicker = (NumberPicker) findViewById(R.id.month); 114 mMonthPicker.setFormatter(NumberPicker.getTwoDigitFormatter()); 115 DateFormatSymbols dfs = new DateFormatSymbols(); 116 String[] months = dfs.getShortMonths(); 117 118 /* 119 * If the user is in a locale where the month names are numeric, 120 * use just the number instead of the "month" character for 121 * consistency with the other fields. 122 */ 123 if (months[0].startsWith("1")) { 124 for (int i = 0; i < months.length; i++) { 125 months[i] = String.valueOf(i + 1); 126 } 127 mMonthPicker.setMinValue(1); 128 mMonthPicker.setMaxValue(12); 129 } else { 130 mMonthPicker.setMinValue(1); 131 mMonthPicker.setMaxValue(12); 132 mMonthPicker.setDisplayedValues(months); 133 } 134 135 mMonthPicker.setOnLongPressUpdateInterval(200); 136 mMonthPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 137 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 138 139 /* We display the month 1-12 but store it 0-11 so always 140 * subtract by one to ensure our internal state is always 0-11 141 */ 142 mMonth = newVal - 1; 143 // Adjust max day of the month 144 adjustMaxDay(); 145 notifyDateChanged(); 146 updateDaySpinner(); 147 } 148 }); 149 mYearPicker = (NumberPicker) findViewById(R.id.year); 150 mYearPicker.setOnLongPressUpdateInterval(100); 151 mYearPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 152 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 153 mYear = newVal; 154 // Adjust max day for leap years if needed 155 adjustMaxDay(); 156 notifyDateChanged(); 157 updateDaySpinner(); 158 } 159 }); 160 161 mYearToggle = (CheckBox) findViewById(R.id.yearToggle); 162 mYearToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 163 @Override 164 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 165 mHasYear = isChecked; 166 adjustMaxDay(); 167 notifyDateChanged(); 168 updateSpinners(); 169 } 170 }); 171 172 // attributes 173 TypedArray a = context.obtainStyledAttributes(attrs, 174 com.android.internal.R.styleable.DatePicker); 175 176 int mStartYear = 177 a.getInt(com.android.internal.R.styleable.DatePicker_startYear, DEFAULT_START_YEAR); 178 int mEndYear = 179 a.getInt(com.android.internal.R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); 180 mYearPicker.setMinValue(mStartYear); 181 mYearPicker.setMaxValue(mEndYear); 182 183 a.recycle(); 184 185 // initialize to current date 186 Calendar cal = Calendar.getInstance(); 187 init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null); 188 189 // re-order the number pickers to match the current date format 190 reorderPickers(months); 191 192 if (!isEnabled()) { 193 setEnabled(false); 194 } 195 } 196 197 @Override 198 public void setEnabled(boolean enabled) { 199 super.setEnabled(enabled); 200 mDayPicker.setEnabled(enabled); 201 mMonthPicker.setEnabled(enabled); 202 mYearPicker.setEnabled(enabled); 203 } 204 205 private void reorderPickers(String[] months) { 206 java.text.DateFormat format; 207 String order; 208 209 /* 210 * If the user is in a locale where the medium date format is 211 * still numeric (Japanese and Czech, for example), respect 212 * the date format order setting. Otherwise, use the order 213 * that the locale says is appropriate for a spelled-out date. 214 */ 215 216 if (months[0].startsWith("1")) { 217 format = DateFormat.getDateFormat(getContext()); 218 } else { 219 format = DateFormat.getMediumDateFormat(getContext()); 220 } 221 222 if (format instanceof SimpleDateFormat) { 223 order = ((SimpleDateFormat) format).toPattern(); 224 } else { 225 // Shouldn't happen, but just in case. 226 order = new String(DateFormat.getDateFormatOrder(getContext())); 227 } 228 229 /* Remove the 3 pickers from their parent and then add them back in the 230 * required order. 231 */ 232 LinearLayout parent = (LinearLayout) findViewById(R.id.parent); 233 parent.removeAllViews(); 234 235 boolean quoted = false; 236 boolean didDay = false, didMonth = false, didYear = false; 237 238 for (int i = 0; i < order.length(); i++) { 239 char c = order.charAt(i); 240 241 if (c == '\'') { 242 quoted = !quoted; 243 } 244 245 if (!quoted) { 246 if (c == DateFormat.DATE && !didDay) { 247 parent.addView(mDayPicker); 248 didDay = true; 249 } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { 250 parent.addView(mMonthPicker); 251 didMonth = true; 252 } else if (c == DateFormat.YEAR && !didYear) { 253 parent.addView (mYearPicker); 254 didYear = true; 255 } 256 } 257 } 258 259 // Shouldn't happen, but just in case. 260 if (!didMonth) { 261 parent.addView(mMonthPicker); 262 } 263 if (!didDay) { 264 parent.addView(mDayPicker); 265 } 266 if (!didYear) { 267 parent.addView(mYearPicker); 268 } 269 } 270 271 public void updateDate(int year, int monthOfYear, int dayOfMonth) { 272 if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) { 273 mYear = (mYearOptional && year == 0) ? getCurrentYear() : year; 274 mMonth = monthOfYear; 275 mDay = dayOfMonth; 276 updateSpinners(); 277 reorderPickers(new DateFormatSymbols().getShortMonths()); 278 notifyDateChanged(); 279 } 280 } 281 282 private static int getCurrentYear() { 283 return Calendar.getInstance().get(Calendar.YEAR); 284 } 285 286 private static class SavedState extends BaseSavedState { 287 288 private final int mYear; 289 private final int mMonth; 290 private final int mDay; 291 private final boolean mHasYear; 292 private final boolean mYearOptional; 293 294 /** 295 * Constructor called from {@link DatePicker#onSaveInstanceState()} 296 */ 297 private SavedState(Parcelable superState, int year, int month, int day, boolean hasYear, 298 boolean yearOptional) { 299 super(superState); 300 mYear = year; 301 mMonth = month; 302 mDay = day; 303 mHasYear = hasYear; 304 mYearOptional = yearOptional; 305 } 306 307 /** 308 * Constructor called from {@link #CREATOR} 309 */ 310 private SavedState(Parcel in) { 311 super(in); 312 mYear = in.readInt(); 313 mMonth = in.readInt(); 314 mDay = in.readInt(); 315 mHasYear = in.readInt() != 0; 316 mYearOptional = in.readInt() != 0; 317 } 318 319 public int getYear() { 320 return mYear; 321 } 322 323 public int getMonth() { 324 return mMonth; 325 } 326 327 public int getDay() { 328 return mDay; 329 } 330 331 public boolean hasYear() { 332 return mHasYear; 333 } 334 335 public boolean isYearOptional() { 336 return mYearOptional; 337 } 338 339 @Override 340 public void writeToParcel(Parcel dest, int flags) { 341 super.writeToParcel(dest, flags); 342 dest.writeInt(mYear); 343 dest.writeInt(mMonth); 344 dest.writeInt(mDay); 345 dest.writeInt(mHasYear ? 1 : 0); 346 dest.writeInt(mYearOptional ? 1 : 0); 347 } 348 349 @SuppressWarnings("unused") 350 public static final Parcelable.Creator<SavedState> CREATOR = 351 new Creator<SavedState>() { 352 353 public SavedState createFromParcel(Parcel in) { 354 return new SavedState(in); 355 } 356 357 public SavedState[] newArray(int size) { 358 return new SavedState[size]; 359 } 360 }; 361 } 362 363 364 /** 365 * Override so we are in complete control of save / restore for this widget. 366 */ 367 @Override 368 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 369 dispatchThawSelfOnly(container); 370 } 371 372 @Override 373 protected Parcelable onSaveInstanceState() { 374 Parcelable superState = super.onSaveInstanceState(); 375 376 return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional); 377 } 378 379 @Override 380 protected void onRestoreInstanceState(Parcelable state) { 381 SavedState ss = (SavedState) state; 382 super.onRestoreInstanceState(ss.getSuperState()); 383 mYear = ss.getYear(); 384 mMonth = ss.getMonth(); 385 mDay = ss.getDay(); 386 mHasYear = ss.hasYear(); 387 mYearOptional = ss.isYearOptional(); 388 updateSpinners(); 389 } 390 391 /** 392 * Initialize the state. 393 * @param year The initial year. 394 * @param monthOfYear The initial month. 395 * @param dayOfMonth The initial day of the month. 396 * @param onDateChangedListener How user is notified date is changed by user, can be null. 397 */ 398 public void init(int year, int monthOfYear, int dayOfMonth, 399 OnDateChangedListener onDateChangedListener) { 400 init(year, monthOfYear, dayOfMonth, false, onDateChangedListener); 401 } 402 403 /** 404 * Initialize the state. 405 * @param year The initial year or 0 if no year has been specified 406 * @param monthOfYear The initial month. 407 * @param dayOfMonth The initial day of the month. 408 * @param yearOptional True if the user can toggle the year 409 * @param onDateChangedListener How user is notified date is changed by user, can be null. 410 */ 411 public void init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional, 412 OnDateChangedListener onDateChangedListener) { 413 mYear = (yearOptional && year == 0) ? getCurrentYear() : year; 414 mMonth = monthOfYear; 415 mDay = dayOfMonth; 416 mYearOptional = yearOptional; 417 mHasYear = !yearOptional || (year != 0); 418 mOnDateChangedListener = onDateChangedListener; 419 updateSpinners(); 420 } 421 422 private void updateSpinners() { 423 updateDaySpinner(); 424 mYearToggle.setChecked(mHasYear); 425 mYearToggle.setVisibility(mYearOptional ? View.VISIBLE : View.GONE); 426 mYearPicker.setValue(mYear); 427 mYearPicker.setVisibility(mHasYear ? View.VISIBLE : View.GONE); 428 429 /* The month display uses 1-12 but our internal state stores it 430 * 0-11 so add one when setting the display. 431 */ 432 mMonthPicker.setValue(mMonth + 1); 433 } 434 435 private void updateDaySpinner() { 436 Calendar cal = Calendar.getInstance(); 437 // if year was not set, use 2000 as it was a leap year 438 cal.set(mHasYear ? mYear : 2000, mMonth, 1); 439 int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); 440 mDayPicker.setMinValue(1); 441 mDayPicker.setMaxValue(max); 442 mDayPicker.setValue(mDay); 443 } 444 445 public int getYear() { 446 return (mYearOptional && !mHasYear) ? 0 : mYear; 447 } 448 449 public int getMonth() { 450 return mMonth; 451 } 452 453 public int getDayOfMonth() { 454 return mDay; 455 } 456 457 private void adjustMaxDay(){ 458 Calendar cal = Calendar.getInstance(); 459 // if year was not set, use 2000 as it was a leap year 460 cal.set(Calendar.YEAR, mHasYear ? mYear : 2000); 461 cal.set(Calendar.MONTH, mMonth); 462 int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); 463 if (mDay > max) { 464 mDay = max; 465 } 466 } 467 468 private void notifyDateChanged() { 469 if (mOnDateChangedListener != null) { 470 int year = (mYearOptional && !mHasYear) ? 0 : mYear; 471 mOnDateChangedListener.onDateChanged(DatePicker.this, year, mMonth, mDay); 472 } 473 } 474 } 475