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