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