| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.datepicker; |
| |
| import android.animation.LayoutTransition; |
| import android.content.Context; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.format.DateFormat; |
| import android.util.AttributeSet; |
| import android.util.SparseArray; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.widget.CheckBox; |
| import android.widget.CompoundButton; |
| import android.widget.CompoundButton.OnCheckedChangeListener; |
| import android.widget.FrameLayout; |
| import android.widget.LinearLayout; |
| import android.widget.NumberPicker; |
| import android.widget.NumberPicker.OnValueChangeListener; |
| |
| import com.android.contacts.R; |
| |
| import java.text.DateFormatSymbols; |
| import java.util.Calendar; |
| import java.util.Locale; |
| |
| /** |
| * This is a fork of the standard Android DatePicker that additionally allows toggling the year |
| * on/off. |
| * |
| * A view for selecting a month / year / day based on a calendar like layout. |
| * |
| * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date Picker |
| * tutorial</a>.</p> |
| * |
| * For a dialog using this view, see {@link android.app.DatePickerDialog}. |
| */ |
| public class DatePicker extends FrameLayout { |
| /** Magic year that represents "no year" */ |
| public static int NO_YEAR = 0; |
| |
| private static final int DEFAULT_START_YEAR = 1900; |
| private static final int DEFAULT_END_YEAR = 2100; |
| private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); |
| |
| /* UI Components */ |
| private final LinearLayout mPickerContainer; |
| private final CheckBox mYearToggle; |
| private final NumberPicker mDayPicker; |
| private final NumberPicker mMonthPicker; |
| private final NumberPicker mYearPicker; |
| |
| /** |
| * How we notify users the date has changed. |
| */ |
| private OnDateChangedListener mOnDateChangedListener; |
| |
| private int mDay; |
| private int mMonth; |
| private int mYear; |
| private boolean mYearOptional; |
| private boolean mHasYear; |
| |
| /** |
| * The callback used to indicate the user changes the date. |
| */ |
| public interface OnDateChangedListener { |
| |
| /** |
| * @param view The view associated with this listener. |
| * @param year The year that was set or {@link DatePicker#NO_YEAR} if no year was set |
| * @param monthOfYear The month that was set (0-11) for compatibility |
| * with {@link java.util.Calendar}. |
| * @param dayOfMonth The day of the month that was set. |
| */ |
| void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); |
| } |
| |
| public DatePicker(Context context) { |
| this(context, null); |
| } |
| |
| public DatePicker(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public DatePicker(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| |
| LayoutInflater inflater = (LayoutInflater) context.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| inflater.inflate(R.layout.date_picker, this, true); |
| |
| mPickerContainer = (LinearLayout) findViewById(R.id.parent); |
| mDayPicker = (NumberPicker) findViewById(R.id.day); |
| mDayPicker.setFormatter(sTwoDigitFormatter); |
| mDayPicker.setOnLongPressUpdateInterval(100); |
| mDayPicker.setOnValueChangedListener(new OnValueChangeListener() { |
| @Override |
| public void onValueChange(NumberPicker picker, int oldVal, int newVal) { |
| mDay = newVal; |
| notifyDateChanged(); |
| } |
| }); |
| mMonthPicker = (NumberPicker) findViewById(R.id.month); |
| mMonthPicker.setFormatter(sTwoDigitFormatter); |
| DateFormatSymbols dfs = new DateFormatSymbols(); |
| String[] months = dfs.getShortMonths(); |
| |
| /* |
| * If the user is in a locale where the month names are numeric, |
| * use just the number instead of the "month" character for |
| * consistency with the other fields. |
| */ |
| if (months[0].startsWith("1")) { |
| for (int i = 0; i < months.length; i++) { |
| months[i] = String.valueOf(i + 1); |
| } |
| mMonthPicker.setMinValue(1); |
| mMonthPicker.setMaxValue(12); |
| } else { |
| mMonthPicker.setMinValue(1); |
| mMonthPicker.setMaxValue(12); |
| mMonthPicker.setDisplayedValues(months); |
| } |
| |
| mMonthPicker.setOnLongPressUpdateInterval(200); |
| mMonthPicker.setOnValueChangedListener(new OnValueChangeListener() { |
| @Override |
| public void onValueChange(NumberPicker picker, int oldVal, int newVal) { |
| |
| /* We display the month 1-12 but store it 0-11 so always |
| * subtract by one to ensure our internal state is always 0-11 |
| */ |
| mMonth = newVal - 1; |
| // Adjust max day of the month |
| adjustMaxDay(); |
| notifyDateChanged(); |
| updateDaySpinner(); |
| } |
| }); |
| mYearPicker = (NumberPicker) findViewById(R.id.year); |
| mYearPicker.setOnLongPressUpdateInterval(100); |
| mYearPicker.setOnValueChangedListener(new OnValueChangeListener() { |
| @Override |
| public void onValueChange(NumberPicker picker, int oldVal, int newVal) { |
| mYear = newVal; |
| // Adjust max day for leap years if needed |
| adjustMaxDay(); |
| notifyDateChanged(); |
| updateDaySpinner(); |
| } |
| }); |
| mYearPicker.setMinValue(DEFAULT_START_YEAR); |
| mYearPicker.setMaxValue(DEFAULT_END_YEAR); |
| |
| mYearToggle = (CheckBox) findViewById(R.id.yearToggle); |
| mYearToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() { |
| @Override |
| public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { |
| mHasYear = isChecked; |
| adjustMaxDay(); |
| notifyDateChanged(); |
| updateSpinners(); |
| } |
| }); |
| |
| // initialize to current date |
| Calendar cal = Calendar.getInstance(); |
| init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null); |
| |
| // re-order the number pickers to match the current date format |
| reorderPickers(); |
| |
| mPickerContainer.setLayoutTransition(new LayoutTransition()); |
| if (!isEnabled()) { |
| setEnabled(false); |
| } |
| } |
| |
| @Override |
| public void setEnabled(boolean enabled) { |
| super.setEnabled(enabled); |
| mDayPicker.setEnabled(enabled); |
| mMonthPicker.setEnabled(enabled); |
| mYearPicker.setEnabled(enabled); |
| } |
| |
| private void reorderPickers() { |
| // We use numeric spinners for year and day, but textual months. Ask icu4c what |
| // order the user's locale uses for that combination. http://b/7207103. |
| String skeleton = mHasYear ? "yyyyMMMdd" : "MMMdd"; |
| String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); |
| char[] order = ICU.getDateFormatOrder(pattern); |
| |
| /* Remove the 3 pickers from their parent and then add them back in the |
| * required order. |
| */ |
| mPickerContainer.removeAllViews(); |
| for (char field : order) { |
| if (field == 'd') { |
| mPickerContainer.addView(mDayPicker); |
| } else if (field == 'M') { |
| mPickerContainer.addView(mMonthPicker); |
| } else { |
| // Either 'y' or '\u0000' depending on whether we're showing a year. |
| // If we're not showing a year, it doesn't matter where we put it, |
| // but the rest of this class assumes that it will be present (but GONE). |
| mPickerContainer.addView(mYearPicker); |
| } |
| } |
| } |
| |
| public void updateDate(int year, int monthOfYear, int dayOfMonth) { |
| if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) { |
| mYear = (mYearOptional && year == NO_YEAR) ? getCurrentYear() : year; |
| mMonth = monthOfYear; |
| mDay = dayOfMonth; |
| updateSpinners(); |
| reorderPickers(); |
| notifyDateChanged(); |
| } |
| } |
| |
| private int getCurrentYear() { |
| return Calendar.getInstance().get(Calendar.YEAR); |
| } |
| |
| private static class SavedState extends BaseSavedState { |
| |
| private final int mYear; |
| private final int mMonth; |
| private final int mDay; |
| private final boolean mHasYear; |
| private final boolean mYearOptional; |
| |
| /** |
| * Constructor called from {@link DatePicker#onSaveInstanceState()} |
| */ |
| private SavedState(Parcelable superState, int year, int month, int day, boolean hasYear, |
| boolean yearOptional) { |
| super(superState); |
| mYear = year; |
| mMonth = month; |
| mDay = day; |
| mHasYear = hasYear; |
| mYearOptional = yearOptional; |
| } |
| |
| /** |
| * Constructor called from {@link #CREATOR} |
| */ |
| private SavedState(Parcel in) { |
| super(in); |
| mYear = in.readInt(); |
| mMonth = in.readInt(); |
| mDay = in.readInt(); |
| mHasYear = in.readInt() != 0; |
| mYearOptional = in.readInt() != 0; |
| } |
| |
| public int getYear() { |
| return mYear; |
| } |
| |
| public int getMonth() { |
| return mMonth; |
| } |
| |
| public int getDay() { |
| return mDay; |
| } |
| |
| public boolean hasYear() { |
| return mHasYear; |
| } |
| |
| public boolean isYearOptional() { |
| return mYearOptional; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| super.writeToParcel(dest, flags); |
| dest.writeInt(mYear); |
| dest.writeInt(mMonth); |
| dest.writeInt(mDay); |
| dest.writeInt(mHasYear ? 1 : 0); |
| dest.writeInt(mYearOptional ? 1 : 0); |
| } |
| |
| @SuppressWarnings("unused") |
| public static final Parcelable.Creator<SavedState> CREATOR = |
| new Creator<SavedState>() { |
| |
| @Override |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| @Override |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| |
| |
| /** |
| * Override so we are in complete control of save / restore for this widget. |
| */ |
| @Override |
| protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { |
| dispatchThawSelfOnly(container); |
| } |
| |
| @Override |
| protected Parcelable onSaveInstanceState() { |
| Parcelable superState = super.onSaveInstanceState(); |
| |
| return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional); |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Parcelable state) { |
| SavedState ss = (SavedState) state; |
| super.onRestoreInstanceState(ss.getSuperState()); |
| mYear = ss.getYear(); |
| mMonth = ss.getMonth(); |
| mDay = ss.getDay(); |
| mHasYear = ss.hasYear(); |
| mYearOptional = ss.isYearOptional(); |
| updateSpinners(); |
| } |
| |
| /** |
| * Initialize the state. |
| * @param year The initial year. |
| * @param monthOfYear The initial month. |
| * @param dayOfMonth The initial day of the month. |
| * @param onDateChangedListener How user is notified date is changed by user, can be null. |
| */ |
| public void init(int year, int monthOfYear, int dayOfMonth, |
| OnDateChangedListener onDateChangedListener) { |
| init(year, monthOfYear, dayOfMonth, false, onDateChangedListener); |
| } |
| |
| /** |
| * Initialize the state. |
| * @param year The initial year or {@link #NO_YEAR} if no year has been specified |
| * @param monthOfYear The initial month. |
| * @param dayOfMonth The initial day of the month. |
| * @param yearOptional True if the user can toggle the year |
| * @param onDateChangedListener How user is notified date is changed by user, can be null. |
| */ |
| public void init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional, |
| OnDateChangedListener onDateChangedListener) { |
| mYear = (yearOptional && year == NO_YEAR) ? getCurrentYear() : year; |
| mMonth = monthOfYear; |
| mDay = dayOfMonth; |
| mYearOptional = yearOptional; |
| mHasYear = yearOptional ? (year != NO_YEAR) : true; |
| mOnDateChangedListener = onDateChangedListener; |
| updateSpinners(); |
| } |
| |
| private void updateSpinners() { |
| updateDaySpinner(); |
| mYearToggle.setChecked(mHasYear); |
| mYearToggle.setVisibility(mYearOptional ? View.VISIBLE : View.GONE); |
| mYearPicker.setValue(mYear); |
| mYearPicker.setVisibility(mHasYear ? View.VISIBLE : View.GONE); |
| |
| /* The month display uses 1-12 but our internal state stores it |
| * 0-11 so add one when setting the display. |
| */ |
| mMonthPicker.setValue(mMonth + 1); |
| } |
| |
| private void updateDaySpinner() { |
| Calendar cal = Calendar.getInstance(); |
| // if year was not set, use 2000 as it was a leap year |
| cal.set(mHasYear ? mYear : 2000, mMonth, 1); |
| int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); |
| mDayPicker.setMinValue(1); |
| mDayPicker.setMaxValue(max); |
| mDayPicker.setValue(mDay); |
| } |
| |
| public int getYear() { |
| return (mYearOptional && !mHasYear) ? NO_YEAR : mYear; |
| } |
| |
| public boolean isYearOptional() { |
| return mYearOptional; |
| } |
| |
| public int getMonth() { |
| return mMonth; |
| } |
| |
| public int getDayOfMonth() { |
| return mDay; |
| } |
| |
| private void adjustMaxDay(){ |
| Calendar cal = Calendar.getInstance(); |
| // if year was not set, use 2000 as it was a leap year |
| cal.set(Calendar.YEAR, mHasYear ? mYear : 2000); |
| cal.set(Calendar.MONTH, mMonth); |
| int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); |
| if (mDay > max) { |
| mDay = max; |
| } |
| } |
| |
| private void notifyDateChanged() { |
| if (mOnDateChangedListener != null) { |
| int year = (mYearOptional && !mHasYear) ? NO_YEAR : mYear; |
| mOnDateChangedListener.onDateChanged(DatePicker.this, year, mMonth, mDay); |
| } |
| } |
| } |