DayView.java revision f7cf45455ce8cc5588479dcbb5d876f7638f79dc
1/* 2 * Copyright (C) 2007 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; 18 19import com.android.calendar.CalendarController.EventType; 20import com.android.calendar.CalendarController.ViewType; 21 22import android.animation.Animator; 23import android.animation.AnimatorListenerAdapter; 24import android.animation.ObjectAnimator; 25import android.animation.TypeEvaluator; 26import android.animation.ValueAnimator; 27import android.app.AlertDialog; 28import android.app.Service; 29import android.content.ContentResolver; 30import android.content.ContentUris; 31import android.content.Context; 32import android.content.DialogInterface; 33import android.content.res.Resources; 34import android.content.res.TypedArray; 35import android.database.Cursor; 36import android.graphics.Canvas; 37import android.graphics.Paint; 38import android.graphics.Paint.Align; 39import android.graphics.Paint.Style; 40import android.graphics.Rect; 41import android.graphics.Typeface; 42import android.graphics.drawable.Drawable; 43import android.net.Uri; 44import android.os.Handler; 45import android.provider.CalendarContract.Attendees; 46import android.provider.CalendarContract.Calendars; 47import android.provider.CalendarContract.Events; 48import android.text.Layout.Alignment; 49import android.text.SpannableStringBuilder; 50import android.text.StaticLayout; 51import android.text.TextPaint; 52import android.text.TextUtils; 53import android.text.format.DateFormat; 54import android.text.format.DateUtils; 55import android.text.format.Time; 56import android.text.style.StyleSpan; 57import android.util.Log; 58import android.view.ContextMenu; 59import android.view.ContextMenu.ContextMenuInfo; 60import android.view.GestureDetector; 61import android.view.Gravity; 62import android.view.KeyEvent; 63import android.view.LayoutInflater; 64import android.view.MenuItem; 65import android.view.MotionEvent; 66import android.view.ScaleGestureDetector; 67import android.view.View; 68import android.view.ViewConfiguration; 69import android.view.ViewGroup; 70import android.view.WindowManager; 71import android.view.accessibility.AccessibilityEvent; 72import android.view.accessibility.AccessibilityManager; 73import android.view.animation.AccelerateDecelerateInterpolator; 74import android.view.animation.Animation; 75import android.view.animation.Interpolator; 76import android.view.animation.TranslateAnimation; 77import android.widget.EdgeEffect; 78import android.widget.ImageView; 79import android.widget.OverScroller; 80import android.widget.PopupWindow; 81import android.widget.TextView; 82import android.widget.ViewSwitcher; 83 84import java.util.ArrayList; 85import java.util.Arrays; 86import java.util.Calendar; 87import java.util.Formatter; 88import java.util.Locale; 89import java.util.regex.Matcher; 90import java.util.regex.Pattern; 91 92/** 93 * View for multi-day view. So far only 1 and 7 day have been tested. 94 */ 95public class DayView extends View implements View.OnCreateContextMenuListener, 96 ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener 97 { 98 private static String TAG = "DayView"; 99 private static boolean DEBUG = false; 100 private static boolean DEBUG_SCALING = false; 101 private static final String PERIOD_SPACE = ". "; 102 103 private static float mScale = 0; // Used for supporting different screen densities 104 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event 105 // Duration of the allday expansion 106 private static final long ANIMATION_DURATION = 400; 107 // duration of the more allday event text fade 108 private static final long ANIMATION_SECONDARY_DURATION = 200; 109 110 private static final int MENU_AGENDA = 2; 111 private static final int MENU_DAY = 3; 112 private static final int MENU_EVENT_VIEW = 5; 113 private static final int MENU_EVENT_CREATE = 6; 114 private static final int MENU_EVENT_EDIT = 7; 115 private static final int MENU_EVENT_DELETE = 8; 116 117 private static int DEFAULT_CELL_HEIGHT = 64; 118 private static int MAX_CELL_HEIGHT = 150; 119 private static int MIN_Y_SPAN = 100; 120 121 private boolean mOnFlingCalled; 122 private boolean mStartingScroll = false; 123 /** 124 * ID of the last event which was displayed with the toast popup. 125 * 126 * This is used to prevent popping up multiple quick views for the same event, especially 127 * during calendar syncs. This becomes valid when an event is selected, either by default 128 * on starting calendar or by scrolling to an event. It becomes invalid when the user 129 * explicitly scrolls to an empty time slot, changes views, or deletes the event. 130 */ 131 private long mLastPopupEventID; 132 133 protected Context mContext; 134 135 private static final String[] CALENDARS_PROJECTION = new String[] { 136 Calendars._ID, // 0 137 Calendars.CALENDAR_ACCESS_LEVEL, // 1 138 Calendars.OWNER_ACCOUNT, // 2 139 }; 140 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; 141 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 142 private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; 143 144 private static final int FROM_NONE = 0; 145 private static final int FROM_ABOVE = 1; 146 private static final int FROM_BELOW = 2; 147 private static final int FROM_LEFT = 4; 148 private static final int FROM_RIGHT = 8; 149 150 private static final int ACCESS_LEVEL_NONE = 0; 151 private static final int ACCESS_LEVEL_DELETE = 1; 152 private static final int ACCESS_LEVEL_EDIT = 2; 153 154 private static int mHorizontalSnapBackThreshold = 128; 155 156 private ContinueScroll mContinueScroll = new ContinueScroll(); 157 158 // Make this visible within the package for more informative debugging 159 Time mBaseDate; 160 private Time mCurrentTime; 161 //Update the current time line every five minutes if the window is left open that long 162 private static final int UPDATE_CURRENT_TIME_DELAY = 300000; 163 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); 164 private int mTodayJulianDay; 165 166 private Typeface mBold = Typeface.DEFAULT_BOLD; 167 private int mFirstJulianDay; 168 private int mLastJulianDay; 169 170 private int mMonthLength; 171 private int mFirstVisibleDate; 172 private int mFirstVisibleDayOfWeek; 173 private int[] mEarliestStartHour; // indexed by the week day offset 174 private boolean[] mHasAllDayEvent; // indexed by the week day offset 175 private String mEventCountTemplate; 176 private CharSequence[] mLongPressItems; 177 private String mLongPressTitle; 178 179 protected static StringBuilder mStringBuilder = new StringBuilder(50); 180 // TODO recreate formatter when locale changes 181 protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 182 183 private Runnable mTZUpdater = new Runnable() { 184 @Override 185 public void run() { 186 String tz = Utils.getTimeZone(mContext, this); 187 mBaseDate.timezone = tz; 188 mBaseDate.normalize(true); 189 mCurrentTime.switchTimezone(tz); 190 invalidate(); 191 } 192 }; 193 194 AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { 195 @Override 196 public void onAnimationStart(Animator animation) { 197 mScrolling = true; 198 } 199 200 @Override 201 public void onAnimationCancel(Animator animation) { 202 mScrolling = false; 203 } 204 205 @Override 206 public void onAnimationEnd(Animator animation) { 207 mScrolling = false; 208 resetSelectedHour(); 209 invalidate(); 210 } 211 }; 212 213 /** 214 * This variable helps to avoid unnecessarily reloading events by keeping 215 * track of the start millis parameter used for the most recent loading 216 * of events. If the next reload matches this, then the events are not 217 * reloaded. To force a reload, set this to zero (this is set to zero 218 * in the method clearCachedEvents()). 219 */ 220 private long mLastReloadMillis; 221 222 private ArrayList<Event> mEvents = new ArrayList<Event>(); 223 private ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); 224 private StaticLayout[] mLayouts = null; 225 private StaticLayout[] mAllDayLayouts = null; 226 private int mSelectionDay; // Julian day 227 private int mSelectionHour; 228 229 boolean mSelectionAllday; 230 231 private int mLastSelectionDay; 232 private int mLastSelectionHour; 233 private Event mLastSelectedEvent; 234 235 /** Width of a day or non-conflicting event */ 236 private int mCellWidth; 237 238 // Pre-allocate these objects and re-use them 239 private Rect mRect = new Rect(); 240 private Rect mDestRect = new Rect(); 241 private Rect mSelectionRect = new Rect(); 242 // This encloses the more allDay events icon 243 private Rect mExpandAllDayRect = new Rect(); 244 // TODO Clean up paint usage 245 private Paint mPaint = new Paint(); 246 private Paint mEventTextPaint = new Paint(); 247 private Paint mSelectionPaint = new Paint(); 248 private float[] mLines; 249 250 private int mFirstDayOfWeek; // First day of the week 251 252 private PopupWindow mPopup; 253 private View mPopupView; 254 255 // The number of milliseconds to show the popup window 256 private static final int POPUP_DISMISS_DELAY = 3000; 257 private DismissPopup mDismissPopup = new DismissPopup(); 258 259 private boolean mRemeasure = true; 260 261 private final EventLoader mEventLoader; 262 protected final EventGeometry mEventGeometry; 263 264 private static float GRID_LINE_LEFT_MARGIN = 0; 265 private static final float GRID_LINE_INNER_WIDTH = 1; 266 267 private static final int DAY_GAP = 1; 268 private static final int HOUR_GAP = 1; 269 // This is the standard height of an allday event with no restrictions 270 private static int SINGLE_ALLDAY_HEIGHT = 34; 271 /** 272 * This is the minimum desired height of a allday event. 273 * When unexpanded, allday events will use this height. 274 * When expanded allDay events will attempt to grow to fit all 275 * events at this height. 276 */ 277 private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels 278 /** 279 * This is how big the unexpanded allday height is allowed to be. 280 * It will get adjusted based on screen size 281 */ 282 private static int MAX_UNEXPANDED_ALLDAY_HEIGHT = 283 (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); 284 /** 285 * This is the minimum size reserved for displaying regular events. 286 * The expanded allDay region can't expand into this. 287 */ 288 private static int MIN_HOURS_HEIGHT = 180; 289 private static int ALLDAY_TOP_MARGIN = 1; 290 // The largest a single allDay event will become. 291 private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; 292 293 private static int HOURS_TOP_MARGIN = 2; 294 private static int HOURS_LEFT_MARGIN = 2; 295 private static int HOURS_RIGHT_MARGIN = 4; 296 private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 297 private static int NEW_EVENT_MARGIN = 4; 298 private static int NEW_EVENT_WIDTH = 5; 299 private static int NEW_EVENT_MAX_LENGTH = 16; 300 301 private static int CURRENT_TIME_LINE_HEIGHT = 2; 302 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1; 303 private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4; 304 private static int CURRENT_TIME_LINE_TOP_OFFSET = 2; 305 306 /* package */ static final int MINUTES_PER_HOUR = 60; 307 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; 308 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; 309 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); 310 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 311 312 private static final int DECLINED_ALPHA = 0x66000000; 313 // More events text will transition between invisible and this alpha 314 private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; 315 private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; 316 private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; 317 private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; 318 private static int DAY_HEADER_LEFT_MARGIN = 5; 319 private static int DAY_HEADER_RIGHT_MARGIN = 4; 320 private static int DAY_HEADER_BOTTOM_MARGIN = 3; 321 private static float DAY_HEADER_FONT_SIZE = 14; 322 private static float DATE_HEADER_FONT_SIZE = 32; 323 private static float NORMAL_FONT_SIZE = 12; 324 private static float EVENT_TEXT_FONT_SIZE = 12; 325 private static float HOURS_TEXT_SIZE = 12; 326 private static float AMPM_TEXT_SIZE = 9; 327 private static int MIN_HOURS_WIDTH = 96; 328 private static int MIN_CELL_WIDTH_FOR_TEXT = 20; 329 private static final int MAX_EVENT_TEXT_LEN = 500; 330 // smallest height to draw an event with 331 private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels 332 private static int CALENDAR_COLOR_SQUARE_SIZE = 10; 333 private static int EVENT_RECT_TOP_MARGIN = 1; 334 private static int EVENT_RECT_BOTTOM_MARGIN = 0; 335 private static int EVENT_RECT_LEFT_MARGIN = 1; 336 private static int EVENT_RECT_RIGHT_MARGIN = 0; 337 private static int EVENT_RECT_STROKE_WIDTH = 2; 338 private static int EVENT_TEXT_TOP_MARGIN = 2; 339 private static int EVENT_TEXT_BOTTOM_MARGIN = 2; 340 private static int EVENT_TEXT_LEFT_MARGIN = 6; 341 private static int EVENT_TEXT_RIGHT_MARGIN = 6; 342 private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; 343 private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 344 private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; 345 private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 346 private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; 347 // margins and sizing for the expand allday icon 348 private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; 349 // sizing for "box +n" in allDay events 350 private static int EVENT_SQUARE_WIDTH = 10; 351 private static int EVENT_LINE_PADDING = 4; 352 353 private static int mPressedColor; 354 private static int mEventTextColor; 355 private static int mMoreEventsTextColor; 356 357 private static int mWeek_saturdayColor; 358 private static int mWeek_sundayColor; 359 private static int mCalendarDateBannerTextColor; 360 private static int mCalendarAmPmLabel; 361 private static int mCalendarGridAreaSelected; 362 private static int mCalendarGridLineInnerHorizontalColor; 363 private static int mCalendarGridLineInnerVerticalColor; 364 private static int mFutureBgColor; 365 private static int mBgColor; 366 private static int mCalendarHourLabelColor; 367 private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; 368 369 private float mAnimationDistance = 0; 370 private int mViewStartX; 371 private int mViewStartY; 372 private int mMaxViewStartY; 373 private int mViewHeight; 374 private int mViewWidth; 375 private int mGridAreaHeight = -1; 376 private static int mCellHeight = 0; // shared among all DayViews 377 private static int mMinCellHeight = 32; 378 private int mScrollStartY; 379 private int mPreviousDirection; 380 381 /** 382 * Vertical distance or span between the two touch points at the start of a 383 * scaling gesture 384 */ 385 private float mStartingSpanY = 0; 386 /** Height of 1 hour in pixels at the start of a scaling gesture */ 387 private int mCellHeightBeforeScaleGesture; 388 /** The hour at the center two touch points */ 389 private float mGestureCenterHour = 0; 390 /** 391 * Flag to decide whether to handle the up event. Cases where up events 392 * should be ignored are 1) right after a scale gesture and 2) finger was 393 * down before app launch 394 */ 395 private boolean mHandleActionUp = true; 396 397 private int mHoursTextHeight; 398 /** 399 * The height of the area used for allday events 400 */ 401 private int mAlldayHeight; 402 /** 403 * The height of the allday event area used during animation 404 */ 405 private int mAnimateDayHeight = 0; 406 /** 407 * The height of an individual allday event during animation 408 */ 409 private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 410 /** 411 * Whether to use the expand or collapse icon. 412 */ 413 private static boolean mUseExpandIcon = true; 414 /** 415 * The height of the day names/numbers 416 */ 417 private static int DAY_HEADER_HEIGHT = 45; 418 /** 419 * The height of the day names/numbers for multi-day views 420 */ 421 private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 422 /** 423 * The height of the day names/numbers when viewing a single day 424 */ 425 private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 426 /** 427 * Max of all day events in a given day in this view. 428 */ 429 private int mMaxAlldayEvents; 430 /** 431 * A count of the number of allday events that were not drawn for each day 432 */ 433 private int[] mSkippedAlldayEvents; 434 /** 435 * The number of allDay events at which point we start hiding allDay events. 436 */ 437 private int mMaxUnexpandedAlldayEventCount = 4; 438 /** 439 * Whether or not to expand the allDay area to fill the screen 440 */ 441 private static boolean mShowAllAllDayEvents = false; 442 443 protected int mNumDays = 7; 444 private int mNumHours = 10; 445 446 /** Width of the time line (list of hours) to the left. */ 447 private int mHoursWidth; 448 private int mDateStrWidth; 449 /** Top of the scrollable region i.e. below date labels and all day events */ 450 private int mFirstCell; 451 /** First fully visibile hour */ 452 private int mFirstHour = -1; 453 /** Distance between the mFirstCell and the top of first fully visible hour. */ 454 private int mFirstHourOffset; 455 private String[] mHourStrs; 456 private String[] mDayStrs; 457 private String[] mDayStrs2Letter; 458 private boolean mIs24HourFormat; 459 460 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); 461 private boolean mComputeSelectedEvents; 462 private boolean mUpdateToast; 463 private Event mSelectedEvent; 464 private Event mPrevSelectedEvent; 465 private Rect mPrevBox = new Rect(); 466 protected final Resources mResources; 467 protected final Drawable mCurrentTimeLine; 468 protected final Drawable mTodayHeaderDrawable; 469 protected final Drawable mExpandAlldayDrawable; 470 protected final Drawable mCollapseAlldayDrawable; 471 protected Drawable mAcceptedOrTentativeEventBoxDrawable; 472 private String mAmString; 473 private String mPmString; 474 private DeleteEventHelper mDeleteEventHelper; 475 private static int sCounter = 0; 476 477 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); 478 479 ScaleGestureDetector mScaleGestureDetector; 480 481 /** 482 * The initial state of the touch mode when we enter this view. 483 */ 484 private static final int TOUCH_MODE_INITIAL_STATE = 0; 485 486 /** 487 * Indicates we just received the touch event and we are waiting to see if 488 * it is a tap or a scroll gesture. 489 */ 490 private static final int TOUCH_MODE_DOWN = 1; 491 492 /** 493 * Indicates the touch gesture is a vertical scroll 494 */ 495 private static final int TOUCH_MODE_VSCROLL = 0x20; 496 497 /** 498 * Indicates the touch gesture is a horizontal scroll 499 */ 500 private static final int TOUCH_MODE_HSCROLL = 0x40; 501 502 private int mTouchMode = TOUCH_MODE_INITIAL_STATE; 503 504 /** 505 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. 506 */ 507 private static final int SELECTION_HIDDEN = 0; 508 private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet 509 private static final int SELECTION_SELECTED = 2; 510 private static final int SELECTION_LONGPRESS = 3; 511 512 private int mSelectionMode = SELECTION_HIDDEN; 513 514 private boolean mScrolling = false; 515 516 private float mInitialScrollX; 517 private float mInitialScrollY; 518 519 // Animates the height of the allday region 520 ObjectAnimator mAlldayAnimator; 521 // Animates the height of events in the allday region 522 ObjectAnimator mAlldayEventAnimator; 523 // Animates the transparency of the more events text 524 ObjectAnimator mMoreAlldayEventsAnimator; 525 // whether or not an event is stopping because it was cancelled 526 private boolean mCancellingAnimations = false; 527 // tracks whether a touch originated in the allday area 528 private boolean mTouchStartedInAlldayArea = false; 529 530 private CalendarController mController; 531 private ViewSwitcher mViewSwitcher; 532 private GestureDetector mGestureDetector; 533 private OverScroller mScroller; 534 private EdgeEffect mEdgeEffectTop; 535 private EdgeEffect mEdgeEffectBottom; 536 private boolean mCallEdgeEffectOnAbsorb; 537 private final int OVERFLING_DISTANCE; 538 private float mLastVelocity; 539 540 private ScrollInterpolator mHScrollInterpolator; 541 private AccessibilityManager mAccessibilityMgr = null; 542 private boolean mIsAccessibilityEnabled = false; 543 private boolean mTouchExplorationEnabled = false; 544 private String mCreateNewEventString; 545 546 public DayView(Context context, CalendarController controller, 547 ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { 548 super(context); 549 mContext = context; 550 initAccessibilityVariables(); 551 552 mResources = context.getResources(); 553 mCreateNewEventString = mResources.getString(R.string.event_create); 554 555 DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); 556 DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); 557 ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); 558 DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); 559 EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); 560 HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); 561 AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); 562 MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); 563 HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); 564 HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); 565 MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); 566 EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(R.dimen.event_text_size); 567 MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height); 568 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT; 569 EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin); 570 EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; 571 EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 572 EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; 573 574 EVENT_TEXT_LEFT_MARGIN = (int) mResources 575 .getDimension(R.dimen.event_text_horizontal_margin); 576 EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 577 EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 578 EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 579 580 if (mScale == 0) { 581 582 mScale = mResources.getDisplayMetrics().density; 583 if (mScale != 1) { 584 SINGLE_ALLDAY_HEIGHT *= mScale; 585 ALLDAY_TOP_MARGIN *= mScale; 586 MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; 587 588 NORMAL_FONT_SIZE *= mScale; 589 GRID_LINE_LEFT_MARGIN *= mScale; 590 HOURS_TOP_MARGIN *= mScale; 591 MIN_CELL_WIDTH_FOR_TEXT *= mScale; 592 MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; 593 mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 594 595 CURRENT_TIME_LINE_HEIGHT *= mScale; 596 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale; 597 CURRENT_TIME_LINE_SIDE_BUFFER *= mScale; 598 CURRENT_TIME_LINE_TOP_OFFSET *= mScale; 599 600 MIN_Y_SPAN *= mScale; 601 MAX_CELL_HEIGHT *= mScale; 602 DEFAULT_CELL_HEIGHT *= mScale; 603 DAY_HEADER_HEIGHT *= mScale; 604 DAY_HEADER_LEFT_MARGIN *= mScale; 605 DAY_HEADER_RIGHT_MARGIN *= mScale; 606 DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; 607 DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; 608 DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; 609 CALENDAR_COLOR_SQUARE_SIZE *= mScale; 610 EVENT_RECT_TOP_MARGIN *= mScale; 611 EVENT_RECT_BOTTOM_MARGIN *= mScale; 612 ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; 613 EVENT_RECT_LEFT_MARGIN *= mScale; 614 EVENT_RECT_RIGHT_MARGIN *= mScale; 615 EVENT_RECT_STROKE_WIDTH *= mScale; 616 EVENT_SQUARE_WIDTH *= mScale; 617 EVENT_LINE_PADDING *= mScale; 618 NEW_EVENT_MARGIN *= mScale; 619 NEW_EVENT_WIDTH *= mScale; 620 NEW_EVENT_MAX_LENGTH *= mScale; 621 } 622 } 623 HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 624 DAY_HEADER_HEIGHT = numDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; 625 626 mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light); 627 mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); 628 mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_expand_holo_light); 629 mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_collapse_holo_light); 630 mAcceptedOrTentativeEventBoxDrawable = mResources 631 .getDrawable(R.drawable.panel_month_event_holo_light); 632 633 mEventLoader = eventLoader; 634 mEventGeometry = new EventGeometry(); 635 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); 636 mEventGeometry.setHourGap(HOUR_GAP); 637 mEventGeometry.setCellMargin(DAY_GAP); 638 mLongPressItems = new CharSequence[] { 639 mResources.getString(R.string.new_event_dialog_option) 640 }; 641 mLongPressTitle = mResources.getString(R.string.new_event_dialog_label); 642 mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); 643 mLastPopupEventID = INVALID_EVENT_ID; 644 mController = controller; 645 mViewSwitcher = viewSwitcher; 646 mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); 647 mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); 648 mNumDays = numDays; 649 if (mCellHeight == 0) { 650 mCellHeight = Utils.getSharedPreference(mContext, 651 GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); 652 } 653 mScroller = new OverScroller(context); 654 mHScrollInterpolator = new ScrollInterpolator(); 655 mEdgeEffectTop = new EdgeEffect(context); 656 mEdgeEffectBottom = new EdgeEffect(context); 657 OVERFLING_DISTANCE = ViewConfiguration.get(context).getScaledOverflingDistance(); 658 659 init(context); 660 } 661 662 private void init(Context context) { 663 setFocusable(true); 664 665 // Allow focus in touch mode so that we can do keyboard shortcuts 666 // even after we've entered touch mode. 667 setFocusableInTouchMode(true); 668 setClickable(true); 669 setOnCreateContextMenuListener(this); 670 671 mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); 672 673 mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); 674 long currentTime = System.currentTimeMillis(); 675 mCurrentTime.set(currentTime); 676 //The % makes it go off at the next increment of 5 minutes. 677 postDelayed(mUpdateCurrentTime, 678 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 679 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 680 681 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); 682 mWeek_sundayColor = mResources.getColor(R.color.week_sunday); 683 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); 684 mFutureBgColor = mResources.getColor(R.color.calendar_future_bg_color); 685 mBgColor = mResources.getColor(R.color.calendar_hour_background); 686 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); 687 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); 688 mCalendarGridLineInnerHorizontalColor = mResources 689 .getColor(R.color.calendar_grid_line_inner_horizontal_color); 690 mCalendarGridLineInnerVerticalColor = mResources 691 .getColor(R.color.calendar_grid_line_inner_vertical_color); 692 mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); 693 mPressedColor = mResources.getColor(R.color.pressed); 694 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); 695 mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color); 696 697 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); 698 mEventTextPaint.setTextAlign(Paint.Align.LEFT); 699 mEventTextPaint.setAntiAlias(true); 700 701 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); 702 Paint p = mSelectionPaint; 703 p.setColor(gridLineColor); 704 p.setStyle(Style.FILL); 705 p.setAntiAlias(false); 706 707 p = mPaint; 708 p.setAntiAlias(true); 709 710 // Allocate space for 2 weeks worth of weekday names so that we can 711 // easily start the week display at any week day. 712 mDayStrs = new String[14]; 713 714 // Also create an array of 2-letter abbreviations. 715 mDayStrs2Letter = new String[14]; 716 717 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 718 int index = i - Calendar.SUNDAY; 719 // e.g. Tue for Tuesday 720 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM) 721 .toUpperCase(); 722 mDayStrs[index + 7] = mDayStrs[index]; 723 // e.g. Tu for Tuesday 724 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) 725 .toUpperCase(); 726 727 // If we don't have 2-letter day strings, fall back to 1-letter. 728 if (mDayStrs2Letter[index].equals(mDayStrs[index])) { 729 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); 730 } 731 732 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; 733 } 734 735 // Figure out how much space we need for the 3-letter abbrev names 736 // in the worst case. 737 p.setTextSize(DATE_HEADER_FONT_SIZE); 738 p.setTypeface(mBold); 739 String[] dateStrs = {" 28", " 30"}; 740 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); 741 p.setTextSize(DAY_HEADER_FONT_SIZE); 742 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); 743 744 p.setTextSize(HOURS_TEXT_SIZE); 745 p.setTypeface(null); 746 handleOnResume(); 747 748 mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); 749 mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); 750 String[] ampm = {mAmString, mPmString}; 751 p.setTextSize(AMPM_TEXT_SIZE); 752 mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) 753 + HOURS_RIGHT_MARGIN); 754 mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); 755 756 LayoutInflater inflater; 757 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 758 mPopupView = inflater.inflate(R.layout.bubble_event, null); 759 mPopupView.setLayoutParams(new ViewGroup.LayoutParams( 760 ViewGroup.LayoutParams.MATCH_PARENT, 761 ViewGroup.LayoutParams.WRAP_CONTENT)); 762 mPopup = new PopupWindow(context); 763 mPopup.setContentView(mPopupView); 764 Resources.Theme dialogTheme = getResources().newTheme(); 765 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); 766 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { 767 android.R.attr.windowBackground }); 768 mPopup.setBackgroundDrawable(ta.getDrawable(0)); 769 ta.recycle(); 770 771 // Enable touching the popup window 772 mPopupView.setOnClickListener(this); 773 // Catch long clicks for creating a new event 774 setOnLongClickListener(this); 775 776 mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); 777 long millis = System.currentTimeMillis(); 778 mBaseDate.set(millis); 779 780 mEarliestStartHour = new int[mNumDays]; 781 mHasAllDayEvent = new boolean[mNumDays]; 782 783 // mLines is the array of points used with Canvas.drawLines() in 784 // drawGridBackground() and drawAllDayEvents(). Its size depends 785 // on the max number of lines that can ever be drawn by any single 786 // drawLines() call in either of those methods. 787 final int maxGridLines = (24 + 1) // max horizontal lines we might draw 788 + (mNumDays + 1); // max vertical lines we might draw 789 mLines = new float[maxGridLines * 4]; 790 } 791 792 /** 793 * This is called when the popup window is pressed. 794 */ 795 public void onClick(View v) { 796 if (v == mPopupView) { 797 // Pretend it was a trackball click because that will always 798 // jump to the "View event" screen. 799 switchViews(true /* trackball */); 800 } 801 } 802 803 public void handleOnResume() { 804 initAccessibilityVariables(); 805 mIs24HourFormat = DateFormat.is24HourFormat(mContext); 806 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; 807 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 808 mLastSelectionDay = 0; 809 mLastSelectionHour = 0; 810 mLastSelectedEvent = null; 811 mSelectionMode = SELECTION_HIDDEN; 812 } 813 814 private void initAccessibilityVariables() { 815 mAccessibilityMgr = (AccessibilityManager) mContext 816 .getSystemService(Service.ACCESSIBILITY_SERVICE); 817 mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr.isEnabled(); 818 mTouchExplorationEnabled = isTouchExplorationEnabled(); 819 } 820 821 /** 822 * Returns the start of the selected time in milliseconds since the epoch. 823 * 824 * @return selected time in UTC milliseconds since the epoch. 825 */ 826 long getSelectedTimeInMillis() { 827 Time time = new Time(mBaseDate); 828 time.setJulianDay(mSelectionDay); 829 time.hour = mSelectionHour; 830 831 // We ignore the "isDst" field because we want normalize() to figure 832 // out the correct DST value and not adjust the selected time based 833 // on the current setting of DST. 834 return time.normalize(true /* ignore isDst */); 835 } 836 837 Time getSelectedTime() { 838 Time time = new Time(mBaseDate); 839 time.setJulianDay(mSelectionDay); 840 time.hour = mSelectionHour; 841 842 // We ignore the "isDst" field because we want normalize() to figure 843 // out the correct DST value and not adjust the selected time based 844 // on the current setting of DST. 845 time.normalize(true /* ignore isDst */); 846 return time; 847 } 848 849 /** 850 * Returns the start of the selected time in minutes since midnight, 851 * local time. The derived class must ensure that this is consistent 852 * with the return value from getSelectedTimeInMillis(). 853 */ 854 int getSelectedMinutesSinceMidnight() { 855 return mSelectionHour * MINUTES_PER_HOUR; 856 } 857 858 int getFirstVisibleHour() { 859 return mFirstHour; 860 } 861 862 void setFirstVisibleHour(int firstHour) { 863 mFirstHour = firstHour; 864 mFirstHourOffset = 0; 865 } 866 867 public void setSelected(Time time, boolean ignoreTime) { 868 mBaseDate.set(time); 869 mSelectionHour = mBaseDate.hour; 870 mSelectedEvent = null; 871 mPrevSelectedEvent = null; 872 long millis = mBaseDate.toMillis(false /* use isDst */); 873 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff); 874 mSelectedEvents.clear(); 875 mComputeSelectedEvents = true; 876 877 int gotoY = Integer.MIN_VALUE; 878 879 if (!ignoreTime && mGridAreaHeight != -1) { 880 int lastHour = 0; 881 882 if (mBaseDate.hour < mFirstHour) { 883 // Above visible region 884 gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); 885 } else { 886 lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) 887 + mFirstHour; 888 889 if (mBaseDate.hour >= lastHour) { 890 // Below visible region 891 892 // target hour + 1 (to give it room to see the event) - 893 // grid height (to get the y of the top of the visible 894 // region) 895 gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) 896 * (mCellHeight + HOUR_GAP) - mGridAreaHeight); 897 } 898 } 899 900 if (DEBUG) { 901 Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " 902 + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight 903 + " ymax " + mMaxViewStartY); 904 } 905 906 if (gotoY > mMaxViewStartY) { 907 gotoY = mMaxViewStartY; 908 } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { 909 gotoY = 0; 910 } 911 } 912 913 recalc(); 914 915 mRemeasure = true; 916 invalidate(); 917 918 if (gotoY != Integer.MIN_VALUE) { 919 TypeEvaluator<Object> evaluator = new TypeEvaluator<Object>() { 920 @Override 921 public Object evaluate(float fraction, Object startValue, Object endValue) { 922 int start = (Integer) startValue; 923 int end = (Integer) endValue; 924 final int newValue = (int) ((end - start) * fraction + start); 925 setViewStartY(newValue); 926 return new Integer(newValue); 927 } 928 }; 929 ValueAnimator scrollAnim = ObjectAnimator.ofObject(evaluator, new Integer(mViewStartY), 930 new Integer(gotoY)); 931// TODO The following line is supposed to replace the two statements above. 932// Need to investigate why it's not working. 933 934// ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); 935 scrollAnim.setDuration(200); 936 scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 937 scrollAnim.addListener(mAnimatorListener); 938 scrollAnim.start(); 939 } 940 sendAccessibilityEventAsNeeded(false); 941 } 942 943 public void setViewStartY(int viewStartY) { 944 if (viewStartY > mMaxViewStartY) { 945 viewStartY = mMaxViewStartY; 946 } 947 948 mViewStartY = viewStartY; 949 950 computeFirstHour(); 951 invalidate(); 952 } 953 954 public Time getSelectedDay() { 955 Time time = new Time(mBaseDate); 956 time.setJulianDay(mSelectionDay); 957 time.hour = mSelectionHour; 958 959 // We ignore the "isDst" field because we want normalize() to figure 960 // out the correct DST value and not adjust the selected time based 961 // on the current setting of DST. 962 time.normalize(true /* ignore isDst */); 963 return time; 964 } 965 966 public void updateTitle() { 967 Time start = new Time(mBaseDate); 968 start.normalize(true); 969 Time end = new Time(start); 970 end.monthDay += mNumDays - 1; 971 // Move it forward one minute so the formatter doesn't lose a day 972 end.minute += 1; 973 end.normalize(true); 974 975 long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 976 if (mNumDays != 1) { 977 // Don't show day of the month if for multi-day view 978 formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; 979 980 // Abbreviate the month if showing multiple months 981 if (start.month != end.month) { 982 formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; 983 } 984 } 985 986 mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, 987 formatFlags, null, null); 988 } 989 990 /** 991 * return a negative number if "time" is comes before the visible time 992 * range, a positive number if "time" is after the visible time range, and 0 993 * if it is in the visible time range. 994 */ 995 public int compareToVisibleTimeRange(Time time) { 996 997 int savedHour = mBaseDate.hour; 998 int savedMinute = mBaseDate.minute; 999 int savedSec = mBaseDate.second; 1000 1001 mBaseDate.hour = 0; 1002 mBaseDate.minute = 0; 1003 mBaseDate.second = 0; 1004 1005 if (DEBUG) { 1006 Log.d(TAG, "Begin " + mBaseDate.toString()); 1007 Log.d(TAG, "Diff " + time.toString()); 1008 } 1009 1010 // Compare beginning of range 1011 int diff = Time.compare(time, mBaseDate); 1012 if (diff > 0) { 1013 // Compare end of range 1014 mBaseDate.monthDay += mNumDays; 1015 mBaseDate.normalize(true); 1016 diff = Time.compare(time, mBaseDate); 1017 1018 if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); 1019 1020 mBaseDate.monthDay -= mNumDays; 1021 mBaseDate.normalize(true); 1022 if (diff < 0) { 1023 // in visible time 1024 diff = 0; 1025 } else if (diff == 0) { 1026 // Midnight of following day 1027 diff = 1; 1028 } 1029 } 1030 1031 if (DEBUG) Log.d(TAG, "Diff: " + diff); 1032 1033 mBaseDate.hour = savedHour; 1034 mBaseDate.minute = savedMinute; 1035 mBaseDate.second = savedSec; 1036 return diff; 1037 } 1038 1039 private void recalc() { 1040 // Set the base date to the beginning of the week if we are displaying 1041 // 7 days at a time. 1042 if (mNumDays == 7) { 1043 adjustToBeginningOfWeek(mBaseDate); 1044 } 1045 1046 final long start = mBaseDate.toMillis(false /* use isDst */); 1047 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); 1048 mLastJulianDay = mFirstJulianDay + mNumDays - 1; 1049 1050 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); 1051 mFirstVisibleDate = mBaseDate.monthDay; 1052 mFirstVisibleDayOfWeek = mBaseDate.weekDay; 1053 } 1054 1055 private void adjustToBeginningOfWeek(Time time) { 1056 int dayOfWeek = time.weekDay; 1057 int diff = dayOfWeek - mFirstDayOfWeek; 1058 if (diff != 0) { 1059 if (diff < 0) { 1060 diff += 7; 1061 } 1062 time.monthDay -= diff; 1063 time.normalize(true /* ignore isDst */); 1064 } 1065 } 1066 1067 @Override 1068 protected void onSizeChanged(int width, int height, int oldw, int oldh) { 1069 mViewWidth = width; 1070 mViewHeight = height; 1071 mEdgeEffectTop.setSize(mViewWidth, mViewHeight); 1072 mEdgeEffectBottom.setSize(mViewWidth, mViewHeight); 1073 int gridAreaWidth = width - mHoursWidth; 1074 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; 1075 1076 // This would be about 1 day worth in a 7 day view 1077 mHorizontalSnapBackThreshold = width / 7; 1078 1079 Paint p = new Paint(); 1080 p.setTextSize(HOURS_TEXT_SIZE); 1081 mHoursTextHeight = (int) Math.abs(p.ascent()); 1082 remeasure(width, height); 1083 } 1084 1085 /** 1086 * Measures the space needed for various parts of the view after 1087 * loading new events. This can change if there are all-day events. 1088 */ 1089 private void remeasure(int width, int height) { 1090 // Shrink to fit available space but make sure we can display at least two events 1091 MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); 1092 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); 1093 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, 1094 (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); 1095 mMaxUnexpandedAlldayEventCount = 1096 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1097 1098 // First, clear the array of earliest start times, and the array 1099 // indicating presence of an all-day event. 1100 for (int day = 0; day < mNumDays; day++) { 1101 mEarliestStartHour[day] = 25; // some big number 1102 mHasAllDayEvent[day] = false; 1103 } 1104 1105 int maxAllDayEvents = mMaxAlldayEvents; 1106 1107 // The min is where 24 hours cover the entire visible area 1108 mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); 1109 if (mCellHeight < mMinCellHeight) { 1110 mCellHeight = mMinCellHeight; 1111 } 1112 1113 // Calculate mAllDayHeight 1114 mFirstCell = DAY_HEADER_HEIGHT; 1115 int allDayHeight = 0; 1116 if (maxAllDayEvents > 0) { 1117 int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 1118 // If there is at most one all-day event per day, then use less 1119 // space (but more than the space for a single event). 1120 if (maxAllDayEvents == 1) { 1121 allDayHeight = SINGLE_ALLDAY_HEIGHT; 1122 } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ 1123 // Allow the all-day area to grow in height depending on the 1124 // number of all-day events we need to show, up to a limit. 1125 allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 1126 if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1127 allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; 1128 } 1129 } else { 1130 // if we have more than the magic number, check if we're animating 1131 // and if not adjust the sizes appropriately 1132 if (mAnimateDayHeight != 0) { 1133 // Don't shrink the space past the final allDay space. The animation 1134 // continues to hide the last event so the more events text can 1135 // fade in. 1136 allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); 1137 } else { 1138 // Try to fit all the events in 1139 allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1140 // But clip the area depending on which mode we're in 1141 if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1142 allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * 1143 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1144 } else if (allDayHeight > maxAllAllDayHeight) { 1145 allDayHeight = maxAllAllDayHeight; 1146 } 1147 } 1148 } 1149 mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; 1150 } else { 1151 mSelectionAllday = false; 1152 } 1153 mAlldayHeight = allDayHeight; 1154 1155 mGridAreaHeight = height - mFirstCell; 1156 1157 // Set up the expand icon position 1158 int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); 1159 mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, 1160 EVENT_ALL_DAY_TEXT_LEFT_MARGIN); 1161 mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth 1162 - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); 1163 mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; 1164 mExpandAllDayRect.top = mExpandAllDayRect.bottom 1165 - mExpandAlldayDrawable.getIntrinsicHeight(); 1166 1167 mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); 1168 mEventGeometry.setHourHeight(mCellHeight); 1169 1170 final long minimumDurationMillis = (long) 1171 (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); 1172 Event.computePositions(mEvents, minimumDurationMillis); 1173 1174 // Compute the top of our reachable view 1175 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 1176 if (DEBUG) { 1177 Log.e(TAG, "mViewStartY: " + mViewStartY); 1178 Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); 1179 } 1180 if (mViewStartY > mMaxViewStartY) { 1181 mViewStartY = mMaxViewStartY; 1182 computeFirstHour(); 1183 } 1184 1185 if (mFirstHour == -1) { 1186 initFirstHour(); 1187 mFirstHourOffset = 0; 1188 } 1189 1190 // When we change the base date, the number of all-day events may 1191 // change and that changes the cell height. When we switch dates, 1192 // we use the mFirstHourOffset from the previous view, but that may 1193 // be too large for the new view if the cell height is smaller. 1194 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { 1195 mFirstHourOffset = mCellHeight + HOUR_GAP - 1; 1196 } 1197 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; 1198 1199 final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); 1200 //When we get new events we don't want to dismiss the popup unless the event changes 1201 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { 1202 mPopup.dismiss(); 1203 } 1204 mPopup.setWidth(eventAreaWidth - 20); 1205 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 1206 } 1207 1208 /** 1209 * Initialize the state for another view. The given view is one that has 1210 * its own bitmap and will use an animation to replace the current view. 1211 * The current view and new view are either both Week views or both Day 1212 * views. They differ in their base date. 1213 * 1214 * @param view the view to initialize. 1215 */ 1216 private void initView(DayView view) { 1217 view.mSelectionHour = mSelectionHour; 1218 view.mSelectedEvents.clear(); 1219 view.mComputeSelectedEvents = true; 1220 view.mFirstHour = mFirstHour; 1221 view.mFirstHourOffset = mFirstHourOffset; 1222 view.remeasure(getWidth(), getHeight()); 1223 view.initAllDayHeights(); 1224 1225 view.mSelectedEvent = null; 1226 view.mPrevSelectedEvent = null; 1227 view.mFirstDayOfWeek = mFirstDayOfWeek; 1228 if (view.mEvents.size() > 0) { 1229 view.mSelectionAllday = mSelectionAllday; 1230 } else { 1231 view.mSelectionAllday = false; 1232 } 1233 1234 // Redraw the screen so that the selection box will be redrawn. We may 1235 // have scrolled to a different part of the day in some other view 1236 // so the selection box in this view may no longer be visible. 1237 view.recalc(); 1238 } 1239 1240 /** 1241 * Switch to another view based on what was selected (an event or a free 1242 * slot) and how it was selected (by touch or by trackball). 1243 * 1244 * @param trackBallSelection true if the selection was made using the 1245 * trackball. 1246 */ 1247 private void switchViews(boolean trackBallSelection) { 1248 Event selectedEvent = mSelectedEvent; 1249 1250 mPopup.dismiss(); 1251 mLastPopupEventID = INVALID_EVENT_ID; 1252 if (mNumDays > 1) { 1253 // This is the Week view. 1254 // With touch, we always switch to Day/Agenda View 1255 // With track ball, if we selected a free slot, then create an event. 1256 // If we selected a specific event, switch to EventInfo view. 1257 if (trackBallSelection) { 1258 if (selectedEvent == null) { 1259 // Switch to the EditEvent view 1260 long startMillis = getSelectedTimeInMillis(); 1261 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1262 long extraLong = 0; 1263 if (mSelectionAllday) { 1264 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 1265 } 1266 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 1267 startMillis, endMillis, -1, -1, extraLong, -1); 1268 } else { 1269 if (mIsAccessibilityEnabled) { 1270 mAccessibilityMgr.interrupt(); 1271 } 1272 // Switch to the EventInfo view 1273 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1274 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1275 getSelectedTimeInMillis()); 1276 } 1277 } else { 1278 // This was a touch selection. If the touch selected a single 1279 // unambiguous event, then view that event. Otherwise go to 1280 // Day/Agenda view. 1281 if (mSelectedEvents.size() == 1) { 1282 if (mIsAccessibilityEnabled) { 1283 mAccessibilityMgr.interrupt(); 1284 } 1285 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1286 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1287 getSelectedTimeInMillis()); 1288 } 1289 } 1290 } else { 1291 // This is the Day view. 1292 // If we selected a free slot, then create an event. 1293 // If we selected an event, then go to the EventInfo view. 1294 if (selectedEvent == null) { 1295 // Switch to the EditEvent view 1296 long startMillis = getSelectedTimeInMillis(); 1297 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1298 long extraLong = 0; 1299 if (mSelectionAllday) { 1300 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 1301 } 1302 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 1303 startMillis, endMillis, -1, -1, extraLong, -1); 1304 } else { 1305 if (mIsAccessibilityEnabled) { 1306 mAccessibilityMgr.interrupt(); 1307 } 1308 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1309 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1310 getSelectedTimeInMillis()); 1311 } 1312 } 1313 } 1314 1315 @Override 1316 public boolean onKeyUp(int keyCode, KeyEvent event) { 1317 mScrolling = false; 1318 long duration = event.getEventTime() - event.getDownTime(); 1319 1320 switch (keyCode) { 1321 case KeyEvent.KEYCODE_DPAD_CENTER: 1322 if (mSelectionMode == SELECTION_HIDDEN) { 1323 // Don't do anything unless the selection is visible. 1324 break; 1325 } 1326 1327 if (mSelectionMode == SELECTION_PRESSED) { 1328 // This was the first press when there was nothing selected. 1329 // Change the selection from the "pressed" state to the 1330 // the "selected" state. We treat short-press and 1331 // long-press the same here because nothing was selected. 1332 mSelectionMode = SELECTION_SELECTED; 1333 invalidate(); 1334 break; 1335 } 1336 1337 // Check the duration to determine if this was a short press 1338 if (duration < ViewConfiguration.getLongPressTimeout()) { 1339 switchViews(true /* trackball */); 1340 } else { 1341 mSelectionMode = SELECTION_LONGPRESS; 1342 invalidate(); 1343 performLongClick(); 1344 } 1345 break; 1346// case KeyEvent.KEYCODE_BACK: 1347// if (event.isTracking() && !event.isCanceled()) { 1348// mPopup.dismiss(); 1349// mContext.finish(); 1350// return true; 1351// } 1352// break; 1353 } 1354 return super.onKeyUp(keyCode, event); 1355 } 1356 1357 @Override 1358 public boolean onKeyDown(int keyCode, KeyEvent event) { 1359 if (mSelectionMode == SELECTION_HIDDEN) { 1360 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1361 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP 1362 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1363 // Display the selection box but don't move or select it 1364 // on this key press. 1365 mSelectionMode = SELECTION_SELECTED; 1366 invalidate(); 1367 return true; 1368 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 1369 // Display the selection box but don't select it 1370 // on this key press. 1371 mSelectionMode = SELECTION_PRESSED; 1372 invalidate(); 1373 return true; 1374 } 1375 } 1376 1377 mSelectionMode = SELECTION_SELECTED; 1378 mScrolling = false; 1379 boolean redraw; 1380 int selectionDay = mSelectionDay; 1381 1382 switch (keyCode) { 1383 case KeyEvent.KEYCODE_DEL: 1384 // Delete the selected event, if any 1385 Event selectedEvent = mSelectedEvent; 1386 if (selectedEvent == null) { 1387 return false; 1388 } 1389 mPopup.dismiss(); 1390 mLastPopupEventID = INVALID_EVENT_ID; 1391 1392 long begin = selectedEvent.startMillis; 1393 long end = selectedEvent.endMillis; 1394 long id = selectedEvent.id; 1395 mDeleteEventHelper.delete(begin, end, id, -1); 1396 return true; 1397 case KeyEvent.KEYCODE_ENTER: 1398 switchViews(true /* trackball or keyboard */); 1399 return true; 1400 case KeyEvent.KEYCODE_BACK: 1401 if (event.getRepeatCount() == 0) { 1402 event.startTracking(); 1403 return true; 1404 } 1405 return super.onKeyDown(keyCode, event); 1406 case KeyEvent.KEYCODE_DPAD_LEFT: 1407 if (mSelectedEvent != null) { 1408 mSelectedEvent = mSelectedEvent.nextLeft; 1409 } 1410 if (mSelectedEvent == null) { 1411 mLastPopupEventID = INVALID_EVENT_ID; 1412 selectionDay -= 1; 1413 } 1414 redraw = true; 1415 break; 1416 1417 case KeyEvent.KEYCODE_DPAD_RIGHT: 1418 if (mSelectedEvent != null) { 1419 mSelectedEvent = mSelectedEvent.nextRight; 1420 } 1421 if (mSelectedEvent == null) { 1422 mLastPopupEventID = INVALID_EVENT_ID; 1423 selectionDay += 1; 1424 } 1425 redraw = true; 1426 break; 1427 1428 case KeyEvent.KEYCODE_DPAD_UP: 1429 if (mSelectedEvent != null) { 1430 mSelectedEvent = mSelectedEvent.nextUp; 1431 } 1432 if (mSelectedEvent == null) { 1433 mLastPopupEventID = INVALID_EVENT_ID; 1434 if (!mSelectionAllday) { 1435 mSelectionHour -= 1; 1436 adjustHourSelection(); 1437 mSelectedEvents.clear(); 1438 mComputeSelectedEvents = true; 1439 } 1440 } 1441 redraw = true; 1442 break; 1443 1444 case KeyEvent.KEYCODE_DPAD_DOWN: 1445 if (mSelectedEvent != null) { 1446 mSelectedEvent = mSelectedEvent.nextDown; 1447 } 1448 if (mSelectedEvent == null) { 1449 mLastPopupEventID = INVALID_EVENT_ID; 1450 if (mSelectionAllday) { 1451 mSelectionAllday = false; 1452 } else { 1453 mSelectionHour++; 1454 adjustHourSelection(); 1455 mSelectedEvents.clear(); 1456 mComputeSelectedEvents = true; 1457 } 1458 } 1459 redraw = true; 1460 break; 1461 1462 default: 1463 return super.onKeyDown(keyCode, event); 1464 } 1465 1466 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { 1467 DayView view = (DayView) mViewSwitcher.getNextView(); 1468 Time date = view.mBaseDate; 1469 date.set(mBaseDate); 1470 if (selectionDay < mFirstJulianDay) { 1471 date.monthDay -= mNumDays; 1472 } else { 1473 date.monthDay += mNumDays; 1474 } 1475 date.normalize(true /* ignore isDst */); 1476 view.mSelectionDay = selectionDay; 1477 1478 initView(view); 1479 1480 Time end = new Time(date); 1481 end.monthDay += mNumDays - 1; 1482 mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); 1483 return true; 1484 } 1485 if (mSelectionDay != selectionDay) { 1486 Time date = new Time(mBaseDate); 1487 date.setJulianDay(selectionDay); 1488 date.hour = mSelectionHour; 1489 mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); 1490 } 1491 mSelectionDay = selectionDay; 1492 mSelectedEvents.clear(); 1493 mComputeSelectedEvents = true; 1494 mUpdateToast = true; 1495 1496 if (redraw) { 1497 invalidate(); 1498 return true; 1499 } 1500 1501 return super.onKeyDown(keyCode, event); 1502 } 1503 1504 1505 @Override 1506 public boolean onHoverEvent(MotionEvent event) { 1507 if (DEBUG) { 1508 int action = event.getAction(); 1509 switch (action) { 1510 case MotionEvent.ACTION_HOVER_ENTER: 1511 Log.e(TAG, "ACTION_HOVER_ENTER"); 1512 break; 1513 case MotionEvent.ACTION_HOVER_MOVE: 1514 Log.e(TAG, "ACTION_HOVER_MOVE"); 1515 break; 1516 case MotionEvent.ACTION_HOVER_EXIT: 1517 Log.e(TAG, "ACTION_HOVER_EXIT"); 1518 break; 1519 default: 1520 Log.e(TAG, "Unknown hover event action. " + event); 1521 } 1522 } 1523 1524 // Mouse also generates hover events 1525 // Send accessibility events if accessibility and exploration are on. 1526 if (!mTouchExplorationEnabled) { 1527 return super.onHoverEvent(event); 1528 } 1529 if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { 1530 setSelectionFromPosition((int) event.getX(), (int) event.getY()); 1531 invalidate(); 1532 } 1533 return true; 1534 } 1535 1536 private boolean isTouchExplorationEnabled() { 1537 return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled(); 1538 } 1539 1540 private void sendAccessibilityEventAsNeeded(boolean speakEvents) { 1541 if (!mIsAccessibilityEnabled) { 1542 return; 1543 } 1544 boolean dayChanged = mLastSelectionDay != mSelectionDay; 1545 boolean hourChanged = mLastSelectionHour != mSelectionHour; 1546 if (dayChanged || hourChanged || mLastSelectedEvent != mSelectedEvent) { 1547 mLastSelectionDay = mSelectionDay; 1548 mLastSelectionHour = mSelectionHour; 1549 mLastSelectedEvent = mSelectedEvent; 1550 1551 StringBuilder b = new StringBuilder(); 1552 1553 // Announce only the changes i.e. day or hour or both 1554 if (dayChanged) { 1555 b.append(getSelectedTime().format("%A ")); 1556 } 1557 if (hourChanged) { 1558 b.append(getSelectedTime().format(mIs24HourFormat ? "%k" : "%l%p")); 1559 } 1560 if (dayChanged || hourChanged) { 1561 b.append(PERIOD_SPACE); 1562 } 1563 1564 if (speakEvents) { 1565 if (mEventCountTemplate == null) { 1566 mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); 1567 } 1568 1569 // Read out the relevant event(s) 1570 int numEvents = mSelectedEvents.size(); 1571 if (numEvents > 0) { 1572 if (mSelectedEvent == null) { 1573 // Read out all the events 1574 int i = 1; 1575 for (Event calEvent : mSelectedEvents) { 1576 if (numEvents > 1) { 1577 // Read out x of numEvents if there are more than one event 1578 mStringBuilder.setLength(0); 1579 b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); 1580 b.append(" "); 1581 } 1582 appendEventAccessibilityString(b, calEvent); 1583 } 1584 } else { 1585 if (numEvents > 1) { 1586 // Read out x of numEvents if there are more than one event 1587 mStringBuilder.setLength(0); 1588 b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents 1589 .indexOf(mSelectedEvent) + 1, numEvents)); 1590 b.append(" "); 1591 } 1592 appendEventAccessibilityString(b, mSelectedEvent); 1593 } 1594 } else { 1595 b.append(mCreateNewEventString); 1596 } 1597 } 1598 1599 if (dayChanged || hourChanged || speakEvents) { 1600 AccessibilityEvent event = AccessibilityEvent 1601 .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); 1602 CharSequence msg = b.toString(); 1603 event.getText().add(msg); 1604 event.setAddedCount(msg.length()); 1605 sendAccessibilityEventUnchecked(event); 1606 } 1607 } 1608 } 1609 1610 /** 1611 * @param b 1612 * @param calEvent 1613 */ 1614 private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { 1615 b.append(calEvent.getTitleAndLocation()); 1616 b.append(PERIOD_SPACE); 1617 String when; 1618 int flags = DateUtils.FORMAT_SHOW_DATE; 1619 if (calEvent.allDay) { 1620 flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; 1621 } else { 1622 flags |= DateUtils.FORMAT_SHOW_TIME; 1623 if (DateFormat.is24HourFormat(mContext)) { 1624 flags |= DateUtils.FORMAT_24HOUR; 1625 } 1626 } 1627 when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); 1628 b.append(when); 1629 b.append(PERIOD_SPACE); 1630 } 1631 1632 private class GotoBroadcaster implements Animation.AnimationListener { 1633 private final int mCounter; 1634 private final Time mStart; 1635 private final Time mEnd; 1636 1637 public GotoBroadcaster(Time start, Time end) { 1638 mCounter = ++sCounter; 1639 mStart = start; 1640 mEnd = end; 1641 } 1642 1643 @Override 1644 public void onAnimationEnd(Animation animation) { 1645 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1646 view.mViewStartX = 0; 1647 view = (DayView) mViewSwitcher.getNextView(); 1648 view.mViewStartX = 0; 1649 1650 if (mCounter == sCounter) { 1651 mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, 1652 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 1653 } 1654 } 1655 1656 @Override 1657 public void onAnimationRepeat(Animation animation) { 1658 } 1659 1660 @Override 1661 public void onAnimationStart(Animation animation) { 1662 } 1663 } 1664 1665 private View switchViews(boolean forward, float xOffSet, float width, float velocity) { 1666 mAnimationDistance = width - xOffSet; 1667 if (DEBUG) { 1668 Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); 1669 } 1670 1671 float progress = Math.abs(xOffSet) / width; 1672 if (progress > 1.0f) { 1673 progress = 1.0f; 1674 } 1675 1676 float inFromXValue, inToXValue; 1677 float outFromXValue, outToXValue; 1678 if (forward) { 1679 inFromXValue = 1.0f - progress; 1680 inToXValue = 0.0f; 1681 outFromXValue = -progress; 1682 outToXValue = -1.0f; 1683 } else { 1684 inFromXValue = progress - 1.0f; 1685 inToXValue = 0.0f; 1686 outFromXValue = progress; 1687 outToXValue = 1.0f; 1688 } 1689 1690 final Time start = new Time(mBaseDate.timezone); 1691 start.set(mController.getTime()); 1692 if (forward) { 1693 start.monthDay += mNumDays; 1694 } else { 1695 start.monthDay -= mNumDays; 1696 } 1697 mController.setTime(start.normalize(true)); 1698 1699 Time newSelected = start; 1700 1701 if (mNumDays == 7) { 1702 newSelected = new Time(start); 1703 adjustToBeginningOfWeek(start); 1704 } 1705 1706 final Time end = new Time(start); 1707 end.monthDay += mNumDays - 1; 1708 1709 // We have to allocate these animation objects each time we switch views 1710 // because that is the only way to set the animation parameters. 1711 TranslateAnimation inAnimation = new TranslateAnimation( 1712 Animation.RELATIVE_TO_SELF, inFromXValue, 1713 Animation.RELATIVE_TO_SELF, inToXValue, 1714 Animation.ABSOLUTE, 0.0f, 1715 Animation.ABSOLUTE, 0.0f); 1716 1717 TranslateAnimation outAnimation = new TranslateAnimation( 1718 Animation.RELATIVE_TO_SELF, outFromXValue, 1719 Animation.RELATIVE_TO_SELF, outToXValue, 1720 Animation.ABSOLUTE, 0.0f, 1721 Animation.ABSOLUTE, 0.0f); 1722 1723 long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); 1724 inAnimation.setDuration(duration); 1725 inAnimation.setInterpolator(mHScrollInterpolator); 1726 outAnimation.setInterpolator(mHScrollInterpolator); 1727 outAnimation.setDuration(duration); 1728 outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); 1729 mViewSwitcher.setInAnimation(inAnimation); 1730 mViewSwitcher.setOutAnimation(outAnimation); 1731 1732 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1733 view.cleanup(); 1734 mViewSwitcher.showNext(); 1735 view = (DayView) mViewSwitcher.getCurrentView(); 1736 view.setSelected(newSelected, true); 1737 view.requestFocus(); 1738 view.reloadEvents(); 1739 view.updateTitle(); 1740 1741 return view; 1742 } 1743 1744 // This is called after scrolling stops to move the selected hour 1745 // to the visible part of the screen. 1746 private void resetSelectedHour() { 1747 if (mSelectionHour < mFirstHour + 1) { 1748 mSelectionHour = mFirstHour + 1; 1749 mSelectedEvent = null; 1750 mSelectedEvents.clear(); 1751 mComputeSelectedEvents = true; 1752 } else if (mSelectionHour > mFirstHour + mNumHours - 3) { 1753 mSelectionHour = mFirstHour + mNumHours - 3; 1754 mSelectedEvent = null; 1755 mSelectedEvents.clear(); 1756 mComputeSelectedEvents = true; 1757 } 1758 } 1759 1760 private void initFirstHour() { 1761 mFirstHour = mSelectionHour - mNumHours / 5; 1762 if (mFirstHour < 0) { 1763 mFirstHour = 0; 1764 } else if (mFirstHour + mNumHours > 24) { 1765 mFirstHour = 24 - mNumHours; 1766 } 1767 } 1768 1769 /** 1770 * Recomputes the first full hour that is visible on screen after the 1771 * screen is scrolled. 1772 */ 1773 private void computeFirstHour() { 1774 // Compute the first full hour that is visible on screen 1775 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); 1776 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; 1777 } 1778 1779 private void adjustHourSelection() { 1780 if (mSelectionHour < 0) { 1781 mSelectionHour = 0; 1782 if (mMaxAlldayEvents > 0) { 1783 mPrevSelectedEvent = null; 1784 mSelectionAllday = true; 1785 } 1786 } 1787 1788 if (mSelectionHour > 23) { 1789 mSelectionHour = 23; 1790 } 1791 1792 // If the selected hour is at least 2 time slots from the top and 1793 // bottom of the screen, then don't scroll the view. 1794 if (mSelectionHour < mFirstHour + 1) { 1795 // If there are all-days events for the selected day but there 1796 // are no more normal events earlier in the day, then jump to 1797 // the all-day event area. 1798 // Exception 1: allow the user to scroll to 8am with the trackball 1799 // before jumping to the all-day event area. 1800 // Exception 2: if 12am is on screen, then allow the user to select 1801 // 12am before going up to the all-day event area. 1802 int daynum = mSelectionDay - mFirstJulianDay; 1803 if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1804 && mFirstHour > 0 && mFirstHour < 8) { 1805 mPrevSelectedEvent = null; 1806 mSelectionAllday = true; 1807 mSelectionHour = mFirstHour + 1; 1808 return; 1809 } 1810 1811 if (mFirstHour > 0) { 1812 mFirstHour -= 1; 1813 mViewStartY -= (mCellHeight + HOUR_GAP); 1814 if (mViewStartY < 0) { 1815 mViewStartY = 0; 1816 } 1817 return; 1818 } 1819 } 1820 1821 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1822 if (mFirstHour < 24 - mNumHours) { 1823 mFirstHour += 1; 1824 mViewStartY += (mCellHeight + HOUR_GAP); 1825 if (mViewStartY > mMaxViewStartY) { 1826 mViewStartY = mMaxViewStartY; 1827 } 1828 return; 1829 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1830 mViewStartY = mMaxViewStartY; 1831 } 1832 } 1833 } 1834 1835 void clearCachedEvents() { 1836 mLastReloadMillis = 0; 1837 } 1838 1839 private Runnable mCancelCallback = new Runnable() { 1840 public void run() { 1841 clearCachedEvents(); 1842 } 1843 }; 1844 1845 /* package */ void reloadEvents() { 1846 // Protect against this being called before this view has been 1847 // initialized. 1848// if (mContext == null) { 1849// return; 1850// } 1851 1852 // Make sure our time zones are up to date 1853 mTZUpdater.run(); 1854 1855 mSelectedEvent = null; 1856 mPrevSelectedEvent = null; 1857 mSelectedEvents.clear(); 1858 1859 // The start date is the beginning of the week at 12am 1860 Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); 1861 weekStart.set(mBaseDate); 1862 weekStart.hour = 0; 1863 weekStart.minute = 0; 1864 weekStart.second = 0; 1865 long millis = weekStart.normalize(true /* ignore isDst */); 1866 1867 // Avoid reloading events unnecessarily. 1868 if (millis == mLastReloadMillis) { 1869 return; 1870 } 1871 mLastReloadMillis = millis; 1872 1873 // load events in the background 1874// mContext.startProgressSpinner(); 1875 final ArrayList<Event> events = new ArrayList<Event>(); 1876 mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { 1877 public void run() { 1878 mEvents = events; 1879 if (mAllDayEvents == null) { 1880 mAllDayEvents = new ArrayList<Event>(); 1881 } else { 1882 mAllDayEvents.clear(); 1883 } 1884 1885 // Create a shorter array for all day events 1886 for (Event e : events) { 1887 if (e.drawAsAllday()) { 1888 mAllDayEvents.add(e); 1889 } 1890 } 1891 1892 // New events, new layouts 1893 if (mLayouts == null || mLayouts.length < events.size()) { 1894 mLayouts = new StaticLayout[events.size()]; 1895 } else { 1896 Arrays.fill(mLayouts, null); 1897 } 1898 1899 if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { 1900 mAllDayLayouts = new StaticLayout[events.size()]; 1901 } else { 1902 Arrays.fill(mAllDayLayouts, null); 1903 } 1904 1905 computeEventRelations(); 1906 1907 mRemeasure = true; 1908 mComputeSelectedEvents = true; 1909 recalc(); 1910// mContext.stopProgressSpinner(); 1911 invalidate(); 1912 } 1913 }, mCancelCallback); 1914 } 1915 1916 private void computeEventRelations() { 1917 // Compute the layout relation between each event before measuring cell 1918 // width, as the cell width should be adjusted along with the relation. 1919 // 1920 // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) 1921 // We should mark them as "overwapped". Though they are not overwapped logically, but 1922 // minimum cell height implicitly expands the cell height of A and it should look like 1923 // (1:00pm - 1:15pm) after the cell height adjustment. 1924 1925 // Compute the space needed for the all-day events, if any. 1926 // Make a pass over all the events, and keep track of the maximum 1927 // number of all-day events in any one day. Also, keep track of 1928 // the earliest event in each day. 1929 int maxAllDayEvents = 0; 1930 final ArrayList<Event> events = mEvents; 1931 final int len = events.size(); 1932 // Num of all-day-events on each day. 1933 final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; 1934 Arrays.fill(eventsCount, 0); 1935 for (int ii = 0; ii < len; ii++) { 1936 Event event = events.get(ii); 1937 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { 1938 continue; 1939 } 1940 if (event.drawAsAllday()) { 1941 // Count all the events being drawn as allDay events 1942 final int firstDay = Math.max(event.startDay, mFirstJulianDay); 1943 final int lastDay = Math.min(event.endDay, mLastJulianDay); 1944 for (int day = firstDay; day <= lastDay; day++) { 1945 final int count = ++eventsCount[day - mFirstJulianDay]; 1946 if (maxAllDayEvents < count) { 1947 maxAllDayEvents = count; 1948 } 1949 } 1950 1951 int daynum = event.startDay - mFirstJulianDay; 1952 int durationDays = event.endDay - event.startDay + 1; 1953 if (daynum < 0) { 1954 durationDays += daynum; 1955 daynum = 0; 1956 } 1957 if (daynum + durationDays > mNumDays) { 1958 durationDays = mNumDays - daynum; 1959 } 1960 for (int day = daynum; durationDays > 0; day++, durationDays--) { 1961 mHasAllDayEvent[day] = true; 1962 } 1963 } else { 1964 int daynum = event.startDay - mFirstJulianDay; 1965 int hour = event.startTime / 60; 1966 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 1967 mEarliestStartHour[daynum] = hour; 1968 } 1969 1970 // Also check the end hour in case the event spans more than 1971 // one day. 1972 daynum = event.endDay - mFirstJulianDay; 1973 hour = event.endTime / 60; 1974 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 1975 mEarliestStartHour[daynum] = hour; 1976 } 1977 } 1978 } 1979 mMaxAlldayEvents = maxAllDayEvents; 1980 initAllDayHeights(); 1981 } 1982 1983 @Override 1984 protected void onDraw(Canvas canvas) { 1985 if (mRemeasure) { 1986 remeasure(getWidth(), getHeight()); 1987 mRemeasure = false; 1988 } 1989 canvas.save(); 1990 1991 float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; 1992 // offset canvas by the current drag and header position 1993 canvas.translate(-mViewStartX, yTranslate); 1994 // clip to everything below the allDay area 1995 Rect dest = mDestRect; 1996 dest.top = (int) (mFirstCell - yTranslate); 1997 dest.bottom = (int) (mViewHeight - yTranslate); 1998 dest.left = 0; 1999 dest.right = mViewWidth; 2000 canvas.save(); 2001 canvas.clipRect(dest); 2002 // Draw the movable part of the view 2003 doDraw(canvas); 2004 // restore to having no clip 2005 canvas.restore(); 2006 2007 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 2008 float xTranslate; 2009 if (mViewStartX > 0) { 2010 xTranslate = mViewWidth; 2011 } else { 2012 xTranslate = -mViewWidth; 2013 } 2014 // Move the canvas around to prep it for the next view 2015 // specifically, shift it by a screen and undo the 2016 // yTranslation which will be redone in the nextView's onDraw(). 2017 canvas.translate(xTranslate, -yTranslate); 2018 DayView nextView = (DayView) mViewSwitcher.getNextView(); 2019 2020 // Prevent infinite recursive calls to onDraw(). 2021 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 2022 2023 nextView.onDraw(canvas); 2024 // Move it back for this view 2025 canvas.translate(-xTranslate, 0); 2026 } else { 2027 // If we drew another view we already translated it back 2028 // If we didn't draw another view we should be at the edge of the 2029 // screen 2030 canvas.translate(mViewStartX, -yTranslate); 2031 } 2032 2033 // Draw the fixed areas (that don't scroll) directly to the canvas. 2034 drawAfterScroll(canvas); 2035 if (mComputeSelectedEvents && mUpdateToast) { 2036 updateEventDetails(); 2037 mUpdateToast = false; 2038 } 2039 mComputeSelectedEvents = false; 2040 2041 // Draw overscroll glow 2042 if (!mEdgeEffectTop.isFinished()) { 2043 if (DAY_HEADER_HEIGHT != 0) { 2044 canvas.translate(0, DAY_HEADER_HEIGHT); 2045 } 2046 if (mEdgeEffectTop.draw(canvas)) { 2047 invalidate(); 2048 } 2049 if (DAY_HEADER_HEIGHT != 0) { 2050 canvas.translate(0, -DAY_HEADER_HEIGHT); 2051 } 2052 } 2053 if (!mEdgeEffectBottom.isFinished()) { 2054 canvas.rotate(180, mViewWidth/2, mViewHeight/2); 2055 if (mEdgeEffectBottom.draw(canvas)) { 2056 invalidate(); 2057 } 2058 } 2059 canvas.restore(); 2060 } 2061 2062 private void drawAfterScroll(Canvas canvas) { 2063 Paint p = mPaint; 2064 Rect r = mRect; 2065 2066 drawAllDayHighlights(r, canvas, p); 2067 if (mMaxAlldayEvents != 0) { 2068 drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); 2069 drawUpperLeftCorner(r, canvas, p); 2070 } 2071 2072 drawScrollLine(r, canvas, p); 2073 drawDayHeaderLoop(r, canvas, p); 2074 2075 // Draw the AM and PM indicators if we're in 12 hour mode 2076 if (!mIs24HourFormat) { 2077 drawAmPm(canvas, p); 2078 } 2079 } 2080 2081 // This isn't really the upper-left corner. It's the square area just 2082 // below the upper-left corner, above the hours and to the left of the 2083 // all-day area. 2084 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 2085 setupHourTextPaint(p); 2086 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2087 // Draw the allDay expand/collapse icon 2088 if (mUseExpandIcon) { 2089 mExpandAlldayDrawable.setBounds(mExpandAllDayRect); 2090 mExpandAlldayDrawable.draw(canvas); 2091 } else { 2092 mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); 2093 mCollapseAlldayDrawable.draw(canvas); 2094 } 2095 } 2096 } 2097 2098 private void drawScrollLine(Rect r, Canvas canvas, Paint p) { 2099 final int right = computeDayLeftPosition(mNumDays + 1); 2100 final int y = mFirstCell - 1; 2101 2102 p.setAntiAlias(false); 2103 p.setStyle(Style.FILL); 2104 2105 p.setColor(mCalendarGridLineInnerHorizontalColor); 2106 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2107 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 2108 p.setAntiAlias(true); 2109 } 2110 2111 // Computes the x position for the left side of the given day 2112 private int computeDayLeftPosition(int day) { 2113 int effectiveWidth = mViewWidth - mHoursWidth; 2114 return day * effectiveWidth / mNumDays + mHoursWidth; 2115 } 2116 2117 private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { 2118 // First, color the labels area light gray 2119 r.top = 0; 2120 r.bottom = DAY_HEADER_HEIGHT; 2121 r.left = 0; 2122 r.right = mViewWidth; 2123 p.setColor(mBgColor); 2124 p.setStyle(Style.FILL); 2125 canvas.drawRect(r, p); 2126 // and the area that says All day 2127 r.top = DAY_HEADER_HEIGHT; 2128 r.bottom = mFirstCell - 1; 2129 r.left = 0; 2130 r.right = mHoursWidth; 2131 canvas.drawRect(r, p); 2132 2133 int startIndex = -1; 2134 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2135 if (todayIndex < 0) { 2136 // Future 2137 startIndex = 0; 2138 } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { 2139 // Multiday - tomorrow is visible. 2140 startIndex = todayIndex + 1; 2141 } 2142 2143 if (startIndex >= 0) { 2144 // Draw the future highlight 2145 r.top = 0; 2146 r.bottom = mFirstCell - 1; 2147 r.left = computeDayLeftPosition(startIndex) + 1; 2148 r.right = computeDayLeftPosition(mNumDays + 1); 2149 p.setColor(mFutureBgColor); 2150 p.setStyle(Style.FILL); 2151 canvas.drawRect(r, p); 2152 } 2153 2154 if (mSelectionAllday) { 2155 // Draw the selection highlight on the selected all-day area 2156 mRect.top = DAY_HEADER_HEIGHT + 1; 2157 mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; 2158 int daynum = mSelectionDay - mFirstJulianDay; 2159 mRect.left = computeDayLeftPosition(daynum) + 1; 2160 mRect.right = computeDayLeftPosition(daynum + 1); 2161 p.setColor(mCalendarGridAreaSelected); 2162 canvas.drawRect(mRect, p); 2163 } 2164 } 2165 2166 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 2167 // Draw the horizontal day background banner 2168 // p.setColor(mCalendarDateBannerBackground); 2169 // r.top = 0; 2170 // r.bottom = DAY_HEADER_HEIGHT; 2171 // r.left = 0; 2172 // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 2173 // canvas.drawRect(r, p); 2174 // 2175 // Fill the extra space on the right side with the default background 2176 // r.left = r.right; 2177 // r.right = mViewWidth; 2178 // p.setColor(mCalendarGridAreaBackground); 2179 // canvas.drawRect(r, p); 2180 if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { 2181 return; 2182 } 2183 2184 p.setTypeface(mBold); 2185 p.setTextAlign(Paint.Align.RIGHT); 2186 int cell = mFirstJulianDay; 2187 2188 String[] dayNames; 2189 if (mDateStrWidth < mCellWidth) { 2190 dayNames = mDayStrs; 2191 } else { 2192 dayNames = mDayStrs2Letter; 2193 } 2194 2195 p.setAntiAlias(true); 2196 for (int day = 0; day < mNumDays; day++, cell++) { 2197 int dayOfWeek = day + mFirstVisibleDayOfWeek; 2198 if (dayOfWeek >= 14) { 2199 dayOfWeek -= 14; 2200 } 2201 2202 int color = mCalendarDateBannerTextColor; 2203 if (mNumDays == 1) { 2204 if (dayOfWeek == Time.SATURDAY) { 2205 color = mWeek_saturdayColor; 2206 } else if (dayOfWeek == Time.SUNDAY) { 2207 color = mWeek_sundayColor; 2208 } 2209 } else { 2210 final int column = day % 7; 2211 if (Utils.isSaturday(column, mFirstDayOfWeek)) { 2212 color = mWeek_saturdayColor; 2213 } else if (Utils.isSunday(column, mFirstDayOfWeek)) { 2214 color = mWeek_sundayColor; 2215 } 2216 } 2217 2218 p.setColor(color); 2219 drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); 2220 } 2221 p.setTypeface(null); 2222 } 2223 2224 private void drawAmPm(Canvas canvas, Paint p) { 2225 p.setColor(mCalendarAmPmLabel); 2226 p.setTextSize(AMPM_TEXT_SIZE); 2227 p.setTypeface(mBold); 2228 p.setAntiAlias(true); 2229 p.setTextAlign(Paint.Align.RIGHT); 2230 String text = mAmString; 2231 if (mFirstHour >= 12) { 2232 text = mPmString; 2233 } 2234 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 2235 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2236 2237 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 2238 // Also draw the "PM" 2239 text = mPmString; 2240 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 2241 + 2 * mHoursTextHeight + HOUR_GAP; 2242 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2243 } 2244 } 2245 2246 private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, 2247 Paint p) { 2248 r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2249 r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2250 2251 r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; 2252 r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); 2253 2254 mCurrentTimeLine.setBounds(r); 2255 mCurrentTimeLine.draw(canvas); 2256 } 2257 2258 private void doDraw(Canvas canvas) { 2259 Paint p = mPaint; 2260 Rect r = mRect; 2261 2262 drawBgColors(r, canvas, p); 2263 drawGridBackground(r, canvas, p); 2264 drawHours(r, canvas, p); 2265 2266 // Draw each day 2267 int cell = mFirstJulianDay; 2268 p.setAntiAlias(false); 2269 for (int day = 0; day < mNumDays; day++, cell++) { 2270 // TODO Wow, this needs cleanup. drawEvents loop through all the 2271 // events on every call. 2272 drawEvents(cell, day, HOUR_GAP, canvas, p); 2273 // If this is today 2274 if (cell == mTodayJulianDay) { 2275 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2276 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2277 2278 // And the current time shows up somewhere on the screen 2279 if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 2280 drawCurrentTimeLine(r, day, lineY, canvas, p); 2281 } 2282 } 2283 } 2284 p.setAntiAlias(true); 2285 2286 drawSelectedRect(r, canvas, p); 2287 } 2288 2289 private void drawSelectedRect(Rect r, Canvas canvas, Paint p) { 2290 // Draw a highlight on the selected hour (if needed) 2291 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { 2292 int daynum = mSelectionDay - mFirstJulianDay; 2293 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2294 r.bottom = r.top + mCellHeight + HOUR_GAP; 2295 r.left = computeDayLeftPosition(daynum) + 1; 2296 r.right = computeDayLeftPosition(daynum + 1) + 1; 2297 2298 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 2299 2300 // Draw the highlight on the grid 2301 p.setColor(mCalendarGridAreaSelected); 2302 r.top += HOUR_GAP; 2303 r.right -= DAY_GAP; 2304 p.setAntiAlias(false); 2305 canvas.drawRect(r, p); 2306 2307 // Draw a + on top of the highlight 2308 int width = r.right - r.left; 2309 int midX = r.left + width / 2; 2310 int midY = r.top + mCellHeight / 2; 2311 int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2; 2312 length = Math.min(length, NEW_EVENT_MAX_LENGTH); 2313 int verticalPadding = (mCellHeight - length) / 2; 2314 int horizontalPadding = (width - length) / 2; 2315 p.setStrokeWidth(NEW_EVENT_WIDTH); 2316 p.setColor(0xFFFFFFFF); 2317 canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, midY, p); 2318 canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p); 2319 } 2320 } 2321 2322 private void drawHours(Rect r, Canvas canvas, Paint p) { 2323 setupHourTextPaint(p); 2324 2325 int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; 2326 2327 for (int i = 0; i < 24; i++) { 2328 String time = mHourStrs[i]; 2329 canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); 2330 y += mCellHeight + HOUR_GAP; 2331 } 2332 } 2333 2334 private void setupHourTextPaint(Paint p) { 2335 p.setColor(mCalendarHourLabelColor); 2336 p.setTextSize(HOURS_TEXT_SIZE); 2337 p.setTypeface(Typeface.DEFAULT); 2338 p.setTextAlign(Paint.Align.RIGHT); 2339 p.setAntiAlias(true); 2340 } 2341 2342 private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { 2343 int dateNum = mFirstVisibleDate + day; 2344 int x; 2345 if (dateNum > mMonthLength) { 2346 dateNum -= mMonthLength; 2347 } 2348 p.setAntiAlias(true); 2349 2350 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2351 // Draw day of the month 2352 String dateNumStr = String.valueOf(dateNum); 2353 if (mNumDays > 1) { 2354 float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; 2355 2356 // Draw day of the month 2357 x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; 2358 p.setTextAlign(Align.RIGHT); 2359 p.setTextSize(DATE_HEADER_FONT_SIZE); 2360 2361 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2362 canvas.drawText(dateNumStr, x, y, p); 2363 2364 // Draw day of the week 2365 x -= p.measureText(" " + dateNumStr); 2366 p.setTextSize(DAY_HEADER_FONT_SIZE); 2367 p.setTypeface(Typeface.DEFAULT); 2368 canvas.drawText(dayStr, x, y, p); 2369 } else { 2370 float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; 2371 p.setTextAlign(Align.LEFT); 2372 2373 2374 // Draw day of the week 2375 x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; 2376 p.setTextSize(DAY_HEADER_FONT_SIZE); 2377 p.setTypeface(Typeface.DEFAULT); 2378 canvas.drawText(dayStr, x, y, p); 2379 2380 // Draw day of the month 2381 x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; 2382 p.setTextSize(DATE_HEADER_FONT_SIZE); 2383 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2384 canvas.drawText(dateNumStr, x, y, p); 2385 } 2386 } 2387 2388 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 2389 Paint.Style savedStyle = p.getStyle(); 2390 2391 final float stopX = computeDayLeftPosition(mNumDays + 1); 2392 float y = 0; 2393 final float deltaY = mCellHeight + HOUR_GAP; 2394 int linesIndex = 0; 2395 final float startY = 0; 2396 final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 2397 float x = mHoursWidth; 2398 2399 // Draw the inner horizontal grid lines 2400 p.setColor(mCalendarGridLineInnerHorizontalColor); 2401 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2402 p.setAntiAlias(false); 2403 y = 0; 2404 linesIndex = 0; 2405 for (int hour = 0; hour <= 24; hour++) { 2406 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2407 mLines[linesIndex++] = y; 2408 mLines[linesIndex++] = stopX; 2409 mLines[linesIndex++] = y; 2410 y += deltaY; 2411 } 2412 if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { 2413 canvas.drawLines(mLines, 0, linesIndex, p); 2414 linesIndex = 0; 2415 p.setColor(mCalendarGridLineInnerVerticalColor); 2416 } 2417 2418 // Draw the inner vertical grid lines 2419 for (int day = 0; day <= mNumDays; day++) { 2420 x = computeDayLeftPosition(day); 2421 mLines[linesIndex++] = x; 2422 mLines[linesIndex++] = startY; 2423 mLines[linesIndex++] = x; 2424 mLines[linesIndex++] = stopY; 2425 } 2426 canvas.drawLines(mLines, 0, linesIndex, p); 2427 2428 // Restore the saved style. 2429 p.setStyle(savedStyle); 2430 p.setAntiAlias(true); 2431 } 2432 2433 /** 2434 * @param r 2435 * @param canvas 2436 * @param p 2437 */ 2438 private void drawBgColors(Rect r, Canvas canvas, Paint p) { 2439 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2440 // Draw the hours background color 2441 r.top = mDestRect.top; 2442 r.bottom = mDestRect.bottom; 2443 r.left = 0; 2444 r.right = mHoursWidth; 2445 p.setColor(mBgColor); 2446 p.setStyle(Style.FILL); 2447 p.setAntiAlias(false); 2448 canvas.drawRect(r, p); 2449 2450 // Draw background for grid area 2451 if (mNumDays == 1 && todayIndex == 0) { 2452 // Draw a white background for the time later than current time 2453 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2454 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2455 if (lineY < mViewStartY + mViewHeight) { 2456 lineY = Math.max(lineY, mViewStartY); 2457 r.left = mHoursWidth; 2458 r.right = mViewWidth; 2459 r.top = lineY; 2460 r.bottom = mViewStartY + mViewHeight; 2461 p.setColor(mFutureBgColor); 2462 canvas.drawRect(r, p); 2463 } 2464 } else if (todayIndex >= 0 && todayIndex < mNumDays) { 2465 // Draw today with a white background for the time later than current time 2466 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2467 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2468 if (lineY < mViewStartY + mViewHeight) { 2469 lineY = Math.max(lineY, mViewStartY); 2470 r.left = computeDayLeftPosition(todayIndex) + 1; 2471 r.right = computeDayLeftPosition(todayIndex + 1); 2472 r.top = lineY; 2473 r.bottom = mViewStartY + mViewHeight; 2474 p.setColor(mFutureBgColor); 2475 canvas.drawRect(r, p); 2476 } 2477 2478 // Paint Tomorrow and later days with future color 2479 if (todayIndex + 1 < mNumDays) { 2480 r.left = computeDayLeftPosition(todayIndex + 1) + 1; 2481 r.right = computeDayLeftPosition(mNumDays + 1); 2482 r.top = mDestRect.top; 2483 r.bottom = mDestRect.bottom; 2484 p.setColor(mFutureBgColor); 2485 canvas.drawRect(r, p); 2486 } 2487 } else if (todayIndex < 0) { 2488 // Future 2489 r.left = computeDayLeftPosition(0) + 1; 2490 r.right = computeDayLeftPosition(mNumDays + 1); 2491 r.top = mDestRect.top; 2492 r.bottom = mDestRect.bottom; 2493 p.setColor(mFutureBgColor); 2494 canvas.drawRect(r, p); 2495 } 2496 p.setAntiAlias(true); 2497 } 2498 2499 Event getSelectedEvent() { 2500 if (mSelectedEvent == null) { 2501 // There is no event at the selected hour, so create a new event. 2502 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2503 getSelectedMinutesSinceMidnight()); 2504 } 2505 return mSelectedEvent; 2506 } 2507 2508 boolean isEventSelected() { 2509 return (mSelectedEvent != null); 2510 } 2511 2512 Event getNewEvent() { 2513 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2514 getSelectedMinutesSinceMidnight()); 2515 } 2516 2517 static Event getNewEvent(int julianDay, long utcMillis, 2518 int minutesSinceMidnight) { 2519 Event event = Event.newInstance(); 2520 event.startDay = julianDay; 2521 event.endDay = julianDay; 2522 event.startMillis = utcMillis; 2523 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 2524 event.startTime = minutesSinceMidnight; 2525 event.endTime = event.startTime + MINUTES_PER_HOUR; 2526 return event; 2527 } 2528 2529 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 2530 float maxWidthF = 0.0f; 2531 2532 int len = strings.length; 2533 for (int i = 0; i < len; i++) { 2534 float width = p.measureText(strings[i]); 2535 maxWidthF = Math.max(width, maxWidthF); 2536 } 2537 int maxWidth = (int) (maxWidthF + 0.5); 2538 if (maxWidth < currentMax) { 2539 maxWidth = currentMax; 2540 } 2541 return maxWidth; 2542 } 2543 2544 private void saveSelectionPosition(float left, float top, float right, float bottom) { 2545 mPrevBox.left = (int) left; 2546 mPrevBox.right = (int) right; 2547 mPrevBox.top = (int) top; 2548 mPrevBox.bottom = (int) bottom; 2549 } 2550 2551 private Rect getCurrentSelectionPosition() { 2552 Rect box = new Rect(); 2553 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2554 box.bottom = box.top + mCellHeight + HOUR_GAP; 2555 int daynum = mSelectionDay - mFirstJulianDay; 2556 box.left = computeDayLeftPosition(daynum) + 1; 2557 box.right = computeDayLeftPosition(daynum + 1); 2558 return box; 2559 } 2560 2561 private void setupTextRect(Rect r) { 2562 if (r.bottom <= r.top || r.right <= r.left) { 2563 r.bottom = r.top; 2564 r.right = r.left; 2565 return; 2566 } 2567 2568 if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { 2569 r.top += EVENT_TEXT_TOP_MARGIN; 2570 r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; 2571 } 2572 if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { 2573 r.left += EVENT_TEXT_LEFT_MARGIN; 2574 r.right -= EVENT_TEXT_RIGHT_MARGIN; 2575 } 2576 } 2577 2578 private void setupAllDayTextRect(Rect r) { 2579 if (r.bottom <= r.top || r.right <= r.left) { 2580 r.bottom = r.top; 2581 r.right = r.left; 2582 return; 2583 } 2584 2585 if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { 2586 r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; 2587 r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; 2588 } 2589 if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { 2590 r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2591 r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2592 } 2593 } 2594 2595 /** 2596 * Return the layout for a numbered event. Create it if not already existing 2597 */ 2598 private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, 2599 Rect r) { 2600 if (i < 0 || i >= layouts.length) { 2601 return null; 2602 } 2603 2604 StaticLayout layout = layouts[i]; 2605 // Check if we have already initialized the StaticLayout and that 2606 // the width hasn't changed (due to vertical resizing which causes 2607 // re-layout of events at min height) 2608 if (layout == null || r.width() != layout.getWidth()) { 2609 SpannableStringBuilder bob = new SpannableStringBuilder(); 2610 if (event.title != null) { 2611 bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN)); 2612 bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); 2613 bob.append(' '); 2614 } 2615 if (event.location != null) { 2616 bob.append(drawTextSanitizer(event.location.toString(), 2617 MAX_EVENT_TEXT_LEN - bob.length())); 2618 } 2619 2620 switch (event.selfAttendeeStatus) { 2621 case Attendees.ATTENDEE_STATUS_INVITED: 2622 paint.setColor(event.color); 2623 break; 2624 case Attendees.ATTENDEE_STATUS_DECLINED: 2625 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 2626 case Attendees.ATTENDEE_STATUS_ACCEPTED: 2627 case Attendees.ATTENDEE_STATUS_TENTATIVE: 2628 default: 2629 paint.setColor(mEventTextColor); 2630 break; 2631 } 2632 2633 // Leave a one pixel boundary on the left and right of the rectangle for the event 2634 layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), 2635 Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); 2636 2637 layouts[i] = layout; 2638 } 2639 2640 return layout; 2641 } 2642 2643 private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { 2644 2645 p.setTextSize(NORMAL_FONT_SIZE); 2646 p.setTextAlign(Paint.Align.LEFT); 2647 Paint eventTextPaint = mEventTextPaint; 2648 2649 final float startY = DAY_HEADER_HEIGHT; 2650 final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; 2651 float x = 0; 2652 int linesIndex = 0; 2653 2654 // Draw the inner vertical grid lines 2655 p.setColor(mCalendarGridLineInnerVerticalColor); 2656 x = mHoursWidth; 2657 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2658 // Line bounding the top of the all day area 2659 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2660 mLines[linesIndex++] = startY; 2661 mLines[linesIndex++] = computeDayLeftPosition(mNumDays + 1); 2662 mLines[linesIndex++] = startY; 2663 2664 for (int day = 0; day <= mNumDays; day++) { 2665 x = computeDayLeftPosition(day); 2666 mLines[linesIndex++] = x; 2667 mLines[linesIndex++] = startY; 2668 mLines[linesIndex++] = x; 2669 mLines[linesIndex++] = stopY; 2670 } 2671 p.setAntiAlias(false); 2672 canvas.drawLines(mLines, 0, linesIndex, p); 2673 p.setStyle(Style.FILL); 2674 2675 int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 2676 int lastDay = firstDay + numDays - 1; 2677 final ArrayList<Event> events = mAllDayEvents; 2678 int numEvents = events.size(); 2679 // Whether or not we should draw the more events text 2680 boolean hasMoreEvents = false; 2681 // size of the allDay area 2682 float drawHeight = mAlldayHeight; 2683 // max number of events being drawn in one day of the allday area 2684 float numRectangles = mMaxAlldayEvents; 2685 // Where to cut off drawn allday events 2686 int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; 2687 // The number of events that weren't drawn in each day 2688 mSkippedAlldayEvents = new int[numDays]; 2689 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && 2690 mAnimateDayHeight == 0) { 2691 // We draw one fewer event than will fit so that more events text 2692 // can be drawn 2693 numRectangles = mMaxUnexpandedAlldayEventCount - 1; 2694 // We also clip the events above the more events text 2695 allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 2696 hasMoreEvents = true; 2697 } else if (mAnimateDayHeight != 0) { 2698 // clip at the end of the animating space 2699 allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; 2700 } 2701 for (int i = 0; i < numEvents; i++) { 2702 Event event = events.get(i); 2703 int startDay = event.startDay; 2704 int endDay = event.endDay; 2705 if (startDay > lastDay || endDay < firstDay) { 2706 continue; 2707 } 2708 if (startDay < firstDay) { 2709 startDay = firstDay; 2710 } 2711 if (endDay > lastDay) { 2712 endDay = lastDay; 2713 } 2714 int startIndex = startDay - firstDay; 2715 int endIndex = endDay - firstDay; 2716 float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : 2717 drawHeight / numRectangles; 2718 2719 // Prevent a single event from getting too big 2720 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 2721 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 2722 } 2723 2724 // Leave a one-pixel space between the vertical day lines and the 2725 // event rectangle. 2726 event.left = computeDayLeftPosition(startIndex); 2727 event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; 2728 event.top = y + height * event.getColumn(); 2729 event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; 2730 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2731 // check if we should skip this event. We skip if it starts 2732 // after the clip bound or ends after the skip bound and we're 2733 // not animating. 2734 if (event.top >= allDayEventClip) { 2735 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2736 continue; 2737 } else if (event.bottom > allDayEventClip) { 2738 if (hasMoreEvents) { 2739 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2740 continue; 2741 } 2742 event.bottom = allDayEventClip; 2743 } 2744 } 2745 Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, 2746 (int) event.bottom); 2747 setupAllDayTextRect(r); 2748 StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); 2749 drawEventText(layout, r, canvas, r.top, r.bottom); 2750 2751 // Check if this all-day event intersects the selected day 2752 if (mSelectionAllday && mComputeSelectedEvents) { 2753 if (startDay <= mSelectionDay && endDay >= mSelectionDay) { 2754 mSelectedEvents.add(event); 2755 } 2756 } 2757 } 2758 if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { 2759 // If the more allday text should be visible, draw it. 2760 p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); 2761 for (int i = 0; i < mSkippedAlldayEvents.length; i++) { 2762 if (mSkippedAlldayEvents[i] > 0) { 2763 drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); 2764 } 2765 } 2766 } 2767 2768 if (mSelectionAllday) { 2769 // Compute the neighbors for the list of all-day events that 2770 // intersect the selected day. 2771 computeAllDayNeighbors(); 2772 2773 // Set the selection position to zero so that when we move down 2774 // to the normal event area, we will highlight the topmost event. 2775 saveSelectionPosition(0f, 0f, 0f, 0f); 2776 } 2777 } 2778 2779 // Helper method for counting the number of allday events skipped on each day 2780 private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { 2781 if (counts == null || startIndex < 0 || endIndex > counts.length) { 2782 return; 2783 } 2784 for (int i = startIndex; i <= endIndex; i++) { 2785 counts[i]++; 2786 } 2787 } 2788 2789 // Draws the "box +n" text for hidden allday events 2790 protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { 2791 int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2792 int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f 2793 * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); 2794 Rect r = mRect; 2795 r.top = y; 2796 r.left = x; 2797 r.bottom = y + EVENT_SQUARE_WIDTH; 2798 r.right = x + EVENT_SQUARE_WIDTH; 2799 p.setColor(mMoreEventsTextColor); 2800 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 2801 p.setStyle(Style.STROKE); 2802 p.setAntiAlias(false); 2803 canvas.drawRect(r, p); 2804 p.setAntiAlias(true); 2805 p.setStyle(Style.FILL); 2806 p.setTextSize(EVENT_TEXT_FONT_SIZE); 2807 String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); 2808 y += EVENT_SQUARE_WIDTH; 2809 x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; 2810 canvas.drawText(String.format(text, remainingEvents), x, y, p); 2811 } 2812 2813 private void computeAllDayNeighbors() { 2814 int len = mSelectedEvents.size(); 2815 if (len == 0 || mSelectedEvent != null) { 2816 return; 2817 } 2818 2819 // First, clear all the links 2820 for (int ii = 0; ii < len; ii++) { 2821 Event ev = mSelectedEvents.get(ii); 2822 ev.nextUp = null; 2823 ev.nextDown = null; 2824 ev.nextLeft = null; 2825 ev.nextRight = null; 2826 } 2827 2828 // For each event in the selected event list "mSelectedEvents", find 2829 // its neighbors in the up and down directions. This could be done 2830 // more efficiently by sorting on the Event.getColumn() field, but 2831 // the list is expected to be very small. 2832 2833 // Find the event in the same row as the previously selected all-day 2834 // event, if any. 2835 int startPosition = -1; 2836 if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) { 2837 startPosition = mPrevSelectedEvent.getColumn(); 2838 } 2839 int maxPosition = -1; 2840 Event startEvent = null; 2841 Event maxPositionEvent = null; 2842 for (int ii = 0; ii < len; ii++) { 2843 Event ev = mSelectedEvents.get(ii); 2844 int position = ev.getColumn(); 2845 if (position == startPosition) { 2846 startEvent = ev; 2847 } else if (position > maxPosition) { 2848 maxPositionEvent = ev; 2849 maxPosition = position; 2850 } 2851 for (int jj = 0; jj < len; jj++) { 2852 if (jj == ii) { 2853 continue; 2854 } 2855 Event neighbor = mSelectedEvents.get(jj); 2856 int neighborPosition = neighbor.getColumn(); 2857 if (neighborPosition == position - 1) { 2858 ev.nextUp = neighbor; 2859 } else if (neighborPosition == position + 1) { 2860 ev.nextDown = neighbor; 2861 } 2862 } 2863 } 2864 if (startEvent != null) { 2865 mSelectedEvent = startEvent; 2866 } else { 2867 mSelectedEvent = maxPositionEvent; 2868 } 2869 } 2870 2871 private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { 2872 Paint eventTextPaint = mEventTextPaint; 2873 int left = computeDayLeftPosition(dayIndex) + 1; 2874 int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; 2875 int cellHeight = mCellHeight; 2876 2877 // Use the selected hour as the selection region 2878 Rect selectionArea = mSelectionRect; 2879 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); 2880 selectionArea.bottom = selectionArea.top + cellHeight; 2881 selectionArea.left = left; 2882 selectionArea.right = selectionArea.left + cellWidth; 2883 2884 final ArrayList<Event> events = mEvents; 2885 int numEvents = events.size(); 2886 EventGeometry geometry = mEventGeometry; 2887 2888 final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; 2889 for (int i = 0; i < numEvents; i++) { 2890 Event event = events.get(i); 2891 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 2892 continue; 2893 } 2894 2895 // Don't draw it if it is not visible 2896 if (event.bottom < mViewStartY || event.top > viewEndY) { 2897 continue; 2898 } 2899 2900 if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents 2901 && geometry.eventIntersectsSelection(event, selectionArea)) { 2902 mSelectedEvents.add(event); 2903 } 2904 2905 Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); 2906 setupTextRect(r); 2907 2908 // Don't draw text if it is not visible 2909 if (r.top > viewEndY || r.bottom < mViewStartY) { 2910 continue; 2911 } 2912 StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); 2913 // TODO: not sure why we are 4 pixels off 2914 drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight 2915 - DAY_HEADER_HEIGHT - mAlldayHeight); 2916 } 2917 2918 if (date == mSelectionDay && !mSelectionAllday && isFocused() 2919 && mSelectionMode != SELECTION_HIDDEN) { 2920 computeNeighbors(); 2921 } 2922 } 2923 2924 // Computes the "nearest" neighbor event in four directions (left, right, 2925 // up, down) for each of the events in the mSelectedEvents array. 2926 private void computeNeighbors() { 2927 int len = mSelectedEvents.size(); 2928 if (len == 0 || mSelectedEvent != null) { 2929 return; 2930 } 2931 2932 // First, clear all the links 2933 for (int ii = 0; ii < len; ii++) { 2934 Event ev = mSelectedEvents.get(ii); 2935 ev.nextUp = null; 2936 ev.nextDown = null; 2937 ev.nextLeft = null; 2938 ev.nextRight = null; 2939 } 2940 2941 Event startEvent = mSelectedEvents.get(0); 2942 int startEventDistance1 = 100000; // any large number 2943 int startEventDistance2 = 100000; // any large number 2944 int prevLocation = FROM_NONE; 2945 int prevTop; 2946 int prevBottom; 2947 int prevLeft; 2948 int prevRight; 2949 int prevCenter = 0; 2950 Rect box = getCurrentSelectionPosition(); 2951 if (mPrevSelectedEvent != null) { 2952 prevTop = (int) mPrevSelectedEvent.top; 2953 prevBottom = (int) mPrevSelectedEvent.bottom; 2954 prevLeft = (int) mPrevSelectedEvent.left; 2955 prevRight = (int) mPrevSelectedEvent.right; 2956 // Check if the previously selected event intersects the previous 2957 // selection box. (The previously selected event may be from a 2958 // much older selection box.) 2959 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top 2960 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { 2961 mPrevSelectedEvent = null; 2962 prevTop = mPrevBox.top; 2963 prevBottom = mPrevBox.bottom; 2964 prevLeft = mPrevBox.left; 2965 prevRight = mPrevBox.right; 2966 } else { 2967 // Clip the top and bottom to the previous selection box. 2968 if (prevTop < mPrevBox.top) { 2969 prevTop = mPrevBox.top; 2970 } 2971 if (prevBottom > mPrevBox.bottom) { 2972 prevBottom = mPrevBox.bottom; 2973 } 2974 } 2975 } else { 2976 // Just use the previously drawn selection box 2977 prevTop = mPrevBox.top; 2978 prevBottom = mPrevBox.bottom; 2979 prevLeft = mPrevBox.left; 2980 prevRight = mPrevBox.right; 2981 } 2982 2983 // Figure out where we came from and compute the center of that area. 2984 if (prevLeft >= box.right) { 2985 // The previously selected event was to the right of us. 2986 prevLocation = FROM_RIGHT; 2987 prevCenter = (prevTop + prevBottom) / 2; 2988 } else if (prevRight <= box.left) { 2989 // The previously selected event was to the left of us. 2990 prevLocation = FROM_LEFT; 2991 prevCenter = (prevTop + prevBottom) / 2; 2992 } else if (prevBottom <= box.top) { 2993 // The previously selected event was above us. 2994 prevLocation = FROM_ABOVE; 2995 prevCenter = (prevLeft + prevRight) / 2; 2996 } else if (prevTop >= box.bottom) { 2997 // The previously selected event was below us. 2998 prevLocation = FROM_BELOW; 2999 prevCenter = (prevLeft + prevRight) / 2; 3000 } 3001 3002 // For each event in the selected event list "mSelectedEvents", search 3003 // all the other events in that list for the nearest neighbor in 4 3004 // directions. 3005 for (int ii = 0; ii < len; ii++) { 3006 Event ev = mSelectedEvents.get(ii); 3007 3008 int startTime = ev.startTime; 3009 int endTime = ev.endTime; 3010 int left = (int) ev.left; 3011 int right = (int) ev.right; 3012 int top = (int) ev.top; 3013 if (top < box.top) { 3014 top = box.top; 3015 } 3016 int bottom = (int) ev.bottom; 3017 if (bottom > box.bottom) { 3018 bottom = box.bottom; 3019 } 3020// if (false) { 3021// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 3022// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3023// if (DateFormat.is24HourFormat(mContext)) { 3024// flags |= DateUtils.FORMAT_24HOUR; 3025// } 3026// String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis, 3027// ev.endMillis, flags); 3028// Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: " 3029// + bottom + " ev: " + timeRange + " " + ev.title); 3030// } 3031 int upDistanceMin = 10000; // any large number 3032 int downDistanceMin = 10000; // any large number 3033 int leftDistanceMin = 10000; // any large number 3034 int rightDistanceMin = 10000; // any large number 3035 Event upEvent = null; 3036 Event downEvent = null; 3037 Event leftEvent = null; 3038 Event rightEvent = null; 3039 3040 // Pick the starting event closest to the previously selected event, 3041 // if any. distance1 takes precedence over distance2. 3042 int distance1 = 0; 3043 int distance2 = 0; 3044 if (prevLocation == FROM_ABOVE) { 3045 if (left >= prevCenter) { 3046 distance1 = left - prevCenter; 3047 } else if (right <= prevCenter) { 3048 distance1 = prevCenter - right; 3049 } 3050 distance2 = top - prevBottom; 3051 } else if (prevLocation == FROM_BELOW) { 3052 if (left >= prevCenter) { 3053 distance1 = left - prevCenter; 3054 } else if (right <= prevCenter) { 3055 distance1 = prevCenter - right; 3056 } 3057 distance2 = prevTop - bottom; 3058 } else if (prevLocation == FROM_LEFT) { 3059 if (bottom <= prevCenter) { 3060 distance1 = prevCenter - bottom; 3061 } else if (top >= prevCenter) { 3062 distance1 = top - prevCenter; 3063 } 3064 distance2 = left - prevRight; 3065 } else if (prevLocation == FROM_RIGHT) { 3066 if (bottom <= prevCenter) { 3067 distance1 = prevCenter - bottom; 3068 } else if (top >= prevCenter) { 3069 distance1 = top - prevCenter; 3070 } 3071 distance2 = prevLeft - right; 3072 } 3073 if (distance1 < startEventDistance1 3074 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { 3075 startEvent = ev; 3076 startEventDistance1 = distance1; 3077 startEventDistance2 = distance2; 3078 } 3079 3080 // For each neighbor, figure out if it is above or below or left 3081 // or right of me and compute the distance. 3082 for (int jj = 0; jj < len; jj++) { 3083 if (jj == ii) { 3084 continue; 3085 } 3086 Event neighbor = mSelectedEvents.get(jj); 3087 int neighborLeft = (int) neighbor.left; 3088 int neighborRight = (int) neighbor.right; 3089 if (neighbor.endTime <= startTime) { 3090 // This neighbor is entirely above me. 3091 // If we overlap the same column, then compute the distance. 3092 if (neighborLeft < right && neighborRight > left) { 3093 int distance = startTime - neighbor.endTime; 3094 if (distance < upDistanceMin) { 3095 upDistanceMin = distance; 3096 upEvent = neighbor; 3097 } else if (distance == upDistanceMin) { 3098 int center = (left + right) / 2; 3099 int currentDistance = 0; 3100 int currentLeft = (int) upEvent.left; 3101 int currentRight = (int) upEvent.right; 3102 if (currentRight <= center) { 3103 currentDistance = center - currentRight; 3104 } else if (currentLeft >= center) { 3105 currentDistance = currentLeft - center; 3106 } 3107 3108 int neighborDistance = 0; 3109 if (neighborRight <= center) { 3110 neighborDistance = center - neighborRight; 3111 } else if (neighborLeft >= center) { 3112 neighborDistance = neighborLeft - center; 3113 } 3114 if (neighborDistance < currentDistance) { 3115 upDistanceMin = distance; 3116 upEvent = neighbor; 3117 } 3118 } 3119 } 3120 } else if (neighbor.startTime >= endTime) { 3121 // This neighbor is entirely below me. 3122 // If we overlap the same column, then compute the distance. 3123 if (neighborLeft < right && neighborRight > left) { 3124 int distance = neighbor.startTime - endTime; 3125 if (distance < downDistanceMin) { 3126 downDistanceMin = distance; 3127 downEvent = neighbor; 3128 } else if (distance == downDistanceMin) { 3129 int center = (left + right) / 2; 3130 int currentDistance = 0; 3131 int currentLeft = (int) downEvent.left; 3132 int currentRight = (int) downEvent.right; 3133 if (currentRight <= center) { 3134 currentDistance = center - currentRight; 3135 } else if (currentLeft >= center) { 3136 currentDistance = currentLeft - center; 3137 } 3138 3139 int neighborDistance = 0; 3140 if (neighborRight <= center) { 3141 neighborDistance = center - neighborRight; 3142 } else if (neighborLeft >= center) { 3143 neighborDistance = neighborLeft - center; 3144 } 3145 if (neighborDistance < currentDistance) { 3146 downDistanceMin = distance; 3147 downEvent = neighbor; 3148 } 3149 } 3150 } 3151 } 3152 3153 if (neighborLeft >= right) { 3154 // This neighbor is entirely to the right of me. 3155 // Take the closest neighbor in the y direction. 3156 int center = (top + bottom) / 2; 3157 int distance = 0; 3158 int neighborBottom = (int) neighbor.bottom; 3159 int neighborTop = (int) neighbor.top; 3160 if (neighborBottom <= center) { 3161 distance = center - neighborBottom; 3162 } else if (neighborTop >= center) { 3163 distance = neighborTop - center; 3164 } 3165 if (distance < rightDistanceMin) { 3166 rightDistanceMin = distance; 3167 rightEvent = neighbor; 3168 } else if (distance == rightDistanceMin) { 3169 // Pick the closest in the x direction 3170 int neighborDistance = neighborLeft - right; 3171 int currentDistance = (int) rightEvent.left - right; 3172 if (neighborDistance < currentDistance) { 3173 rightDistanceMin = distance; 3174 rightEvent = neighbor; 3175 } 3176 } 3177 } else if (neighborRight <= left) { 3178 // This neighbor is entirely to the left of me. 3179 // Take the closest neighbor in the y direction. 3180 int center = (top + bottom) / 2; 3181 int distance = 0; 3182 int neighborBottom = (int) neighbor.bottom; 3183 int neighborTop = (int) neighbor.top; 3184 if (neighborBottom <= center) { 3185 distance = center - neighborBottom; 3186 } else if (neighborTop >= center) { 3187 distance = neighborTop - center; 3188 } 3189 if (distance < leftDistanceMin) { 3190 leftDistanceMin = distance; 3191 leftEvent = neighbor; 3192 } else if (distance == leftDistanceMin) { 3193 // Pick the closest in the x direction 3194 int neighborDistance = left - neighborRight; 3195 int currentDistance = left - (int) leftEvent.right; 3196 if (neighborDistance < currentDistance) { 3197 leftDistanceMin = distance; 3198 leftEvent = neighbor; 3199 } 3200 } 3201 } 3202 } 3203 ev.nextUp = upEvent; 3204 ev.nextDown = downEvent; 3205 ev.nextLeft = leftEvent; 3206 ev.nextRight = rightEvent; 3207 } 3208 mSelectedEvent = startEvent; 3209 } 3210 3211 private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, 3212 int visibleTop, int visibleBot) { 3213 // Draw the Event Rect 3214 Rect r = mRect; 3215 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); 3216 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); 3217 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3218 r.right = (int) event.right; 3219 3220 int color = event.color; 3221 switch (event.selfAttendeeStatus) { 3222 case Attendees.ATTENDEE_STATUS_INVITED: 3223 p.setStyle(Style.STROKE); 3224 break; 3225 case Attendees.ATTENDEE_STATUS_DECLINED: 3226 color = Utils.getDeclinedColorFromColor(color); 3227 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 3228 case Attendees.ATTENDEE_STATUS_ACCEPTED: 3229 case Attendees.ATTENDEE_STATUS_TENTATIVE: 3230 default: 3231 p.setStyle(Style.FILL_AND_STROKE); 3232 break; 3233 } 3234 3235 p.setAntiAlias(false); 3236 3237 int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); 3238 int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); 3239 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); 3240 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, 3241 visibleBot); 3242 r.left += floorHalfStroke; 3243 r.right -= ceilHalfStroke; 3244 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 3245 p.setColor(color); 3246 canvas.drawRect(r, p); 3247 3248 p.setStyle(Style.FILL); 3249 3250 // If this event is selected, then use the selection color 3251 if (mSelectedEvent == event) { 3252 boolean paintIt = false; 3253 color = 0; 3254 if (mSelectionMode == SELECTION_PRESSED) { 3255 // Also, remember the last selected event that we drew 3256 mPrevSelectedEvent = event; 3257 color = mPressedColor; 3258 paintIt = true; 3259 } else if (mSelectionMode == SELECTION_SELECTED) { 3260 // Also, remember the last selected event that we drew 3261 mPrevSelectedEvent = event; 3262 color = mPressedColor; 3263 paintIt = true; 3264 } 3265 3266 if (paintIt) { 3267 p.setColor(color); 3268 canvas.drawRect(r, p); 3269 } 3270 3271 p.setAntiAlias(true); 3272 } 3273 3274 // Draw cal color square border 3275 // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; 3276 // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; 3277 // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; 3278 // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; 3279 // p.setColor(0xFFFFFFFF); 3280 // canvas.drawRect(r, p); 3281 3282 // Draw cal color 3283 // r.top++; 3284 // r.left++; 3285 // r.bottom--; 3286 // r.right--; 3287 // p.setColor(event.color); 3288 // canvas.drawRect(r, p); 3289 3290 // Setup rect for drawEventText which follows 3291 r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; 3292 r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; 3293 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3294 r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; 3295 return r; 3296 } 3297 3298 private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); 3299 3300 // Sanitize a string before passing it to drawText or else we get little 3301 // squares. For newlines and tabs before a comma, delete the character. 3302 // Otherwise, just replace them with a space. 3303 private String drawTextSanitizer(String string, int maxEventTextLen) { 3304 Matcher m = drawTextSanitizerFilter.matcher(string); 3305 string = m.replaceAll(","); 3306 3307 int len = string.length(); 3308 if (len > maxEventTextLen) { 3309 string = string.substring(0, maxEventTextLen); 3310 len = maxEventTextLen; 3311 } 3312 3313 return string.replace('\n', ' '); 3314 } 3315 3316 private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, 3317 int bottom) { 3318 // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging 3319 3320 int width = rect.right - rect.left; 3321 int height = rect.bottom - rect.top; 3322 3323 // If the rectangle is too small for text, then return 3324 if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { 3325 return; 3326 } 3327 3328 int totalLineHeight = 0; 3329 int lineCount = eventLayout.getLineCount(); 3330 for (int i = 0; i < lineCount; i++) { 3331 int lineBottom = eventLayout.getLineBottom(i); 3332 if (lineBottom <= height) { 3333 totalLineHeight = lineBottom; 3334 } else { 3335 break; 3336 } 3337 } 3338 3339 if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { 3340 return; 3341 } 3342 3343 // Use a StaticLayout to format the string. 3344 canvas.save(); 3345 canvas.translate(rect.left, rect.top); 3346 rect.left = 0; 3347 rect.right = width; 3348 rect.top = 0; 3349 rect.bottom = totalLineHeight; 3350 3351 // There's a bug somewhere. If this rect is outside of a previous 3352 // cliprect, this becomes a no-op. What happens is that the text draw 3353 // past the event rect. The current fix is to not draw the staticLayout 3354 // at all if it is completely out of bound. 3355 canvas.clipRect(rect); 3356 eventLayout.draw(canvas); 3357 canvas.restore(); 3358 } 3359 3360 // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it 3361 // doesn't work well with hardware acceleration 3362// private void drawEmptyRect(Canvas canvas, Rect r, int color) { 3363// int linesIndex = 0; 3364// mLines[linesIndex++] = r.left; 3365// mLines[linesIndex++] = r.top; 3366// mLines[linesIndex++] = r.right; 3367// mLines[linesIndex++] = r.top; 3368// 3369// mLines[linesIndex++] = r.left; 3370// mLines[linesIndex++] = r.bottom; 3371// mLines[linesIndex++] = r.right; 3372// mLines[linesIndex++] = r.bottom; 3373// 3374// mLines[linesIndex++] = r.left; 3375// mLines[linesIndex++] = r.top; 3376// mLines[linesIndex++] = r.left; 3377// mLines[linesIndex++] = r.bottom; 3378// 3379// mLines[linesIndex++] = r.right; 3380// mLines[linesIndex++] = r.top; 3381// mLines[linesIndex++] = r.right; 3382// mLines[linesIndex++] = r.bottom; 3383// mPaint.setColor(color); 3384// canvas.drawLines(mLines, 0, linesIndex, mPaint); 3385// } 3386 3387 private void updateEventDetails() { 3388 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN 3389 || mSelectionMode == SELECTION_LONGPRESS) { 3390 mPopup.dismiss(); 3391 return; 3392 } 3393 if (mLastPopupEventID == mSelectedEvent.id) { 3394 return; 3395 } 3396 3397 mLastPopupEventID = mSelectedEvent.id; 3398 3399 // Remove any outstanding callbacks to dismiss the popup. 3400 getHandler().removeCallbacks(mDismissPopup); 3401 3402 Event event = mSelectedEvent; 3403 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); 3404 titleView.setText(event.title); 3405 3406 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); 3407 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); 3408 3409 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); 3410 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); 3411 3412 int flags; 3413 if (event.allDay) { 3414 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE 3415 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; 3416 } else { 3417 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 3418 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL 3419 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3420 } 3421 if (DateFormat.is24HourFormat(mContext)) { 3422 flags |= DateUtils.FORMAT_24HOUR; 3423 } 3424 String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis, 3425 flags); 3426 TextView timeView = (TextView) mPopupView.findViewById(R.id.time); 3427 timeView.setText(timeRange); 3428 3429 TextView whereView = (TextView) mPopupView.findViewById(R.id.where); 3430 final boolean empty = TextUtils.isEmpty(event.location); 3431 whereView.setVisibility(empty ? View.GONE : View.VISIBLE); 3432 if (!empty) whereView.setText(event.location); 3433 3434 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); 3435 postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); 3436 } 3437 3438 // The following routines are called from the parent activity when certain 3439 // touch events occur. 3440 private void doDown(MotionEvent ev) { 3441 mTouchMode = TOUCH_MODE_DOWN; 3442 mViewStartX = 0; 3443 mOnFlingCalled = false; 3444 getHandler().removeCallbacks(mContinueScroll); 3445 } 3446 3447 // Kicks off all the animations when the expand allday area is tapped 3448 private void doExpandAllDayClick() { 3449 mShowAllAllDayEvents = !mShowAllAllDayEvents; 3450 3451 ObjectAnimator.setFrameDelay(0); 3452 3453 // Determine the starting height 3454 if (mAnimateDayHeight == 0) { 3455 mAnimateDayHeight = mShowAllAllDayEvents ? 3456 mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; 3457 } 3458 // Cancel current animations 3459 mCancellingAnimations = true; 3460 if (mAlldayAnimator != null) { 3461 mAlldayAnimator.cancel(); 3462 } 3463 if (mAlldayEventAnimator != null) { 3464 mAlldayEventAnimator.cancel(); 3465 } 3466 if (mMoreAlldayEventsAnimator != null) { 3467 mMoreAlldayEventsAnimator.cancel(); 3468 } 3469 mCancellingAnimations = false; 3470 // get new animators 3471 mAlldayAnimator = getAllDayAnimator(); 3472 mAlldayEventAnimator = getAllDayEventAnimator(); 3473 mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, 3474 "moreAllDayEventsTextAlpha", 3475 mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, 3476 mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); 3477 3478 // Set up delays and start the animators 3479 mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3480 mAlldayAnimator.start(); 3481 mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); 3482 mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); 3483 mMoreAlldayEventsAnimator.start(); 3484 if (mAlldayEventAnimator != null) { 3485 // This is the only animator that can return null, so check it 3486 mAlldayEventAnimator 3487 .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3488 mAlldayEventAnimator.start(); 3489 } 3490 } 3491 3492 /** 3493 * Figures out the initial heights for allDay events and space when 3494 * a view is being set up. 3495 */ 3496 public void initAllDayHeights() { 3497 if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { 3498 return; 3499 } 3500 if (mShowAllAllDayEvents) { 3501 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3502 maxADHeight = Math.min(maxADHeight, 3503 (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3504 mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; 3505 } else { 3506 mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3507 } 3508 } 3509 3510 // Sets up an animator for changing the height of allday events 3511 private ObjectAnimator getAllDayEventAnimator() { 3512 // First calculate the absolute max height 3513 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3514 // Now expand to fit but not beyond the absolute max 3515 maxADHeight = 3516 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3517 // calculate the height of individual events in order to fit 3518 int fitHeight = maxADHeight / mMaxAlldayEvents; 3519 int currentHeight = mAnimateDayEventHeight; 3520 int desiredHeight = 3521 mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3522 // if there's nothing to animate just return 3523 if (currentHeight == desiredHeight) { 3524 return null; 3525 } 3526 3527 // Set up the animator with the calculated values 3528 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", 3529 currentHeight, desiredHeight); 3530 animator.setDuration(ANIMATION_DURATION); 3531 return animator; 3532 } 3533 3534 // Sets up an animator for changing the height of the allday area 3535 private ObjectAnimator getAllDayAnimator() { 3536 // Calculate the absolute max height 3537 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3538 // Find the desired height but don't exceed abs max 3539 maxADHeight = 3540 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3541 // calculate the current and desired heights 3542 int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; 3543 int desiredHeight = mShowAllAllDayEvents ? maxADHeight : 3544 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); 3545 3546 // Set up the animator with the calculated values 3547 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", 3548 currentHeight, desiredHeight); 3549 animator.setDuration(ANIMATION_DURATION); 3550 3551 animator.addListener(new AnimatorListenerAdapter() { 3552 @Override 3553 public void onAnimationEnd(Animator animation) { 3554 if (!mCancellingAnimations) { 3555 // when finished, set this to 0 to signify not animating 3556 mAnimateDayHeight = 0; 3557 mUseExpandIcon = !mShowAllAllDayEvents; 3558 } 3559 mRemeasure = true; 3560 invalidate(); 3561 } 3562 }); 3563 return animator; 3564 } 3565 3566 // setter for the 'box +n' alpha text used by the animator 3567 public void setMoreAllDayEventsTextAlpha(int alpha) { 3568 mMoreAlldayEventsTextAlpha = alpha; 3569 invalidate(); 3570 } 3571 3572 // setter for the height of the allday area used by the animator 3573 public void setAnimateDayHeight(int height) { 3574 mAnimateDayHeight = height; 3575 mRemeasure = true; 3576 invalidate(); 3577 } 3578 3579 // setter for the height of allday events used by the animator 3580 public void setAnimateDayEventHeight(int height) { 3581 mAnimateDayEventHeight = height; 3582 mRemeasure = true; 3583 invalidate(); 3584 } 3585 3586 private void doSingleTapUp(MotionEvent ev) { 3587 if (!mHandleActionUp || mScrolling) { 3588 return; 3589 } 3590 3591 int x = (int) ev.getX(); 3592 int y = (int) ev.getY(); 3593 int selectedDay = mSelectionDay; 3594 int selectedHour = mSelectionHour; 3595 3596 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 3597 // check if the tap was in the allday expansion area 3598 int bottom = mFirstCell; 3599 if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) 3600 || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && 3601 y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { 3602 doExpandAllDayClick(); 3603 return; 3604 } 3605 } 3606 3607 boolean validPosition = setSelectionFromPosition(x, y); 3608 if (!validPosition) { 3609 if (y < DAY_HEADER_HEIGHT) { 3610 Time selectedTime = new Time(mBaseDate); 3611 selectedTime.setJulianDay(mSelectionDay); 3612 selectedTime.hour = mSelectionHour; 3613 selectedTime.normalize(true /* ignore isDst */); 3614 mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, 3615 ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); 3616 } 3617 return; 3618 } 3619 3620 boolean hasSelection = mSelectionMode == SELECTION_SELECTED; 3621 boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) 3622 && selectedDay == mSelectionDay && selectedHour == mSelectionHour; 3623 3624 if (pressedSelected) { 3625 // If the tap is on an already selected hour slot, then create a new 3626 // event 3627 long extraLong = 0; 3628 if (mSelectionAllday) { 3629 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 3630 } 3631 mSelectionMode = SELECTION_SELECTED; 3632 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 3633 getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), 3634 extraLong, -1); 3635 } else if (mSelectedEvent != null) { 3636 // If the tap is on an event, launch the "View event" view 3637 if (mIsAccessibilityEnabled) { 3638 mAccessibilityMgr.interrupt(); 3639 } 3640 3641 mSelectionMode = SELECTION_HIDDEN; 3642 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mSelectedEvent.id, 3643 mSelectedEvent.startMillis, mSelectedEvent.endMillis, (int) ev.getRawX(), 3644 (int) ev.getRawY(), getSelectedTimeInMillis()); 3645 } else { 3646 // Select time 3647 Time startTime = new Time(mBaseDate); 3648 startTime.setJulianDay(mSelectionDay); 3649 startTime.hour = mSelectionHour; 3650 startTime.normalize(true /* ignore isDst */); 3651 3652 Time endTime = new Time(startTime); 3653 endTime.hour++; 3654 3655 mSelectionMode = SELECTION_SELECTED; 3656 mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, 3657 CalendarController.EXTRA_GOTO_TIME, null, null); 3658 } 3659 invalidate(); 3660 } 3661 3662 private void doLongPress(MotionEvent ev) { 3663 if (mScrolling) { 3664 return; 3665 } 3666 3667 // Scale gesture in progress 3668 if (mStartingSpanY != 0) { 3669 return; 3670 } 3671 3672 int x = (int) ev.getX(); 3673 int y = (int) ev.getY(); 3674 3675 boolean validPosition = setSelectionFromPosition(x, y); 3676 if (!validPosition) { 3677 // return if the touch wasn't on an area of concern 3678 return; 3679 } 3680 3681 mSelectionMode = SELECTION_LONGPRESS; 3682 invalidate(); 3683 performLongClick(); 3684 } 3685 3686 private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { 3687 cancelAnimation(); 3688 if (mStartingScroll) { 3689 mInitialScrollX = 0; 3690 mInitialScrollY = 0; 3691 mStartingScroll = false; 3692 } 3693 3694 mInitialScrollX += deltaX; 3695 mInitialScrollY += deltaY; 3696 int distanceX = (int) mInitialScrollX; 3697 int distanceY = (int) mInitialScrollY; 3698 3699 // If we haven't figured out the predominant scroll direction yet, 3700 // then do it now. 3701 if (mTouchMode == TOUCH_MODE_DOWN) { 3702 int absDistanceX = Math.abs(distanceX); 3703 int absDistanceY = Math.abs(distanceY); 3704 mScrollStartY = mViewStartY; 3705 mPreviousDirection = 0; 3706 3707 if (absDistanceX > absDistanceY) { 3708 mTouchMode = TOUCH_MODE_HSCROLL; 3709 mViewStartX = distanceX; 3710 initNextView(-mViewStartX); 3711 } else { 3712 mTouchMode = TOUCH_MODE_VSCROLL; 3713 } 3714 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3715 // We are already scrolling horizontally, so check if we 3716 // changed the direction of scrolling so that the other week 3717 // is now visible. 3718 mViewStartX = distanceX; 3719 if (distanceX != 0) { 3720 int direction = (distanceX > 0) ? 1 : -1; 3721 if (direction != mPreviousDirection) { 3722 // The user has switched the direction of scrolling 3723 // so re-init the next view 3724 initNextView(-mViewStartX); 3725 mPreviousDirection = direction; 3726 } 3727 } 3728 } 3729 3730 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { 3731 mViewStartY = mScrollStartY + distanceY; 3732 3733 // If dragging while already at the end, do a glow 3734 final int pulledToY = (int) (mScrollStartY + deltaY); 3735 if (pulledToY < 0) { 3736 mEdgeEffectTop.onPull(deltaY / mViewHeight); 3737 if (!mEdgeEffectBottom.isFinished()) { 3738 mEdgeEffectBottom.onRelease(); 3739 } 3740 } else if (pulledToY > mMaxViewStartY) { 3741 mEdgeEffectBottom.onPull(deltaY / mViewHeight); 3742 if (!mEdgeEffectTop.isFinished()) { 3743 mEdgeEffectTop.onRelease(); 3744 } 3745 } 3746 3747 if (mViewStartY < 0) { 3748 mViewStartY = 0; 3749 } else if (mViewStartY > mMaxViewStartY) { 3750 mViewStartY = mMaxViewStartY; 3751 } 3752 computeFirstHour(); 3753 } 3754 3755 mScrolling = true; 3756 3757 mSelectionMode = SELECTION_HIDDEN; 3758 invalidate(); 3759 } 3760 3761 private void cancelAnimation() { 3762 Animation in = mViewSwitcher.getInAnimation(); 3763 if (in != null) { 3764 // cancel() doesn't terminate cleanly. 3765 in.scaleCurrentDuration(0); 3766 } 3767 Animation out = mViewSwitcher.getOutAnimation(); 3768 if (out != null) { 3769 // cancel() doesn't terminate cleanly. 3770 out.scaleCurrentDuration(0); 3771 } 3772 } 3773 3774 private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 3775 cancelAnimation(); 3776 3777 mSelectionMode = SELECTION_HIDDEN; 3778 mOnFlingCalled = true; 3779 3780 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3781 // Horizontal fling. 3782 // initNextView(deltaX); 3783 mTouchMode = TOUCH_MODE_INITIAL_STATE; 3784 if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); 3785 int deltaX = (int) e2.getX() - (int) e1.getX(); 3786 switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); 3787 mViewStartX = 0; 3788 return; 3789 } 3790 3791 if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { 3792 if (DEBUG) Log.d(TAG, "doFling: no fling"); 3793 return; 3794 } 3795 3796 // Vertical fling. 3797 mTouchMode = TOUCH_MODE_INITIAL_STATE; 3798 mViewStartX = 0; 3799 3800 if (DEBUG) { 3801 Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); 3802 } 3803 3804 // Continue scrolling vertically 3805 mScrolling = true; 3806 mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, 3807 (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, 3808 mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); 3809 3810 // When flinging down, show a glow when it hits the end only if it 3811 // wasn't started at the top 3812 if (velocityY > 0 && mViewStartY != 0) { 3813 mCallEdgeEffectOnAbsorb = true; 3814 } 3815 // When flinging up, show a glow when it hits the end only if it wasn't 3816 // started at the bottom 3817 else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { 3818 mCallEdgeEffectOnAbsorb = true; 3819 } 3820 post(mContinueScroll); 3821 } 3822 3823 private boolean initNextView(int deltaX) { 3824 // Change the view to the previous day or week 3825 DayView view = (DayView) mViewSwitcher.getNextView(); 3826 Time date = view.mBaseDate; 3827 date.set(mBaseDate); 3828 boolean switchForward; 3829 if (deltaX > 0) { 3830 date.monthDay -= mNumDays; 3831 view.mSelectionDay = mSelectionDay - mNumDays; 3832 switchForward = false; 3833 } else { 3834 date.monthDay += mNumDays; 3835 view.mSelectionDay = mSelectionDay + mNumDays; 3836 switchForward = true; 3837 } 3838 date.normalize(true /* ignore isDst */); 3839 initView(view); 3840 view.layout(getLeft(), getTop(), getRight(), getBottom()); 3841 view.reloadEvents(); 3842 return switchForward; 3843 } 3844 3845 // ScaleGestureDetector.OnScaleGestureListener 3846 public boolean onScaleBegin(ScaleGestureDetector detector) { 3847 mHandleActionUp = false; 3848 float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 3849 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); 3850 3851 mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 3852 mCellHeightBeforeScaleGesture = mCellHeight; 3853 3854 if (DEBUG_SCALING) { 3855 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 3856 Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour 3857 + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY 3858 + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 3859 } 3860 3861 return true; 3862 } 3863 3864 // ScaleGestureDetector.OnScaleGestureListener 3865 public boolean onScale(ScaleGestureDetector detector) { 3866 float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 3867 3868 mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); 3869 3870 if (mCellHeight < mMinCellHeight) { 3871 // If mStartingSpanY is too small, even a small increase in the 3872 // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT 3873 mStartingSpanY = spanY; 3874 mCellHeight = mMinCellHeight; 3875 mCellHeightBeforeScaleGesture = mMinCellHeight; 3876 } else if (mCellHeight > MAX_CELL_HEIGHT) { 3877 mStartingSpanY = spanY; 3878 mCellHeight = MAX_CELL_HEIGHT; 3879 mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; 3880 } 3881 3882 int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 3883 mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; 3884 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 3885 3886 if (DEBUG_SCALING) { 3887 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 3888 Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " 3889 + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" 3890 + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 3891 } 3892 3893 if (mViewStartY < 0) { 3894 mViewStartY = 0; 3895 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 3896 / (float) (mCellHeight + DAY_GAP); 3897 } else if (mViewStartY > mMaxViewStartY) { 3898 mViewStartY = mMaxViewStartY; 3899 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 3900 / (float) (mCellHeight + DAY_GAP); 3901 } 3902 computeFirstHour(); 3903 3904 mRemeasure = true; 3905 invalidate(); 3906 return true; 3907 } 3908 3909 // ScaleGestureDetector.OnScaleGestureListener 3910 public void onScaleEnd(ScaleGestureDetector detector) { 3911 mScrollStartY = mViewStartY; 3912 mInitialScrollY = 0; 3913 mInitialScrollX = 0; 3914 mStartingSpanY = 0; 3915 } 3916 3917 @Override 3918 public boolean onTouchEvent(MotionEvent ev) { 3919 int action = ev.getAction(); 3920 if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); 3921 3922 if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { 3923 mScaleGestureDetector.onTouchEvent(ev); 3924 if (mScaleGestureDetector.isInProgress()) { 3925 return true; 3926 } 3927 } 3928 3929 switch (action) { 3930 case MotionEvent.ACTION_DOWN: 3931 mStartingScroll = true; 3932 if (DEBUG) { 3933 Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" 3934 + ev.getPointerCount()); 3935 } 3936 3937 int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 3938 if (ev.getY() < bottom) { 3939 mTouchStartedInAlldayArea = true; 3940 } else { 3941 mTouchStartedInAlldayArea = false; 3942 } 3943 mHandleActionUp = true; 3944 mGestureDetector.onTouchEvent(ev); 3945 return true; 3946 3947 case MotionEvent.ACTION_MOVE: 3948 if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); 3949 mGestureDetector.onTouchEvent(ev); 3950 return true; 3951 3952 case MotionEvent.ACTION_UP: 3953 if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); 3954 mEdgeEffectTop.onRelease(); 3955 mEdgeEffectBottom.onRelease(); 3956 mStartingScroll = false; 3957 mGestureDetector.onTouchEvent(ev); 3958 if (!mHandleActionUp) { 3959 mHandleActionUp = true; 3960 return true; 3961 } 3962 if (mOnFlingCalled) { 3963 return true; 3964 } 3965 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3966 mTouchMode = TOUCH_MODE_INITIAL_STATE; 3967 if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { 3968 // The user has gone beyond the threshold so switch views 3969 if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); 3970 switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); 3971 mViewStartX = 0; 3972 return true; 3973 } else { 3974 // Not beyond the threshold so invalidate which will cause 3975 // the view to snap back. Also call recalc() to ensure 3976 // that we have the correct starting date and title. 3977 if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); 3978 recalc(); 3979 invalidate(); 3980 mViewStartX = 0; 3981 } 3982 } 3983 3984 // If we were scrolling, then reset the selected hour so that it 3985 // is visible. 3986 if (mScrolling) { 3987 mScrolling = false; 3988 resetSelectedHour(); 3989 invalidate(); 3990 } 3991 return true; 3992 3993 // This case isn't expected to happen. 3994 case MotionEvent.ACTION_CANCEL: 3995 if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); 3996 mGestureDetector.onTouchEvent(ev); 3997 mScrolling = false; 3998 resetSelectedHour(); 3999 return true; 4000 4001 default: 4002 if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); 4003 if (mGestureDetector.onTouchEvent(ev)) { 4004 return true; 4005 } 4006 return super.onTouchEvent(ev); 4007 } 4008 } 4009 4010 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 4011 MenuItem item; 4012 4013 // If the trackball is held down, then the context menu pops up and 4014 // we never get onKeyUp() for the long-press. So check for it here 4015 // and change the selection to the long-press state. 4016 if (mSelectionMode != SELECTION_LONGPRESS) { 4017 mSelectionMode = SELECTION_LONGPRESS; 4018 invalidate(); 4019 } 4020 4021 final long startMillis = getSelectedTimeInMillis(); 4022 int flags = DateUtils.FORMAT_SHOW_TIME 4023 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT 4024 | DateUtils.FORMAT_SHOW_WEEKDAY; 4025 final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); 4026 menu.setHeaderTitle(title); 4027 4028 int numSelectedEvents = mSelectedEvents.size(); 4029 if (mNumDays == 1) { 4030 // Day view. 4031 4032 // If there is a selected event, then allow it to be viewed and 4033 // edited. 4034 if (numSelectedEvents >= 1) { 4035 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4036 item.setOnMenuItemClickListener(mContextMenuHandler); 4037 item.setIcon(android.R.drawable.ic_menu_info_details); 4038 4039 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4040 if (accessLevel == ACCESS_LEVEL_EDIT) { 4041 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4042 item.setOnMenuItemClickListener(mContextMenuHandler); 4043 item.setIcon(android.R.drawable.ic_menu_edit); 4044 item.setAlphabeticShortcut('e'); 4045 } 4046 4047 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4048 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4049 item.setOnMenuItemClickListener(mContextMenuHandler); 4050 item.setIcon(android.R.drawable.ic_menu_delete); 4051 } 4052 4053 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4054 item.setOnMenuItemClickListener(mContextMenuHandler); 4055 item.setIcon(android.R.drawable.ic_menu_add); 4056 item.setAlphabeticShortcut('n'); 4057 } else { 4058 // Otherwise, if the user long-pressed on a blank hour, allow 4059 // them to create an event. They can also do this by tapping. 4060 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4061 item.setOnMenuItemClickListener(mContextMenuHandler); 4062 item.setIcon(android.R.drawable.ic_menu_add); 4063 item.setAlphabeticShortcut('n'); 4064 } 4065 } else { 4066 // Week view. 4067 4068 // If there is a selected event, then allow it to be viewed and 4069 // edited. 4070 if (numSelectedEvents >= 1) { 4071 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4072 item.setOnMenuItemClickListener(mContextMenuHandler); 4073 item.setIcon(android.R.drawable.ic_menu_info_details); 4074 4075 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4076 if (accessLevel == ACCESS_LEVEL_EDIT) { 4077 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4078 item.setOnMenuItemClickListener(mContextMenuHandler); 4079 item.setIcon(android.R.drawable.ic_menu_edit); 4080 item.setAlphabeticShortcut('e'); 4081 } 4082 4083 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4084 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4085 item.setOnMenuItemClickListener(mContextMenuHandler); 4086 item.setIcon(android.R.drawable.ic_menu_delete); 4087 } 4088 } 4089 4090 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4091 item.setOnMenuItemClickListener(mContextMenuHandler); 4092 item.setIcon(android.R.drawable.ic_menu_add); 4093 item.setAlphabeticShortcut('n'); 4094 4095 item = menu.add(0, MENU_DAY, 0, R.string.show_day_view); 4096 item.setOnMenuItemClickListener(mContextMenuHandler); 4097 item.setIcon(android.R.drawable.ic_menu_day); 4098 item.setAlphabeticShortcut('d'); 4099 } 4100 4101 mPopup.dismiss(); 4102 } 4103 4104 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { 4105 public boolean onMenuItemClick(MenuItem item) { 4106 switch (item.getItemId()) { 4107 case MENU_EVENT_VIEW: { 4108 if (mSelectedEvent != null) { 4109 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, 4110 mSelectedEvent.id, mSelectedEvent.startMillis, 4111 mSelectedEvent.endMillis, 0, 0, -1); 4112 } 4113 break; 4114 } 4115 case MENU_EVENT_EDIT: { 4116 if (mSelectedEvent != null) { 4117 mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, 4118 mSelectedEvent.id, mSelectedEvent.startMillis, 4119 mSelectedEvent.endMillis, 0, 0, -1); 4120 } 4121 break; 4122 } 4123 case MENU_DAY: { 4124 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4125 ViewType.DAY); 4126 break; 4127 } 4128 case MENU_AGENDA: { 4129 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4130 ViewType.AGENDA); 4131 break; 4132 } 4133 case MENU_EVENT_CREATE: { 4134 long startMillis = getSelectedTimeInMillis(); 4135 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 4136 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 4137 startMillis, endMillis, 0, 0, -1); 4138 break; 4139 } 4140 case MENU_EVENT_DELETE: { 4141 if (mSelectedEvent != null) { 4142 Event selectedEvent = mSelectedEvent; 4143 long begin = selectedEvent.startMillis; 4144 long end = selectedEvent.endMillis; 4145 long id = selectedEvent.id; 4146 mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin, 4147 end, 0, 0, -1); 4148 } 4149 break; 4150 } 4151 default: { 4152 return false; 4153 } 4154 } 4155 return true; 4156 } 4157 } 4158 4159 private static int getEventAccessLevel(Context context, Event e) { 4160 ContentResolver cr = context.getContentResolver(); 4161 4162 int accessLevel = Calendars.CAL_ACCESS_NONE; 4163 4164 // Get the calendar id for this event 4165 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id), 4166 new String[] { Events.CALENDAR_ID }, 4167 null /* selection */, 4168 null /* selectionArgs */, 4169 null /* sort */); 4170 4171 if (cursor == null) { 4172 return ACCESS_LEVEL_NONE; 4173 } 4174 4175 if (cursor.getCount() == 0) { 4176 cursor.close(); 4177 return ACCESS_LEVEL_NONE; 4178 } 4179 4180 cursor.moveToFirst(); 4181 long calId = cursor.getLong(0); 4182 cursor.close(); 4183 4184 Uri uri = Calendars.CONTENT_URI; 4185 String where = String.format(CALENDARS_WHERE, calId); 4186 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null); 4187 4188 String calendarOwnerAccount = null; 4189 if (cursor != null) { 4190 cursor.moveToFirst(); 4191 accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); 4192 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 4193 cursor.close(); 4194 } 4195 4196 if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) { 4197 return ACCESS_LEVEL_NONE; 4198 } 4199 4200 if (e.guestsCanModify) { 4201 return ACCESS_LEVEL_EDIT; 4202 } 4203 4204 if (!TextUtils.isEmpty(calendarOwnerAccount) 4205 && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { 4206 return ACCESS_LEVEL_EDIT; 4207 } 4208 4209 return ACCESS_LEVEL_DELETE; 4210 } 4211 4212 /** 4213 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. 4214 * If the touch position is not within the displayed grid, then this 4215 * method returns false. 4216 * 4217 * @param x the x position of the touch 4218 * @param y the y position of the touch 4219 * @return true if the touch position is valid 4220 */ 4221 private boolean setSelectionFromPosition(int x, final int y) { 4222 if (x < mHoursWidth) { 4223 x = mHoursWidth; 4224 } 4225 4226 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); 4227 if (day >= mNumDays) { 4228 day = mNumDays - 1; 4229 } 4230 day += mFirstJulianDay; 4231 mSelectionDay = day; 4232 4233 if (y < DAY_HEADER_HEIGHT) { 4234 sendAccessibilityEventAsNeeded(false); 4235 return false; 4236 } 4237 4238 mSelectionHour = mFirstHour; /* First fully visible hour */ 4239 4240 if (y < mFirstCell) { 4241 mSelectionAllday = true; 4242 } else { 4243 // y is now offset from top of the scrollable region 4244 int adjustedY = y - mFirstCell; 4245 4246 if (adjustedY < mFirstHourOffset) { 4247 --mSelectionHour; /* In the partially visible hour */ 4248 } else { 4249 mSelectionHour += (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP); 4250 } 4251 4252 mSelectionAllday = false; 4253 } 4254 4255 findSelectedEvent(x, y); 4256 4257// Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: " 4258// + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " 4259// + mFirstHourOffset); 4260// if (mSelectedEvent != null) { 4261// Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " 4262// + mSelectedEvent.title); 4263// for (Event ev : mSelectedEvents) { 4264// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 4265// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 4266// String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags); 4267// 4268// Log.i("Cal", " " + timeRange + " " + ev.title); 4269// } 4270// } 4271 sendAccessibilityEventAsNeeded(true); 4272 return true; 4273 } 4274 4275 private void findSelectedEvent(int x, int y) { 4276 int date = mSelectionDay; 4277 int cellWidth = mCellWidth; 4278 ArrayList<Event> events = mEvents; 4279 int numEvents = events.size(); 4280 int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay); 4281 int top = 0; 4282 mSelectedEvent = null; 4283 4284 mSelectedEvents.clear(); 4285 if (mSelectionAllday) { 4286 float yDistance; 4287 float minYdistance = 10000.0f; // any large number 4288 Event closestEvent = null; 4289 float drawHeight = mAlldayHeight; 4290 int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 4291 int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount; 4292 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 4293 // Leave a gap for the 'box +n' text 4294 maxUnexpandedColumn--; 4295 } 4296 events = mAllDayEvents; 4297 numEvents = events.size(); 4298 for (int i = 0; i < numEvents; i++) { 4299 Event event = events.get(i); 4300 if (!event.drawAsAllday() || 4301 (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { 4302 // Don't check non-allday events or events that aren't shown 4303 continue; 4304 } 4305 4306 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { 4307 float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents 4308 : mMaxUnexpandedAlldayEventCount; 4309 float height = drawHeight / numRectangles; 4310 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 4311 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 4312 } 4313 float eventTop = yOffset + height * event.getColumn(); 4314 float eventBottom = eventTop + height; 4315 if (eventTop < y && eventBottom > y) { 4316 // If the touch is inside the event rectangle, then 4317 // add the event. 4318 mSelectedEvents.add(event); 4319 closestEvent = event; 4320 break; 4321 } else { 4322 // Find the closest event 4323 if (eventTop >= y) { 4324 yDistance = eventTop - y; 4325 } else { 4326 yDistance = y - eventBottom; 4327 } 4328 if (yDistance < minYdistance) { 4329 minYdistance = yDistance; 4330 closestEvent = event; 4331 } 4332 } 4333 } 4334 } 4335 mSelectedEvent = closestEvent; 4336 return; 4337 } 4338 4339 // Adjust y for the scrollable bitmap 4340 y += mViewStartY - mFirstCell; 4341 4342 // Use a region around (x,y) for the selection region 4343 Rect region = mRect; 4344 region.left = x - 10; 4345 region.right = x + 10; 4346 region.top = y - 10; 4347 region.bottom = y + 10; 4348 4349 EventGeometry geometry = mEventGeometry; 4350 4351 for (int i = 0; i < numEvents; i++) { 4352 Event event = events.get(i); 4353 // Compute the event rectangle. 4354 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 4355 continue; 4356 } 4357 4358 // If the event intersects the selection region, then add it to 4359 // mSelectedEvents. 4360 if (geometry.eventIntersectsSelection(event, region)) { 4361 mSelectedEvents.add(event); 4362 } 4363 } 4364 4365 // If there are any events in the selected region, then assign the 4366 // closest one to mSelectedEvent. 4367 if (mSelectedEvents.size() > 0) { 4368 int len = mSelectedEvents.size(); 4369 Event closestEvent = null; 4370 float minDist = mViewWidth + mViewHeight; // some large distance 4371 for (int index = 0; index < len; index++) { 4372 Event ev = mSelectedEvents.get(index); 4373 float dist = geometry.pointToEvent(x, y, ev); 4374 if (dist < minDist) { 4375 minDist = dist; 4376 closestEvent = ev; 4377 } 4378 } 4379 mSelectedEvent = closestEvent; 4380 4381 // Keep the selected hour and day consistent with the selected 4382 // event. They could be different if we touched on an empty hour 4383 // slot very close to an event in the previous hour slot. In 4384 // that case we will select the nearby event. 4385 int startDay = mSelectedEvent.startDay; 4386 int endDay = mSelectedEvent.endDay; 4387 if (mSelectionDay < startDay) { 4388 mSelectionDay = startDay; 4389 } else if (mSelectionDay > endDay) { 4390 mSelectionDay = endDay; 4391 } 4392 4393 int startHour = mSelectedEvent.startTime / 60; 4394 int endHour; 4395 if (mSelectedEvent.startTime < mSelectedEvent.endTime) { 4396 endHour = (mSelectedEvent.endTime - 1) / 60; 4397 } else { 4398 endHour = mSelectedEvent.endTime / 60; 4399 } 4400 4401 if (mSelectionHour < startHour && mSelectionDay == startDay) { 4402 mSelectionHour = startHour; 4403 } else if (mSelectionHour > endHour && mSelectionDay == endDay) { 4404 mSelectionHour = endHour; 4405 } 4406 } 4407 } 4408 4409 // Encapsulates the code to continue the scrolling after the 4410 // finger is lifted. Instead of stopping the scroll immediately, 4411 // the scroll continues to "free spin" and gradually slows down. 4412 private class ContinueScroll implements Runnable { 4413 public void run() { 4414 mScrolling = mScrolling && mScroller.computeScrollOffset(); 4415 if (!mScrolling) { 4416 resetSelectedHour(); 4417 invalidate(); 4418 return; 4419 } 4420 4421 mViewStartY = mScroller.getCurrY(); 4422 4423 if (mCallEdgeEffectOnAbsorb) { 4424 if (mViewStartY < 0) { 4425 mEdgeEffectTop.onAbsorb((int) mLastVelocity); 4426 mCallEdgeEffectOnAbsorb = false; 4427 } else if (mViewStartY > mMaxViewStartY) { 4428 mEdgeEffectBottom.onAbsorb((int) mLastVelocity); 4429 mCallEdgeEffectOnAbsorb = false; 4430 } 4431 mLastVelocity = mScroller.getCurrVelocity(); 4432 } 4433 4434 if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { 4435 // Allow overscroll/springback only on a fling, 4436 // not a pull/fling from the end 4437 if (mViewStartY < 0) { 4438 mViewStartY = 0; 4439 } else if (mViewStartY > mMaxViewStartY) { 4440 mViewStartY = mMaxViewStartY; 4441 } 4442 } 4443 4444 computeFirstHour(); 4445 post(this); 4446 invalidate(); 4447 } 4448 } 4449 4450 /** 4451 * Cleanup the pop-up and timers. 4452 */ 4453 public void cleanup() { 4454 // Protect against null-pointer exceptions 4455 if (mPopup != null) { 4456 mPopup.dismiss(); 4457 } 4458 mLastPopupEventID = INVALID_EVENT_ID; 4459 Handler handler = getHandler(); 4460 if (handler != null) { 4461 handler.removeCallbacks(mDismissPopup); 4462 handler.removeCallbacks(mUpdateCurrentTime); 4463 } 4464 4465 Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, 4466 mCellHeight); 4467 4468 // Turn off redraw 4469 mRemeasure = false; 4470 } 4471 4472 /** 4473 * Restart the update timer 4474 */ 4475 public void restartCurrentTimeUpdates() { 4476 post(mUpdateCurrentTime); 4477 } 4478 4479 @Override 4480 protected void onDetachedFromWindow() { 4481 cleanup(); 4482 super.onDetachedFromWindow(); 4483 } 4484 4485 class DismissPopup implements Runnable { 4486 public void run() { 4487 // Protect against null-pointer exceptions 4488 if (mPopup != null) { 4489 mPopup.dismiss(); 4490 } 4491 } 4492 } 4493 4494 class UpdateCurrentTime implements Runnable { 4495 public void run() { 4496 long currentTime = System.currentTimeMillis(); 4497 mCurrentTime.set(currentTime); 4498 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) 4499 postDelayed(mUpdateCurrentTime, 4500 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 4501 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 4502 invalidate(); 4503 } 4504 } 4505 4506 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 4507 @Override 4508 public boolean onSingleTapUp(MotionEvent ev) { 4509 if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp"); 4510 DayView.this.doSingleTapUp(ev); 4511 return true; 4512 } 4513 4514 @Override 4515 public void onLongPress(MotionEvent ev) { 4516 if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress"); 4517 DayView.this.doLongPress(ev); 4518 } 4519 4520 @Override 4521 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 4522 if (DEBUG) Log.e(TAG, "GestureDetector.onScroll"); 4523 if (mTouchStartedInAlldayArea) { 4524 if (Math.abs(distanceX) < Math.abs(distanceY)) { 4525 return false; 4526 } 4527 // don't scroll vertically if this started in the allday area 4528 distanceY = 0; 4529 } 4530 DayView.this.doScroll(e1, e2, distanceX, distanceY); 4531 return true; 4532 } 4533 4534 @Override 4535 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 4536 if (DEBUG) Log.e(TAG, "GestureDetector.onFling"); 4537 4538 if (mTouchStartedInAlldayArea) { 4539 if (Math.abs(velocityX) < Math.abs(velocityY)) { 4540 return false; 4541 } 4542 // don't fling vertically if this started in the allday area 4543 velocityY = 0; 4544 } 4545 DayView.this.doFling(e1, e2, velocityX, velocityY); 4546 return true; 4547 } 4548 4549 @Override 4550 public boolean onDown(MotionEvent ev) { 4551 if (DEBUG) Log.e(TAG, "GestureDetector.onDown"); 4552 DayView.this.doDown(ev); 4553 return true; 4554 } 4555 } 4556 4557 @Override 4558 public boolean onLongClick(View v) { 4559 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 4560 long time = getSelectedTimeInMillis(); 4561 if (!mSelectionAllday) { 4562 flags |= DateUtils.FORMAT_SHOW_TIME; 4563 } 4564 if (DateFormat.is24HourFormat(mContext)) { 4565 flags |= DateUtils.FORMAT_24HOUR; 4566 } 4567 mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags); 4568 new AlertDialog.Builder(mContext).setTitle(mLongPressTitle) 4569 .setItems(mLongPressItems, new DialogInterface.OnClickListener() { 4570 @Override 4571 public void onClick(DialogInterface dialog, int which) { 4572 if (which == 0) { 4573 long extraLong = 0; 4574 if (mSelectionAllday) { 4575 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 4576 } 4577 mController.sendEventRelatedEventWithExtra(this, 4578 EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1, 4579 -1, extraLong, -1); 4580 } 4581 } 4582 }).show().setCanceledOnTouchOutside(true); 4583 return true; 4584 } 4585 4586 // The rest of this file was borrowed from Launcher2 - PagedView.java 4587 private static final int MINIMUM_SNAP_VELOCITY = 2200; 4588 4589 private class ScrollInterpolator implements Interpolator { 4590 public ScrollInterpolator() { 4591 } 4592 4593 public float getInterpolation(float t) { 4594 t -= 1.0f; 4595 t = t * t * t * t * t + 1; 4596 4597 if ((1 - t) * mAnimationDistance < 1) { 4598 cancelAnimation(); 4599 } 4600 4601 return t; 4602 } 4603 } 4604 4605 private long calculateDuration(float delta, float width, float velocity) { 4606 /* 4607 * Here we compute a "distance" that will be used in the computation of 4608 * the overall snap duration. This is a function of the actual distance 4609 * that needs to be traveled; we keep this value close to half screen 4610 * size in order to reduce the variance in snap duration as a function 4611 * of the distance the page needs to travel. 4612 */ 4613 final float halfScreenSize = width / 2; 4614 float distanceRatio = delta / width; 4615 float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); 4616 float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; 4617 4618 velocity = Math.abs(velocity); 4619 velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); 4620 4621 /* 4622 * we want the page's snap velocity to approximately match the velocity 4623 * at which the user flings, so we scale the duration by a value near to 4624 * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to 4625 * make it a little slower. 4626 */ 4627 long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); 4628 if (DEBUG) { 4629 Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" 4630 + distanceRatio + " distance:" + distance + " velocity:" + velocity 4631 + " duration:" + duration + " distanceInfluenceForSnapDuration:" 4632 + distanceInfluenceForSnapDuration); 4633 } 4634 return duration; 4635 } 4636 4637 /* 4638 * We want the duration of the page snap animation to be influenced by the 4639 * distance that the screen has to travel, however, we don't want this 4640 * duration to be effected in a purely linear fashion. Instead, we use this 4641 * method to moderate the effect that the distance of travel has on the 4642 * overall snap duration. 4643 */ 4644 private float distanceInfluenceForSnapDuration(float f) { 4645 f -= 0.5f; // center the values about 0. 4646 f *= 0.3f * Math.PI / 2.0f; 4647 return (float) Math.sin(f); 4648 } 4649} 4650