blob: c6708a148e4096b15e07dc91ad7a3b4950b66660 [file] [log] [blame]
/*
* 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);
}
}
}