1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser.input; 6 7import android.content.Context; 8import android.text.format.DateUtils; 9import android.view.LayoutInflater; 10import android.view.accessibility.AccessibilityEvent; 11import android.widget.FrameLayout; 12import android.widget.NumberPicker; 13import android.widget.NumberPicker.OnValueChangeListener; 14 15import org.chromium.content.R; 16 17import java.util.Calendar; 18import java.util.TimeZone; 19 20/** 21 * This class is heavily based on android.widget.DatePicker. 22 */ 23public abstract class TwoFieldDatePicker extends FrameLayout { 24 25 private final NumberPicker mPositionInYearSpinner; 26 27 private final NumberPicker mYearSpinner; 28 29 private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener; 30 31 // It'd be nice to use android.text.Time like in other Dialogs but 32 // it suffers from the 2038 effect so it would prevent us from 33 // having dates over 2038. 34 private Calendar mMinDate; 35 36 private Calendar mMaxDate; 37 38 private Calendar mCurrentDate; 39 40 /** 41 * The callback used to indicate the user changes\d the date. 42 */ 43 public interface OnMonthOrWeekChangedListener { 44 45 /** 46 * Called upon a date change. 47 * 48 * @param view The view associated with this listener. 49 * @param year The year that was set. 50 * @param positionInYear The month or week in year. 51 */ 52 void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear); 53 } 54 55 public TwoFieldDatePicker(Context context, double minValue, double maxValue) { 56 super(context, null, android.R.attr.datePickerStyle); 57 58 LayoutInflater inflater = (LayoutInflater) context 59 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 60 inflater.inflate(R.layout.two_field_date_picker, this, true); 61 62 OnValueChangeListener onChangeListener = new OnValueChangeListener() { 63 @Override 64 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 65 int year = getYear(); 66 int positionInYear = getPositionInYear(); 67 // take care of wrapping of days and months to update greater fields 68 if (picker == mPositionInYearSpinner) { 69 positionInYear = newVal; 70 if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) { 71 year += 1; 72 positionInYear = getMinPositionInYear(year); 73 } else if (oldVal == picker.getMinValue() && newVal == picker.getMaxValue()) { 74 year -= 1; 75 positionInYear = getMaxPositionInYear(year); 76 } 77 } else if (picker == mYearSpinner) { 78 year = newVal; 79 } else { 80 throw new IllegalArgumentException(); 81 } 82 83 // now set the date to the adjusted one 84 setCurrentDate(year, positionInYear); 85 updateSpinners(); 86 notifyDateChanged(); 87 } 88 }; 89 90 mCurrentDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 91 if (minValue >= maxValue) { 92 mMinDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 93 mMinDate.set(0, 0, 1); 94 mMaxDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 95 mMaxDate.set(9999, 0, 1); 96 } else { 97 mMinDate = getDateForValue(minValue); 98 mMaxDate = getDateForValue(maxValue); 99 } 100 101 // month 102 mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year); 103 mPositionInYearSpinner.setOnLongPressUpdateInterval(200); 104 mPositionInYearSpinner.setOnValueChangedListener(onChangeListener); 105 106 // year 107 mYearSpinner = (NumberPicker) findViewById(R.id.year); 108 mYearSpinner.setOnLongPressUpdateInterval(100); 109 mYearSpinner.setOnValueChangedListener(onChangeListener); 110 } 111 112 /** 113 * Initialize the state. If the provided values designate an inconsistent 114 * date the values are normalized before updating the spinners. 115 * 116 * @param year The initial year. 117 * @param positionInYear The initial month <strong>starting from zero</strong> or week in year. 118 * @param onMonthOrWeekChangedListener How user is notified date is changed by 119 * user, can be null. 120 */ 121 public void init(int year, int positionInYear, 122 OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) { 123 setCurrentDate(year, positionInYear); 124 updateSpinners(); 125 mMonthOrWeekChangedListener = onMonthOrWeekChangedListener; 126 } 127 128 public boolean isNewDate(int year, int positionInYear) { 129 return (getYear() != year || getPositionInYear() != positionInYear); 130 } 131 132 /** 133 * Subclasses know the semantics of @value, and need to return 134 * a Calendar corresponding to it. 135 */ 136 protected abstract Calendar getDateForValue(double value); 137 138 /** 139 * Updates the current date. 140 * 141 * @param year The year. 142 * @param positionInYear The month or week in year. 143 */ 144 public void updateDate(int year, int positionInYear) { 145 if (!isNewDate(year, positionInYear)) { 146 return; 147 } 148 setCurrentDate(year, positionInYear); 149 updateSpinners(); 150 notifyDateChanged(); 151 } 152 153 /** 154 * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the 155 * appropriate date. 156 */ 157 protected abstract void setCurrentDate(int year, int positionInYear); 158 159 protected void setCurrentDate(Calendar date) { 160 mCurrentDate = date; 161 } 162 163 @Override 164 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 165 onPopulateAccessibilityEvent(event); 166 return true; 167 } 168 169 @Override 170 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 171 super.onPopulateAccessibilityEvent(event); 172 173 final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 174 String selectedDateUtterance = DateUtils.formatDateTime(getContext(), 175 mCurrentDate.getTimeInMillis(), flags); 176 event.getText().add(selectedDateUtterance); 177 } 178 179 /** 180 * @return The selected year. 181 */ 182 public int getYear() { 183 return mCurrentDate.get(Calendar.YEAR); 184 } 185 186 /** 187 * @return The selected month or week. 188 */ 189 public abstract int getPositionInYear(); 190 191 protected abstract int getMaxYear(); 192 193 protected abstract int getMinYear(); 194 195 protected abstract int getMaxPositionInYear(int year); 196 197 protected abstract int getMinPositionInYear(int year); 198 199 protected Calendar getMaxDate() { 200 return mMaxDate; 201 } 202 203 protected Calendar getMinDate() { 204 return mMinDate; 205 } 206 207 protected Calendar getCurrentDate() { 208 return mCurrentDate; 209 } 210 211 protected NumberPicker getPositionInYearSpinner() { 212 return mPositionInYearSpinner; 213 } 214 215 protected NumberPicker getYearSpinner() { 216 return mYearSpinner; 217 } 218 219 /** 220 * This method should be subclassed to update the spinners based on mCurrentDate. 221 */ 222 protected void updateSpinners() { 223 mPositionInYearSpinner.setDisplayedValues(null); 224 225 // set the spinner ranges respecting the min and max dates 226 mPositionInYearSpinner.setMinValue(getMinPositionInYear(getYear())); 227 mPositionInYearSpinner.setMaxValue(getMaxPositionInYear(getYear())); 228 mPositionInYearSpinner.setWrapSelectorWheel( 229 !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate)); 230 231 // year spinner range does not change based on the current date 232 mYearSpinner.setMinValue(getMinYear()); 233 mYearSpinner.setMaxValue(getMaxYear()); 234 mYearSpinner.setWrapSelectorWheel(false); 235 236 // set the spinner values 237 mYearSpinner.setValue(getYear()); 238 mPositionInYearSpinner.setValue(getPositionInYear()); 239 } 240 241 /** 242 * Notifies the listener, if such, for a change in the selected date. 243 */ 244 protected void notifyDateChanged() { 245 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 246 if (mMonthOrWeekChangedListener != null) { 247 mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear()); 248 } 249 } 250} 251