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