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