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