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