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 17package com.android.datetimepicker.date; 18 19import android.animation.ObjectAnimator; 20import android.app.Activity; 21import android.app.DialogFragment; 22import android.content.res.Resources; 23import android.os.Bundle; 24import android.text.format.DateUtils; 25import android.util.Log; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.View.OnClickListener; 29import android.view.ViewGroup; 30import android.view.Window; 31import android.view.WindowManager; 32import android.view.animation.AlphaAnimation; 33import android.view.animation.Animation; 34import android.widget.Button; 35import android.widget.LinearLayout; 36import android.widget.TextView; 37 38import com.android.datetimepicker.HapticFeedbackController; 39import com.android.datetimepicker.R; 40import com.android.datetimepicker.Utils; 41import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; 42 43import java.text.SimpleDateFormat; 44import java.util.Calendar; 45import java.util.HashSet; 46import java.util.Iterator; 47import java.util.Locale; 48 49/** 50 * Dialog allowing users to select a date. 51 */ 52public 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 101 private HapticFeedbackController mHapticFeedbackController; 102 103 private boolean mDelayAnimation = true; 104 105 // Accessibility strings. 106 private String mDayPickerDescription; 107 private String mSelectDay; 108 private String mYearPickerDescription; 109 private String mSelectYear; 110 111 /** 112 * The callback used to indicate the user is done filling in the date. 113 */ 114 public interface OnDateSetListener { 115 116 /** 117 * @param view The view associated with this listener. 118 * @param year The year that was set. 119 * @param monthOfYear The month that was set (0-11) for compatibility 120 * with {@link java.util.Calendar}. 121 * @param dayOfMonth The day of the month that was set. 122 */ 123 void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth); 124 } 125 126 /** 127 * The callback used to notify other date picker components of a change in selected date. 128 */ 129 public interface OnDateChangedListener { 130 131 public void onDateChanged(); 132 } 133 134 135 public DatePickerDialog() { 136 // Empty constructor required for dialog fragment. 137 } 138 139 /** 140 * @param callBack How the parent is notified that the date is set. 141 * @param year The initial year of the dialog. 142 * @param monthOfYear The initial month of the dialog. 143 * @param dayOfMonth The initial day of the dialog. 144 */ 145 public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, 146 int monthOfYear, 147 int dayOfMonth) { 148 DatePickerDialog ret = new DatePickerDialog(); 149 ret.initialize(callBack, year, monthOfYear, dayOfMonth); 150 return ret; 151 } 152 153 public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { 154 mCallBack = callBack; 155 mCalendar.set(Calendar.YEAR, year); 156 mCalendar.set(Calendar.MONTH, monthOfYear); 157 mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); 158 } 159 160 @Override 161 public void onCreate(Bundle savedInstanceState) { 162 super.onCreate(savedInstanceState); 163 final Activity activity = getActivity(); 164 activity.getWindow().setSoftInputMode( 165 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 166 if (savedInstanceState != null) { 167 mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR)); 168 mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH)); 169 mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY)); 170 } 171 } 172 173 @Override 174 public void onSaveInstanceState(Bundle outState) { 175 super.onSaveInstanceState(outState); 176 outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR)); 177 outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH)); 178 outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH)); 179 outState.putInt(KEY_WEEK_START, mWeekStart); 180 outState.putInt(KEY_YEAR_START, mMinYear); 181 outState.putInt(KEY_YEAR_END, mMaxYear); 182 outState.putInt(KEY_CURRENT_VIEW, mCurrentView); 183 int listPosition = -1; 184 if (mCurrentView == MONTH_AND_DAY_VIEW) { 185 listPosition = mDayPickerView.getMostVisiblePosition(); 186 } else if (mCurrentView == YEAR_VIEW) { 187 listPosition = mYearPickerView.getFirstVisiblePosition(); 188 outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); 189 } 190 outState.putInt(KEY_LIST_POSITION, listPosition); 191 } 192 193 @Override 194 public View onCreateView(LayoutInflater inflater, ViewGroup container, 195 Bundle savedInstanceState) { 196 Log.d(TAG, "onCreateView: "); 197 getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); 198 199 View view = inflater.inflate(R.layout.date_picker_dialog, null); 200 201 mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header); 202 mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day); 203 mMonthAndDayView.setOnClickListener(this); 204 mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month); 205 mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day); 206 mYearView = (TextView) view.findViewById(R.id.date_picker_year); 207 mYearView.setOnClickListener(this); 208 209 int listPosition = -1; 210 int listPositionOffset = 0; 211 int currentView = MONTH_AND_DAY_VIEW; 212 if (savedInstanceState != null) { 213 mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); 214 mMinYear = savedInstanceState.getInt(KEY_YEAR_START); 215 mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); 216 currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); 217 listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); 218 listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); 219 } 220 221 final Activity activity = getActivity(); 222 mDayPickerView = new DayPickerView(activity, this); 223 mYearPickerView = new YearPickerView(activity, this); 224 225 Resources res = getResources(); 226 mDayPickerDescription = res.getString(R.string.day_picker_description); 227 mSelectDay = res.getString(R.string.select_day); 228 mYearPickerDescription = res.getString(R.string.year_picker_description); 229 mSelectYear = res.getString(R.string.select_year); 230 231 mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator); 232 mAnimator.addView(mDayPickerView); 233 mAnimator.addView(mYearPickerView); 234 mAnimator.setDateMillis(mCalendar.getTimeInMillis()); 235 // TODO: Replace with animation decided upon by the design team. 236 Animation animation = new AlphaAnimation(0.0f, 1.0f); 237 animation.setDuration(ANIMATION_DURATION); 238 mAnimator.setInAnimation(animation); 239 // TODO: Replace with animation decided upon by the design team. 240 Animation animation2 = new AlphaAnimation(1.0f, 0.0f); 241 animation2.setDuration(ANIMATION_DURATION); 242 mAnimator.setOutAnimation(animation2); 243 244 mDoneButton = (Button) view.findViewById(R.id.done); 245 mDoneButton.setOnClickListener(new OnClickListener() { 246 247 @Override 248 public void onClick(View v) { 249 tryVibrate(); 250 if (mCallBack != null) { 251 mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR), 252 mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); 253 } 254 dismiss(); 255 } 256 }); 257 258 updateDisplay(false); 259 setCurrentView(currentView); 260 261 if (listPosition != -1) { 262 if (currentView == MONTH_AND_DAY_VIEW) { 263 mDayPickerView.postSetSelection(listPosition); 264 } else if (currentView == YEAR_VIEW) { 265 mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); 266 } 267 } 268 269 mHapticFeedbackController = new HapticFeedbackController(activity); 270 return view; 271 } 272 273 @Override 274 public void onResume() { 275 super.onResume(); 276 mHapticFeedbackController.start(); 277 } 278 279 @Override 280 public void onPause() { 281 super.onPause(); 282 mHapticFeedbackController.stop(); 283 } 284 285 private void setCurrentView(final int viewIndex) { 286 long millis = mCalendar.getTimeInMillis(); 287 288 switch (viewIndex) { 289 case MONTH_AND_DAY_VIEW: 290 ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, 291 1.05f); 292 if (mDelayAnimation) { 293 pulseAnimator.setStartDelay(ANIMATION_DELAY); 294 mDelayAnimation = false; 295 } 296 mDayPickerView.onDateChanged(); 297 if (mCurrentView != viewIndex) { 298 mMonthAndDayView.setSelected(true); 299 mYearView.setSelected(false); 300 mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); 301 mCurrentView = viewIndex; 302 } 303 pulseAnimator.start(); 304 305 int flags = DateUtils.FORMAT_SHOW_DATE; 306 String dayString = DateUtils.formatDateTime(getActivity(), millis, flags); 307 mAnimator.setContentDescription(mDayPickerDescription+": "+dayString); 308 Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); 309 break; 310 case YEAR_VIEW: 311 pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); 312 if (mDelayAnimation) { 313 pulseAnimator.setStartDelay(ANIMATION_DELAY); 314 mDelayAnimation = false; 315 } 316 mYearPickerView.onDateChanged(); 317 if (mCurrentView != viewIndex) { 318 mMonthAndDayView.setSelected(false); 319 mYearView.setSelected(true); 320 mAnimator.setDisplayedChild(YEAR_VIEW); 321 mCurrentView = viewIndex; 322 } 323 pulseAnimator.start(); 324 325 CharSequence yearString = YEAR_FORMAT.format(millis); 326 mAnimator.setContentDescription(mYearPickerDescription+": "+yearString); 327 Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); 328 break; 329 } 330 } 331 332 private void updateDisplay(boolean announce) { 333 if (mDayOfWeekView != null) { 334 mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, 335 Locale.getDefault()).toUpperCase(Locale.getDefault())); 336 } 337 338 mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, 339 Locale.getDefault()).toUpperCase(Locale.getDefault())); 340 mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime())); 341 mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime())); 342 343 // Accessibility. 344 long millis = mCalendar.getTimeInMillis(); 345 mAnimator.setDateMillis(millis); 346 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; 347 String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags); 348 mMonthAndDayView.setContentDescription(monthAndDayText); 349 350 if (announce) { 351 flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 352 String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags); 353 Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); 354 } 355 } 356 357 public void setFirstDayOfWeek(int startOfWeek) { 358 if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { 359 throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " + 360 "Calendar.SATURDAY"); 361 } 362 mWeekStart = startOfWeek; 363 if (mDayPickerView != null) { 364 mDayPickerView.onChange(); 365 } 366 } 367 368 public void setYearRange(int startYear, int endYear) { 369 if (endYear <= startYear) { 370 throw new IllegalArgumentException("Year end must be larger than year start"); 371 } 372 mMinYear = startYear; 373 mMaxYear = endYear; 374 if (mDayPickerView != null) { 375 mDayPickerView.onChange(); 376 } 377 } 378 379 public void setOnDateSetListener(OnDateSetListener listener) { 380 mCallBack = listener; 381 } 382 383 // If the newly selected month / year does not contain the currently selected day number, 384 // change the selected day number to the last day of the selected month or year. 385 // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 386 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 387 private void adjustDayInMonthIfNeeded(int month, int year) { 388 int day = mCalendar.get(Calendar.DAY_OF_MONTH); 389 int daysInMonth = Utils.getDaysInMonth(month, year); 390 if (day > daysInMonth) { 391 mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth); 392 } 393 } 394 395 @Override 396 public void onClick(View v) { 397 tryVibrate(); 398 if (v.getId() == R.id.date_picker_year) { 399 setCurrentView(YEAR_VIEW); 400 } else if (v.getId() == R.id.date_picker_month_and_day) { 401 setCurrentView(MONTH_AND_DAY_VIEW); 402 } 403 } 404 405 @Override 406 public void onYearSelected(int year) { 407 adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year); 408 mCalendar.set(Calendar.YEAR, year); 409 updatePickers(); 410 setCurrentView(MONTH_AND_DAY_VIEW); 411 updateDisplay(true); 412 } 413 414 @Override 415 public void onDayOfMonthSelected(int year, int month, int day) { 416 mCalendar.set(Calendar.YEAR, year); 417 mCalendar.set(Calendar.MONTH, month); 418 mCalendar.set(Calendar.DAY_OF_MONTH, day); 419 updatePickers(); 420 updateDisplay(true); 421 } 422 423 private void updatePickers() { 424 Iterator<OnDateChangedListener> iterator = mListeners.iterator(); 425 while (iterator.hasNext()) { 426 iterator.next().onDateChanged(); 427 } 428 } 429 430 431 @Override 432 public CalendarDay getSelectedDay() { 433 return new CalendarDay(mCalendar); 434 } 435 436 @Override 437 public int getMinYear() { 438 return mMinYear; 439 } 440 441 @Override 442 public int getMaxYear() { 443 return mMaxYear; 444 } 445 446 @Override 447 public int getFirstDayOfWeek() { 448 return mWeekStart; 449 } 450 451 @Override 452 public void registerOnDateChangedListener(OnDateChangedListener listener) { 453 mListeners.add(listener); 454 } 455 456 @Override 457 public void unregisterOnDateChangedListener(OnDateChangedListener listener) { 458 mListeners.remove(listener); 459 } 460 461 @Override 462 public void tryVibrate() { 463 mHapticFeedbackController.tryVibrate(); 464 } 465} 466