// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser.input; import android.content.Context; import android.content.res.Configuration; import android.os.Parcel; import android.os.Parcelable; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.widget.DatePicker; import android.widget.FrameLayout; import android.widget.NumberPicker; import android.widget.NumberPicker.OnValueChangeListener; import java.text.DateFormatSymbols; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; import java.util.TimeZone; import com.borqs.browser.R; // This class is heavily based on android.widget.DatePicker. public class MonthPicker extends FrameLayout { private static final int DEFAULT_START_YEAR = 1900; private static final int DEFAULT_END_YEAR = 2100; private static final boolean DEFAULT_ENABLED_STATE = true; private final NumberPicker mMonthSpinner; private final NumberPicker mYearSpinner; private Locale mCurrentLocale; private OnMonthChangedListener mMonthChangedListener; private String[] mShortMonths; private int mNumberOfMonths; private Calendar mMinDate; private Calendar mMaxDate; private Calendar mCurrentDate; private boolean mIsEnabled = DEFAULT_ENABLED_STATE; /** * The callback used to indicate the user changes\d the date. */ public interface OnMonthChangedListener { /** * Called upon a date change. * * @param view The view associated with this listener. * @param year The year that was set. * @param monthOfYear The month that was set (0-11) for compatibility * with {@link java.util.Calendar}. */ void onMonthChanged(MonthPicker view, int year, int monthOfYear); } public MonthPicker(Context context) { this(context, null); } public MonthPicker(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.datePickerStyle); } public MonthPicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // initialization based on locale setCurrentLocale(Locale.getDefault()); int startYear = DEFAULT_START_YEAR; int endYear = DEFAULT_END_YEAR; LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.month_picker, this, true); OnValueChangeListener onChangeListener = new OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); tempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); // take care of wrapping of days and months to update greater fields if (picker == mMonthSpinner) { if (oldVal == 11 && newVal == 0) { tempDate.add(Calendar.MONTH, 1); } else if (oldVal == 0 && newVal == 11) { tempDate.add(Calendar.MONTH, -1); } else { tempDate.add(Calendar.MONTH, newVal - oldVal); } } else if (picker == mYearSpinner) { tempDate.set(Calendar.YEAR, newVal); } else { throw new IllegalArgumentException(); } // now set the date to the adjusted one setDate(tempDate.get(Calendar.YEAR), tempDate.get(Calendar.MONTH)); updateSpinners(); notifyDateChanged(); } }; // month mMonthSpinner = (NumberPicker) findViewById(R.id.month); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(mNumberOfMonths - 1); mMonthSpinner.setDisplayedValues(mShortMonths); mMonthSpinner.setOnLongPressUpdateInterval(200); mMonthSpinner.setOnValueChangedListener(onChangeListener); // year mYearSpinner = (NumberPicker) findViewById(R.id.year); mYearSpinner.setOnLongPressUpdateInterval(100); mYearSpinner.setOnValueChangedListener(onChangeListener); Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); tempDate.set(startYear, 0, 1); setMinDate(tempDate.getTimeInMillis()); tempDate.set(endYear, 11, 31); setMaxDate(tempDate.getTimeInMillis()); // initialize to current date mCurrentDate.setTimeInMillis(System.currentTimeMillis()); init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), null); } /** * Gets the minimal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * <p> * Note: The default minimal date is 01/01/1900. * <p> * * @return The minimal supported date. */ public long getMinDate() { return mMinDate.getTimeInMillis(); } /** * Sets the minimal date supported by this {@link NumberPicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * * @param minDate The minimal supported date. */ public void setMinDate(long minDate) { Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); tempDate.setTimeInMillis(minDate); if (tempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) && tempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { return; } mMinDate.setTimeInMillis(minDate); if (mCurrentDate.before(mMinDate)) { mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); } updateSpinners(); } /** * Gets the maximal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * <p> * Note: The default maximal date is 12/31/2100. * <p> * * @return The maximal supported date. */ public long getMaxDate() { return mMaxDate.getTimeInMillis(); } /** * Sets the maximal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * * @param maxDate The maximal supported date. */ public void setMaxDate(long maxDate) { Calendar tempDate = getCalendarForLocale(null, mCurrentLocale); tempDate.setTimeInMillis(maxDate); if (tempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) && tempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { return; } mMaxDate.setTimeInMillis(maxDate); if (mCurrentDate.after(mMaxDate)) { mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); } updateSpinners(); } @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { return; } super.setEnabled(enabled); mMonthSpinner.setEnabled(enabled); mYearSpinner.setEnabled(enabled); mIsEnabled = enabled; } @Override public boolean isEnabled() { return mIsEnabled; } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { onPopulateAccessibilityEvent(event); return true; } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; String selectedDateUtterance = DateUtils.formatDateTime(getContext(), mCurrentDate.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setCurrentLocale(newConfig.locale); } /** * Sets the current locale. * * @param locale The current locale. */ private void setCurrentLocale(Locale locale) { if (locale.equals(mCurrentLocale)) { return; } mCurrentLocale = locale; mMinDate = getCalendarForLocale(mMinDate, locale); mMaxDate = getCalendarForLocale(mMaxDate, locale); mCurrentDate = getCalendarForLocale(mCurrentDate, locale); mShortMonths = DateFormatSymbols.getInstance(mCurrentLocale).getShortMonths(); mNumberOfMonths = mShortMonths.length; } /** * Gets a calendar for locale bootstrapped with the value of a given calendar. * * @param oldCalendar The old calendar. * @param locale The locale. */ private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { if (oldCalendar == null) { return Calendar.getInstance(locale); } else { final long currentTimeMillis = oldCalendar.getTimeInMillis(); Calendar newCalendar = Calendar.getInstance(locale); newCalendar.setTimeInMillis(currentTimeMillis); return newCalendar; } } /** * Updates the current date. * * @param year The year. * @param month The month which is <strong>starting from zero</strong>. */ public void updateMonth(int year, int month) { if (!isNewDate(year, month)) { return; } setDate(year, month); updateSpinners(); notifyDateChanged(); } // 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, getYear(), getMonth()); } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setDate(ss.mYear, ss.mMonth); updateSpinners(); } /** * Initialize the state. If the provided values designate an inconsistent * date the values are normalized before updating the spinners. * * @param year The initial year. * @param monthOfYear The initial month <strong>starting from zero</strong>. * @param onMonthChangedListener How user is notified date is changed by * user, can be null. */ public void init(int year, int monthOfYear, OnMonthChangedListener onMonthChangedListener) { setDate(year, monthOfYear); updateSpinners(); mMonthChangedListener = onMonthChangedListener; } private boolean isNewDate(int year, int month) { return (mCurrentDate.get(Calendar.YEAR) != year || mCurrentDate.get(Calendar.MONTH) != month); } private void setDate(int year, int month) { mCurrentDate.set(year, month, 1); if (mCurrentDate.before(mMinDate)) { mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); } else if (mCurrentDate.after(mMaxDate)) { mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); } } private void updateSpinners() { // set the spinner ranges respecting the min and max dates if (mCurrentDate.equals(mMinDate)) { mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); mMonthSpinner.setWrapSelectorWheel(false); } else if (mCurrentDate.equals(mMaxDate)) { mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); mMonthSpinner.setWrapSelectorWheel(false); } else { mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(11); mMonthSpinner.setWrapSelectorWheel(true); } // make sure the month names are a zero based array // with the months in the month spinner String[] displayedValues = Arrays.copyOfRange(mShortMonths, mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); mMonthSpinner.setDisplayedValues(displayedValues); // year spinner range does not change based on the current date mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); mYearSpinner.setWrapSelectorWheel(false); // set the spinner values mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); } /** * @return The selected year. */ public int getYear() { return mCurrentDate.get(Calendar.YEAR); } /** * @return The selected month. */ public int getMonth() { return mCurrentDate.get(Calendar.MONTH); } /** * @return The selected day of month. */ public int getDayOfMonth() { return mCurrentDate.get(Calendar.DAY_OF_MONTH); } /** * Notifies the listener, if such, for a change in the selected date. */ private void notifyDateChanged() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); if (mMonthChangedListener != null) { mMonthChangedListener.onMonthChanged(this, getYear(), getMonth()); } } /** * Class for managing state storing/restoring. */ private static class SavedState extends BaseSavedState { private final int mYear; private final int mMonth; /** * Constructor called from {@link DatePicker#onSaveInstanceState()} */ private SavedState(Parcelable superState, int year, int month) { super(superState); mYear = year; mMonth = month; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mYear = in.readInt(); mMonth = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mYear); dest.writeInt(mMonth); } @SuppressWarnings("all") // suppress unused and hiding public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }