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