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 (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1958 && mFirstHour > 0 && mFirstHour < 8) { 1959 mPrevSelectedEvent = null; 1960 mSelectionAllday = true; 1961 setSelectedHour(mFirstHour + 1); 1962 return; 1963 } 1964 1965 if (mFirstHour > 0) { 1966 mFirstHour -= 1; 1967 mViewStartY -= (mCellHeight + HOUR_GAP); 1968 if (mViewStartY < 0) { 1969 mViewStartY = 0; 1970 } 1971 return; 1972 } 1973 } 1974 1975 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1976 if (mFirstHour < 24 - mNumHours) { 1977 mFirstHour += 1; 1978 mViewStartY += (mCellHeight + HOUR_GAP); 1979 if (mViewStartY > mMaxViewStartY) { 1980 mViewStartY = mMaxViewStartY; 1981 } 1982 return; 1983 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1984 mViewStartY = mMaxViewStartY; 1985 } 1986 } 1987 } 1988 1989 void clearCachedEvents() { 1990 mLastReloadMillis = 0; 1991 } 1992 1993 private final Runnable mCancelCallback = new Runnable() { 1994 public void run() { 1995 clearCachedEvents(); 1996 } 1997 }; 1998 1999 /* package */ void reloadEvents() { 2000 // Protect against this being called before this view has been 2001 // initialized. 2002 // if (mContext == null) { 2003 // return; 2004 // } 2005 2006 // Make sure our time zones are up to date 2007 mTZUpdater.run(); 2008 2009 setSelectedEvent(null); 2010 mPrevSelectedEvent = null; 2011 mSelectedEvents.clear(); 2012 2013 // The start date is the beginning of the week at 12am 2014 Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); 2015 weekStart.set(mBaseDate); 2016 weekStart.hour = 0; 2017 weekStart.minute = 0; 2018 weekStart.second = 0; 2019 long millis = weekStart.normalize(true /* ignore isDst */); 2020 2021 // Avoid reloading events unnecessarily. 2022 if (millis == mLastReloadMillis) { 2023 return; 2024 } 2025 mLastReloadMillis = millis; 2026 2027 // load events in the background 2028 // mContext.startProgressSpinner(); 2029 final ArrayList<Event> events = new ArrayList<Event>(); 2030 mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { 2031 public void run() { 2032 boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay; 2033 mEvents = events; 2034 mLoadedFirstJulianDay = mFirstJulianDay; 2035 if (mAllDayEvents == null) { 2036 mAllDayEvents = new ArrayList<Event>(); 2037 } else { 2038 mAllDayEvents.clear(); 2039 } 2040 2041 // Create a shorter array for all day events 2042 for (Event e : events) { 2043 if (e.drawAsAllday()) { 2044 mAllDayEvents.add(e); 2045 } 2046 } 2047 2048 // New events, new layouts 2049 if (mLayouts == null || mLayouts.length < events.size()) { 2050 mLayouts = new StaticLayout[events.size()]; 2051 } else { 2052 Arrays.fill(mLayouts, null); 2053 } 2054 2055 if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { 2056 mAllDayLayouts = new StaticLayout[events.size()]; 2057 } else { 2058 Arrays.fill(mAllDayLayouts, null); 2059 } 2060 2061 computeEventRelations(); 2062 2063 mRemeasure = true; 2064 mComputeSelectedEvents = true; 2065 recalc(); 2066 2067 // Start animation to cross fade the events 2068 if (fadeinEvents) { 2069 if (mEventsCrossFadeAnimation == null) { 2070 mEventsCrossFadeAnimation = 2071 ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255); 2072 mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION); 2073 } 2074 mEventsCrossFadeAnimation.start(); 2075 } else{ 2076 invalidate(); 2077 } 2078 } 2079 }, mCancelCallback); 2080 } 2081 2082 public void setEventsAlpha(int alpha) { 2083 mEventsAlpha = alpha; 2084 invalidate(); 2085 } 2086 2087 public int getEventsAlpha() { 2088 return mEventsAlpha; 2089 } 2090 2091 public void stopEventsAnimation() { 2092 if (mEventsCrossFadeAnimation != null) { 2093 mEventsCrossFadeAnimation.cancel(); 2094 } 2095 mEventsAlpha = 255; 2096 } 2097 2098 private void computeEventRelations() { 2099 // Compute the layout relation between each event before measuring cell 2100 // width, as the cell width should be adjusted along with the relation. 2101 // 2102 // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) 2103 // We should mark them as "overwapped". Though they are not overwapped logically, but 2104 // minimum cell height implicitly expands the cell height of A and it should look like 2105 // (1:00pm - 1:15pm) after the cell height adjustment. 2106 2107 // Compute the space needed for the all-day events, if any. 2108 // Make a pass over all the events, and keep track of the maximum 2109 // number of all-day events in any one day. Also, keep track of 2110 // the earliest event in each day. 2111 int maxAllDayEvents = 0; 2112 final ArrayList<Event> events = mEvents; 2113 final int len = events.size(); 2114 // Num of all-day-events on each day. 2115 final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; 2116 Arrays.fill(eventsCount, 0); 2117 for (int ii = 0; ii < len; ii++) { 2118 Event event = events.get(ii); 2119 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { 2120 continue; 2121 } 2122 if (event.drawAsAllday()) { 2123 // Count all the events being drawn as allDay events 2124 final int firstDay = Math.max(event.startDay, mFirstJulianDay); 2125 final int lastDay = Math.min(event.endDay, mLastJulianDay); 2126 for (int day = firstDay; day <= lastDay; day++) { 2127 final int count = ++eventsCount[day - mFirstJulianDay]; 2128 if (maxAllDayEvents < count) { 2129 maxAllDayEvents = count; 2130 } 2131 } 2132 2133 int daynum = event.startDay - mFirstJulianDay; 2134 int durationDays = event.endDay - event.startDay + 1; 2135 if (daynum < 0) { 2136 durationDays += daynum; 2137 daynum = 0; 2138 } 2139 if (daynum + durationDays > mNumDays) { 2140 durationDays = mNumDays - daynum; 2141 } 2142 for (int day = daynum; durationDays > 0; day++, durationDays--) { 2143 mHasAllDayEvent[day] = true; 2144 } 2145 } else { 2146 int daynum = event.startDay - mFirstJulianDay; 2147 int hour = event.startTime / 60; 2148 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 2149 mEarliestStartHour[daynum] = hour; 2150 } 2151 2152 // Also check the end hour in case the event spans more than 2153 // one day. 2154 daynum = event.endDay - mFirstJulianDay; 2155 hour = event.endTime / 60; 2156 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 2157 mEarliestStartHour[daynum] = hour; 2158 } 2159 } 2160 } 2161 mMaxAlldayEvents = maxAllDayEvents; 2162 initAllDayHeights(); 2163 } 2164 2165 @Override 2166 protected void onDraw(Canvas canvas) { 2167 if (mRemeasure) { 2168 remeasure(getWidth(), getHeight()); 2169 mRemeasure = false; 2170 } 2171 canvas.save(); 2172 2173 float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; 2174 // offset canvas by the current drag and header position 2175 canvas.translate(-mViewStartX, yTranslate); 2176 // clip to everything below the allDay area 2177 Rect dest = mDestRect; 2178 dest.top = (int) (mFirstCell - yTranslate); 2179 dest.bottom = (int) (mViewHeight - yTranslate); 2180 dest.left = 0; 2181 dest.right = mViewWidth; 2182 canvas.save(); 2183 canvas.clipRect(dest); 2184 // Draw the movable part of the view 2185 doDraw(canvas); 2186 // restore to having no clip 2187 canvas.restore(); 2188 2189 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 2190 float xTranslate; 2191 if (mViewStartX > 0) { 2192 xTranslate = mViewWidth; 2193 } else { 2194 xTranslate = -mViewWidth; 2195 } 2196 // Move the canvas around to prep it for the next view 2197 // specifically, shift it by a screen and undo the 2198 // yTranslation which will be redone in the nextView's onDraw(). 2199 canvas.translate(xTranslate, -yTranslate); 2200 DayView nextView = (DayView) mViewSwitcher.getNextView(); 2201 2202 // Prevent infinite recursive calls to onDraw(). 2203 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 2204 2205 nextView.onDraw(canvas); 2206 // Move it back for this view 2207 canvas.translate(-xTranslate, 0); 2208 } else { 2209 // If we drew another view we already translated it back 2210 // If we didn't draw another view we should be at the edge of the 2211 // screen 2212 canvas.translate(mViewStartX, -yTranslate); 2213 } 2214 2215 // Draw the fixed areas (that don't scroll) directly to the canvas. 2216 drawAfterScroll(canvas); 2217 if (mComputeSelectedEvents && mUpdateToast) { 2218 updateEventDetails(); 2219 mUpdateToast = false; 2220 } 2221 mComputeSelectedEvents = false; 2222 2223 // Draw overscroll glow 2224 if (!mEdgeEffectTop.isFinished()) { 2225 if (DAY_HEADER_HEIGHT != 0) { 2226 canvas.translate(0, DAY_HEADER_HEIGHT); 2227 } 2228 if (mEdgeEffectTop.draw(canvas)) { 2229 invalidate(); 2230 } 2231 if (DAY_HEADER_HEIGHT != 0) { 2232 canvas.translate(0, -DAY_HEADER_HEIGHT); 2233 } 2234 } 2235 if (!mEdgeEffectBottom.isFinished()) { 2236 canvas.rotate(180, mViewWidth/2, mViewHeight/2); 2237 if (mEdgeEffectBottom.draw(canvas)) { 2238 invalidate(); 2239 } 2240 } 2241 canvas.restore(); 2242 } 2243 2244 private void drawAfterScroll(Canvas canvas) { 2245 Paint p = mPaint; 2246 Rect r = mRect; 2247 2248 drawAllDayHighlights(r, canvas, p); 2249 if (mMaxAlldayEvents != 0) { 2250 drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); 2251 drawUpperLeftCorner(r, canvas, p); 2252 } 2253 2254 drawScrollLine(r, canvas, p); 2255 drawDayHeaderLoop(r, canvas, p); 2256 2257 // Draw the AM and PM indicators if we're in 12 hour mode 2258 if (!mIs24HourFormat) { 2259 drawAmPm(canvas, p); 2260 } 2261 } 2262 2263 // This isn't really the upper-left corner. It's the square area just 2264 // below the upper-left corner, above the hours and to the left of the 2265 // all-day area. 2266 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 2267 setupHourTextPaint(p); 2268 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2269 // Draw the allDay expand/collapse icon 2270 if (mUseExpandIcon) { 2271 mExpandAlldayDrawable.setBounds(mExpandAllDayRect); 2272 mExpandAlldayDrawable.draw(canvas); 2273 } else { 2274 mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); 2275 mCollapseAlldayDrawable.draw(canvas); 2276 } 2277 } 2278 } 2279 2280 private void drawScrollLine(Rect r, Canvas canvas, Paint p) { 2281 final int right = computeDayLeftPosition(mNumDays); 2282 final int y = mFirstCell - 1; 2283 2284 p.setAntiAlias(false); 2285 p.setStyle(Style.FILL); 2286 2287 p.setColor(mCalendarGridLineInnerHorizontalColor); 2288 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2289 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 2290 p.setAntiAlias(true); 2291 } 2292 2293 // Computes the x position for the left side of the given day (base 0) 2294 private int computeDayLeftPosition(int day) { 2295 int effectiveWidth = mViewWidth - mHoursWidth; 2296 return day * effectiveWidth / mNumDays + mHoursWidth; 2297 } 2298 2299 private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { 2300 if (mFutureBgColor != 0) { 2301 // First, color the labels area light gray 2302 r.top = 0; 2303 r.bottom = DAY_HEADER_HEIGHT; 2304 r.left = 0; 2305 r.right = mViewWidth; 2306 p.setColor(mBgColor); 2307 p.setStyle(Style.FILL); 2308 canvas.drawRect(r, p); 2309 // and the area that says All day 2310 r.top = DAY_HEADER_HEIGHT; 2311 r.bottom = mFirstCell - 1; 2312 r.left = 0; 2313 r.right = mHoursWidth; 2314 canvas.drawRect(r, p); 2315 2316 int startIndex = -1; 2317 2318 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2319 if (todayIndex < 0) { 2320 // Future 2321 startIndex = 0; 2322 } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { 2323 // Multiday - tomorrow is visible. 2324 startIndex = todayIndex + 1; 2325 } 2326 2327 if (startIndex >= 0) { 2328 // Draw the future highlight 2329 r.top = 0; 2330 r.bottom = mFirstCell - 1; 2331 r.left = computeDayLeftPosition(startIndex) + 1; 2332 r.right = computeDayLeftPosition(mNumDays); 2333 p.setColor(mFutureBgColor); 2334 p.setStyle(Style.FILL); 2335 canvas.drawRect(r, p); 2336 } 2337 } 2338 2339 if (mSelectionAllday && mSelectionMode != SELECTION_HIDDEN) { 2340 // Draw the selection highlight on the selected all-day area 2341 mRect.top = DAY_HEADER_HEIGHT + 1; 2342 mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; 2343 int daynum = mSelectionDay - mFirstJulianDay; 2344 mRect.left = computeDayLeftPosition(daynum) + 1; 2345 mRect.right = computeDayLeftPosition(daynum + 1); 2346 p.setColor(mCalendarGridAreaSelected); 2347 canvas.drawRect(mRect, p); 2348 } 2349 } 2350 2351 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 2352 // Draw the horizontal day background banner 2353 // p.setColor(mCalendarDateBannerBackground); 2354 // r.top = 0; 2355 // r.bottom = DAY_HEADER_HEIGHT; 2356 // r.left = 0; 2357 // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 2358 // canvas.drawRect(r, p); 2359 // 2360 // Fill the extra space on the right side with the default background 2361 // r.left = r.right; 2362 // r.right = mViewWidth; 2363 // p.setColor(mCalendarGridAreaBackground); 2364 // canvas.drawRect(r, p); 2365 if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { 2366 return; 2367 } 2368 2369 p.setTypeface(mBold); 2370 p.setTextAlign(Paint.Align.RIGHT); 2371 int cell = mFirstJulianDay; 2372 2373 String[] dayNames; 2374 if (mDateStrWidth < mCellWidth) { 2375 dayNames = mDayStrs; 2376 } else { 2377 dayNames = mDayStrs2Letter; 2378 } 2379 2380 p.setAntiAlias(true); 2381 for (int day = 0; day < mNumDays; day++, cell++) { 2382 int dayOfWeek = day + mFirstVisibleDayOfWeek; 2383 if (dayOfWeek >= 14) { 2384 dayOfWeek -= 14; 2385 } 2386 2387 int color = mCalendarDateBannerTextColor; 2388 if (mNumDays == 1) { 2389 if (dayOfWeek == Time.SATURDAY) { 2390 color = mWeek_saturdayColor; 2391 } else if (dayOfWeek == Time.SUNDAY) { 2392 color = mWeek_sundayColor; 2393 } 2394 } else { 2395 final int column = day % 7; 2396 if (Utils.isSaturday(column, mFirstDayOfWeek)) { 2397 color = mWeek_saturdayColor; 2398 } else if (Utils.isSunday(column, mFirstDayOfWeek)) { 2399 color = mWeek_sundayColor; 2400 } 2401 } 2402 2403 p.setColor(color); 2404 drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); 2405 } 2406 p.setTypeface(null); 2407 } 2408 2409 private void drawAmPm(Canvas canvas, Paint p) { 2410 p.setColor(mCalendarAmPmLabel); 2411 p.setTextSize(AMPM_TEXT_SIZE); 2412 p.setTypeface(mBold); 2413 p.setAntiAlias(true); 2414 p.setTextAlign(Paint.Align.RIGHT); 2415 String text = mAmString; 2416 if (mFirstHour >= 12) { 2417 text = mPmString; 2418 } 2419 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 2420 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2421 2422 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 2423 // Also draw the "PM" 2424 text = mPmString; 2425 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 2426 + 2 * mHoursTextHeight + HOUR_GAP; 2427 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2428 } 2429 } 2430 2431 private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, 2432 Paint p) { 2433 r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2434 r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2435 2436 r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; 2437 r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); 2438 2439 mCurrentTimeLine.setBounds(r); 2440 mCurrentTimeLine.draw(canvas); 2441 if (mAnimateToday) { 2442 mCurrentTimeAnimateLine.setBounds(r); 2443 mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha); 2444 mCurrentTimeAnimateLine.draw(canvas); 2445 } 2446 } 2447 2448 private void doDraw(Canvas canvas) { 2449 Paint p = mPaint; 2450 Rect r = mRect; 2451 2452 if (mFutureBgColor != 0) { 2453 drawBgColors(r, canvas, p); 2454 } 2455 drawGridBackground(r, canvas, p); 2456 drawHours(r, canvas, p); 2457 2458 // Draw each day 2459 int cell = mFirstJulianDay; 2460 p.setAntiAlias(false); 2461 int alpha = p.getAlpha(); 2462 p.setAlpha(mEventsAlpha); 2463 for (int day = 0; day < mNumDays; day++, cell++) { 2464 // TODO Wow, this needs cleanup. drawEvents loop through all the 2465 // events on every call. 2466 drawEvents(cell, day, HOUR_GAP, canvas, p); 2467 // If this is today 2468 if (cell == mTodayJulianDay) { 2469 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2470 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2471 2472 // And the current time shows up somewhere on the screen 2473 if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 2474 drawCurrentTimeLine(r, day, lineY, canvas, p); 2475 } 2476 } 2477 } 2478 p.setAntiAlias(true); 2479 p.setAlpha(alpha); 2480 2481 drawSelectedRect(r, canvas, p); 2482 } 2483 2484 private void drawSelectedRect(Rect r, Canvas canvas, Paint p) { 2485 // Draw a highlight on the selected hour (if needed) 2486 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { 2487 int daynum = mSelectionDay - mFirstJulianDay; 2488 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2489 r.bottom = r.top + mCellHeight + HOUR_GAP; 2490 r.left = computeDayLeftPosition(daynum) + 1; 2491 r.right = computeDayLeftPosition(daynum + 1) + 1; 2492 2493 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 2494 2495 // Draw the highlight on the grid 2496 p.setColor(mCalendarGridAreaSelected); 2497 r.top += HOUR_GAP; 2498 r.right -= DAY_GAP; 2499 p.setAntiAlias(false); 2500 canvas.drawRect(r, p); 2501 2502 // Draw a "new event hint" on top of the highlight 2503 // For the week view, show a "+", for day view, show "+ New event" 2504 p.setColor(mNewEventHintColor); 2505 if (mNumDays > 1) { 2506 p.setStrokeWidth(NEW_EVENT_WIDTH); 2507 int width = r.right - r.left; 2508 int midX = r.left + width / 2; 2509 int midY = r.top + mCellHeight / 2; 2510 int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2; 2511 length = Math.min(length, NEW_EVENT_MAX_LENGTH); 2512 int verticalPadding = (mCellHeight - length) / 2; 2513 int horizontalPadding = (width - length) / 2; 2514 canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, 2515 midY, p); 2516 canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p); 2517 } else { 2518 p.setStyle(Paint.Style.FILL); 2519 p.setTextSize(NEW_EVENT_HINT_FONT_SIZE); 2520 p.setTextAlign(Paint.Align.LEFT); 2521 p.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 2522 canvas.drawText(mNewEventHintString, r.left + EVENT_TEXT_LEFT_MARGIN, 2523 r.top + Math.abs(p.getFontMetrics().ascent) + EVENT_TEXT_TOP_MARGIN , p); 2524 } 2525 } 2526 } 2527 2528 private void drawHours(Rect r, Canvas canvas, Paint p) { 2529 setupHourTextPaint(p); 2530 2531 int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; 2532 2533 for (int i = 0; i < 24; i++) { 2534 String time = mHourStrs[i]; 2535 canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); 2536 y += mCellHeight + HOUR_GAP; 2537 } 2538 } 2539 2540 private void setupHourTextPaint(Paint p) { 2541 p.setColor(mCalendarHourLabelColor); 2542 p.setTextSize(HOURS_TEXT_SIZE); 2543 p.setTypeface(Typeface.DEFAULT); 2544 p.setTextAlign(Paint.Align.RIGHT); 2545 p.setAntiAlias(true); 2546 } 2547 2548 private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { 2549 int dateNum = mFirstVisibleDate + day; 2550 int x; 2551 if (dateNum > mMonthLength) { 2552 dateNum -= mMonthLength; 2553 } 2554 p.setAntiAlias(true); 2555 2556 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2557 // Draw day of the month 2558 String dateNumStr = String.valueOf(dateNum); 2559 if (mNumDays > 1) { 2560 float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; 2561 2562 // Draw day of the month 2563 x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; 2564 p.setTextAlign(Align.RIGHT); 2565 p.setTextSize(DATE_HEADER_FONT_SIZE); 2566 2567 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2568 canvas.drawText(dateNumStr, x, y, p); 2569 2570 // Draw day of the week 2571 x -= p.measureText(" " + dateNumStr); 2572 p.setTextSize(DAY_HEADER_FONT_SIZE); 2573 p.setTypeface(Typeface.DEFAULT); 2574 canvas.drawText(dayStr, x, y, p); 2575 } else { 2576 float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; 2577 p.setTextAlign(Align.LEFT); 2578 2579 2580 // Draw day of the week 2581 x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; 2582 p.setTextSize(DAY_HEADER_FONT_SIZE); 2583 p.setTypeface(Typeface.DEFAULT); 2584 canvas.drawText(dayStr, x, y, p); 2585 2586 // Draw day of the month 2587 x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; 2588 p.setTextSize(DATE_HEADER_FONT_SIZE); 2589 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2590 canvas.drawText(dateNumStr, x, y, p); 2591 } 2592 } 2593 2594 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 2595 Paint.Style savedStyle = p.getStyle(); 2596 2597 final float stopX = computeDayLeftPosition(mNumDays); 2598 float y = 0; 2599 final float deltaY = mCellHeight + HOUR_GAP; 2600 int linesIndex = 0; 2601 final float startY = 0; 2602 final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 2603 float x = mHoursWidth; 2604 2605 // Draw the inner horizontal grid lines 2606 p.setColor(mCalendarGridLineInnerHorizontalColor); 2607 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2608 p.setAntiAlias(false); 2609 y = 0; 2610 linesIndex = 0; 2611 for (int hour = 0; hour <= 24; hour++) { 2612 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2613 mLines[linesIndex++] = y; 2614 mLines[linesIndex++] = stopX; 2615 mLines[linesIndex++] = y; 2616 y += deltaY; 2617 } 2618 if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { 2619 canvas.drawLines(mLines, 0, linesIndex, p); 2620 linesIndex = 0; 2621 p.setColor(mCalendarGridLineInnerVerticalColor); 2622 } 2623 2624 // Draw the inner vertical grid lines 2625 for (int day = 0; day <= mNumDays; day++) { 2626 x = computeDayLeftPosition(day); 2627 mLines[linesIndex++] = x; 2628 mLines[linesIndex++] = startY; 2629 mLines[linesIndex++] = x; 2630 mLines[linesIndex++] = stopY; 2631 } 2632 canvas.drawLines(mLines, 0, linesIndex, p); 2633 2634 // Restore the saved style. 2635 p.setStyle(savedStyle); 2636 p.setAntiAlias(true); 2637 } 2638 2639 /** 2640 * @param r 2641 * @param canvas 2642 * @param p 2643 */ 2644 private void drawBgColors(Rect r, Canvas canvas, Paint p) { 2645 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2646 // Draw the hours background color 2647 r.top = mDestRect.top; 2648 r.bottom = mDestRect.bottom; 2649 r.left = 0; 2650 r.right = mHoursWidth; 2651 p.setColor(mBgColor); 2652 p.setStyle(Style.FILL); 2653 p.setAntiAlias(false); 2654 canvas.drawRect(r, p); 2655 2656 // Draw background for grid area 2657 if (mNumDays == 1 && todayIndex == 0) { 2658 // Draw a white background for the time later than current time 2659 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2660 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2661 if (lineY < mViewStartY + mViewHeight) { 2662 lineY = Math.max(lineY, mViewStartY); 2663 r.left = mHoursWidth; 2664 r.right = mViewWidth; 2665 r.top = lineY; 2666 r.bottom = mViewStartY + mViewHeight; 2667 p.setColor(mFutureBgColor); 2668 canvas.drawRect(r, p); 2669 } 2670 } else if (todayIndex >= 0 && todayIndex < mNumDays) { 2671 // Draw today with a white background for the time later than current time 2672 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2673 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2674 if (lineY < mViewStartY + mViewHeight) { 2675 lineY = Math.max(lineY, mViewStartY); 2676 r.left = computeDayLeftPosition(todayIndex) + 1; 2677 r.right = computeDayLeftPosition(todayIndex + 1); 2678 r.top = lineY; 2679 r.bottom = mViewStartY + mViewHeight; 2680 p.setColor(mFutureBgColor); 2681 canvas.drawRect(r, p); 2682 } 2683 2684 // Paint Tomorrow and later days with future color 2685 if (todayIndex + 1 < mNumDays) { 2686 r.left = computeDayLeftPosition(todayIndex + 1) + 1; 2687 r.right = computeDayLeftPosition(mNumDays); 2688 r.top = mDestRect.top; 2689 r.bottom = mDestRect.bottom; 2690 p.setColor(mFutureBgColor); 2691 canvas.drawRect(r, p); 2692 } 2693 } else if (todayIndex < 0) { 2694 // Future 2695 r.left = computeDayLeftPosition(0) + 1; 2696 r.right = computeDayLeftPosition(mNumDays); 2697 r.top = mDestRect.top; 2698 r.bottom = mDestRect.bottom; 2699 p.setColor(mFutureBgColor); 2700 canvas.drawRect(r, p); 2701 } 2702 p.setAntiAlias(true); 2703 } 2704 2705 Event getSelectedEvent() { 2706 if (mSelectedEvent == null) { 2707 // There is no event at the selected hour, so create a new event. 2708 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2709 getSelectedMinutesSinceMidnight()); 2710 } 2711 return mSelectedEvent; 2712 } 2713 2714 boolean isEventSelected() { 2715 return (mSelectedEvent != null); 2716 } 2717 2718 Event getNewEvent() { 2719 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2720 getSelectedMinutesSinceMidnight()); 2721 } 2722 2723 static Event getNewEvent(int julianDay, long utcMillis, 2724 int minutesSinceMidnight) { 2725 Event event = Event.newInstance(); 2726 event.startDay = julianDay; 2727 event.endDay = julianDay; 2728 event.startMillis = utcMillis; 2729 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 2730 event.startTime = minutesSinceMidnight; 2731 event.endTime = event.startTime + MINUTES_PER_HOUR; 2732 return event; 2733 } 2734 2735 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 2736 float maxWidthF = 0.0f; 2737 2738 int len = strings.length; 2739 for (int i = 0; i < len; i++) { 2740 float width = p.measureText(strings[i]); 2741 maxWidthF = Math.max(width, maxWidthF); 2742 } 2743 int maxWidth = (int) (maxWidthF + 0.5); 2744 if (maxWidth < currentMax) { 2745 maxWidth = currentMax; 2746 } 2747 return maxWidth; 2748 } 2749 2750 private void saveSelectionPosition(float left, float top, float right, float bottom) { 2751 mPrevBox.left = (int) left; 2752 mPrevBox.right = (int) right; 2753 mPrevBox.top = (int) top; 2754 mPrevBox.bottom = (int) bottom; 2755 } 2756 2757 private Rect getCurrentSelectionPosition() { 2758 Rect box = new Rect(); 2759 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2760 box.bottom = box.top + mCellHeight + HOUR_GAP; 2761 int daynum = mSelectionDay - mFirstJulianDay; 2762 box.left = computeDayLeftPosition(daynum) + 1; 2763 box.right = computeDayLeftPosition(daynum + 1); 2764 return box; 2765 } 2766 2767 private void setupTextRect(Rect r) { 2768 if (r.bottom <= r.top || r.right <= r.left) { 2769 r.bottom = r.top; 2770 r.right = r.left; 2771 return; 2772 } 2773 2774 if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { 2775 r.top += EVENT_TEXT_TOP_MARGIN; 2776 r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; 2777 } 2778 if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { 2779 r.left += EVENT_TEXT_LEFT_MARGIN; 2780 r.right -= EVENT_TEXT_RIGHT_MARGIN; 2781 } 2782 } 2783 2784 private void setupAllDayTextRect(Rect r) { 2785 if (r.bottom <= r.top || r.right <= r.left) { 2786 r.bottom = r.top; 2787 r.right = r.left; 2788 return; 2789 } 2790 2791 if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { 2792 r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; 2793 r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; 2794 } 2795 if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { 2796 r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2797 r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2798 } 2799 } 2800 2801 /** 2802 * Return the layout for a numbered event. Create it if not already existing 2803 */ 2804 private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, 2805 Rect r) { 2806 if (i < 0 || i >= layouts.length) { 2807 return null; 2808 } 2809 2810 StaticLayout layout = layouts[i]; 2811 // Check if we have already initialized the StaticLayout and that 2812 // the width hasn't changed (due to vertical resizing which causes 2813 // re-layout of events at min height) 2814 if (layout == null || r.width() != layout.getWidth()) { 2815 SpannableStringBuilder bob = new SpannableStringBuilder(); 2816 if (event.title != null) { 2817 // MAX - 1 since we add a space 2818 bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1)); 2819 bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); 2820 bob.append(' '); 2821 } 2822 if (event.location != null) { 2823 bob.append(drawTextSanitizer(event.location.toString(), 2824 MAX_EVENT_TEXT_LEN - bob.length())); 2825 } 2826 2827 switch (event.selfAttendeeStatus) { 2828 case Attendees.ATTENDEE_STATUS_INVITED: 2829 paint.setColor(event.color); 2830 break; 2831 case Attendees.ATTENDEE_STATUS_DECLINED: 2832 paint.setColor(mEventTextColor); 2833 paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA); 2834 break; 2835 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 2836 case Attendees.ATTENDEE_STATUS_ACCEPTED: 2837 case Attendees.ATTENDEE_STATUS_TENTATIVE: 2838 default: 2839 paint.setColor(mEventTextColor); 2840 break; 2841 } 2842 2843 // Leave a one pixel boundary on the left and right of the rectangle for the event 2844 layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), 2845 Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); 2846 2847 layouts[i] = layout; 2848 } 2849 layout.getPaint().setAlpha(mEventsAlpha); 2850 return layout; 2851 } 2852 2853 private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { 2854 2855 p.setTextSize(NORMAL_FONT_SIZE); 2856 p.setTextAlign(Paint.Align.LEFT); 2857 Paint eventTextPaint = mEventTextPaint; 2858 2859 final float startY = DAY_HEADER_HEIGHT; 2860 final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; 2861 float x = 0; 2862 int linesIndex = 0; 2863 2864 // Draw the inner vertical grid lines 2865 p.setColor(mCalendarGridLineInnerVerticalColor); 2866 x = mHoursWidth; 2867 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2868 // Line bounding the top of the all day area 2869 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2870 mLines[linesIndex++] = startY; 2871 mLines[linesIndex++] = computeDayLeftPosition(mNumDays); 2872 mLines[linesIndex++] = startY; 2873 2874 for (int day = 0; day <= mNumDays; day++) { 2875 x = computeDayLeftPosition(day); 2876 mLines[linesIndex++] = x; 2877 mLines[linesIndex++] = startY; 2878 mLines[linesIndex++] = x; 2879 mLines[linesIndex++] = stopY; 2880 } 2881 p.setAntiAlias(false); 2882 canvas.drawLines(mLines, 0, linesIndex, p); 2883 p.setStyle(Style.FILL); 2884 2885 int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 2886 int lastDay = firstDay + numDays - 1; 2887 final ArrayList<Event> events = mAllDayEvents; 2888 int numEvents = events.size(); 2889 // Whether or not we should draw the more events text 2890 boolean hasMoreEvents = false; 2891 // size of the allDay area 2892 float drawHeight = mAlldayHeight; 2893 // max number of events being drawn in one day of the allday area 2894 float numRectangles = mMaxAlldayEvents; 2895 // Where to cut off drawn allday events 2896 int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; 2897 // The number of events that weren't drawn in each day 2898 mSkippedAlldayEvents = new int[numDays]; 2899 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && 2900 mAnimateDayHeight == 0) { 2901 // We draw one fewer event than will fit so that more events text 2902 // can be drawn 2903 numRectangles = mMaxUnexpandedAlldayEventCount - 1; 2904 // We also clip the events above the more events text 2905 allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 2906 hasMoreEvents = true; 2907 } else if (mAnimateDayHeight != 0) { 2908 // clip at the end of the animating space 2909 allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; 2910 } 2911 2912 int alpha = eventTextPaint.getAlpha(); 2913 eventTextPaint.setAlpha(mEventsAlpha); 2914 for (int i = 0; i < numEvents; i++) { 2915 Event event = events.get(i); 2916 int startDay = event.startDay; 2917 int endDay = event.endDay; 2918 if (startDay > lastDay || endDay < firstDay) { 2919 continue; 2920 } 2921 if (startDay < firstDay) { 2922 startDay = firstDay; 2923 } 2924 if (endDay > lastDay) { 2925 endDay = lastDay; 2926 } 2927 int startIndex = startDay - firstDay; 2928 int endIndex = endDay - firstDay; 2929 float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : 2930 drawHeight / numRectangles; 2931 2932 // Prevent a single event from getting too big 2933 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 2934 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 2935 } 2936 2937 // Leave a one-pixel space between the vertical day lines and the 2938 // event rectangle. 2939 event.left = computeDayLeftPosition(startIndex); 2940 event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; 2941 event.top = y + height * event.getColumn(); 2942 event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; 2943 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2944 // check if we should skip this event. We skip if it starts 2945 // after the clip bound or ends after the skip bound and we're 2946 // not animating. 2947 if (event.top >= allDayEventClip) { 2948 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2949 continue; 2950 } else if (event.bottom > allDayEventClip) { 2951 if (hasMoreEvents) { 2952 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2953 continue; 2954 } 2955 event.bottom = allDayEventClip; 2956 } 2957 } 2958 Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, 2959 (int) event.bottom); 2960 setupAllDayTextRect(r); 2961 StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); 2962 drawEventText(layout, r, canvas, r.top, r.bottom, true); 2963 2964 // Check if this all-day event intersects the selected day 2965 if (mSelectionAllday && mComputeSelectedEvents) { 2966 if (startDay <= mSelectionDay && endDay >= mSelectionDay) { 2967 mSelectedEvents.add(event); 2968 } 2969 } 2970 } 2971 eventTextPaint.setAlpha(alpha); 2972 2973 if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { 2974 // If the more allday text should be visible, draw it. 2975 alpha = p.getAlpha(); 2976 p.setAlpha(mEventsAlpha); 2977 p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); 2978 for (int i = 0; i < mSkippedAlldayEvents.length; i++) { 2979 if (mSkippedAlldayEvents[i] > 0) { 2980 drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); 2981 } 2982 } 2983 p.setAlpha(alpha); 2984 } 2985 2986 if (mSelectionAllday) { 2987 // Compute the neighbors for the list of all-day events that 2988 // intersect the selected day. 2989 computeAllDayNeighbors(); 2990 2991 // Set the selection position to zero so that when we move down 2992 // to the normal event area, we will highlight the topmost event. 2993 saveSelectionPosition(0f, 0f, 0f, 0f); 2994 } 2995 } 2996 2997 // Helper method for counting the number of allday events skipped on each day 2998 private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { 2999 if (counts == null || startIndex < 0 || endIndex > counts.length) { 3000 return; 3001 } 3002 for (int i = startIndex; i <= endIndex; i++) { 3003 counts[i]++; 3004 } 3005 } 3006 3007 // Draws the "box +n" text for hidden allday events 3008 protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { 3009 int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 3010 int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f 3011 * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); 3012 Rect r = mRect; 3013 r.top = y; 3014 r.left = x; 3015 r.bottom = y + EVENT_SQUARE_WIDTH; 3016 r.right = x + EVENT_SQUARE_WIDTH; 3017 p.setColor(mMoreEventsTextColor); 3018 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 3019 p.setStyle(Style.STROKE); 3020 p.setAntiAlias(false); 3021 canvas.drawRect(r, p); 3022 p.setAntiAlias(true); 3023 p.setStyle(Style.FILL); 3024 p.setTextSize(EVENT_TEXT_FONT_SIZE); 3025 String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); 3026 y += EVENT_SQUARE_WIDTH; 3027 x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; 3028 canvas.drawText(String.format(text, remainingEvents), x, y, p); 3029 } 3030 3031 private void computeAllDayNeighbors() { 3032 int len = mSelectedEvents.size(); 3033 if (len == 0 || mSelectedEvent != null) { 3034 return; 3035 } 3036 3037 // First, clear all the links 3038 for (int ii = 0; ii < len; ii++) { 3039 Event ev = mSelectedEvents.get(ii); 3040 ev.nextUp = null; 3041 ev.nextDown = null; 3042 ev.nextLeft = null; 3043 ev.nextRight = null; 3044 } 3045 3046 // For each event in the selected event list "mSelectedEvents", find 3047 // its neighbors in the up and down directions. This could be done 3048 // more efficiently by sorting on the Event.getColumn() field, but 3049 // the list is expected to be very small. 3050 3051 // Find the event in the same row as the previously selected all-day 3052 // event, if any. 3053 int startPosition = -1; 3054 if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) { 3055 startPosition = mPrevSelectedEvent.getColumn(); 3056 } 3057 int maxPosition = -1; 3058 Event startEvent = null; 3059 Event maxPositionEvent = null; 3060 for (int ii = 0; ii < len; ii++) { 3061 Event ev = mSelectedEvents.get(ii); 3062 int position = ev.getColumn(); 3063 if (position == startPosition) { 3064 startEvent = ev; 3065 } else if (position > maxPosition) { 3066 maxPositionEvent = ev; 3067 maxPosition = position; 3068 } 3069 for (int jj = 0; jj < len; jj++) { 3070 if (jj == ii) { 3071 continue; 3072 } 3073 Event neighbor = mSelectedEvents.get(jj); 3074 int neighborPosition = neighbor.getColumn(); 3075 if (neighborPosition == position - 1) { 3076 ev.nextUp = neighbor; 3077 } else if (neighborPosition == position + 1) { 3078 ev.nextDown = neighbor; 3079 } 3080 } 3081 } 3082 if (startEvent != null) { 3083 setSelectedEvent(startEvent); 3084 } else { 3085 setSelectedEvent(maxPositionEvent); 3086 } 3087 } 3088 3089 private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { 3090 Paint eventTextPaint = mEventTextPaint; 3091 int left = computeDayLeftPosition(dayIndex) + 1; 3092 int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; 3093 int cellHeight = mCellHeight; 3094 3095 // Use the selected hour as the selection region 3096 Rect selectionArea = mSelectionRect; 3097 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); 3098 selectionArea.bottom = selectionArea.top + cellHeight; 3099 selectionArea.left = left; 3100 selectionArea.right = selectionArea.left + cellWidth; 3101 3102 final ArrayList<Event> events = mEvents; 3103 int numEvents = events.size(); 3104 EventGeometry geometry = mEventGeometry; 3105 3106 final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; 3107 3108 int alpha = eventTextPaint.getAlpha(); 3109 eventTextPaint.setAlpha(mEventsAlpha); 3110 for (int i = 0; i < numEvents; i++) { 3111 Event event = events.get(i); 3112 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 3113 continue; 3114 } 3115 3116 // Don't draw it if it is not visible 3117 if (event.bottom < mViewStartY || event.top > viewEndY) { 3118 continue; 3119 } 3120 3121 if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents 3122 && geometry.eventIntersectsSelection(event, selectionArea)) { 3123 mSelectedEvents.add(event); 3124 } 3125 3126 Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); 3127 setupTextRect(r); 3128 3129 // Don't draw text if it is not visible 3130 if (r.top > viewEndY || r.bottom < mViewStartY) { 3131 continue; 3132 } 3133 StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); 3134 // TODO: not sure why we are 4 pixels off 3135 drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight 3136 - DAY_HEADER_HEIGHT - mAlldayHeight, false); 3137 } 3138 eventTextPaint.setAlpha(alpha); 3139 3140 if (date == mSelectionDay && !mSelectionAllday && isFocused() 3141 && mSelectionMode != SELECTION_HIDDEN) { 3142 computeNeighbors(); 3143 } 3144 } 3145 3146 // Computes the "nearest" neighbor event in four directions (left, right, 3147 // up, down) for each of the events in the mSelectedEvents array. 3148 private void computeNeighbors() { 3149 int len = mSelectedEvents.size(); 3150 if (len == 0 || mSelectedEvent != null) { 3151 return; 3152 } 3153 3154 // First, clear all the links 3155 for (int ii = 0; ii < len; ii++) { 3156 Event ev = mSelectedEvents.get(ii); 3157 ev.nextUp = null; 3158 ev.nextDown = null; 3159 ev.nextLeft = null; 3160 ev.nextRight = null; 3161 } 3162 3163 Event startEvent = mSelectedEvents.get(0); 3164 int startEventDistance1 = 100000; // any large number 3165 int startEventDistance2 = 100000; // any large number 3166 int prevLocation = FROM_NONE; 3167 int prevTop; 3168 int prevBottom; 3169 int prevLeft; 3170 int prevRight; 3171 int prevCenter = 0; 3172 Rect box = getCurrentSelectionPosition(); 3173 if (mPrevSelectedEvent != null) { 3174 prevTop = (int) mPrevSelectedEvent.top; 3175 prevBottom = (int) mPrevSelectedEvent.bottom; 3176 prevLeft = (int) mPrevSelectedEvent.left; 3177 prevRight = (int) mPrevSelectedEvent.right; 3178 // Check if the previously selected event intersects the previous 3179 // selection box. (The previously selected event may be from a 3180 // much older selection box.) 3181 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top 3182 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { 3183 mPrevSelectedEvent = null; 3184 prevTop = mPrevBox.top; 3185 prevBottom = mPrevBox.bottom; 3186 prevLeft = mPrevBox.left; 3187 prevRight = mPrevBox.right; 3188 } else { 3189 // Clip the top and bottom to the previous selection box. 3190 if (prevTop < mPrevBox.top) { 3191 prevTop = mPrevBox.top; 3192 } 3193 if (prevBottom > mPrevBox.bottom) { 3194 prevBottom = mPrevBox.bottom; 3195 } 3196 } 3197 } else { 3198 // Just use the previously drawn selection box 3199 prevTop = mPrevBox.top; 3200 prevBottom = mPrevBox.bottom; 3201 prevLeft = mPrevBox.left; 3202 prevRight = mPrevBox.right; 3203 } 3204 3205 // Figure out where we came from and compute the center of that area. 3206 if (prevLeft >= box.right) { 3207 // The previously selected event was to the right of us. 3208 prevLocation = FROM_RIGHT; 3209 prevCenter = (prevTop + prevBottom) / 2; 3210 } else if (prevRight <= box.left) { 3211 // The previously selected event was to the left of us. 3212 prevLocation = FROM_LEFT; 3213 prevCenter = (prevTop + prevBottom) / 2; 3214 } else if (prevBottom <= box.top) { 3215 // The previously selected event was above us. 3216 prevLocation = FROM_ABOVE; 3217 prevCenter = (prevLeft + prevRight) / 2; 3218 } else if (prevTop >= box.bottom) { 3219 // The previously selected event was below us. 3220 prevLocation = FROM_BELOW; 3221 prevCenter = (prevLeft + prevRight) / 2; 3222 } 3223 3224 // For each event in the selected event list "mSelectedEvents", search 3225 // all the other events in that list for the nearest neighbor in 4 3226 // directions. 3227 for (int ii = 0; ii < len; ii++) { 3228 Event ev = mSelectedEvents.get(ii); 3229 3230 int startTime = ev.startTime; 3231 int endTime = ev.endTime; 3232 int left = (int) ev.left; 3233 int right = (int) ev.right; 3234 int top = (int) ev.top; 3235 if (top < box.top) { 3236 top = box.top; 3237 } 3238 int bottom = (int) ev.bottom; 3239 if (bottom > box.bottom) { 3240 bottom = box.bottom; 3241 } 3242 // if (false) { 3243 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 3244 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3245 // if (DateFormat.is24HourFormat(mContext)) { 3246 // flags |= DateUtils.FORMAT_24HOUR; 3247 // } 3248 // String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis, 3249 // ev.endMillis, flags); 3250 // Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: " 3251 // + bottom + " ev: " + timeRange + " " + ev.title); 3252 // } 3253 int upDistanceMin = 10000; // any large number 3254 int downDistanceMin = 10000; // any large number 3255 int leftDistanceMin = 10000; // any large number 3256 int rightDistanceMin = 10000; // any large number 3257 Event upEvent = null; 3258 Event downEvent = null; 3259 Event leftEvent = null; 3260 Event rightEvent = null; 3261 3262 // Pick the starting event closest to the previously selected event, 3263 // if any. distance1 takes precedence over distance2. 3264 int distance1 = 0; 3265 int distance2 = 0; 3266 if (prevLocation == FROM_ABOVE) { 3267 if (left >= prevCenter) { 3268 distance1 = left - prevCenter; 3269 } else if (right <= prevCenter) { 3270 distance1 = prevCenter - right; 3271 } 3272 distance2 = top - prevBottom; 3273 } else if (prevLocation == FROM_BELOW) { 3274 if (left >= prevCenter) { 3275 distance1 = left - prevCenter; 3276 } else if (right <= prevCenter) { 3277 distance1 = prevCenter - right; 3278 } 3279 distance2 = prevTop - bottom; 3280 } else if (prevLocation == FROM_LEFT) { 3281 if (bottom <= prevCenter) { 3282 distance1 = prevCenter - bottom; 3283 } else if (top >= prevCenter) { 3284 distance1 = top - prevCenter; 3285 } 3286 distance2 = left - prevRight; 3287 } else if (prevLocation == FROM_RIGHT) { 3288 if (bottom <= prevCenter) { 3289 distance1 = prevCenter - bottom; 3290 } else if (top >= prevCenter) { 3291 distance1 = top - prevCenter; 3292 } 3293 distance2 = prevLeft - right; 3294 } 3295 if (distance1 < startEventDistance1 3296 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { 3297 startEvent = ev; 3298 startEventDistance1 = distance1; 3299 startEventDistance2 = distance2; 3300 } 3301 3302 // For each neighbor, figure out if it is above or below or left 3303 // or right of me and compute the distance. 3304 for (int jj = 0; jj < len; jj++) { 3305 if (jj == ii) { 3306 continue; 3307 } 3308 Event neighbor = mSelectedEvents.get(jj); 3309 int neighborLeft = (int) neighbor.left; 3310 int neighborRight = (int) neighbor.right; 3311 if (neighbor.endTime <= startTime) { 3312 // This neighbor is entirely above me. 3313 // If we overlap the same column, then compute the distance. 3314 if (neighborLeft < right && neighborRight > left) { 3315 int distance = startTime - neighbor.endTime; 3316 if (distance < upDistanceMin) { 3317 upDistanceMin = distance; 3318 upEvent = neighbor; 3319 } else if (distance == upDistanceMin) { 3320 int center = (left + right) / 2; 3321 int currentDistance = 0; 3322 int currentLeft = (int) upEvent.left; 3323 int currentRight = (int) upEvent.right; 3324 if (currentRight <= center) { 3325 currentDistance = center - currentRight; 3326 } else if (currentLeft >= center) { 3327 currentDistance = currentLeft - center; 3328 } 3329 3330 int neighborDistance = 0; 3331 if (neighborRight <= center) { 3332 neighborDistance = center - neighborRight; 3333 } else if (neighborLeft >= center) { 3334 neighborDistance = neighborLeft - center; 3335 } 3336 if (neighborDistance < currentDistance) { 3337 upDistanceMin = distance; 3338 upEvent = neighbor; 3339 } 3340 } 3341 } 3342 } else if (neighbor.startTime >= endTime) { 3343 // This neighbor is entirely below me. 3344 // If we overlap the same column, then compute the distance. 3345 if (neighborLeft < right && neighborRight > left) { 3346 int distance = neighbor.startTime - endTime; 3347 if (distance < downDistanceMin) { 3348 downDistanceMin = distance; 3349 downEvent = neighbor; 3350 } else if (distance == downDistanceMin) { 3351 int center = (left + right) / 2; 3352 int currentDistance = 0; 3353 int currentLeft = (int) downEvent.left; 3354 int currentRight = (int) downEvent.right; 3355 if (currentRight <= center) { 3356 currentDistance = center - currentRight; 3357 } else if (currentLeft >= center) { 3358 currentDistance = currentLeft - center; 3359 } 3360 3361 int neighborDistance = 0; 3362 if (neighborRight <= center) { 3363 neighborDistance = center - neighborRight; 3364 } else if (neighborLeft >= center) { 3365 neighborDistance = neighborLeft - center; 3366 } 3367 if (neighborDistance < currentDistance) { 3368 downDistanceMin = distance; 3369 downEvent = neighbor; 3370 } 3371 } 3372 } 3373 } 3374 3375 if (neighborLeft >= right) { 3376 // This neighbor is entirely to the right of me. 3377 // Take the closest neighbor in the y direction. 3378 int center = (top + bottom) / 2; 3379 int distance = 0; 3380 int neighborBottom = (int) neighbor.bottom; 3381 int neighborTop = (int) neighbor.top; 3382 if (neighborBottom <= center) { 3383 distance = center - neighborBottom; 3384 } else if (neighborTop >= center) { 3385 distance = neighborTop - center; 3386 } 3387 if (distance < rightDistanceMin) { 3388 rightDistanceMin = distance; 3389 rightEvent = neighbor; 3390 } else if (distance == rightDistanceMin) { 3391 // Pick the closest in the x direction 3392 int neighborDistance = neighborLeft - right; 3393 int currentDistance = (int) rightEvent.left - right; 3394 if (neighborDistance < currentDistance) { 3395 rightDistanceMin = distance; 3396 rightEvent = neighbor; 3397 } 3398 } 3399 } else if (neighborRight <= left) { 3400 // This neighbor is entirely to the left of me. 3401 // Take the closest neighbor in the y direction. 3402 int center = (top + bottom) / 2; 3403 int distance = 0; 3404 int neighborBottom = (int) neighbor.bottom; 3405 int neighborTop = (int) neighbor.top; 3406 if (neighborBottom <= center) { 3407 distance = center - neighborBottom; 3408 } else if (neighborTop >= center) { 3409 distance = neighborTop - center; 3410 } 3411 if (distance < leftDistanceMin) { 3412 leftDistanceMin = distance; 3413 leftEvent = neighbor; 3414 } else if (distance == leftDistanceMin) { 3415 // Pick the closest in the x direction 3416 int neighborDistance = left - neighborRight; 3417 int currentDistance = left - (int) leftEvent.right; 3418 if (neighborDistance < currentDistance) { 3419 leftDistanceMin = distance; 3420 leftEvent = neighbor; 3421 } 3422 } 3423 } 3424 } 3425 ev.nextUp = upEvent; 3426 ev.nextDown = downEvent; 3427 ev.nextLeft = leftEvent; 3428 ev.nextRight = rightEvent; 3429 } 3430 setSelectedEvent(startEvent); 3431 } 3432 3433 private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, 3434 int visibleTop, int visibleBot) { 3435 // Draw the Event Rect 3436 Rect r = mRect; 3437 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); 3438 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); 3439 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3440 r.right = (int) event.right; 3441 3442 int color; 3443 if (event == mClickedEvent) { 3444 color = mClickedColor; 3445 } else { 3446 color = event.color; 3447 } 3448 3449 switch (event.selfAttendeeStatus) { 3450 case Attendees.ATTENDEE_STATUS_INVITED: 3451 if (event != mClickedEvent) { 3452 p.setStyle(Style.STROKE); 3453 } 3454 break; 3455 case Attendees.ATTENDEE_STATUS_DECLINED: 3456 if (event != mClickedEvent) { 3457 color = Utils.getDeclinedColorFromColor(color); 3458 } 3459 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 3460 case Attendees.ATTENDEE_STATUS_ACCEPTED: 3461 case Attendees.ATTENDEE_STATUS_TENTATIVE: 3462 default: 3463 p.setStyle(Style.FILL_AND_STROKE); 3464 break; 3465 } 3466 3467 p.setAntiAlias(false); 3468 3469 int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); 3470 int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); 3471 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); 3472 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, 3473 visibleBot); 3474 r.left += floorHalfStroke; 3475 r.right -= ceilHalfStroke; 3476 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 3477 p.setColor(color); 3478 int alpha = p.getAlpha(); 3479 p.setAlpha(mEventsAlpha); 3480 canvas.drawRect(r, p); 3481 p.setAlpha(alpha); 3482 p.setStyle(Style.FILL); 3483 3484 // If this event is selected, then use the selection color 3485 if (mSelectedEvent == event && mClickedEvent != null) { 3486 boolean paintIt = false; 3487 color = 0; 3488 if (mSelectionMode == SELECTION_PRESSED) { 3489 // Also, remember the last selected event that we drew 3490 mPrevSelectedEvent = event; 3491 color = mPressedColor; 3492 paintIt = true; 3493 } else if (mSelectionMode == SELECTION_SELECTED) { 3494 // Also, remember the last selected event that we drew 3495 mPrevSelectedEvent = event; 3496 color = mPressedColor; 3497 paintIt = true; 3498 } 3499 3500 if (paintIt) { 3501 p.setColor(color); 3502 canvas.drawRect(r, p); 3503 } 3504 p.setAntiAlias(true); 3505 } 3506 3507 // Draw cal color square border 3508 // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; 3509 // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; 3510 // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; 3511 // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; 3512 // p.setColor(0xFFFFFFFF); 3513 // canvas.drawRect(r, p); 3514 3515 // Draw cal color 3516 // r.top++; 3517 // r.left++; 3518 // r.bottom--; 3519 // r.right--; 3520 // p.setColor(event.color); 3521 // canvas.drawRect(r, p); 3522 3523 // Setup rect for drawEventText which follows 3524 r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; 3525 r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; 3526 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3527 r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; 3528 return r; 3529 } 3530 3531 private final Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); 3532 3533 // Sanitize a string before passing it to drawText or else we get little 3534 // squares. For newlines and tabs before a comma, delete the character. 3535 // Otherwise, just replace them with a space. 3536 private String drawTextSanitizer(String string, int maxEventTextLen) { 3537 Matcher m = drawTextSanitizerFilter.matcher(string); 3538 string = m.replaceAll(","); 3539 3540 int len = string.length(); 3541 if (maxEventTextLen <= 0) { 3542 string = ""; 3543 len = 0; 3544 } else if (len > maxEventTextLen) { 3545 string = string.substring(0, maxEventTextLen); 3546 len = maxEventTextLen; 3547 } 3548 3549 return string.replace('\n', ' '); 3550 } 3551 3552 private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, 3553 int bottom, boolean center) { 3554 // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging 3555 3556 int width = rect.right - rect.left; 3557 int height = rect.bottom - rect.top; 3558 3559 // If the rectangle is too small for text, then return 3560 if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { 3561 return; 3562 } 3563 3564 int totalLineHeight = 0; 3565 int lineCount = eventLayout.getLineCount(); 3566 for (int i = 0; i < lineCount; i++) { 3567 int lineBottom = eventLayout.getLineBottom(i); 3568 if (lineBottom <= height) { 3569 totalLineHeight = lineBottom; 3570 } else { 3571 break; 3572 } 3573 } 3574 3575 if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { 3576 return; 3577 } 3578 3579 // Use a StaticLayout to format the string. 3580 canvas.save(); 3581 // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); 3582 int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0; 3583 canvas.translate(rect.left, rect.top + padding); 3584 rect.left = 0; 3585 rect.right = width; 3586 rect.top = 0; 3587 rect.bottom = totalLineHeight; 3588 3589 // There's a bug somewhere. If this rect is outside of a previous 3590 // cliprect, this becomes a no-op. What happens is that the text draw 3591 // past the event rect. The current fix is to not draw the staticLayout 3592 // at all if it is completely out of bound. 3593 canvas.clipRect(rect); 3594 eventLayout.draw(canvas); 3595 canvas.restore(); 3596 } 3597 3598 // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it 3599 // doesn't work well with hardware acceleration 3600 // private void drawEmptyRect(Canvas canvas, Rect r, int color) { 3601 // int linesIndex = 0; 3602 // mLines[linesIndex++] = r.left; 3603 // mLines[linesIndex++] = r.top; 3604 // mLines[linesIndex++] = r.right; 3605 // mLines[linesIndex++] = r.top; 3606 // 3607 // mLines[linesIndex++] = r.left; 3608 // mLines[linesIndex++] = r.bottom; 3609 // mLines[linesIndex++] = r.right; 3610 // mLines[linesIndex++] = r.bottom; 3611 // 3612 // mLines[linesIndex++] = r.left; 3613 // mLines[linesIndex++] = r.top; 3614 // mLines[linesIndex++] = r.left; 3615 // mLines[linesIndex++] = r.bottom; 3616 // 3617 // mLines[linesIndex++] = r.right; 3618 // mLines[linesIndex++] = r.top; 3619 // mLines[linesIndex++] = r.right; 3620 // mLines[linesIndex++] = r.bottom; 3621 // mPaint.setColor(color); 3622 // canvas.drawLines(mLines, 0, linesIndex, mPaint); 3623 // } 3624 3625 private void updateEventDetails() { 3626 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN 3627 || mSelectionMode == SELECTION_LONGPRESS) { 3628 mPopup.dismiss(); 3629 return; 3630 } 3631 if (mLastPopupEventID == mSelectedEvent.id) { 3632 return; 3633 } 3634 3635 mLastPopupEventID = mSelectedEvent.id; 3636 3637 // Remove any outstanding callbacks to dismiss the popup. 3638 mHandler.removeCallbacks(mDismissPopup); 3639 3640 Event event = mSelectedEvent; 3641 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); 3642 titleView.setText(event.title); 3643 3644 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); 3645 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); 3646 3647 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); 3648 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); 3649 3650 int flags; 3651 if (event.allDay) { 3652 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE 3653 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; 3654 } else { 3655 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 3656 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL 3657 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3658 } 3659 if (DateFormat.is24HourFormat(mContext)) { 3660 flags |= DateUtils.FORMAT_24HOUR; 3661 } 3662 String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis, 3663 flags); 3664 TextView timeView = (TextView) mPopupView.findViewById(R.id.time); 3665 timeView.setText(timeRange); 3666 3667 TextView whereView = (TextView) mPopupView.findViewById(R.id.where); 3668 final boolean empty = TextUtils.isEmpty(event.location); 3669 whereView.setVisibility(empty ? View.GONE : View.VISIBLE); 3670 if (!empty) whereView.setText(event.location); 3671 3672 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); 3673 mHandler.postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); 3674 } 3675 3676 // The following routines are called from the parent activity when certain 3677 // touch events occur. 3678 private void doDown(MotionEvent ev) { 3679 mTouchMode = TOUCH_MODE_DOWN; 3680 mViewStartX = 0; 3681 mOnFlingCalled = false; 3682 mHandler.removeCallbacks(mContinueScroll); 3683 int x = (int) ev.getX(); 3684 int y = (int) ev.getY(); 3685 3686 // Save selection information: we use setSelectionFromPosition to find the selected event 3687 // in order to show the "clicked" color. But since it is also setting the selected info 3688 // for new events, we need to restore the old info after calling the function. 3689 Event oldSelectedEvent = mSelectedEvent; 3690 int oldSelectionDay = mSelectionDay; 3691 int oldSelectionHour = mSelectionHour; 3692 if (setSelectionFromPosition(x, y, false)) { 3693 // If a time was selected (a blue selection box is visible) and the click location 3694 // is in the selected time, do not show a click on an event to prevent a situation 3695 // of both a selection and an event are clicked when they overlap. 3696 boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN) 3697 && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour; 3698 if (!pressedSelected && mSelectedEvent != null) { 3699 mSavedClickedEvent = mSelectedEvent; 3700 mDownTouchTime = System.currentTimeMillis(); 3701 postDelayed (mSetClick,mOnDownDelay); 3702 } else { 3703 eventClickCleanup(); 3704 } 3705 } 3706 mSelectedEvent = oldSelectedEvent; 3707 mSelectionDay = oldSelectionDay; 3708 mSelectionHour = oldSelectionHour; 3709 invalidate(); 3710 } 3711 3712 // Kicks off all the animations when the expand allday area is tapped 3713 private void doExpandAllDayClick() { 3714 mShowAllAllDayEvents = !mShowAllAllDayEvents; 3715 3716 ObjectAnimator.setFrameDelay(0); 3717 3718 // Determine the starting height 3719 if (mAnimateDayHeight == 0) { 3720 mAnimateDayHeight = mShowAllAllDayEvents ? 3721 mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; 3722 } 3723 // Cancel current animations 3724 mCancellingAnimations = true; 3725 if (mAlldayAnimator != null) { 3726 mAlldayAnimator.cancel(); 3727 } 3728 if (mAlldayEventAnimator != null) { 3729 mAlldayEventAnimator.cancel(); 3730 } 3731 if (mMoreAlldayEventsAnimator != null) { 3732 mMoreAlldayEventsAnimator.cancel(); 3733 } 3734 mCancellingAnimations = false; 3735 // get new animators 3736 mAlldayAnimator = getAllDayAnimator(); 3737 mAlldayEventAnimator = getAllDayEventAnimator(); 3738 mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, 3739 "moreAllDayEventsTextAlpha", 3740 mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, 3741 mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); 3742 3743 // Set up delays and start the animators 3744 mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3745 mAlldayAnimator.start(); 3746 mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); 3747 mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); 3748 mMoreAlldayEventsAnimator.start(); 3749 if (mAlldayEventAnimator != null) { 3750 // This is the only animator that can return null, so check it 3751 mAlldayEventAnimator 3752 .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3753 mAlldayEventAnimator.start(); 3754 } 3755 } 3756 3757 /** 3758 * Figures out the initial heights for allDay events and space when 3759 * a view is being set up. 3760 */ 3761 public void initAllDayHeights() { 3762 if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { 3763 return; 3764 } 3765 if (mShowAllAllDayEvents) { 3766 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3767 maxADHeight = Math.min(maxADHeight, 3768 (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3769 mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; 3770 } else { 3771 mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3772 } 3773 } 3774 3775 // Sets up an animator for changing the height of allday events 3776 private ObjectAnimator getAllDayEventAnimator() { 3777 // First calculate the absolute max height 3778 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3779 // Now expand to fit but not beyond the absolute max 3780 maxADHeight = 3781 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3782 // calculate the height of individual events in order to fit 3783 int fitHeight = maxADHeight / mMaxAlldayEvents; 3784 int currentHeight = mAnimateDayEventHeight; 3785 int desiredHeight = 3786 mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3787 // if there's nothing to animate just return 3788 if (currentHeight == desiredHeight) { 3789 return null; 3790 } 3791 3792 // Set up the animator with the calculated values 3793 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", 3794 currentHeight, desiredHeight); 3795 animator.setDuration(ANIMATION_DURATION); 3796 return animator; 3797 } 3798 3799 // Sets up an animator for changing the height of the allday area 3800 private ObjectAnimator getAllDayAnimator() { 3801 // Calculate the absolute max height 3802 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3803 // Find the desired height but don't exceed abs max 3804 maxADHeight = 3805 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3806 // calculate the current and desired heights 3807 int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; 3808 int desiredHeight = mShowAllAllDayEvents ? maxADHeight : 3809 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); 3810 3811 // Set up the animator with the calculated values 3812 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", 3813 currentHeight, desiredHeight); 3814 animator.setDuration(ANIMATION_DURATION); 3815 3816 animator.addListener(new AnimatorListenerAdapter() { 3817 @Override 3818 public void onAnimationEnd(Animator animation) { 3819 if (!mCancellingAnimations) { 3820 // when finished, set this to 0 to signify not animating 3821 mAnimateDayHeight = 0; 3822 mUseExpandIcon = !mShowAllAllDayEvents; 3823 } 3824 mRemeasure = true; 3825 invalidate(); 3826 } 3827 }); 3828 return animator; 3829 } 3830 3831 // setter for the 'box +n' alpha text used by the animator 3832 public void setMoreAllDayEventsTextAlpha(int alpha) { 3833 mMoreAlldayEventsTextAlpha = alpha; 3834 invalidate(); 3835 } 3836 3837 // setter for the height of the allday area used by the animator 3838 public void setAnimateDayHeight(int height) { 3839 mAnimateDayHeight = height; 3840 mRemeasure = true; 3841 invalidate(); 3842 } 3843 3844 // setter for the height of allday events used by the animator 3845 public void setAnimateDayEventHeight(int height) { 3846 mAnimateDayEventHeight = height; 3847 mRemeasure = true; 3848 invalidate(); 3849 } 3850 3851 private void doSingleTapUp(MotionEvent ev) { 3852 if (!mHandleActionUp || mScrolling) { 3853 return; 3854 } 3855 3856 int x = (int) ev.getX(); 3857 int y = (int) ev.getY(); 3858 int selectedDay = mSelectionDay; 3859 int selectedHour = mSelectionHour; 3860 3861 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 3862 // check if the tap was in the allday expansion area 3863 int bottom = mFirstCell; 3864 if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) 3865 || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && 3866 y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { 3867 doExpandAllDayClick(); 3868 return; 3869 } 3870 } 3871 3872 boolean validPosition = setSelectionFromPosition(x, y, false); 3873 if (!validPosition) { 3874 if (y < DAY_HEADER_HEIGHT) { 3875 Time selectedTime = new Time(mBaseDate); 3876 selectedTime.setJulianDay(mSelectionDay); 3877 selectedTime.hour = mSelectionHour; 3878 selectedTime.normalize(true /* ignore isDst */); 3879 mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, 3880 ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); 3881 } 3882 return; 3883 } 3884 3885 boolean hasSelection = mSelectionMode != SELECTION_HIDDEN; 3886 boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) 3887 && selectedDay == mSelectionDay && selectedHour == mSelectionHour; 3888 3889 if (pressedSelected && mSavedClickedEvent == null) { 3890 // If the tap is on an already selected hour slot, then create a new 3891 // event 3892 long extraLong = 0; 3893 if (mSelectionAllday) { 3894 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 3895 } 3896 mSelectionMode = SELECTION_SELECTED; 3897 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 3898 getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), 3899 extraLong, -1); 3900 } else if (mSelectedEvent != null) { 3901 // If the tap is on an event, launch the "View event" view 3902 if (mIsAccessibilityEnabled) { 3903 mAccessibilityMgr.interrupt(); 3904 } 3905 3906 mSelectionMode = SELECTION_HIDDEN; 3907 3908 int yLocation = 3909 (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2); 3910 // Y location is affected by the position of the event in the scrolling 3911 // view (mViewStartY) and the presence of all day events (mFirstCell) 3912 if (!mSelectedEvent.allDay) { 3913 yLocation += (mFirstCell - mViewStartY); 3914 } 3915 mClickedYLocation = yLocation; 3916 long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) - 3917 (System.currentTimeMillis() - mDownTouchTime); 3918 if (clearDelay > 0) { 3919 this.postDelayed(mClearClick, clearDelay); 3920 } else { 3921 this.post(mClearClick); 3922 } 3923 } else { 3924 // Select time 3925 Time startTime = new Time(mBaseDate); 3926 startTime.setJulianDay(mSelectionDay); 3927 startTime.hour = mSelectionHour; 3928 startTime.normalize(true /* ignore isDst */); 3929 3930 Time endTime = new Time(startTime); 3931 endTime.hour++; 3932 3933 mSelectionMode = SELECTION_SELECTED; 3934 mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, 3935 CalendarController.EXTRA_GOTO_TIME, null, null); 3936 } 3937 invalidate(); 3938 } 3939 3940 private void doLongPress(MotionEvent ev) { 3941 eventClickCleanup(); 3942 if (mScrolling) { 3943 return; 3944 } 3945 3946 // Scale gesture in progress 3947 if (mStartingSpanY != 0) { 3948 return; 3949 } 3950 3951 int x = (int) ev.getX(); 3952 int y = (int) ev.getY(); 3953 3954 boolean validPosition = setSelectionFromPosition(x, y, false); 3955 if (!validPosition) { 3956 // return if the touch wasn't on an area of concern 3957 return; 3958 } 3959 3960 mSelectionMode = SELECTION_LONGPRESS; 3961 invalidate(); 3962 performLongClick(); 3963 } 3964 3965 private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { 3966 cancelAnimation(); 3967 if (mStartingScroll) { 3968 mInitialScrollX = 0; 3969 mInitialScrollY = 0; 3970 mStartingScroll = false; 3971 } 3972 3973 mInitialScrollX += deltaX; 3974 mInitialScrollY += deltaY; 3975 int distanceX = (int) mInitialScrollX; 3976 int distanceY = (int) mInitialScrollY; 3977 3978 final float focusY = getAverageY(e2); 3979 if (mRecalCenterHour) { 3980 // Calculate the hour that correspond to the average of the Y touch points 3981 mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) 3982 / (mCellHeight + DAY_GAP); 3983 mRecalCenterHour = false; 3984 } 3985 3986 // If we haven't figured out the predominant scroll direction yet, 3987 // then do it now. 3988 if (mTouchMode == TOUCH_MODE_DOWN) { 3989 int absDistanceX = Math.abs(distanceX); 3990 int absDistanceY = Math.abs(distanceY); 3991 mScrollStartY = mViewStartY; 3992 mPreviousDirection = 0; 3993 3994 if (absDistanceX > absDistanceY) { 3995 int slopFactor = mScaleGestureDetector.isInProgress() ? 20 : 2; 3996 if (absDistanceX > mScaledPagingTouchSlop * slopFactor) { 3997 mTouchMode = TOUCH_MODE_HSCROLL; 3998 mViewStartX = distanceX; 3999 initNextView(-mViewStartX); 4000 } 4001 } else { 4002 mTouchMode = TOUCH_MODE_VSCROLL; 4003 } 4004 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 4005 // We are already scrolling horizontally, so check if we 4006 // changed the direction of scrolling so that the other week 4007 // is now visible. 4008 mViewStartX = distanceX; 4009 if (distanceX != 0) { 4010 int direction = (distanceX > 0) ? 1 : -1; 4011 if (direction != mPreviousDirection) { 4012 // The user has switched the direction of scrolling 4013 // so re-init the next view 4014 initNextView(-mViewStartX); 4015 mPreviousDirection = direction; 4016 } 4017 } 4018 } 4019 4020 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { 4021 // Calculate the top of the visible region in the calendar grid. 4022 // Increasing/decrease this will scroll the calendar grid up/down. 4023 mViewStartY = (int) ((mGestureCenterHour * (mCellHeight + DAY_GAP)) 4024 - focusY + DAY_HEADER_HEIGHT + mAlldayHeight); 4025 4026 // If dragging while already at the end, do a glow 4027 final int pulledToY = (int) (mScrollStartY + deltaY); 4028 if (pulledToY < 0) { 4029 mEdgeEffectTop.onPull(deltaY / mViewHeight); 4030 if (!mEdgeEffectBottom.isFinished()) { 4031 mEdgeEffectBottom.onRelease(); 4032 } 4033 } else if (pulledToY > mMaxViewStartY) { 4034 mEdgeEffectBottom.onPull(deltaY / mViewHeight); 4035 if (!mEdgeEffectTop.isFinished()) { 4036 mEdgeEffectTop.onRelease(); 4037 } 4038 } 4039 4040 if (mViewStartY < 0) { 4041 mViewStartY = 0; 4042 mRecalCenterHour = true; 4043 } else if (mViewStartY > mMaxViewStartY) { 4044 mViewStartY = mMaxViewStartY; 4045 mRecalCenterHour = true; 4046 } 4047 if (mRecalCenterHour) { 4048 // Calculate the hour that correspond to the average of the Y touch points 4049 mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) 4050 / (mCellHeight + DAY_GAP); 4051 mRecalCenterHour = false; 4052 } 4053 computeFirstHour(); 4054 } 4055 4056 mScrolling = true; 4057 4058 mSelectionMode = SELECTION_HIDDEN; 4059 invalidate(); 4060 } 4061 4062 private float getAverageY(MotionEvent me) { 4063 int count = me.getPointerCount(); 4064 float focusY = 0; 4065 for (int i = 0; i < count; i++) { 4066 focusY += me.getY(i); 4067 } 4068 focusY /= count; 4069 return focusY; 4070 } 4071 4072 private void cancelAnimation() { 4073 Animation in = mViewSwitcher.getInAnimation(); 4074 if (in != null) { 4075 // cancel() doesn't terminate cleanly. 4076 in.scaleCurrentDuration(0); 4077 } 4078 Animation out = mViewSwitcher.getOutAnimation(); 4079 if (out != null) { 4080 // cancel() doesn't terminate cleanly. 4081 out.scaleCurrentDuration(0); 4082 } 4083 } 4084 4085 private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 4086 cancelAnimation(); 4087 4088 mSelectionMode = SELECTION_HIDDEN; 4089 eventClickCleanup(); 4090 4091 mOnFlingCalled = true; 4092 4093 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 4094 // Horizontal fling. 4095 // initNextView(deltaX); 4096 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4097 if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); 4098 int deltaX = (int) e2.getX() - (int) e1.getX(); 4099 switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); 4100 mViewStartX = 0; 4101 return; 4102 } 4103 4104 if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { 4105 if (DEBUG) Log.d(TAG, "doFling: no fling"); 4106 return; 4107 } 4108 4109 // Vertical fling. 4110 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4111 mViewStartX = 0; 4112 4113 if (DEBUG) { 4114 Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); 4115 } 4116 4117 // Continue scrolling vertically 4118 mScrolling = true; 4119 mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, 4120 (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, 4121 mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); 4122 4123 // When flinging down, show a glow when it hits the end only if it 4124 // wasn't started at the top 4125 if (velocityY > 0 && mViewStartY != 0) { 4126 mCallEdgeEffectOnAbsorb = true; 4127 } 4128 // When flinging up, show a glow when it hits the end only if it wasn't 4129 // started at the bottom 4130 else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { 4131 mCallEdgeEffectOnAbsorb = true; 4132 } 4133 mHandler.post(mContinueScroll); 4134 } 4135 4136 private boolean initNextView(int deltaX) { 4137 // Change the view to the previous day or week 4138 DayView view = (DayView) mViewSwitcher.getNextView(); 4139 Time date = view.mBaseDate; 4140 date.set(mBaseDate); 4141 boolean switchForward; 4142 if (deltaX > 0) { 4143 date.monthDay -= mNumDays; 4144 view.setSelectedDay(mSelectionDay - mNumDays); 4145 switchForward = false; 4146 } else { 4147 date.monthDay += mNumDays; 4148 view.setSelectedDay(mSelectionDay + mNumDays); 4149 switchForward = true; 4150 } 4151 date.normalize(true /* ignore isDst */); 4152 initView(view); 4153 view.layout(getLeft(), getTop(), getRight(), getBottom()); 4154 view.reloadEvents(); 4155 return switchForward; 4156 } 4157 4158 // ScaleGestureDetector.OnScaleGestureListener 4159 public boolean onScaleBegin(ScaleGestureDetector detector) { 4160 mHandleActionUp = false; 4161 float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 4162 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); 4163 4164 mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 4165 mCellHeightBeforeScaleGesture = mCellHeight; 4166 4167 if (DEBUG_SCALING) { 4168 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 4169 Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour 4170 + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY 4171 + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 4172 } 4173 4174 return true; 4175 } 4176 4177 // ScaleGestureDetector.OnScaleGestureListener 4178 public boolean onScale(ScaleGestureDetector detector) { 4179 float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 4180 4181 mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); 4182 4183 if (mCellHeight < mMinCellHeight) { 4184 // If mStartingSpanY is too small, even a small increase in the 4185 // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT 4186 mStartingSpanY = spanY; 4187 mCellHeight = mMinCellHeight; 4188 mCellHeightBeforeScaleGesture = mMinCellHeight; 4189 } else if (mCellHeight > MAX_CELL_HEIGHT) { 4190 mStartingSpanY = spanY; 4191 mCellHeight = MAX_CELL_HEIGHT; 4192 mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; 4193 } 4194 4195 int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 4196 mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; 4197 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 4198 4199 if (DEBUG_SCALING) { 4200 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 4201 Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " 4202 + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" 4203 + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 4204 } 4205 4206 if (mViewStartY < 0) { 4207 mViewStartY = 0; 4208 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 4209 / (float) (mCellHeight + DAY_GAP); 4210 } else if (mViewStartY > mMaxViewStartY) { 4211 mViewStartY = mMaxViewStartY; 4212 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 4213 / (float) (mCellHeight + DAY_GAP); 4214 } 4215 computeFirstHour(); 4216 4217 mRemeasure = true; 4218 invalidate(); 4219 return true; 4220 } 4221 4222 // ScaleGestureDetector.OnScaleGestureListener 4223 public void onScaleEnd(ScaleGestureDetector detector) { 4224 mScrollStartY = mViewStartY; 4225 mInitialScrollY = 0; 4226 mInitialScrollX = 0; 4227 mStartingSpanY = 0; 4228 } 4229 4230 @Override 4231 public boolean onTouchEvent(MotionEvent ev) { 4232 int action = ev.getAction(); 4233 if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); 4234 4235 if ((ev.getActionMasked() == MotionEvent.ACTION_DOWN) || 4236 (ev.getActionMasked() == MotionEvent.ACTION_UP) || 4237 (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) || 4238 (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN)) { 4239 mRecalCenterHour = true; 4240 } 4241 4242 if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { 4243 mScaleGestureDetector.onTouchEvent(ev); 4244 } 4245 4246 switch (action) { 4247 case MotionEvent.ACTION_DOWN: 4248 mStartingScroll = true; 4249 if (DEBUG) { 4250 Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" 4251 + ev.getPointerCount()); 4252 } 4253 4254 int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 4255 if (ev.getY() < bottom) { 4256 mTouchStartedInAlldayArea = true; 4257 } else { 4258 mTouchStartedInAlldayArea = false; 4259 } 4260 mHandleActionUp = true; 4261 mGestureDetector.onTouchEvent(ev); 4262 return true; 4263 4264 case MotionEvent.ACTION_MOVE: 4265 if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); 4266 mGestureDetector.onTouchEvent(ev); 4267 return true; 4268 4269 case MotionEvent.ACTION_UP: 4270 if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); 4271 mEdgeEffectTop.onRelease(); 4272 mEdgeEffectBottom.onRelease(); 4273 mStartingScroll = false; 4274 mGestureDetector.onTouchEvent(ev); 4275 if (!mHandleActionUp) { 4276 mHandleActionUp = true; 4277 mViewStartX = 0; 4278 invalidate(); 4279 return true; 4280 } 4281 4282 if (mOnFlingCalled) { 4283 return true; 4284 } 4285 4286 // If we were scrolling, then reset the selected hour so that it 4287 // is visible. 4288 if (mScrolling) { 4289 mScrolling = false; 4290 resetSelectedHour(); 4291 invalidate(); 4292 } 4293 4294 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 4295 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4296 if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { 4297 // The user has gone beyond the threshold so switch views 4298 if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); 4299 switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); 4300 mViewStartX = 0; 4301 return true; 4302 } else { 4303 // Not beyond the threshold so invalidate which will cause 4304 // the view to snap back. Also call recalc() to ensure 4305 // that we have the correct starting date and title. 4306 if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); 4307 recalc(); 4308 invalidate(); 4309 mViewStartX = 0; 4310 } 4311 } 4312 4313 return true; 4314 4315 // This case isn't expected to happen. 4316 case MotionEvent.ACTION_CANCEL: 4317 if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); 4318 mGestureDetector.onTouchEvent(ev); 4319 mScrolling = false; 4320 resetSelectedHour(); 4321 return true; 4322 4323 default: 4324 if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); 4325 if (mGestureDetector.onTouchEvent(ev)) { 4326 return true; 4327 } 4328 return super.onTouchEvent(ev); 4329 } 4330 } 4331 4332 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 4333 MenuItem item; 4334 4335 // If the trackball is held down, then the context menu pops up and 4336 // we never get onKeyUp() for the long-press. So check for it here 4337 // and change the selection to the long-press state. 4338 if (mSelectionMode != SELECTION_LONGPRESS) { 4339 mSelectionMode = SELECTION_LONGPRESS; 4340 invalidate(); 4341 } 4342 4343 final long startMillis = getSelectedTimeInMillis(); 4344 int flags = DateUtils.FORMAT_SHOW_TIME 4345 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT 4346 | DateUtils.FORMAT_SHOW_WEEKDAY; 4347 final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); 4348 menu.setHeaderTitle(title); 4349 4350 int numSelectedEvents = mSelectedEvents.size(); 4351 if (mNumDays == 1) { 4352 // Day view. 4353 4354 // If there is a selected event, then allow it to be viewed and 4355 // edited. 4356 if (numSelectedEvents >= 1) { 4357 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4358 item.setOnMenuItemClickListener(mContextMenuHandler); 4359 item.setIcon(android.R.drawable.ic_menu_info_details); 4360 4361 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4362 if (accessLevel == ACCESS_LEVEL_EDIT) { 4363 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4364 item.setOnMenuItemClickListener(mContextMenuHandler); 4365 item.setIcon(android.R.drawable.ic_menu_edit); 4366 item.setAlphabeticShortcut('e'); 4367 } 4368 4369 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4370 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4371 item.setOnMenuItemClickListener(mContextMenuHandler); 4372 item.setIcon(android.R.drawable.ic_menu_delete); 4373 } 4374 4375 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4376 item.setOnMenuItemClickListener(mContextMenuHandler); 4377 item.setIcon(android.R.drawable.ic_menu_add); 4378 item.setAlphabeticShortcut('n'); 4379 } else { 4380 // Otherwise, if the user long-pressed on a blank hour, allow 4381 // them to create an event. They can also do this by tapping. 4382 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4383 item.setOnMenuItemClickListener(mContextMenuHandler); 4384 item.setIcon(android.R.drawable.ic_menu_add); 4385 item.setAlphabeticShortcut('n'); 4386 } 4387 } else { 4388 // Week view. 4389 4390 // If there is a selected event, then allow it to be viewed and 4391 // edited. 4392 if (numSelectedEvents >= 1) { 4393 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4394 item.setOnMenuItemClickListener(mContextMenuHandler); 4395 item.setIcon(android.R.drawable.ic_menu_info_details); 4396 4397 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4398 if (accessLevel == ACCESS_LEVEL_EDIT) { 4399 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4400 item.setOnMenuItemClickListener(mContextMenuHandler); 4401 item.setIcon(android.R.drawable.ic_menu_edit); 4402 item.setAlphabeticShortcut('e'); 4403 } 4404 4405 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4406 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4407 item.setOnMenuItemClickListener(mContextMenuHandler); 4408 item.setIcon(android.R.drawable.ic_menu_delete); 4409 } 4410 } 4411 4412 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4413 item.setOnMenuItemClickListener(mContextMenuHandler); 4414 item.setIcon(android.R.drawable.ic_menu_add); 4415 item.setAlphabeticShortcut('n'); 4416 4417 item = menu.add(0, MENU_DAY, 0, R.string.show_day_view); 4418 item.setOnMenuItemClickListener(mContextMenuHandler); 4419 item.setIcon(android.R.drawable.ic_menu_day); 4420 item.setAlphabeticShortcut('d'); 4421 } 4422 4423 mPopup.dismiss(); 4424 } 4425 4426 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { 4427 public boolean onMenuItemClick(MenuItem item) { 4428 switch (item.getItemId()) { 4429 case MENU_EVENT_VIEW: { 4430 if (mSelectedEvent != null) { 4431 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, 4432 mSelectedEvent.id, mSelectedEvent.startMillis, 4433 mSelectedEvent.endMillis, 0, 0, -1); 4434 } 4435 break; 4436 } 4437 case MENU_EVENT_EDIT: { 4438 if (mSelectedEvent != null) { 4439 mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, 4440 mSelectedEvent.id, mSelectedEvent.startMillis, 4441 mSelectedEvent.endMillis, 0, 0, -1); 4442 } 4443 break; 4444 } 4445 case MENU_DAY: { 4446 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4447 ViewType.DAY); 4448 break; 4449 } 4450 case MENU_AGENDA: { 4451 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4452 ViewType.AGENDA); 4453 break; 4454 } 4455 case MENU_EVENT_CREATE: { 4456 long startMillis = getSelectedTimeInMillis(); 4457 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 4458 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 4459 startMillis, endMillis, 0, 0, -1); 4460 break; 4461 } 4462 case MENU_EVENT_DELETE: { 4463 if (mSelectedEvent != null) { 4464 Event selectedEvent = mSelectedEvent; 4465 long begin = selectedEvent.startMillis; 4466 long end = selectedEvent.endMillis; 4467 long id = selectedEvent.id; 4468 mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin, 4469 end, 0, 0, -1); 4470 } 4471 break; 4472 } 4473 default: { 4474 return false; 4475 } 4476 } 4477 return true; 4478 } 4479 } 4480 4481 private static int getEventAccessLevel(Context context, Event e) { 4482 ContentResolver cr = context.getContentResolver(); 4483 4484 int accessLevel = Calendars.CAL_ACCESS_NONE; 4485 4486 // Get the calendar id for this event 4487 Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id), 4488 new String[] { Events.CALENDAR_ID }, 4489 null /* selection */, 4490 null /* selectionArgs */, 4491 null /* sort */); 4492 4493 if (cursor == null) { 4494 return ACCESS_LEVEL_NONE; 4495 } 4496 4497 if (cursor.getCount() == 0) { 4498 cursor.close(); 4499 return ACCESS_LEVEL_NONE; 4500 } 4501 4502 cursor.moveToFirst(); 4503 long calId = cursor.getLong(0); 4504 cursor.close(); 4505 4506 Uri uri = Calendars.CONTENT_URI; 4507 String where = String.format(CALENDARS_WHERE, calId); 4508 cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null); 4509 4510 String calendarOwnerAccount = null; 4511 if (cursor != null) { 4512 cursor.moveToFirst(); 4513 accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); 4514 calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 4515 cursor.close(); 4516 } 4517 4518 if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) { 4519 return ACCESS_LEVEL_NONE; 4520 } 4521 4522 if (e.guestsCanModify) { 4523 return ACCESS_LEVEL_EDIT; 4524 } 4525 4526 if (!TextUtils.isEmpty(calendarOwnerAccount) 4527 && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { 4528 return ACCESS_LEVEL_EDIT; 4529 } 4530 4531 return ACCESS_LEVEL_DELETE; 4532 } 4533 4534 /** 4535 * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. 4536 * If the touch position is not within the displayed grid, then this 4537 * method returns false. 4538 * 4539 * @param x the x position of the touch 4540 * @param y the y position of the touch 4541 * @param keepOldSelection - do not change the selection info (used for invoking accessibility 4542 * messages) 4543 * @return true if the touch position is valid 4544 */ 4545 private boolean setSelectionFromPosition(int x, final int y, boolean keepOldSelection) { 4546 4547 Event savedEvent = null; 4548 int savedDay = 0; 4549 int savedHour = 0; 4550 boolean savedAllDay = false; 4551 if (keepOldSelection) { 4552 // Store selection info and restore it at the end. This way, we can invoke the 4553 // right accessibility message without affecting the selection. 4554 savedEvent = mSelectedEvent; 4555 savedDay = mSelectionDay; 4556 savedHour = mSelectionHour; 4557 savedAllDay = mSelectionAllday; 4558 } 4559 if (x < mHoursWidth) { 4560 x = mHoursWidth; 4561 } 4562 4563 int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); 4564 if (day >= mNumDays) { 4565 day = mNumDays - 1; 4566 } 4567 day += mFirstJulianDay; 4568 setSelectedDay(day); 4569 4570 if (y < DAY_HEADER_HEIGHT) { 4571 sendAccessibilityEventAsNeeded(false); 4572 return false; 4573 } 4574 4575 setSelectedHour(mFirstHour); /* First fully visible hour */ 4576 4577 if (y < mFirstCell) { 4578 mSelectionAllday = true; 4579 } else { 4580 // y is now offset from top of the scrollable region 4581 int adjustedY = y - mFirstCell; 4582 4583 if (adjustedY < mFirstHourOffset) { 4584 setSelectedHour(mSelectionHour - 1); /* In the partially visible hour */ 4585 } else { 4586 setSelectedHour(mSelectionHour + 4587 (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP)); 4588 } 4589 4590 mSelectionAllday = false; 4591 } 4592 4593 findSelectedEvent(x, y); 4594 4595 // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: " 4596 // + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " 4597 // + mFirstHourOffset); 4598 // if (mSelectedEvent != null) { 4599 // Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " 4600 // + mSelectedEvent.title); 4601 // for (Event ev : mSelectedEvents) { 4602 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 4603 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 4604 // String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags); 4605 // 4606 // Log.i("Cal", " " + timeRange + " " + ev.title); 4607 // } 4608 // } 4609 sendAccessibilityEventAsNeeded(true); 4610 4611 // Restore old values 4612 if (keepOldSelection) { 4613 mSelectedEvent = savedEvent; 4614 mSelectionDay = savedDay; 4615 mSelectionHour = savedHour; 4616 mSelectionAllday = savedAllDay; 4617 } 4618 return true; 4619 } 4620 4621 private void findSelectedEvent(int x, int y) { 4622 int date = mSelectionDay; 4623 int cellWidth = mCellWidth; 4624 ArrayList<Event> events = mEvents; 4625 int numEvents = events.size(); 4626 int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay); 4627 int top = 0; 4628 setSelectedEvent(null); 4629 4630 mSelectedEvents.clear(); 4631 if (mSelectionAllday) { 4632 float yDistance; 4633 float minYdistance = 10000.0f; // any large number 4634 Event closestEvent = null; 4635 float drawHeight = mAlldayHeight; 4636 int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 4637 int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount; 4638 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 4639 // Leave a gap for the 'box +n' text 4640 maxUnexpandedColumn--; 4641 } 4642 events = mAllDayEvents; 4643 numEvents = events.size(); 4644 for (int i = 0; i < numEvents; i++) { 4645 Event event = events.get(i); 4646 if (!event.drawAsAllday() || 4647 (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { 4648 // Don't check non-allday events or events that aren't shown 4649 continue; 4650 } 4651 4652 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { 4653 float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents 4654 : mMaxUnexpandedAlldayEventCount; 4655 float height = drawHeight / numRectangles; 4656 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 4657 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 4658 } 4659 float eventTop = yOffset + height * event.getColumn(); 4660 float eventBottom = eventTop + height; 4661 if (eventTop < y && eventBottom > y) { 4662 // If the touch is inside the event rectangle, then 4663 // add the event. 4664 mSelectedEvents.add(event); 4665 closestEvent = event; 4666 break; 4667 } else { 4668 // Find the closest event 4669 if (eventTop >= y) { 4670 yDistance = eventTop - y; 4671 } else { 4672 yDistance = y - eventBottom; 4673 } 4674 if (yDistance < minYdistance) { 4675 minYdistance = yDistance; 4676 closestEvent = event; 4677 } 4678 } 4679 } 4680 } 4681 setSelectedEvent(closestEvent); 4682 return; 4683 } 4684 4685 // Adjust y for the scrollable bitmap 4686 y += mViewStartY - mFirstCell; 4687 4688 // Use a region around (x,y) for the selection region 4689 Rect region = mRect; 4690 region.left = x - 10; 4691 region.right = x + 10; 4692 region.top = y - 10; 4693 region.bottom = y + 10; 4694 4695 EventGeometry geometry = mEventGeometry; 4696 4697 for (int i = 0; i < numEvents; i++) { 4698 Event event = events.get(i); 4699 // Compute the event rectangle. 4700 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 4701 continue; 4702 } 4703 4704 // If the event intersects the selection region, then add it to 4705 // mSelectedEvents. 4706 if (geometry.eventIntersectsSelection(event, region)) { 4707 mSelectedEvents.add(event); 4708 } 4709 } 4710 4711 // If there are any events in the selected region, then assign the 4712 // closest one to mSelectedEvent. 4713 if (mSelectedEvents.size() > 0) { 4714 int len = mSelectedEvents.size(); 4715 Event closestEvent = null; 4716 float minDist = mViewWidth + mViewHeight; // some large distance 4717 for (int index = 0; index < len; index++) { 4718 Event ev = mSelectedEvents.get(index); 4719 float dist = geometry.pointToEvent(x, y, ev); 4720 if (dist < minDist) { 4721 minDist = dist; 4722 closestEvent = ev; 4723 } 4724 } 4725 setSelectedEvent(closestEvent); 4726 4727 // Keep the selected hour and day consistent with the selected 4728 // event. They could be different if we touched on an empty hour 4729 // slot very close to an event in the previous hour slot. In 4730 // that case we will select the nearby event. 4731 int startDay = mSelectedEvent.startDay; 4732 int endDay = mSelectedEvent.endDay; 4733 if (mSelectionDay < startDay) { 4734 setSelectedDay(startDay); 4735 } else if (mSelectionDay > endDay) { 4736 setSelectedDay(endDay); 4737 } 4738 4739 int startHour = mSelectedEvent.startTime / 60; 4740 int endHour; 4741 if (mSelectedEvent.startTime < mSelectedEvent.endTime) { 4742 endHour = (mSelectedEvent.endTime - 1) / 60; 4743 } else { 4744 endHour = mSelectedEvent.endTime / 60; 4745 } 4746 4747 if (mSelectionHour < startHour && mSelectionDay == startDay) { 4748 setSelectedHour(startHour); 4749 } else if (mSelectionHour > endHour && mSelectionDay == endDay) { 4750 setSelectedHour(endHour); 4751 } 4752 } 4753 } 4754 4755 // Encapsulates the code to continue the scrolling after the 4756 // finger is lifted. Instead of stopping the scroll immediately, 4757 // the scroll continues to "free spin" and gradually slows down. 4758 private class ContinueScroll implements Runnable { 4759 public void run() { 4760 mScrolling = mScrolling && mScroller.computeScrollOffset(); 4761 if (!mScrolling || mPaused) { 4762 resetSelectedHour(); 4763 invalidate(); 4764 return; 4765 } 4766 4767 mViewStartY = mScroller.getCurrY(); 4768 4769 if (mCallEdgeEffectOnAbsorb) { 4770 if (mViewStartY < 0) { 4771 mEdgeEffectTop.onAbsorb((int) mLastVelocity); 4772 mCallEdgeEffectOnAbsorb = false; 4773 } else if (mViewStartY > mMaxViewStartY) { 4774 mEdgeEffectBottom.onAbsorb((int) mLastVelocity); 4775 mCallEdgeEffectOnAbsorb = false; 4776 } 4777 mLastVelocity = mScroller.getCurrVelocity(); 4778 } 4779 4780 if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { 4781 // Allow overscroll/springback only on a fling, 4782 // not a pull/fling from the end 4783 if (mViewStartY < 0) { 4784 mViewStartY = 0; 4785 } else if (mViewStartY > mMaxViewStartY) { 4786 mViewStartY = mMaxViewStartY; 4787 } 4788 } 4789 4790 computeFirstHour(); 4791 mHandler.post(this); 4792 invalidate(); 4793 } 4794 } 4795 4796 /** 4797 * Cleanup the pop-up and timers. 4798 */ 4799 public void cleanup() { 4800 // Protect against null-pointer exceptions 4801 if (mPopup != null) { 4802 mPopup.dismiss(); 4803 } 4804 mPaused = true; 4805 mLastPopupEventID = INVALID_EVENT_ID; 4806 if (mHandler != null) { 4807 mHandler.removeCallbacks(mDismissPopup); 4808 mHandler.removeCallbacks(mUpdateCurrentTime); 4809 } 4810 4811 Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, 4812 mCellHeight); 4813 // Clear all click animations 4814 eventClickCleanup(); 4815 // Turn off redraw 4816 mRemeasure = false; 4817 // Turn off scrolling to make sure the view is in the correct state if we fling back to it 4818 mScrolling = false; 4819 } 4820 4821 private void eventClickCleanup() { 4822 this.removeCallbacks(mClearClick); 4823 this.removeCallbacks(mSetClick); 4824 mClickedEvent = null; 4825 mSavedClickedEvent = null; 4826 } 4827 4828 private void setSelectedEvent(Event e) { 4829 mSelectedEvent = e; 4830 mSelectedEventForAccessibility = e; 4831 } 4832 4833 private void setSelectedHour(int h) { 4834 mSelectionHour = h; 4835 mSelectionHourForAccessibility = h; 4836 } 4837 private void setSelectedDay(int d) { 4838 mSelectionDay = d; 4839 mSelectionDayForAccessibility = d; 4840 } 4841 4842 /** 4843 * Restart the update timer 4844 */ 4845 public void restartCurrentTimeUpdates() { 4846 mPaused = false; 4847 if (mHandler != null) { 4848 mHandler.removeCallbacks(mUpdateCurrentTime); 4849 mHandler.post(mUpdateCurrentTime); 4850 } 4851 } 4852 4853 @Override 4854 protected void onDetachedFromWindow() { 4855 cleanup(); 4856 super.onDetachedFromWindow(); 4857 } 4858 4859 class DismissPopup implements Runnable { 4860 public void run() { 4861 // Protect against null-pointer exceptions 4862 if (mPopup != null) { 4863 mPopup.dismiss(); 4864 } 4865 } 4866 } 4867 4868 class UpdateCurrentTime implements Runnable { 4869 public void run() { 4870 long currentTime = System.currentTimeMillis(); 4871 mCurrentTime.set(currentTime); 4872 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) 4873 if (!DayView.this.mPaused) { 4874 mHandler.postDelayed(mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY 4875 - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 4876 } 4877 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 4878 invalidate(); 4879 } 4880 } 4881 4882 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 4883 @Override 4884 public boolean onSingleTapUp(MotionEvent ev) { 4885 if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp"); 4886 DayView.this.doSingleTapUp(ev); 4887 return true; 4888 } 4889 4890 @Override 4891 public void onLongPress(MotionEvent ev) { 4892 if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress"); 4893 DayView.this.doLongPress(ev); 4894 } 4895 4896 @Override 4897 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 4898 if (DEBUG) Log.e(TAG, "GestureDetector.onScroll"); 4899 eventClickCleanup(); 4900 if (mTouchStartedInAlldayArea) { 4901 if (Math.abs(distanceX) < Math.abs(distanceY)) { 4902 // Make sure that click feedback is gone when you scroll from the 4903 // all day area 4904 invalidate(); 4905 return false; 4906 } 4907 // don't scroll vertically if this started in the allday area 4908 distanceY = 0; 4909 } 4910 DayView.this.doScroll(e1, e2, distanceX, distanceY); 4911 return true; 4912 } 4913 4914 @Override 4915 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 4916 if (DEBUG) Log.e(TAG, "GestureDetector.onFling"); 4917 4918 if (mTouchStartedInAlldayArea) { 4919 if (Math.abs(velocityX) < Math.abs(velocityY)) { 4920 return false; 4921 } 4922 // don't fling vertically if this started in the allday area 4923 velocityY = 0; 4924 } 4925 DayView.this.doFling(e1, e2, velocityX, velocityY); 4926 return true; 4927 } 4928 4929 @Override 4930 public boolean onDown(MotionEvent ev) { 4931 if (DEBUG) Log.e(TAG, "GestureDetector.onDown"); 4932 DayView.this.doDown(ev); 4933 return true; 4934 } 4935 } 4936 4937 @Override 4938 public boolean onLongClick(View v) { 4939 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 4940 long time = getSelectedTimeInMillis(); 4941 if (!mSelectionAllday) { 4942 flags |= DateUtils.FORMAT_SHOW_TIME; 4943 } 4944 if (DateFormat.is24HourFormat(mContext)) { 4945 flags |= DateUtils.FORMAT_24HOUR; 4946 } 4947 mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags); 4948 new AlertDialog.Builder(mContext).setTitle(mLongPressTitle) 4949 .setItems(mLongPressItems, new DialogInterface.OnClickListener() { 4950 @Override 4951 public void onClick(DialogInterface dialog, int which) { 4952 if (which == 0) { 4953 long extraLong = 0; 4954 if (mSelectionAllday) { 4955 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 4956 } 4957 mController.sendEventRelatedEventWithExtra(this, 4958 EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1, 4959 -1, extraLong, -1); 4960 } 4961 } 4962 }).show().setCanceledOnTouchOutside(true); 4963 return true; 4964 } 4965 4966 // The rest of this file was borrowed from Launcher2 - PagedView.java 4967 private static final int MINIMUM_SNAP_VELOCITY = 2200; 4968 4969 private class ScrollInterpolator implements Interpolator { 4970 public ScrollInterpolator() { 4971 } 4972 4973 public float getInterpolation(float t) { 4974 t -= 1.0f; 4975 t = t * t * t * t * t + 1; 4976 4977 if ((1 - t) * mAnimationDistance < 1) { 4978 cancelAnimation(); 4979 } 4980 4981 return t; 4982 } 4983 } 4984 4985 private long calculateDuration(float delta, float width, float velocity) { 4986 /* 4987 * Here we compute a "distance" that will be used in the computation of 4988 * the overall snap duration. This is a function of the actual distance 4989 * that needs to be traveled; we keep this value close to half screen 4990 * size in order to reduce the variance in snap duration as a function 4991 * of the distance the page needs to travel. 4992 */ 4993 final float halfScreenSize = width / 2; 4994 float distanceRatio = delta / width; 4995 float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); 4996 float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; 4997 4998 velocity = Math.abs(velocity); 4999 velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); 5000 5001 /* 5002 * we want the page's snap velocity to approximately match the velocity 5003 * at which the user flings, so we scale the duration by a value near to 5004 * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to 5005 * make it a little slower. 5006 */ 5007 long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); 5008 if (DEBUG) { 5009 Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" 5010 + distanceRatio + " distance:" + distance + " velocity:" + velocity 5011 + " duration:" + duration + " distanceInfluenceForSnapDuration:" 5012 + distanceInfluenceForSnapDuration); 5013 } 5014 return duration; 5015 } 5016 5017 /* 5018 * We want the duration of the page snap animation to be influenced by the 5019 * distance that the screen has to travel, however, we don't want this 5020 * duration to be effected in a purely linear fashion. Instead, we use this 5021 * method to moderate the effect that the distance of travel has on the 5022 * overall snap duration. 5023 */ 5024 private float distanceInfluenceForSnapDuration(float f) { 5025 f -= 0.5f; // center the values about 0. 5026 f *= 0.3f * Math.PI / 2.0f; 5027 return (float) Math.sin(f); 5028 } 5029 } 5030