SimpleDayPickerFragment.java revision f08cac9c127b248c5ede366a1b51c276bbf2d0e0
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 = 0; 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); 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 doResumeUpdates(); 284 setUpAdapter(); 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 */ 379 public void goTo(long time, boolean animate, boolean setSelected, boolean forceScroll) { 380 if (time == -1) { 381 Log.e(TAG, "time is invalid"); 382 return; 383 } 384 385 // Set the selected day 386 if (setSelected) { 387 mSelectedDay.set(time); 388 mSelectedDay.normalize(true); 389 } 390 391 // If this view isn't returned yet we won't be able to load the lists 392 // current position, so return after setting the selected day. 393 if (!isResumed()) { 394 if (Log.isLoggable(TAG, Log.DEBUG)) { 395 Log.d(TAG, "We're not visible yet"); 396 } 397 return; 398 } 399 400 mTempTime.set(time); 401 long millis = mTempTime.normalize(true); 402 // Get the week we're going to 403 // TODO push Util function into Calendar public api. 404 int position = Utils.getWeeksSinceEpochFromJulianDay( 405 Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek); 406 407 View child; 408 int i = 0; 409 int top = 0; 410 // Find a child that's completely in the view 411 do { 412 child = mListView.getChildAt(i++); 413 if (child == null) { 414 break; 415 } 416 top = child.getTop(); 417 if (Log.isLoggable(TAG, Log.DEBUG)) { 418 Log.d(TAG, "child at " + (i-1) + " has top " + top); 419 } 420 } while (top < 0); 421 422 // Compute the first and last position visible 423 int firstPosition; 424 if (child != null) { 425 firstPosition = mListView.getPositionForView(child); 426 } else { 427 firstPosition = 0; 428 } 429 int lastPosition = firstPosition + mNumWeeks - 1; 430 if (top > BOTTOM_BUFFER) { 431 lastPosition--; 432 } 433 434 if (setSelected) { 435 mAdapter.setSelectedDay(mSelectedDay); 436 } 437 438 if (Log.isLoggable(TAG, Log.DEBUG)) { 439 Log.d(TAG, "GoTo position " + position); 440 } 441 // Check if the selected day is now outside of our visible range 442 // and if so scroll to the month that contains it 443 if (position < firstPosition || position > lastPosition || forceScroll) { 444 mFirstDayOfMonth.set(mTempTime); 445 mFirstDayOfMonth.monthDay = 1; 446 millis = mFirstDayOfMonth.normalize(true); 447 setMonthDisplayed(mFirstDayOfMonth); 448 position = Utils.getWeeksSinceEpochFromJulianDay( 449 Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek); 450 451 mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; 452 if (animate) { 453 mListView.smoothScrollToPositionFromTop( 454 position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); 455 } else { 456 mListView.setSelectionFromTop(position, LIST_TOP_OFFSET); 457 // Perform any after scroll operations that are needed 458 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); 459 } 460 } else if (setSelected) { 461 // Otherwise just set the selection 462 setMonthDisplayed(mSelectedDay); 463 } 464 } 465 466 /** 467 * Updates the title and selected month if the view has moved to a new 468 * month. 469 */ 470 @Override 471 public void onScroll( 472 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 473 SimpleWeekView child = (SimpleWeekView)view.getChildAt(0); 474 if (child == null) { 475 return; 476 } 477 478 // Figure out where we are 479 int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0; 480 long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); 481 mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()); 482 483 // If we have moved since our last call update the direction 484 if (currScroll < mPreviousScrollPosition) { 485 mIsScrollingUp = true; 486 } else if (currScroll > mPreviousScrollPosition) { 487 mIsScrollingUp = false; 488 } else { 489 return; 490 } 491 492 // Use some hysteresis for checking which month to highlight. This 493 // causes the month to transition when two full weeks of a month are 494 // visible when scrolling up, and when the first day in a month reaches 495 // the top of the screen when scrolling down. 496 if (mIsScrollingUp) { 497 child = (SimpleWeekView)view.getChildAt(SCROLL_HYST_WEEKS + offset); 498 } else if (offset != 0) { 499 child = (SimpleWeekView)view.getChildAt(offset); 500 } 501 502 if (child == null) { 503 return; 504 } 505 506 // Find out which month we're moving into 507 int month; 508 if (mIsScrollingUp) { 509 month = child.getFirstMonth(); 510 } else { 511 month = child.getLastMonth(); 512 } 513 514 // And how it relates to our current highlighted month 515 int monthDiff; 516 if (mCurrentMonthDisplayed == 11 && month == 0) { 517 monthDiff = 1; 518 } else if (mCurrentMonthDisplayed == 0 && month == 11) { 519 monthDiff = -1; 520 } else { 521 monthDiff = month - mCurrentMonthDisplayed; 522 } 523 524 // Only switch months if we're scrolling away from the currently 525 // selected month 526 if ((!mIsScrollingUp && monthDiff > 0) 527 || (mIsScrollingUp && monthDiff < 0)) { 528 int julianDay = child.getFirstJulianDay(); 529 if (mIsScrollingUp) { 530 julianDay -= DAYS_PER_WEEK; 531 } else { 532 julianDay += DAYS_PER_WEEK; 533 } 534 mTempTime.setJulianDay(julianDay); 535 setMonthDisplayed(mTempTime); 536 } 537 mPreviousScrollPosition = currScroll; 538 mPreviousScrollState = mCurrentScrollState; 539 } 540 541 /** 542 * Sets the month displayed at the top of this view based on time. Override 543 * to add custom events when the title is changed. 544 * 545 * @param time A day in the new focus month. 546 */ 547 protected void setMonthDisplayed(Time time) { 548 CharSequence oldMonth = mMonthName.getText(); 549 mMonthName.setText(Utils.formatMonthYear(mContext, time)); 550 mMonthName.invalidate(); 551 if (!TextUtils.equals(oldMonth, mMonthName.getText())) { 552 mMonthName.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 553 } 554 mCurrentMonthDisplayed = time.month; 555 mAdapter.updateFocusMonth(mCurrentMonthDisplayed); 556 } 557 558 @Override 559 public void onScrollStateChanged(AbsListView view, int scrollState) { 560 // use a post to prevent re-entering onScrollStateChanged before it 561 // exits 562 mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); 563 } 564 565 protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); 566 567 protected class ScrollStateRunnable implements Runnable { 568 private AbsListView mView; 569 private int mNewState; 570 571 /** 572 * Sets up the runnable with a short delay in case the scroll state 573 * immediately changes again. 574 * 575 * @param view The list view that changed state 576 * @param scrollState The new state it changed to 577 */ 578 public void doScrollStateChange(AbsListView view, int scrollState) { 579 mHandler.removeCallbacks(this); 580 mView = view; 581 mNewState = scrollState; 582 mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); 583 } 584 585 public void run() { 586 mCurrentScrollState = mNewState; 587 if (Log.isLoggable(TAG, Log.DEBUG)) { 588 Log.d(TAG, 589 "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); 590 } 591 // Fix the position after a scroll or a fling ends 592 if (mNewState == OnScrollListener.SCROLL_STATE_IDLE 593 && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { 594 mPreviousScrollState = mNewState; 595 View child = mView.getChildAt(0); 596 if (child == null) { 597 // The view is no longer visible, just return 598 return; 599 } 600 int dist = child.getBottom() - LIST_TOP_OFFSET; 601 if (dist > LIST_TOP_OFFSET) { 602 if (Log.isLoggable(TAG, Log.DEBUG)) { 603 Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp); 604 } 605 int firstPosition = mView.getFirstVisiblePosition(); 606 int lastPosition = mView.getLastVisiblePosition(); 607 boolean scroll = firstPosition != 0 && lastPosition != mView.getCount() - 1; 608 if (mIsScrollingUp && scroll) { 609 mView.smoothScrollBy(dist - child.getHeight(), 500); 610 } else if (!mIsScrollingUp && scroll) { 611 mView.smoothScrollBy(dist, 500); 612 } 613 } 614 } else { 615 mPreviousScrollState = mNewState; 616 } 617 } 618 } 619} 620