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