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