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