1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.datetimepicker.date; 18 19 import android.animation.ObjectAnimator; 20 import android.app.Activity; 21 import android.app.DialogFragment; 22 import android.content.res.Resources; 23 import android.os.Bundle; 24 import android.text.format.DateUtils; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.ViewGroup; 30 import android.view.Window; 31 import android.view.WindowManager; 32 import android.view.animation.AlphaAnimation; 33 import android.view.animation.Animation; 34 import android.widget.Button; 35 import android.widget.LinearLayout; 36 import android.widget.TextView; 37 38 import com.android.datetimepicker.HapticFeedbackController; 39 import com.android.datetimepicker.R; 40 import com.android.datetimepicker.Utils; 41 import com.android.datetimepicker.date.MonthAdapter.CalendarDay; 42 43 import java.text.SimpleDateFormat; 44 import java.util.Calendar; 45 import java.util.HashSet; 46 import java.util.Iterator; 47 import java.util.Locale; 48 49 /** 50 * Dialog allowing users to select a date. 51 */ 52 public class DatePickerDialog extends DialogFragment implements 53 OnClickListener, DatePickerController { 54 55 private static final String TAG = "DatePickerDialog"; 56 57 private static final int UNINITIALIZED = -1; 58 private static final int MONTH_AND_DAY_VIEW = 0; 59 private static final int YEAR_VIEW = 1; 60 61 private static final String KEY_SELECTED_YEAR = "year"; 62 private static final String KEY_SELECTED_MONTH = "month"; 63 private static final String KEY_SELECTED_DAY = "day"; 64 private static final String KEY_LIST_POSITION = "list_position"; 65 private static final String KEY_WEEK_START = "week_start"; 66 private static final String KEY_YEAR_START = "year_start"; 67 private static final String KEY_YEAR_END = "year_end"; 68 private static final String KEY_CURRENT_VIEW = "current_view"; 69 private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; 70 71 private static final int DEFAULT_START_YEAR = 1900; 72 private static final int DEFAULT_END_YEAR = 2100; 73 74 private static final int ANIMATION_DURATION = 300; 75 private static final int ANIMATION_DELAY = 500; 76 77 private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); 78 private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault()); 79 80 private final Calendar mCalendar = Calendar.getInstance(); 81 private OnDateSetListener mCallBack; 82 private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>(); 83 84 private AccessibleDateAnimator mAnimator; 85 86 private TextView mDayOfWeekView; 87 private LinearLayout mMonthAndDayView; 88 private TextView mSelectedMonthTextView; 89 private TextView mSelectedDayTextView; 90 private TextView mYearView; 91 private DayPickerView mDayPickerView; 92 private YearPickerView mYearPickerView; 93 private Button mDoneButton; 94 95 private int mCurrentView = UNINITIALIZED; 96 97 private int mWeekStart = mCalendar.getFirstDayOfWeek(); 98 private int mMinYear = DEFAULT_START_YEAR; 99 private int mMaxYear = DEFAULT_END_YEAR; 100 private Calendar mMinDate; 101 private Calendar mMaxDate; 102 103 private HapticFeedbackController mHapticFeedbackController; 104 105 private boolean mDelayAnimation = true; 106 107 // Accessibility strings. 108 private String mDayPickerDescription; 109 private String mSelectDay; 110 private String mYearPickerDescription; 111 private String mSelectYear; 112 113 /** 114 * The callback used to indicate the user is done filling in the date. 115 */ 116 public interface OnDateSetListener { 117 118 /** 119 * @param view The view associated with this listener. 120 * @param year The year that was set. 121 * @param monthOfYear The month that was set (0-11) for compatibility 122 * with {@link java.util.Calendar}. 123 * @param dayOfMonth The day of the month that was set. 124 */ onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth)125 void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth); 126 } 127 128 /** 129 * The callback used to notify other date picker components of a change in selected date. 130 */ 131 public interface OnDateChangedListener { 132 onDateChanged()133 public void onDateChanged(); 134 } 135 136 DatePickerDialog()137 public DatePickerDialog() { 138 // Empty constructor required for dialog fragment. 139 } 140 141 /** 142 * @param callBack How the parent is notified that the date is set. 143 * @param year The initial year of the dialog. 144 * @param monthOfYear The initial month of the dialog. 145 * @param dayOfMonth The initial day of the dialog. 146 */ newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)147 public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, 148 int monthOfYear, 149 int dayOfMonth) { 150 DatePickerDialog ret = new DatePickerDialog(); 151 ret.initialize(callBack, year, monthOfYear, dayOfMonth); 152 return ret; 153 } 154 initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)155 public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { 156 mCallBack = callBack; 157 mCalendar.set(Calendar.YEAR, year); 158 mCalendar.set(Calendar.MONTH, monthOfYear); 159 mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); 160 } 161 162 @Override onCreate(Bundle savedInstanceState)163 public void onCreate(Bundle savedInstanceState) { 164 super.onCreate(savedInstanceState); 165 final Activity activity = getActivity(); 166 activity.getWindow().setSoftInputMode( 167 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 168 if (savedInstanceState != null) { 169 mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR)); 170 mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH)); 171 mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY)); 172 } 173 } 174 175 @Override onSaveInstanceState(Bundle outState)176 public void onSaveInstanceState(Bundle outState) { 177 super.onSaveInstanceState(outState); 178 outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR)); 179 outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH)); 180 outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH)); 181 outState.putInt(KEY_WEEK_START, mWeekStart); 182 outState.putInt(KEY_YEAR_START, mMinYear); 183 outState.putInt(KEY_YEAR_END, mMaxYear); 184 outState.putInt(KEY_CURRENT_VIEW, mCurrentView); 185 int listPosition = -1; 186 if (mCurrentView == MONTH_AND_DAY_VIEW) { 187 listPosition = mDayPickerView.getMostVisiblePosition(); 188 } else if (mCurrentView == YEAR_VIEW) { 189 listPosition = mYearPickerView.getFirstVisiblePosition(); 190 outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); 191 } 192 outState.putInt(KEY_LIST_POSITION, listPosition); 193 } 194 195 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)196 public View onCreateView(LayoutInflater inflater, ViewGroup container, 197 Bundle savedInstanceState) { 198 Log.d(TAG, "onCreateView: "); 199 getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); 200 201 View view = inflater.inflate(R.layout.date_picker_dialog, null); 202 203 mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header); 204 mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day); 205 mMonthAndDayView.setOnClickListener(this); 206 mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month); 207 mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day); 208 mYearView = (TextView) view.findViewById(R.id.date_picker_year); 209 mYearView.setOnClickListener(this); 210 211 int listPosition = -1; 212 int listPositionOffset = 0; 213 int currentView = MONTH_AND_DAY_VIEW; 214 if (savedInstanceState != null) { 215 mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); 216 mMinYear = savedInstanceState.getInt(KEY_YEAR_START); 217 mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); 218 currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); 219 listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); 220 listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); 221 } 222 223 final Activity activity = getActivity(); 224 mDayPickerView = new SimpleDayPickerView(activity, this); 225 mYearPickerView = new YearPickerView(activity, this); 226 227 Resources res = getResources(); 228 mDayPickerDescription = res.getString(R.string.day_picker_description); 229 mSelectDay = res.getString(R.string.select_day); 230 mYearPickerDescription = res.getString(R.string.year_picker_description); 231 mSelectYear = res.getString(R.string.select_year); 232 233 mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator); 234 mAnimator.addView(mDayPickerView); 235 mAnimator.addView(mYearPickerView); 236 mAnimator.setDateMillis(mCalendar.getTimeInMillis()); 237 // TODO: Replace with animation decided upon by the design team. 238 Animation animation = new AlphaAnimation(0.0f, 1.0f); 239 animation.setDuration(ANIMATION_DURATION); 240 mAnimator.setInAnimation(animation); 241 // TODO: Replace with animation decided upon by the design team. 242 Animation animation2 = new AlphaAnimation(1.0f, 0.0f); 243 animation2.setDuration(ANIMATION_DURATION); 244 mAnimator.setOutAnimation(animation2); 245 246 mDoneButton = (Button) view.findViewById(R.id.done); 247 mDoneButton.setOnClickListener(new OnClickListener() { 248 249 @Override 250 public void onClick(View v) { 251 tryVibrate(); 252 if (mCallBack != null) { 253 mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR), 254 mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); 255 } 256 dismiss(); 257 } 258 }); 259 260 updateDisplay(false); 261 setCurrentView(currentView); 262 263 if (listPosition != -1) { 264 if (currentView == MONTH_AND_DAY_VIEW) { 265 mDayPickerView.postSetSelection(listPosition); 266 } else if (currentView == YEAR_VIEW) { 267 mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); 268 } 269 } 270 271 mHapticFeedbackController = new HapticFeedbackController(activity); 272 return view; 273 } 274 275 @Override onResume()276 public void onResume() { 277 super.onResume(); 278 mHapticFeedbackController.start(); 279 } 280 281 @Override onPause()282 public void onPause() { 283 super.onPause(); 284 mHapticFeedbackController.stop(); 285 } 286 setCurrentView(final int viewIndex)287 private void setCurrentView(final int viewIndex) { 288 long millis = mCalendar.getTimeInMillis(); 289 290 switch (viewIndex) { 291 case MONTH_AND_DAY_VIEW: 292 ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, 293 1.05f); 294 if (mDelayAnimation) { 295 pulseAnimator.setStartDelay(ANIMATION_DELAY); 296 mDelayAnimation = false; 297 } 298 mDayPickerView.onDateChanged(); 299 if (mCurrentView != viewIndex) { 300 mMonthAndDayView.setSelected(true); 301 mYearView.setSelected(false); 302 mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); 303 mCurrentView = viewIndex; 304 } 305 pulseAnimator.start(); 306 307 int flags = DateUtils.FORMAT_SHOW_DATE; 308 String dayString = DateUtils.formatDateTime(getActivity(), millis, flags); 309 mAnimator.setContentDescription(mDayPickerDescription+": "+dayString); 310 Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); 311 break; 312 case YEAR_VIEW: 313 pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); 314 if (mDelayAnimation) { 315 pulseAnimator.setStartDelay(ANIMATION_DELAY); 316 mDelayAnimation = false; 317 } 318 mYearPickerView.onDateChanged(); 319 if (mCurrentView != viewIndex) { 320 mMonthAndDayView.setSelected(false); 321 mYearView.setSelected(true); 322 mAnimator.setDisplayedChild(YEAR_VIEW); 323 mCurrentView = viewIndex; 324 } 325 pulseAnimator.start(); 326 327 CharSequence yearString = YEAR_FORMAT.format(millis); 328 mAnimator.setContentDescription(mYearPickerDescription+": "+yearString); 329 Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); 330 break; 331 } 332 } 333 updateDisplay(boolean announce)334 private void updateDisplay(boolean announce) { 335 if (mDayOfWeekView != null) { 336 mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, 337 Locale.getDefault()).toUpperCase(Locale.getDefault())); 338 } 339 340 mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, 341 Locale.getDefault()).toUpperCase(Locale.getDefault())); 342 mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime())); 343 mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime())); 344 345 // Accessibility. 346 long millis = mCalendar.getTimeInMillis(); 347 mAnimator.setDateMillis(millis); 348 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; 349 String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags); 350 mMonthAndDayView.setContentDescription(monthAndDayText); 351 352 if (announce) { 353 flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 354 String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags); 355 Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); 356 } 357 } 358 setFirstDayOfWeek(int startOfWeek)359 public void setFirstDayOfWeek(int startOfWeek) { 360 if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { 361 throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " + 362 "Calendar.SATURDAY"); 363 } 364 mWeekStart = startOfWeek; 365 if (mDayPickerView != null) { 366 mDayPickerView.onChange(); 367 } 368 } 369 setYearRange(int startYear, int endYear)370 public void setYearRange(int startYear, int endYear) { 371 if (endYear <= startYear) { 372 throw new IllegalArgumentException("Year end must be larger than year start"); 373 } 374 mMinYear = startYear; 375 mMaxYear = endYear; 376 if (mDayPickerView != null) { 377 mDayPickerView.onChange(); 378 } 379 } 380 381 /** 382 * Sets the minimal date supported by this DatePicker. Dates before (but not including) the 383 * specified date will be disallowed from being selected. 384 * @param calendar a Calendar object set to the year, month, day desired as the mindate. 385 */ setMinDate(Calendar calendar)386 public void setMinDate(Calendar calendar) { 387 mMinDate = calendar; 388 389 if (mDayPickerView != null) { 390 mDayPickerView.onChange(); 391 } 392 } 393 394 /** 395 * @return The minimal date supported by this DatePicker. Null if it has not been set. 396 */ 397 @Override getMinDate()398 public Calendar getMinDate() { 399 return mMinDate; 400 } 401 402 /** 403 * Sets the minimal date supported by this DatePicker. Dates after (but not including) the 404 * specified date will be disallowed from being selected. 405 * @param calendar a Calendar object set to the year, month, day desired as the maxdate. 406 */ setMaxDate(Calendar calendar)407 public void setMaxDate(Calendar calendar) { 408 mMaxDate = calendar; 409 410 if (mDayPickerView != null) { 411 mDayPickerView.onChange(); 412 } 413 } 414 415 /** 416 * @return The maximal date supported by this DatePicker. Null if it has not been set. 417 */ 418 @Override getMaxDate()419 public Calendar getMaxDate() { 420 return mMaxDate; 421 } 422 setOnDateSetListener(OnDateSetListener listener)423 public void setOnDateSetListener(OnDateSetListener listener) { 424 mCallBack = listener; 425 } 426 427 // If the newly selected month / year does not contain the currently selected day number, 428 // change the selected day number to the last day of the selected month or year. 429 // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 430 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 adjustDayInMonthIfNeeded(int month, int year)431 private void adjustDayInMonthIfNeeded(int month, int year) { 432 int day = mCalendar.get(Calendar.DAY_OF_MONTH); 433 int daysInMonth = Utils.getDaysInMonth(month, year); 434 if (day > daysInMonth) { 435 mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth); 436 } 437 } 438 439 @Override onClick(View v)440 public void onClick(View v) { 441 tryVibrate(); 442 if (v.getId() == R.id.date_picker_year) { 443 setCurrentView(YEAR_VIEW); 444 } else if (v.getId() == R.id.date_picker_month_and_day) { 445 setCurrentView(MONTH_AND_DAY_VIEW); 446 } 447 } 448 449 @Override onYearSelected(int year)450 public void onYearSelected(int year) { 451 adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year); 452 mCalendar.set(Calendar.YEAR, year); 453 updatePickers(); 454 setCurrentView(MONTH_AND_DAY_VIEW); 455 updateDisplay(true); 456 } 457 458 @Override onDayOfMonthSelected(int year, int month, int day)459 public void onDayOfMonthSelected(int year, int month, int day) { 460 mCalendar.set(Calendar.YEAR, year); 461 mCalendar.set(Calendar.MONTH, month); 462 mCalendar.set(Calendar.DAY_OF_MONTH, day); 463 updatePickers(); 464 updateDisplay(true); 465 } 466 updatePickers()467 private void updatePickers() { 468 Iterator<OnDateChangedListener> iterator = mListeners.iterator(); 469 while (iterator.hasNext()) { 470 iterator.next().onDateChanged(); 471 } 472 } 473 474 475 @Override getSelectedDay()476 public CalendarDay getSelectedDay() { 477 return new CalendarDay(mCalendar); 478 } 479 480 @Override getMinYear()481 public int getMinYear() { 482 return mMinYear; 483 } 484 485 @Override getMaxYear()486 public int getMaxYear() { 487 return mMaxYear; 488 } 489 490 @Override getFirstDayOfWeek()491 public int getFirstDayOfWeek() { 492 return mWeekStart; 493 } 494 495 @Override registerOnDateChangedListener(OnDateChangedListener listener)496 public void registerOnDateChangedListener(OnDateChangedListener listener) { 497 mListeners.add(listener); 498 } 499 500 @Override unregisterOnDateChangedListener(OnDateChangedListener listener)501 public void unregisterOnDateChangedListener(OnDateChangedListener listener) { 502 mListeners.remove(listener); 503 } 504 505 @Override tryVibrate()506 public void tryVibrate() { 507 mHapticFeedbackController.tryVibrate(); 508 } 509 } 510