MonthByWeekFragment.java revision 124b831510317b41acf3e391b25882a785272654
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.CalendarController; 20import com.android.calendar.CalendarController.EventInfo; 21import com.android.calendar.CalendarController.EventType; 22import com.android.calendar.R; 23import com.android.calendar.Utils; 24import com.android.calendar.month.MonthByWeekAdapter.WeekParameters; 25 26import android.app.Activity; 27import android.app.ListFragment; 28import android.app.LoaderManager; 29import android.content.Context; 30import android.content.CursorLoader; 31import android.content.Loader; 32import android.database.Cursor; 33import android.net.Uri; 34import android.os.Bundle; 35import android.provider.Calendar.Calendars; 36import android.provider.Calendar.Instances; 37import android.text.format.Time; 38import android.util.Log; 39import android.view.GestureDetector; 40import android.view.GestureDetector.SimpleOnGestureListener; 41import android.view.Gravity; 42import android.view.LayoutInflater; 43import android.view.MotionEvent; 44import android.view.View; 45import android.view.View.OnTouchListener; 46import android.view.ViewConfiguration; 47import android.view.ViewGroup; 48import android.view.ViewGroup.LayoutParams; 49import android.widget.AbsListView; 50import android.widget.AbsListView.OnScrollListener; 51import android.widget.FrameLayout; 52import android.widget.LinearLayout; 53import android.widget.ListView; 54import android.widget.TextView; 55 56public class MonthByWeekFragment extends ListFragment implements CalendarController.EventHandler, 57 LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener, OnTouchListener{ 58 private static final String TAG = "MonthFragment"; 59 60 // Selection and selection args for adding event queries 61 private static final String WHERE_CALENDARS_SELECTED = Calendars.SELECTED + "=?"; 62 private static final String[] WHERE_CALENDARS_SELECTED_ARGS = {"1"}; 63 64 // How many weeks of hysteresis to use between scrolling up and scrolling 65 // down 66 private static final int SCROLL_HYST_WEEKS = 2; 67 // How far down from top to align the first week 68 private static final int LIST_TOP_OFFSET = 2; 69 // How long to spend on goTo scrolls 70 private static final int GOTO_SCROLL_DURATION = 1000; 71 private static final int DAYS_PER_WEEK = 7; 72 // The number of pixels that must be showing for a week to be counted as 73 // visible 74 private static int WEEK_MIN_VISIBLE_HEIGHT = 12; 75 76 private static final int MINI_MONTH_NAME_TEXT_SIZE = 18; 77 private static int MINI_MONTH_WIDTH = 254; 78 private static int MINI_MONTH_HEIGHT = 212; 79 80 protected int BOTTOM_BUFFER = 20; 81 82 private static float mScale = 0; 83 private float mFriction = .05f; 84 private float mVelocityScale = 0.333f; 85 86 private Context mContext; 87 private CursorLoader mLoader; 88 private Uri mEventUri; 89 90 protected final boolean mIsMiniMonth; 91 protected MonthByWeekAdapter mAdapter; 92 protected ListView mListView; 93 protected ViewGroup mDayNamesHeader; 94 protected String[] mDayLabels; 95 private int mFirstDayOfWeek; 96 private Time mSelectedDay = new Time(); 97 private Time mFirstDayOfMonth = new Time(); 98 private Time mFirstVisibleDay = new Time(); 99 private Time mTempTime = new Time(); 100 private WeekParameters mWeekParams = new WeekParameters(); 101 102 private TextView mMonthName; 103 private int mCurrentMonthDisplayed; 104 private long mPreviousScrollPosition; 105 private boolean mIsScrollingUp = false; 106 private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; 107 private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; 108 private float mMinimumFlingVelocity; 109 private float mMinimumTwoMonthFlingVelocity; 110 111 private GestureDetector mGestureDetector; 112 113 private class MonthGestureListener extends SimpleOnGestureListener { 114 @Override 115 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 116 float velocityY) { 117 // TODO decide how to handle flings 118// float absX = Math.abs(velocityX); 119// float absY = Math.abs(velocityY); 120// Log.d(TAG, "velX: " + velocityX + " velY: " + velocityY); 121// if (absX > absY && absX > mMinimumFlingVelocity) { 122// mTempTime.set(mFirstDayOfMonth); 123// if(velocityX > 0) { 124// mTempTime.month++; 125// } else { 126// mTempTime.month--; 127// } 128// mTempTime.normalize(true); 129// goTo(mTempTime, true, false, true); 130// 131// } else if (absY > absX && absY > mMinimumFlingVelocity) { 132// mTempTime.set(mFirstDayOfMonth); 133// int diff = 1; 134// if (absY > mMinimumTwoMonthFlingVelocity) { 135// diff = 2; 136// } 137// if(velocityY < 0) { 138// mTempTime.month += diff; 139// } else { 140// mTempTime.month -= diff; 141// } 142// mTempTime.normalize(true); 143// 144// goTo(mTempTime, true, false, true); 145// } 146 return false; 147 } 148 } 149 150 private Runnable mTZUpdater = null; 151 152 public MonthByWeekFragment() { 153 this(true); 154 } 155 156 public MonthByWeekFragment(boolean isMiniMonth) { 157 mIsMiniMonth = isMiniMonth; 158 mSelectedDay.setToNow(); 159 } 160 161 @Override 162 public void onAttach(Activity activity) { 163 super.onAttach(activity); 164 mContext = activity; 165 String tz = Utils.getTimeZone(activity, mTZUpdater); 166 ViewConfiguration viewConfig = ViewConfiguration.get(activity); 167 mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity(); 168 mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2; 169 mGestureDetector = new GestureDetector(activity, new MonthGestureListener()); 170 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 if (mScale == 0) { 180 mScale = activity.getResources().getDisplayMetrics().density; 181 if (mScale != 1) { 182 MINI_MONTH_WIDTH *= mScale; 183 MINI_MONTH_HEIGHT *= mScale; 184 WEEK_MIN_VISIBLE_HEIGHT *= mScale; 185 BOTTOM_BUFFER *= mScale; 186 } 187 } 188 189 mAdapter = new MonthByWeekAdapter(getActivity(), mWeekParams); 190 setListAdapter(mAdapter); 191 } 192 193 @Override 194 public void onActivityCreated(Bundle savedInstanceState) { 195 super.onActivityCreated(savedInstanceState); 196 mListView = getListView(); 197 mListView.setCacheColorHint(0); 198 mListView.setDivider(null); 199 mListView.setItemsCanFocus(true); 200 mListView.setFastScrollEnabled(false); 201 mListView.setOnScrollListener(this); 202 mListView.setFriction(mFriction); 203 mListView.setVelocityScale(mVelocityScale); 204 mListView.setOnTouchListener(this); 205 206 207 mDayLabels = getActivity().getResources().getStringArray( 208 R.array.day_of_week_smallest_labels); 209 210 if (mIsMiniMonth) { 211 FrameLayout.LayoutParams listParams = new FrameLayout.LayoutParams( 212 MINI_MONTH_WIDTH, MINI_MONTH_HEIGHT); 213 listParams.gravity = Gravity.CENTER_HORIZONTAL; 214 mListView.setLayoutParams(listParams); 215 LinearLayout.LayoutParams headerParams = new LinearLayout.LayoutParams( 216 MINI_MONTH_WIDTH, LayoutParams.WRAP_CONTENT); 217 headerParams.gravity = Gravity.CENTER_HORIZONTAL; 218 mDayNamesHeader.setLayoutParams(headerParams); 219 } 220 221 mMonthName = (TextView) getView().findViewById(R.id.month_name); 222 MonthWeekView child = (MonthWeekView) mListView.getChildAt(0); 223 if (child == null) { 224 return; 225 } 226 int julianDay = child.getFirstJulianDay(); 227 mFirstVisibleDay.setJulianDay(julianDay); 228 mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK); 229 setMonthDisplayed(mTempTime); 230 } 231 232 @Override 233 public void onResume() { 234 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 235 236 WeekParameters params = mWeekParams; 237 params.firstJulianDay = Time.getJulianDay(System.currentTimeMillis(), 0); 238 params.weekHeight = 50; // This is a dummy value for now 239 params.focusMonth = mCurrentMonthDisplayed; 240 mAdapter.updateParams(params); 241 updateHeader(); 242 goTo(mSelectedDay, false); 243 mAdapter.setSelectedDay(mSelectedDay); 244 super.onResume(); 245 } 246 247 private void updateHeader() { 248 boolean showWeekNumber = Utils.getShowWeekNumber(mContext); 249 TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label); 250 if (showWeekNumber) { 251 label.setVisibility(View.VISIBLE); 252 } else { 253 label.setVisibility(View.GONE); 254 } 255 int offset = mFirstDayOfWeek - 1; 256 for (int i = 1; i < 8; i++) { 257 label = (TextView) mDayNamesHeader.getChildAt(i); 258 label.setText(mDayLabels[(offset + i) % 7]); 259 } 260 mDayNamesHeader.invalidate(); 261 } 262 263 // TODO 264 @Override 265 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 266 Uri.Builder builder = Instances.CONTENT_URI.buildUpon(); 267// ContentUris.appendId(builder, begin); 268// ContentUris.appendId(builder, end); 269// 270// mLoader = new CursorLoader(getActivity(), Calendars.CONTENT_URI, PROJECTION, WHERE_CALENDARS_SELECTED, mArgs, SORT_ORDER);) 271 return null; 272 } 273 274 @Override 275 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 276 // TODO Auto-generated method stub 277 278 } 279 280 @Override 281 public View onCreateView(LayoutInflater inflater, ViewGroup container, 282 Bundle savedInstanceState) { 283 View v = inflater.inflate(R.layout.month_by_week, 284 container, false); 285 mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); 286 return v; 287 } 288 289 @Override 290 public void eventsChanged() { 291 // TODO Auto-generated method stub 292 293 } 294 295 @Override 296 public boolean getAllDay() { 297 return false; 298 } 299 300 @Override 301 public long getSelectedTime() { 302 // TODO Auto-generated method stub 303 return 0; 304 } 305 306 @Override 307 public long getSupportedEventTypes() { 308 return EventType.GO_TO; 309 } 310 311 @Override 312 public void goTo(Time time, boolean animate) { 313 goTo(time, animate, true, false); 314 } 315 316 public void goTo(Time time, boolean animate, boolean setSelected, boolean forceScroll) { 317 318 if (time == null){ 319 Log.e(TAG, "time is null"); 320 return; 321 } 322 323 mTempTime.set(time); 324 long millis = mTempTime.normalize(true); 325 if (setSelected) { 326 mSelectedDay.set(time); 327 mSelectedDay.normalize(true); 328 } 329 330 if (!isResumed()) { 331 if (Log.isLoggable(TAG, Log.DEBUG)) { 332 Log.d(TAG, "We're not visible yet"); 333 } 334 return; 335 } 336 337 // Get the week we're going to 338 int position = Utils.getWeeksSinceEpochFromJulianDay( 339 Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek); 340 341 View child; 342 int i = 0; 343 int top = 0; 344 // Find a child that's completely in the view 345 do { 346 child = mListView.getChildAt(i++); 347 if (child == null) { 348 break; 349 } 350 top = child.getTop(); 351 if (Log.isLoggable(TAG, Log.DEBUG)) { 352 Log.d(TAG, "child at " + (i-1) + " has top " + top); 353 } 354 } while (top < 0); 355 356 // Compute the first and last position visible 357 int firstPosition; 358 if (child != null) { 359 firstPosition = mListView.getPositionForView(child); 360 } else { 361 firstPosition = 0; 362 } 363 int lastPosition = firstPosition + mWeekParams.numWeeks - 1; 364 if (top > BOTTOM_BUFFER) { 365 lastPosition--; 366 } 367 368 if (setSelected) { 369 mAdapter.setSelectedDay(mSelectedDay); 370 } 371 372 // Check if the selected day is now outside of our visible range 373 // and if so scroll to the month that contains it 374 if (position < firstPosition || position > lastPosition || forceScroll) { 375 mFirstDayOfMonth.set(mTempTime); 376 mFirstDayOfMonth.monthDay = 1; 377 millis = mFirstDayOfMonth.normalize(true); 378 setMonthDisplayed(mFirstDayOfMonth); 379 position = Utils.getWeeksSinceEpochFromJulianDay( 380 Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek); 381 382 mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; 383 if (animate) { 384 mListView.smoothScrollToPositionFromTop( 385 position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); 386 } else { 387 mListView.setSelectionFromTop(position, LIST_TOP_OFFSET); 388 } 389 } else if (setSelected) { 390 // Otherwise just set the selection 391 setMonthDisplayed(mSelectedDay); 392 } 393 } 394 395 @Override 396 public void goToToday() { 397 // TODO Auto-generated method stub 398 399 } 400 401 @Override 402 public void handleEvent(EventInfo event) { 403 if (event.eventType == EventType.GO_TO) { 404 goTo(event.selectedTime, true); 405 } 406 } 407 408 @Override 409 public void onScroll( 410 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 411 MonthWeekView child = (MonthWeekView)view.getChildAt(0); 412 if (child == null) { 413 return; 414 } 415 416 // Figure out where we are 417 int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0; 418 long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); 419 mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()); 420 421 // If we have moved since our last call update the direction 422 if (currScroll < mPreviousScrollPosition) { 423 mIsScrollingUp = true; 424 } else if (currScroll > mPreviousScrollPosition) { 425 mIsScrollingUp = false; 426 } else { 427 return; 428 } 429 430 // Use some hysteresis for checking which month to highlight. This 431 // causes the month to transition when two full weeks of a month are 432 // visible when scrolling up, and when the first day in a month reaches 433 // the top of the screen when scrolling down. 434 if (mIsScrollingUp) { 435 child = (MonthWeekView)view.getChildAt(SCROLL_HYST_WEEKS + offset); 436 } else if (offset != 0) { 437 child = (MonthWeekView)view.getChildAt(offset); 438 } 439 440 // Find out which month we're moving into 441 int month; 442 if (mIsScrollingUp) { 443 month = child.getFirstMonth(); 444 } else { 445 month = child.getLastMonth(); 446 } 447 448 // And how it relates to our current highlighted month 449 int monthDiff; 450 if (mCurrentMonthDisplayed == 11 && month == 0) { 451 monthDiff = 1; 452 } else if (mCurrentMonthDisplayed == 0 && month == 11) { 453 monthDiff = -1; 454 } else { 455 monthDiff = month - mCurrentMonthDisplayed; 456 } 457 458 // Only switch months if we're scrolling away from the currently 459 // selected month 460 if ((!mIsScrollingUp && monthDiff > 0) 461 || (mIsScrollingUp && monthDiff < 0)) { 462 int julianDay = child.getFirstJulianDay(); 463 if (mIsScrollingUp) { 464 julianDay -= DAYS_PER_WEEK; 465 } else { 466 julianDay += DAYS_PER_WEEK; 467 } 468 mTempTime.setJulianDay(julianDay); 469 setMonthDisplayed(mTempTime); 470 } 471 mPreviousScrollPosition = currScroll; 472 mPreviousScrollState = mCurrentScrollState; 473 } 474 475 // Updates the month shown and highlighted 476 private void setMonthDisplayed(Time time) { 477 mMonthName.setText(time.format("%B %Y")); 478 mMonthName.invalidate(); 479 mCurrentMonthDisplayed = time.month; 480 mAdapter.updateFocusMonth(mCurrentMonthDisplayed); 481 } 482 483 @Override 484 public void onScrollStateChanged(AbsListView view, int scrollState) { 485 mCurrentScrollState = scrollState; 486 if (Log.isLoggable(TAG, Log.DEBUG)) { 487 Log.d(TAG, "new scroll state: " + scrollState + " old state: " + mPreviousScrollState); 488 } 489 // For now we fix our position after a scroll or a fling ends 490 if (scrollState == OnScrollListener.SCROLL_STATE_IDLE 491 && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE 492 /*&& mPreviousScrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL*/) { 493 View child = view.getChildAt(0); 494 int dist = child.getBottom() - LIST_TOP_OFFSET; 495 if (dist > LIST_TOP_OFFSET) { 496 mPreviousScrollState = scrollState; 497 if (Log.isLoggable(TAG, Log.DEBUG)) { 498 Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp); 499 } 500 if (mIsScrollingUp) { 501 view.smoothScrollBy(dist - child.getHeight(), 500); 502 } else { 503 view.smoothScrollBy(dist, 500); 504 } 505 } 506 } 507 } 508 509 @Override 510 public boolean onTouch(View v, MotionEvent event) { 511 return mGestureDetector.onTouchEvent(event); 512 // TODO post a cleanup to push us back onto the grid if something went 513 // wrong in a scroll such as the user stopping the view but not 514 // scrolling 515 } 516 517} 518