SimpleDayPickerFragment.java revision c0ed322afb50be894dca4c8d270b3f6202c2ac61
1/* 2 * Copyright (C) 2010 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.calendar.month; 18 19import com.android.calendar.R; 20import com.android.calendar.Utils; 21 22import android.app.Activity; 23import android.app.ListFragment; 24import android.content.Context; 25import android.content.res.Resources; 26import android.database.DataSetObserver; 27import android.os.Bundle; 28import android.os.Handler; 29import android.text.TextUtils; 30import android.text.format.DateUtils; 31import android.text.format.Time; 32import android.util.Log; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewGroup; 37import android.view.accessibility.AccessibilityEvent; 38import android.widget.AbsListView; 39import android.widget.AbsListView.OnScrollListener; 40import android.widget.ListView; 41import android.widget.TextView; 42 43import java.util.Calendar; 44import java.util.HashMap; 45import java.util.Locale; 46 47/** 48 * <p> 49 * This displays a titled list of weeks with selectable days. It can be 50 * configured to display the week number, start the week on a given day, show a 51 * reduced number of days, or display an arbitrary number of weeks at a time. By 52 * overriding methods and changing variables this fragment can be customized to 53 * easily display a month selection component in a given style. 54 * </p> 55 */ 56public class SimpleDayPickerFragment extends ListFragment implements OnScrollListener { 57 58 private static final String TAG = "MonthFragment"; 59 private static final String KEY_CURRENT_TIME = "current_time"; 60 61 // Affects when the month selection will change while scrolling up 62 protected static final int SCROLL_HYST_WEEKS = 2; 63 // How long the GoTo fling animation should last 64 protected static final int GOTO_SCROLL_DURATION = 1000; 65 // How long to wait after receiving an onScrollStateChanged notification 66 // before acting on it 67 protected static final int SCROLL_CHANGE_DELAY = 40; 68 // The number of days to display in each week 69 protected static final int DAYS_PER_WEEK = 7; 70 // The size of the month name displayed above the week list 71 protected static final int MINI_MONTH_NAME_TEXT_SIZE = 18; 72 protected static int LIST_TOP_OFFSET = -1; // so that the top line will be under the separator 73 protected int WEEK_MIN_VISIBLE_HEIGHT = 12; 74 protected int BOTTOM_BUFFER = 20; 75 protected int mSaturdayColor = 0; 76 protected int mSundayColor = 0; 77 protected int mDayNameColor = 0; 78 79 // You can override these numbers to get a different appearance 80 protected int mNumWeeks = 6; 81 protected boolean mShowWeekNumber = false; 82 protected int mDaysPerWeek = 7; 83 84 // These affect the scroll speed and feel 85 protected float mFriction = .05f; 86 protected float mVelocityScale = 0.333f; 87 88 protected Context mContext; 89 protected Handler mHandler; 90 91 protected float mMinimumFlingVelocity; 92 93 // highlighted time 94 protected Time mSelectedDay = new Time(); 95 protected SimpleWeeksAdapter mAdapter; 96 protected ListView mListView; 97 protected ViewGroup mDayNamesHeader; 98 protected String[] mDayLabels; 99 100 // disposable variable used for time calculations 101 protected Time mTempTime = new Time(); 102 103 private static float mScale = 0; 104 // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). 105 protected int mFirstDayOfWeek; 106 // The first day of the focus month 107 protected Time mFirstDayOfMonth = new Time(); 108 // The first day that is visible in the view 109 protected Time mFirstVisibleDay = new Time(); 110 // The name of the month to display 111 protected TextView mMonthName; 112 // The last name announced by accessibility 113 protected CharSequence mPrevMonthName; 114 // which month should be displayed/highlighted [0-11] 115 protected int mCurrentMonthDisplayed; 116 // used for tracking during a scroll 117 protected long mPreviousScrollPosition; 118 // used for tracking which direction the view is scrolling 119 protected boolean mIsScrollingUp = false; 120 // used for tracking what state listview is in 121 protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; 122 // used for tracking what state listview is in 123 protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; 124 125 // This causes an update of the view at midnight 126 protected Runnable mTodayUpdater = new Runnable() { 127 @Override 128 public void run() { 129 Time midnight = new Time(mFirstVisibleDay.timezone); 130 midnight.setToNow(); 131 long currentMillis = midnight.toMillis(true); 132 133 midnight.hour = 0; 134 midnight.minute = 0; 135 midnight.second = 0; 136 midnight.monthDay++; 137 long millisToMidnight = midnight.normalize(true) - currentMillis; 138 mHandler.postDelayed(this, millisToMidnight); 139 140 if (mAdapter != null) { 141 mAdapter.notifyDataSetChanged(); 142 } 143 } 144 }; 145 146 // This allows us to update our position when a day is tapped 147 protected DataSetObserver mObserver = new DataSetObserver() { 148 @Override 149 public void onChanged() { 150 Time day = mAdapter.getSelectedDay(); 151 if (day.year != mSelectedDay.year || day.yearDay != mSelectedDay.yearDay) { 152 goTo(day.toMillis(true), true, true, false); 153 } 154 } 155 }; 156 157 public SimpleDayPickerFragment(long initialTime) { 158 goTo(initialTime, false, true, true); 159 mHandler = new Handler(); 160 } 161 162 @Override 163 public void onAttach(Activity activity) { 164 super.onAttach(activity); 165 mContext = activity; 166 String tz = Time.getCurrentTimezone(); 167 ViewConfiguration viewConfig = ViewConfiguration.get(activity); 168 mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity(); 169 170 // Ensure we're in the correct time zone 171 mSelectedDay.switchTimezone(tz); 172 mSelectedDay.normalize(true); 173 mFirstDayOfMonth.timezone = tz; 174 mFirstDayOfMonth.normalize(true); 175 mFirstVisibleDay.timezone = tz; 176 mFirstVisibleDay.normalize(true); 177 mTempTime.timezone = tz; 178 179 Resources res = activity.getResources(); 180 mSaturdayColor = res.getColor(R.color.month_saturday); 181 mSundayColor = res.getColor(R.color.month_sunday); 182 mDayNameColor = res.getColor(R.color.month_day_names_color); 183 184 // Adjust sizes for screen density 185 if (mScale == 0) { 186 mScale = activity.getResources().getDisplayMetrics().density; 187 if (mScale != 1) { 188 WEEK_MIN_VISIBLE_HEIGHT *= mScale; 189 BOTTOM_BUFFER *= mScale; 190 LIST_TOP_OFFSET *= mScale; 191 } 192 } 193 setUpAdapter(); 194 setListAdapter(mAdapter); 195 } 196 197 /** 198 * Creates a new adapter if necessary and sets up its parameters. Override 199 * this method to provide a custom adapter. 200 */ 201 protected void setUpAdapter() { 202 HashMap<String, Integer> weekParams = new HashMap<String, Integer>(); 203 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks); 204 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0); 205 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek); 206 weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, 207 Time.getJulianDay(mSelectedDay.toMillis(false), mSelectedDay.gmtoff)); 208 if (mAdapter == null) { 209 mAdapter = new SimpleWeeksAdapter(getActivity(), weekParams); 210 mAdapter.registerDataSetObserver(mObserver); 211 } else { 212 mAdapter.updateParams(weekParams); 213 } 214 // refresh the view with the new parameters 215 mAdapter.notifyDataSetChanged(); 216 } 217 218 @Override 219 public void onCreate(Bundle savedInstanceState) { 220 super.onCreate(savedInstanceState); 221 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_CURRENT_TIME)) { 222 goTo(savedInstanceState.getLong(KEY_CURRENT_TIME), false, true, true); 223 } 224 } 225 226 @Override 227 public void onActivityCreated(Bundle savedInstanceState) { 228 super.onActivityCreated(savedInstanceState); 229 230 setUpListView(); 231 setUpHeader(); 232 233 mMonthName = (TextView) getView().findViewById(R.id.month_name); 234 SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0); 235 if (child == null) { 236 return; 237 } 238 int julianDay = child.getFirstJulianDay(); 239 mFirstVisibleDay.setJulianDay(julianDay); 240 // set the title to the month of the second week 241 mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK); 242 setMonthDisplayed(mTempTime, true); 243 } 244 245 /** 246 * Sets up the strings to be used by the header. Override this method to use 247 * different strings or modify the view params. 248 */ 249 protected void setUpHeader() { 250 mDayLabels = new String[7]; 251 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 252 mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i, 253 DateUtils.LENGTH_SHORTEST).toUpperCase(); 254 } 255 } 256 257 /** 258 * Sets all the required fields for the list view. Override this method to 259 * set a different list view behavior. 260 */ 261 protected void setUpListView() { 262 // Configure the listview 263 mListView = getListView(); 264 // Transparent background on scroll 265 mListView.setCacheColorHint(0); 266 // No dividers 267 mListView.setDivider(null); 268 // Items are clickable 269 mListView.setItemsCanFocus(true); 270 // The thumb gets in the way, so disable it 271 mListView.setFastScrollEnabled(false); 272 mListView.setVerticalScrollBarEnabled(false); 273 mListView.setOnScrollListener(this); 274 mListView.setFadingEdgeLength(0); 275 // Make the scrolling behavior nicer 276 mListView.setFriction(mFriction); 277 mListView.setVelocityScale(mVelocityScale); 278 } 279 280 @Override 281 public void onResume() { 282 super.onResume(); 283 setUpAdapter(); 284 doResumeUpdates(); 285 } 286 287 @Override 288 public void onPause() { 289 super.onPause(); 290 mHandler.removeCallbacks(mTodayUpdater); 291 } 292 293 @Override 294 public void onSaveInstanceState(Bundle outState) { 295 outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true)); 296 } 297 298 /** 299 * Updates the user preference fields. Override this to use a different 300 * preference space. 301 */ 302 protected void doResumeUpdates() { 303 // Get default week start based on locale, subtracting one for use with android Time. 304 Calendar cal = Calendar.getInstance(Locale.getDefault()); 305 mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1; 306 307 mShowWeekNumber = false; 308 309 updateHeader(); 310 goTo(mSelectedDay.toMillis(true), false, false, false); 311 mAdapter.setSelectedDay(mSelectedDay); 312 mTodayUpdater.run(); 313 } 314 315 /** 316 * Fixes the day names header to provide correct spacing and updates the 317 * label text. Override this to set up a custom header. 318 */ 319 protected void updateHeader() { 320 TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label); 321 if (mShowWeekNumber) { 322 label.setVisibility(View.VISIBLE); 323 } else { 324 label.setVisibility(View.GONE); 325 } 326 int offset = mFirstDayOfWeek - 1; 327 for (int i = 1; i < 8; i++) { 328 label = (TextView) mDayNamesHeader.getChildAt(i); 329 if (i < mDaysPerWeek + 1) { 330 int position = (offset + i) % 7; 331 label.setText(mDayLabels[position]); 332 label.setVisibility(View.VISIBLE); 333 if (position == Time.SATURDAY) { 334 label.setTextColor(mSaturdayColor); 335 } else if (position == Time.SUNDAY) { 336 label.setTextColor(mSundayColor); 337 } else { 338 label.setTextColor(mDayNameColor); 339 } 340 } else { 341 label.setVisibility(View.GONE); 342 } 343 } 344 mDayNamesHeader.invalidate(); 345 } 346 347 @Override 348 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 349 View v = inflater.inflate(R.layout.month_by_week, 350 container, false); 351 mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); 352 return v; 353 } 354 355 /** 356 * Returns the UTC millis since epoch representation of the currently 357 * selected time. 358 * 359 * @return 360 */ 361 public long getSelectedTime() { 362 return mSelectedDay.toMillis(true); 363 } 364 365 /** 366 * This moves to the specified time in the view. If the time is not already 367 * in range it will move the list so that the first of the month containing 368 * the time is at the top of the view. If the new time is already in view 369 * the list will not be scrolled unless forceScroll is true. This time may 370 * optionally be highlighted as selected as well. 371 * 372 * @param time The time to move to 373 * @param animate Whether to scroll to the given time or just redraw at the 374 * new location 375 * @param setSelected Whether to set the given time as selected 376 * @param forceScroll Whether to recenter even if the time is already 377 * visible 378 * @return Whether or not the view animated to the new location 379 */ 380 public boolean goTo(long time, boolean animate, boolean setSelected, boolean forceScroll) { 381 if (time == -1) { 382 Log.e(TAG, "time is invalid"); 383 return false; 384 } 385 386 // Set the selected day 387 if (setSelected) { 388 mSelectedDay.set(time); 389 mSelectedDay.normalize(true); 390 } 391 392 // If this view isn't returned yet we won't be able to load the lists 393 // current position, so return after setting the selected day. 394 if (!isResumed()) { 395 if (Log.isLoggable(TAG, Log.DEBUG)) { 396 Log.d(TAG, "We're not visible yet"); 397 } 398 return false; 399 } 400 401 mTempTime.set(time); 402 long millis = mTempTime.normalize(true); 403 // Get the week we're going to 404 // TODO push Util function into Calendar public api. 405 int position = Utils.getWeeksSinceEpochFromJulianDay( 406 Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek); 407 408 View child; 409 int i = 0; 410 int top = 0; 411 // Find a child that's completely in the view 412 do { 413 child = mListView.getChildAt(i++); 414 if (child == null) { 415 break; 416 } 417 top = child.getTop(); 418 if (Log.isLoggable(TAG, Log.DEBUG)) { 419 Log.d(TAG, "child at " + (i-1) + " has top " + top); 420 } 421 } while (top < 0); 422 423 // Compute the first and last position visible 424 int firstPosition; 425 if (child != null) { 426 firstPosition = mListView.getPositionForView(child); 427 } else { 428 firstPosition = 0; 429 } 430 int lastPosition = firstPosition + mNumWeeks - 1; 431 if (top > BOTTOM_BUFFER) { 432 lastPosition--; 433 } 434 435 if (setSelected) { 436 mAdapter.setSelectedDay(mSelectedDay); 437 } 438 439 if (Log.isLoggable(TAG, Log.DEBUG)) { 440 Log.d(TAG, "GoTo position " + position); 441 } 442 // Check if the selected day is now outside of our visible range 443 // and if so scroll to the month that contains it 444 if (position < firstPosition || position > lastPosition || forceScroll) { 445 mFirstDayOfMonth.set(mTempTime); 446 mFirstDayOfMonth.monthDay = 1; 447 millis = mFirstDayOfMonth.normalize(true); 448 setMonthDisplayed(mFirstDayOfMonth, true); 449 position = Utils.getWeeksSinceEpochFromJulianDay( 450 Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek); 451 452 mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; 453 if (animate) { 454 mListView.smoothScrollToPositionFromTop( 455 position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); 456 return true; 457 } else { 458 mListView.setSelectionFromTop(position, LIST_TOP_OFFSET); 459 // Perform any after scroll operations that are needed 460 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); 461 } 462 } else if (setSelected) { 463 // Otherwise just set the selection 464 setMonthDisplayed(mSelectedDay, true); 465 } 466 return false; 467 } 468 469 /** 470 * Updates the title and selected month if the view has moved to a new 471 * month. 472 */ 473 @Override 474 public void onScroll( 475 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 476 SimpleWeekView child = (SimpleWeekView)view.getChildAt(0); 477 if (child == null) { 478 return; 479 } 480 481 // Figure out where we are 482 long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); 483 mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()); 484 485 // If we have moved since our last call update the direction 486 if (currScroll < mPreviousScrollPosition) { 487 mIsScrollingUp = true; 488 } else if (currScroll > mPreviousScrollPosition) { 489 mIsScrollingUp = false; 490 } else { 491 return; 492 } 493 494 mPreviousScrollPosition = currScroll; 495 mPreviousScrollState = mCurrentScrollState; 496 497 updateMonthHighlight(mListView); 498 } 499 500 /** 501 * Figures out if the month being shown has changed and updates the 502 * highlight if needed 503 * 504 * @param view The ListView containing the weeks 505 */ 506 private void updateMonthHighlight(AbsListView view) { 507 SimpleWeekView child = (SimpleWeekView) view.getChildAt(0); 508 if (child == null) { 509 return; 510 } 511 512 // Figure out where we are 513 int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0; 514 // Use some hysteresis for checking which month to highlight. This 515 // causes the month to transition when two full weeks of a month are 516 // visible. 517 child = (SimpleWeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); 518 519 if (child == null) { 520 return; 521 } 522 523 // Find out which month we're moving into 524 int month; 525 if (mIsScrollingUp) { 526 month = child.getFirstMonth(); 527 } else { 528 month = child.getLastMonth(); 529 } 530 531 // And how it relates to our current highlighted month 532 int monthDiff; 533 if (mCurrentMonthDisplayed == 11 && month == 0) { 534 monthDiff = 1; 535 } else if (mCurrentMonthDisplayed == 0 && month == 11) { 536 monthDiff = -1; 537 } else { 538 monthDiff = month - mCurrentMonthDisplayed; 539 } 540 541 // Only switch months if we're scrolling away from the currently 542 // selected month 543 if (monthDiff != 0) { 544 int julianDay = child.getFirstJulianDay(); 545 if (mIsScrollingUp) { 546 // Takes the start of the week 547 } else { 548 // Takes the start of the following week 549 julianDay += DAYS_PER_WEEK; 550 } 551 mTempTime.setJulianDay(julianDay); 552 setMonthDisplayed(mTempTime, false); 553 } 554 } 555 556 /** 557 * Sets the month displayed at the top of this view based on time. Override 558 * to add custom events when the title is changed. 559 * 560 * @param time A day in the new focus month. 561 * @param updateHighlight TODO(epastern): 562 */ 563 protected void setMonthDisplayed(Time time, boolean updateHighlight) { 564 CharSequence oldMonth = mMonthName.getText(); 565 mMonthName.setText(Utils.formatMonthYear(mContext, time)); 566 mMonthName.invalidate(); 567 if (!TextUtils.equals(oldMonth, mMonthName.getText())) { 568 mMonthName.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 569 } 570 mCurrentMonthDisplayed = time.month; 571 if (updateHighlight) { 572 mAdapter.updateFocusMonth(mCurrentMonthDisplayed); 573 } 574 } 575 576 @Override 577 public void onScrollStateChanged(AbsListView view, int scrollState) { 578 // use a post to prevent re-entering onScrollStateChanged before it 579 // exits 580 mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); 581 } 582 583 protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); 584 585 protected class ScrollStateRunnable implements Runnable { 586 private int mNewState; 587 588 /** 589 * Sets up the runnable with a short delay in case the scroll state 590 * immediately changes again. 591 * 592 * @param view The list view that changed state 593 * @param scrollState The new state it changed to 594 */ 595 public void doScrollStateChange(AbsListView view, int scrollState) { 596 mHandler.removeCallbacks(this); 597 mNewState = scrollState; 598 mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); 599 } 600 601 public void run() { 602 mCurrentScrollState = mNewState; 603 if (Log.isLoggable(TAG, Log.DEBUG)) { 604 Log.d(TAG, 605 "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); 606 } 607 // Fix the position after a scroll or a fling ends 608 if (mNewState == OnScrollListener.SCROLL_STATE_IDLE 609 && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { 610 mPreviousScrollState = mNewState; 611 // Uncomment the below to add snap to week back 612// int i = 0; 613// View child = mView.getChildAt(i); 614// while (child != null && child.getBottom() <= 0) { 615// child = mView.getChildAt(++i); 616// } 617// if (child == null) { 618// // The view is no longer visible, just return 619// return; 620// } 621// int dist = child.getTop(); 622// if (dist < LIST_TOP_OFFSET) { 623// if (Log.isLoggable(TAG, Log.DEBUG)) { 624// Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp); 625// } 626// int firstPosition = mView.getFirstVisiblePosition(); 627// int lastPosition = mView.getLastVisiblePosition(); 628// boolean scroll = firstPosition != 0 && lastPosition != mView.getCount() - 1; 629// if (mIsScrollingUp && scroll) { 630// mView.smoothScrollBy(dist, 500); 631// } else if (!mIsScrollingUp && scroll) { 632// mView.smoothScrollBy(child.getHeight() + dist, 500); 633// } 634// } 635 mAdapter.updateFocusMonth(mCurrentMonthDisplayed); 636 } else { 637 mPreviousScrollState = mNewState; 638 } 639 } 640 } 641} 642