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 */ onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth)96 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); 97 } 98 DatePicker(Context context)99 public DatePicker(Context context) { 100 this(context, null); 101 } 102 DatePicker(Context context, AttributeSet attrs)103 public DatePicker(Context context, AttributeSet attrs) { 104 this(context, attrs, 0); 105 } 106 DatePicker(Context context, AttributeSet attrs, int defStyle)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 mYearPicker.setMinValue(DEFAULT_START_YEAR); 175 mYearPicker.setMaxValue(DEFAULT_END_YEAR); 176 177 mYearToggle = (CheckBox) findViewById(R.id.yearToggle); 178 mYearToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() { 179 @Override 180 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 181 mHasYear = isChecked; 182 adjustMaxDay(); 183 notifyDateChanged(); 184 updateSpinners(); 185 } 186 }); 187 188 // initialize to current date 189 Calendar cal = Calendar.getInstance(); 190 init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null); 191 192 // re-order the number pickers to match the current date format 193 reorderPickers(); 194 195 mPickerContainer.setLayoutTransition(new LayoutTransition()); 196 if (!isEnabled()) { 197 setEnabled(false); 198 } 199 } 200 201 @Override setEnabled(boolean enabled)202 public void setEnabled(boolean enabled) { 203 super.setEnabled(enabled); 204 mDayPicker.setEnabled(enabled); 205 mMonthPicker.setEnabled(enabled); 206 mYearPicker.setEnabled(enabled); 207 } 208 reorderPickers()209 private void reorderPickers() { 210 // We use numeric spinners for year and day, but textual months. Ask icu4c what 211 // order the user's locale uses for that combination. http://b/7207103. 212 String skeleton = mHasYear ? "yyyyMMMdd" : "MMMdd"; 213 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 214 char[] order = ICU.getDateFormatOrder(pattern); 215 216 /* Remove the 3 pickers from their parent and then add them back in the 217 * required order. 218 */ 219 mPickerContainer.removeAllViews(); 220 for (char field : order) { 221 if (field == 'd') { 222 mPickerContainer.addView(mDayPicker); 223 } else if (field == 'M') { 224 mPickerContainer.addView(mMonthPicker); 225 } else { 226 // Either 'y' or '\u0000' depending on whether we're showing a year. 227 // If we're not showing a year, it doesn't matter where we put it, 228 // but the rest of this class assumes that it will be present (but GONE). 229 mPickerContainer.addView(mYearPicker); 230 } 231 } 232 } 233 updateDate(int year, int monthOfYear, int dayOfMonth)234 public void updateDate(int year, int monthOfYear, int dayOfMonth) { 235 if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) { 236 mYear = (mYearOptional && year == NO_YEAR) ? getCurrentYear() : year; 237 mMonth = monthOfYear; 238 mDay = dayOfMonth; 239 updateSpinners(); 240 reorderPickers(); 241 notifyDateChanged(); 242 } 243 } 244 getCurrentYear()245 private int getCurrentYear() { 246 return Calendar.getInstance().get(Calendar.YEAR); 247 } 248 249 private static class SavedState extends BaseSavedState { 250 251 private final int mYear; 252 private final int mMonth; 253 private final int mDay; 254 private final boolean mHasYear; 255 private final boolean mYearOptional; 256 257 /** 258 * Constructor called from {@link DatePicker#onSaveInstanceState()} 259 */ SavedState(Parcelable superState, int year, int month, int day, boolean hasYear, boolean yearOptional)260 private SavedState(Parcelable superState, int year, int month, int day, boolean hasYear, 261 boolean yearOptional) { 262 super(superState); 263 mYear = year; 264 mMonth = month; 265 mDay = day; 266 mHasYear = hasYear; 267 mYearOptional = yearOptional; 268 } 269 270 /** 271 * Constructor called from {@link #CREATOR} 272 */ SavedState(Parcel in)273 private SavedState(Parcel in) { 274 super(in); 275 mYear = in.readInt(); 276 mMonth = in.readInt(); 277 mDay = in.readInt(); 278 mHasYear = in.readInt() != 0; 279 mYearOptional = in.readInt() != 0; 280 } 281 getYear()282 public int getYear() { 283 return mYear; 284 } 285 getMonth()286 public int getMonth() { 287 return mMonth; 288 } 289 getDay()290 public int getDay() { 291 return mDay; 292 } 293 hasYear()294 public boolean hasYear() { 295 return mHasYear; 296 } 297 isYearOptional()298 public boolean isYearOptional() { 299 return mYearOptional; 300 } 301 302 @Override writeToParcel(Parcel dest, int flags)303 public void writeToParcel(Parcel dest, int flags) { 304 super.writeToParcel(dest, flags); 305 dest.writeInt(mYear); 306 dest.writeInt(mMonth); 307 dest.writeInt(mDay); 308 dest.writeInt(mHasYear ? 1 : 0); 309 dest.writeInt(mYearOptional ? 1 : 0); 310 } 311 312 @SuppressWarnings("unused") 313 public static final Parcelable.Creator<SavedState> CREATOR = 314 new Creator<SavedState>() { 315 316 @Override 317 public SavedState createFromParcel(Parcel in) { 318 return new SavedState(in); 319 } 320 321 @Override 322 public SavedState[] newArray(int size) { 323 return new SavedState[size]; 324 } 325 }; 326 } 327 328 329 /** 330 * Override so we are in complete control of save / restore for this widget. 331 */ 332 @Override dispatchRestoreInstanceState(SparseArray<Parcelable> container)333 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 334 dispatchThawSelfOnly(container); 335 } 336 337 @Override onSaveInstanceState()338 protected Parcelable onSaveInstanceState() { 339 Parcelable superState = super.onSaveInstanceState(); 340 341 return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional); 342 } 343 344 @Override onRestoreInstanceState(Parcelable state)345 protected void onRestoreInstanceState(Parcelable state) { 346 SavedState ss = (SavedState) state; 347 super.onRestoreInstanceState(ss.getSuperState()); 348 mYear = ss.getYear(); 349 mMonth = ss.getMonth(); 350 mDay = ss.getDay(); 351 mHasYear = ss.hasYear(); 352 mYearOptional = ss.isYearOptional(); 353 updateSpinners(); 354 } 355 356 /** 357 * Initialize the state. 358 * @param year The initial year. 359 * @param monthOfYear The initial month. 360 * @param dayOfMonth The initial day of the month. 361 * @param onDateChangedListener How user is notified date is changed by user, can be null. 362 */ init(int year, int monthOfYear, int dayOfMonth, OnDateChangedListener onDateChangedListener)363 public void init(int year, int monthOfYear, int dayOfMonth, 364 OnDateChangedListener onDateChangedListener) { 365 init(year, monthOfYear, dayOfMonth, false, onDateChangedListener); 366 } 367 368 /** 369 * Initialize the state. 370 * @param year The initial year or {@link #NO_YEAR} if no year has been specified 371 * @param monthOfYear The initial month. 372 * @param dayOfMonth The initial day of the month. 373 * @param yearOptional True if the user can toggle the year 374 * @param onDateChangedListener How user is notified date is changed by user, can be null. 375 */ init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional, OnDateChangedListener onDateChangedListener)376 public void init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional, 377 OnDateChangedListener onDateChangedListener) { 378 mYear = (yearOptional && year == NO_YEAR) ? getCurrentYear() : year; 379 mMonth = monthOfYear; 380 mDay = dayOfMonth; 381 mYearOptional = yearOptional; 382 mHasYear = yearOptional ? (year != NO_YEAR) : true; 383 mOnDateChangedListener = onDateChangedListener; 384 updateSpinners(); 385 } 386 updateSpinners()387 private void updateSpinners() { 388 updateDaySpinner(); 389 mYearToggle.setChecked(mHasYear); 390 mYearToggle.setVisibility(mYearOptional ? View.VISIBLE : View.GONE); 391 mYearPicker.setValue(mYear); 392 mYearPicker.setVisibility(mHasYear ? View.VISIBLE : View.GONE); 393 394 /* The month display uses 1-12 but our internal state stores it 395 * 0-11 so add one when setting the display. 396 */ 397 mMonthPicker.setValue(mMonth + 1); 398 } 399 updateDaySpinner()400 private void updateDaySpinner() { 401 Calendar cal = Calendar.getInstance(); 402 // if year was not set, use 2000 as it was a leap year 403 cal.set(mHasYear ? mYear : 2000, mMonth, 1); 404 int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); 405 mDayPicker.setMinValue(1); 406 mDayPicker.setMaxValue(max); 407 mDayPicker.setValue(mDay); 408 } 409 getYear()410 public int getYear() { 411 return (mYearOptional && !mHasYear) ? NO_YEAR : mYear; 412 } 413 isYearOptional()414 public boolean isYearOptional() { 415 return mYearOptional; 416 } 417 getMonth()418 public int getMonth() { 419 return mMonth; 420 } 421 getDayOfMonth()422 public int getDayOfMonth() { 423 return mDay; 424 } 425 adjustMaxDay()426 private void adjustMaxDay(){ 427 Calendar cal = Calendar.getInstance(); 428 // if year was not set, use 2000 as it was a leap year 429 cal.set(Calendar.YEAR, mHasYear ? mYear : 2000); 430 cal.set(Calendar.MONTH, mMonth); 431 int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); 432 if (mDay > max) { 433 mDay = max; 434 } 435 } 436 notifyDateChanged()437 private void notifyDateChanged() { 438 if (mOnDateChangedListener != null) { 439 int year = (mYearOptional && !mHasYear) ? NO_YEAR : mYear; 440 mOnDateChangedListener.onDateChanged(DatePicker.this, year, mMonth, mDay); 441 } 442 } 443 } 444