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_HEIGHT = 2; 396 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1; 397 private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4; 398 private static int CURRENT_TIME_LINE_TOP_OFFSET = 2; 399 400 /* package */ static final int MINUTES_PER_HOUR = 60; 401 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; 402 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; 403 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); 404 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 405 406 // More events text will transition between invisible and this alpha 407 private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; 408 private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; 409 private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; 410 private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; 411 private static int DAY_HEADER_LEFT_MARGIN = 5; 412 private static int DAY_HEADER_RIGHT_MARGIN = 4; 413 private static int DAY_HEADER_BOTTOM_MARGIN = 3; 414 private static float DAY_HEADER_FONT_SIZE = 14; 415 private static float DATE_HEADER_FONT_SIZE = 32; 416 private static float NORMAL_FONT_SIZE = 12; 417 private static float EVENT_TEXT_FONT_SIZE = 12; 418 private static float HOURS_TEXT_SIZE = 12; 419 private static float AMPM_TEXT_SIZE = 9; 420 private static int MIN_HOURS_WIDTH = 96; 421 private static int MIN_CELL_WIDTH_FOR_TEXT = 20; 422 private static final int MAX_EVENT_TEXT_LEN = 500; 423 // smallest height to draw an event with 424 private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels 425 private static int CALENDAR_COLOR_SQUARE_SIZE = 10; 426 private static int EVENT_RECT_TOP_MARGIN = 1; 427 private static int EVENT_RECT_BOTTOM_MARGIN = 0; 428 private static int EVENT_RECT_LEFT_MARGIN = 1; 429 private static int EVENT_RECT_RIGHT_MARGIN = 0; 430 private static int EVENT_RECT_STROKE_WIDTH = 2; 431 private static int EVENT_TEXT_TOP_MARGIN = 2; 432 private static int EVENT_TEXT_BOTTOM_MARGIN = 2; 433 private static int EVENT_TEXT_LEFT_MARGIN = 6; 434 private static int EVENT_TEXT_RIGHT_MARGIN = 6; 435 private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; 436 private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 437 private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; 438 private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 439 private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; 440 // margins and sizing for the expand allday icon 441 private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; 442 // sizing for "box +n" in allDay events 443 private static int EVENT_SQUARE_WIDTH = 10; 444 private static int EVENT_LINE_PADDING = 4; 445 private static int NEW_EVENT_HINT_FONT_SIZE = 12; 446 447 private static int mPressedColor; 448 private static int mClickedColor; 449 private static int mEventTextColor; 450 private static int mMoreEventsTextColor; 451 452 private static int mWeek_saturdayColor; 453 private static int mWeek_sundayColor; 454 private static int mCalendarDateBannerTextColor; 455 private static int mCalendarAmPmLabel; 456 private static int mCalendarGridAreaSelected; 457 private static int mCalendarGridLineInnerHorizontalColor; 458 private static int mCalendarGridLineInnerVerticalColor; 459 private static int mFutureBgColor; 460 private static int mFutureBgColorRes; 461 private static int mBgColor; 462 private static int mNewEventHintColor; 463 private static int mCalendarHourLabelColor; 464 private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; 465 466 private float mAnimationDistance = 0; 467 private int mViewStartX; 468 private int mViewStartY; 469 private int mMaxViewStartY; 470 private int mViewHeight; 471 private int mViewWidth; 472 private int mGridAreaHeight = -1; 473 private static int mCellHeight = 0; // shared among all DayViews 474 private static int mMinCellHeight = 32; 475 private int mScrollStartY; 476 private int mPreviousDirection; 477 478 /** 479 * Vertical distance or span between the two touch points at the start of a 480 * scaling gesture 481 */ 482 private float mStartingSpanY = 0; 483 /** Height of 1 hour in pixels at the start of a scaling gesture */ 484 private int mCellHeightBeforeScaleGesture; 485 /** The hour at the center two touch points */ 486 private float mGestureCenterHour = 0; 487 /** 488 * Flag to decide whether to handle the up event. Cases where up events 489 * should be ignored are 1) right after a scale gesture and 2) finger was 490 * down before app launch 491 */ 492 private boolean mHandleActionUp = true; 493 494 private int mHoursTextHeight; 495 /** 496 * The height of the area used for allday events 497 */ 498 private int mAlldayHeight; 499 /** 500 * The height of the allday event area used during animation 501 */ 502 private int mAnimateDayHeight = 0; 503 /** 504 * The height of an individual allday event during animation 505 */ 506 private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 507 /** 508 * Whether to use the expand or collapse icon. 509 */ 510 private static boolean mUseExpandIcon = true; 511 /** 512 * The height of the day names/numbers 513 */ 514 private static int DAY_HEADER_HEIGHT = 45; 515 /** 516 * The height of the day names/numbers for multi-day views 517 */ 518 private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 519 /** 520 * The height of the day names/numbers when viewing a single day 521 */ 522 private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 523 /** 524 * Max of all day events in a given day in this view. 525 */ 526 private int mMaxAlldayEvents; 527 /** 528 * A count of the number of allday events that were not drawn for each day 529 */ 530 private int[] mSkippedAlldayEvents; 531 /** 532 * The number of allDay events at which point we start hiding allDay events. 533 */ 534 private int mMaxUnexpandedAlldayEventCount = 4; 535 /** 536 * Whether or not to expand the allDay area to fill the screen 537 */ 538 private static boolean mShowAllAllDayEvents = false; 539 540 protected int mNumDays = 7; 541 private int mNumHours = 10; 542 543 /** Width of the time line (list of hours) to the left. */ 544 private int mHoursWidth; 545 private int mDateStrWidth; 546 /** Top of the scrollable region i.e. below date labels and all day events */ 547 private int mFirstCell; 548 /** First fully visibile hour */ 549 private int mFirstHour = -1; 550 /** Distance between the mFirstCell and the top of first fully visible hour. */ 551 private int mFirstHourOffset; 552 private String[] mHourStrs; 553 private String[] mDayStrs; 554 private String[] mDayStrs2Letter; 555 private boolean mIs24HourFormat; 556 557 private final ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); 558 private boolean mComputeSelectedEvents; 559 private boolean mUpdateToast; 560 private Event mSelectedEvent; 561 private Event mPrevSelectedEvent; 562 private final Rect mPrevBox = new Rect(); 563 protected final Resources mResources; 564 protected final Drawable mCurrentTimeLine; 565 protected final Drawable mCurrentTimeAnimateLine; 566 protected final Drawable mTodayHeaderDrawable; 567 protected final Drawable mExpandAlldayDrawable; 568 protected final Drawable mCollapseAlldayDrawable; 569 protected Drawable mAcceptedOrTentativeEventBoxDrawable; 570 private String mAmString; 571 private String mPmString; 572 private final DeleteEventHelper mDeleteEventHelper; 573 private static int sCounter = 0; 574 575 private final ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); 576 577 ScaleGestureDetector mScaleGestureDetector; 578 579 /** 580 * The initial state of the touch mode when we enter this view. 581 */ 582 private static final int TOUCH_MODE_INITIAL_STATE = 0; 583 584 /** 585 * Indicates we just received the touch event and we are waiting to see if 586 * it is a tap or a scroll gesture. 587 */ 588 private static final int TOUCH_MODE_DOWN = 1; 589 590 /** 591 * Indicates the touch gesture is a vertical scroll 592 */ 593 private static final int TOUCH_MODE_VSCROLL = 0x20; 594 595 /** 596 * Indicates the touch gesture is a horizontal scroll 597 */ 598 private static final int TOUCH_MODE_HSCROLL = 0x40; 599 600 private int mTouchMode = TOUCH_MODE_INITIAL_STATE; 601 602 /** 603 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. 604 */ 605 private static final int SELECTION_HIDDEN = 0; 606 private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet 607 private static final int SELECTION_SELECTED = 2; 608 private static final int SELECTION_LONGPRESS = 3; 609 610 private int mSelectionMode = SELECTION_HIDDEN; 611 612 private boolean mScrolling = false; 613 614 private float mInitialScrollX; 615 private float mInitialScrollY; 616 617 private boolean mAnimateToday = false; 618 private int mAnimateTodayAlpha = 0; 619 620 // Animates the height of the allday region 621 ObjectAnimator mAlldayAnimator; 622 // Animates the height of events in the allday region 623 ObjectAnimator mAlldayEventAnimator; 624 // Animates the transparency of the more events text 625 ObjectAnimator mMoreAlldayEventsAnimator; 626 // Animates the current time marker when Today is pressed 627 ObjectAnimator mTodayAnimator; 628 // whether or not an event is stopping because it was cancelled 629 private boolean mCancellingAnimations = false; 630 // tracks whether a touch originated in the allday area 631 private boolean mTouchStartedInAlldayArea = false; 632 633 private final CalendarController mController; 634 private final ViewSwitcher mViewSwitcher; 635 private final GestureDetector mGestureDetector; 636 private final OverScroller mScroller; 637 private final EdgeEffect mEdgeEffectTop; 638 private final EdgeEffect mEdgeEffectBottom; 639 private boolean mCallEdgeEffectOnAbsorb; 640 private final int OVERFLING_DISTANCE; 641 private float mLastVelocity; 642 643 private final ScrollInterpolator mHScrollInterpolator; 644 private AccessibilityManager mAccessibilityMgr = null; 645 private boolean mIsAccessibilityEnabled = false; 646 private boolean mTouchExplorationEnabled = false; 647 private final String mCreateNewEventString; 648 private final String mNewEventHintString; 649 650 public DayView(Context context, CalendarController controller, 651 ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { 652 super(context); 653 mContext = context; 654 initAccessibilityVariables(); 655 656 mResources = context.getResources(); 657 mCreateNewEventString = mResources.getString(R.string.event_create); 658 mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint); 659 mNumDays = numDays; 660 661 DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); 662 DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); 663 ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); 664 DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); 665 EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); 666 HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); 667 AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); 668 MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); 669 HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); 670 HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); 671 MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); 672 int eventTextSizeId; 673 if (mNumDays == 1) { 674 eventTextSizeId = R.dimen.day_view_event_text_size; 675 } else { 676 eventTextSizeId = R.dimen.week_view_event_text_size; 677 } 678 EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(eventTextSizeId); 679 NEW_EVENT_HINT_FONT_SIZE = (int) mResources.getDimension(R.dimen.new_event_hint_text_size); 680 MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height); 681 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT; 682 EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin); 683 EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; 684 EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 685 EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; 686 687 EVENT_TEXT_LEFT_MARGIN = (int) mResources 688 .getDimension(R.dimen.event_text_horizontal_margin); 689 EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 690 EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 691 EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 692 693 if (mScale == 0) { 694 695 mScale = mResources.getDisplayMetrics().density; 696 if (mScale != 1) { 697 SINGLE_ALLDAY_HEIGHT *= mScale; 698 ALLDAY_TOP_MARGIN *= mScale; 699 MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; 700 701 NORMAL_FONT_SIZE *= mScale; 702 GRID_LINE_LEFT_MARGIN *= mScale; 703 HOURS_TOP_MARGIN *= mScale; 704 MIN_CELL_WIDTH_FOR_TEXT *= mScale; 705 MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; 706 mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 707 708 CURRENT_TIME_LINE_HEIGHT *= mScale; 709 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale; 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_LEFT_MARGIN *= mScale; 718 DAY_HEADER_RIGHT_MARGIN *= mScale; 719 DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; 720 DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; 721 DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; 722 CALENDAR_COLOR_SQUARE_SIZE *= mScale; 723 EVENT_RECT_TOP_MARGIN *= mScale; 724 EVENT_RECT_BOTTOM_MARGIN *= mScale; 725 ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; 726 EVENT_RECT_LEFT_MARGIN *= mScale; 727 EVENT_RECT_RIGHT_MARGIN *= mScale; 728 EVENT_RECT_STROKE_WIDTH *= mScale; 729 EVENT_SQUARE_WIDTH *= mScale; 730 EVENT_LINE_PADDING *= mScale; 731 NEW_EVENT_MARGIN *= mScale; 732 NEW_EVENT_WIDTH *= mScale; 733 NEW_EVENT_MAX_LENGTH *= mScale; 734 } 735 } 736 HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 737 DAY_HEADER_HEIGHT = mNumDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; 738 739 mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light); 740 mCurrentTimeAnimateLine = mResources 741 .getDrawable(R.drawable.timeline_indicator_activated_holo_light); 742 mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); 743 mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light); 744 mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light); 745 mNewEventHintColor = mResources.getColor(R.color.new_event_hint_text_color); 746 mAcceptedOrTentativeEventBoxDrawable = mResources 747 .getDrawable(R.drawable.panel_month_event_holo_light); 748 749 mEventLoader = eventLoader; 750 mEventGeometry = new EventGeometry(); 751 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); 752 mEventGeometry.setHourGap(HOUR_GAP); 753 mEventGeometry.setCellMargin(DAY_GAP); 754 mLongPressItems = new CharSequence[] { 755 mResources.getString(R.string.new_event_dialog_option) 756 }; 757 mLongPressTitle = mResources.getString(R.string.new_event_dialog_label); 758 mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); 759 mLastPopupEventID = INVALID_EVENT_ID; 760 mController = controller; 761 mViewSwitcher = viewSwitcher; 762 mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); 763 mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); 764 if (mCellHeight == 0) { 765 mCellHeight = Utils.getSharedPreference(mContext, 766 GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); 767 } 768 mScroller = new OverScroller(context); 769 mHScrollInterpolator = new ScrollInterpolator(); 770 mEdgeEffectTop = new EdgeEffect(context); 771 mEdgeEffectBottom = new EdgeEffect(context); 772 ViewConfiguration vc = ViewConfiguration.get(context); 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 public void setViewStartY(int viewStartY) { 1090 if (viewStartY > mMaxViewStartY) { 1091 viewStartY = mMaxViewStartY; 1092 } 1093 1094 mViewStartY = viewStartY; 1095 1096 computeFirstHour(); 1097 invalidate(); 1098 } 1099 1100 public void setAnimateTodayAlpha(int todayAlpha) { 1101 mAnimateTodayAlpha = todayAlpha; 1102 invalidate(); 1103 } 1104 1105 public Time getSelectedDay() { 1106 Time time = new Time(mBaseDate); 1107 time.setJulianDay(mSelectionDay); 1108 time.hour = mSelectionHour; 1109 1110 // We ignore the "isDst" field because we want normalize() to figure 1111 // out the correct DST value and not adjust the selected time based 1112 // on the current setting of DST. 1113 time.normalize(true /* ignore isDst */); 1114 return time; 1115 } 1116 1117 public void updateTitle() { 1118 Time start = new Time(mBaseDate); 1119 start.normalize(true); 1120 Time end = new Time(start); 1121 end.monthDay += mNumDays - 1; 1122 // Move it forward one minute so the formatter doesn't lose a day 1123 end.minute += 1; 1124 end.normalize(true); 1125 1126 long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 1127 if (mNumDays != 1) { 1128 // Don't show day of the month if for multi-day view 1129 formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; 1130 1131 // Abbreviate the month if showing multiple months 1132 if (start.month != end.month) { 1133 formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; 1134 } 1135 } 1136 1137 mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, 1138 formatFlags, null, null); 1139 } 1140 1141 /** 1142 * return a negative number if "time" is comes before the visible time 1143 * range, a positive number if "time" is after the visible time range, and 0 1144 * if it is in the visible time range. 1145 */ 1146 public int compareToVisibleTimeRange(Time time) { 1147 1148 int savedHour = mBaseDate.hour; 1149 int savedMinute = mBaseDate.minute; 1150 int savedSec = mBaseDate.second; 1151 1152 mBaseDate.hour = 0; 1153 mBaseDate.minute = 0; 1154 mBaseDate.second = 0; 1155 1156 if (DEBUG) { 1157 Log.d(TAG, "Begin " + mBaseDate.toString()); 1158 Log.d(TAG, "Diff " + time.toString()); 1159 } 1160 1161 // Compare beginning of range 1162 int diff = Time.compare(time, mBaseDate); 1163 if (diff > 0) { 1164 // Compare end of range 1165 mBaseDate.monthDay += mNumDays; 1166 mBaseDate.normalize(true); 1167 diff = Time.compare(time, mBaseDate); 1168 1169 if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); 1170 1171 mBaseDate.monthDay -= mNumDays; 1172 mBaseDate.normalize(true); 1173 if (diff < 0) { 1174 // in visible time 1175 diff = 0; 1176 } else if (diff == 0) { 1177 // Midnight of following day 1178 diff = 1; 1179 } 1180 } 1181 1182 if (DEBUG) Log.d(TAG, "Diff: " + diff); 1183 1184 mBaseDate.hour = savedHour; 1185 mBaseDate.minute = savedMinute; 1186 mBaseDate.second = savedSec; 1187 return diff; 1188 } 1189 1190 private void recalc() { 1191 // Set the base date to the beginning of the week if we are displaying 1192 // 7 days at a time. 1193 if (mNumDays == 7) { 1194 adjustToBeginningOfWeek(mBaseDate); 1195 } 1196 1197 final long start = mBaseDate.toMillis(false /* use isDst */); 1198 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); 1199 mLastJulianDay = mFirstJulianDay + mNumDays - 1; 1200 1201 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); 1202 mFirstVisibleDate = mBaseDate.monthDay; 1203 mFirstVisibleDayOfWeek = mBaseDate.weekDay; 1204 } 1205 1206 private void adjustToBeginningOfWeek(Time time) { 1207 int dayOfWeek = time.weekDay; 1208 int diff = dayOfWeek - mFirstDayOfWeek; 1209 if (diff != 0) { 1210 if (diff < 0) { 1211 diff += 7; 1212 } 1213 time.monthDay -= diff; 1214 time.normalize(true /* ignore isDst */); 1215 } 1216 } 1217 1218 @Override 1219 protected void onSizeChanged(int width, int height, int oldw, int oldh) { 1220 mViewWidth = width; 1221 mViewHeight = height; 1222 mEdgeEffectTop.setSize(mViewWidth, mViewHeight); 1223 mEdgeEffectBottom.setSize(mViewWidth, mViewHeight); 1224 int gridAreaWidth = width - mHoursWidth; 1225 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; 1226 1227 // This would be about 1 day worth in a 7 day view 1228 mHorizontalSnapBackThreshold = width / 7; 1229 1230 Paint p = new Paint(); 1231 p.setTextSize(HOURS_TEXT_SIZE); 1232 mHoursTextHeight = (int) Math.abs(p.ascent()); 1233 remeasure(width, height); 1234 } 1235 1236 /** 1237 * Measures the space needed for various parts of the view after 1238 * loading new events. This can change if there are all-day events. 1239 */ 1240 private void remeasure(int width, int height) { 1241 // Shrink to fit available space but make sure we can display at least two events 1242 MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); 1243 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); 1244 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, 1245 (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); 1246 mMaxUnexpandedAlldayEventCount = 1247 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1248 1249 // First, clear the array of earliest start times, and the array 1250 // indicating presence of an all-day event. 1251 for (int day = 0; day < mNumDays; day++) { 1252 mEarliestStartHour[day] = 25; // some big number 1253 mHasAllDayEvent[day] = false; 1254 } 1255 1256 int maxAllDayEvents = mMaxAlldayEvents; 1257 1258 // The min is where 24 hours cover the entire visible area 1259 mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); 1260 if (mCellHeight < mMinCellHeight) { 1261 mCellHeight = mMinCellHeight; 1262 } 1263 1264 // Calculate mAllDayHeight 1265 mFirstCell = DAY_HEADER_HEIGHT; 1266 int allDayHeight = 0; 1267 if (maxAllDayEvents > 0) { 1268 int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 1269 // If there is at most one all-day event per day, then use less 1270 // space (but more than the space for a single event). 1271 if (maxAllDayEvents == 1) { 1272 allDayHeight = SINGLE_ALLDAY_HEIGHT; 1273 } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ 1274 // Allow the all-day area to grow in height depending on the 1275 // number of all-day events we need to show, up to a limit. 1276 allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 1277 if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1278 allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; 1279 } 1280 } else { 1281 // if we have more than the magic number, check if we're animating 1282 // and if not adjust the sizes appropriately 1283 if (mAnimateDayHeight != 0) { 1284 // Don't shrink the space past the final allDay space. The animation 1285 // continues to hide the last event so the more events text can 1286 // fade in. 1287 allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); 1288 } else { 1289 // Try to fit all the events in 1290 allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1291 // But clip the area depending on which mode we're in 1292 if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1293 allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * 1294 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1295 } else if (allDayHeight > maxAllAllDayHeight) { 1296 allDayHeight = maxAllAllDayHeight; 1297 } 1298 } 1299 } 1300 mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; 1301 } else { 1302 mSelectionAllday = false; 1303 } 1304 mAlldayHeight = allDayHeight; 1305 1306 mGridAreaHeight = height - mFirstCell; 1307 1308 // Set up the expand icon position 1309 int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); 1310 mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, 1311 EVENT_ALL_DAY_TEXT_LEFT_MARGIN); 1312 mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth 1313 - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); 1314 mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; 1315 mExpandAllDayRect.top = mExpandAllDayRect.bottom 1316 - mExpandAlldayDrawable.getIntrinsicHeight(); 1317 1318 mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); 1319 mEventGeometry.setHourHeight(mCellHeight); 1320 1321 final long minimumDurationMillis = (long) 1322 (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); 1323 Event.computePositions(mEvents, minimumDurationMillis); 1324 1325 // Compute the top of our reachable view 1326 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 1327 if (DEBUG) { 1328 Log.e(TAG, "mViewStartY: " + mViewStartY); 1329 Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); 1330 } 1331 if (mViewStartY > mMaxViewStartY) { 1332 mViewStartY = mMaxViewStartY; 1333 computeFirstHour(); 1334 } 1335 1336 if (mFirstHour == -1) { 1337 initFirstHour(); 1338 mFirstHourOffset = 0; 1339 } 1340 1341 // When we change the base date, the number of all-day events may 1342 // change and that changes the cell height. When we switch dates, 1343 // we use the mFirstHourOffset from the previous view, but that may 1344 // be too large for the new view if the cell height is smaller. 1345 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { 1346 mFirstHourOffset = mCellHeight + HOUR_GAP - 1; 1347 } 1348 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; 1349 1350 final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); 1351 //When we get new events we don't want to dismiss the popup unless the event changes 1352 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { 1353 mPopup.dismiss(); 1354 } 1355 mPopup.setWidth(eventAreaWidth - 20); 1356 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 1357 } 1358 1359 /** 1360 * Initialize the state for another view. The given view is one that has 1361 * its own bitmap and will use an animation to replace the current view. 1362 * The current view and new view are either both Week views or both Day 1363 * views. They differ in their base date. 1364 * 1365 * @param view the view to initialize. 1366 */ 1367 private void initView(DayView view) { 1368 view.setSelectedHour(mSelectionHour); 1369 view.mSelectedEvents.clear(); 1370 view.mComputeSelectedEvents = true; 1371 view.mFirstHour = mFirstHour; 1372 view.mFirstHourOffset = mFirstHourOffset; 1373 view.remeasure(getWidth(), getHeight()); 1374 view.initAllDayHeights(); 1375 1376 view.setSelectedEvent(null); 1377 view.mPrevSelectedEvent = null; 1378 view.mFirstDayOfWeek = mFirstDayOfWeek; 1379 if (view.mEvents.size() > 0) { 1380 view.mSelectionAllday = mSelectionAllday; 1381 } else { 1382 view.mSelectionAllday = false; 1383 } 1384 1385 // Redraw the screen so that the selection box will be redrawn. We may 1386 // have scrolled to a different part of the day in some other view 1387 // so the selection box in this view may no longer be visible. 1388 view.recalc(); 1389 } 1390 1391 /** 1392 * Switch to another view based on what was selected (an event or a free 1393 * slot) and how it was selected (by touch or by trackball). 1394 * 1395 * @param trackBallSelection true if the selection was made using the 1396 * trackball. 1397 */ 1398 private void switchViews(boolean trackBallSelection) { 1399 Event selectedEvent = mSelectedEvent; 1400 1401 mPopup.dismiss(); 1402 mLastPopupEventID = INVALID_EVENT_ID; 1403 if (mNumDays > 1) { 1404 // This is the Week view. 1405 // With touch, we always switch to Day/Agenda View 1406 // With track ball, if we selected a free slot, then create an event. 1407 // If we selected a specific event, switch to EventInfo view. 1408 if (trackBallSelection) { 1409 if (selectedEvent == null) { 1410 // Switch to the EditEvent view 1411 long startMillis = getSelectedTimeInMillis(); 1412 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1413 long extraLong = 0; 1414 if (mSelectionAllday) { 1415 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 1416 } 1417 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 1418 startMillis, endMillis, -1, -1, extraLong, -1); 1419 } else { 1420 if (mIsAccessibilityEnabled) { 1421 mAccessibilityMgr.interrupt(); 1422 } 1423 // Switch to the EventInfo view 1424 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1425 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1426 getSelectedTimeInMillis()); 1427 } 1428 } else { 1429 // This was a touch selection. If the touch selected a single 1430 // unambiguous event, then view that event. Otherwise go to 1431 // Day/Agenda view. 1432 if (mSelectedEvents.size() == 1) { 1433 if (mIsAccessibilityEnabled) { 1434 mAccessibilityMgr.interrupt(); 1435 } 1436 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1437 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1438 getSelectedTimeInMillis()); 1439 } 1440 } 1441 } else { 1442 // This is the Day view. 1443 // If we selected a free slot, then create an event. 1444 // If we selected an event, then go to the EventInfo view. 1445 if (selectedEvent == null) { 1446 // Switch to the EditEvent view 1447 long startMillis = getSelectedTimeInMillis(); 1448 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1449 long extraLong = 0; 1450 if (mSelectionAllday) { 1451 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 1452 } 1453 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 1454 startMillis, endMillis, -1, -1, extraLong, -1); 1455 } else { 1456 if (mIsAccessibilityEnabled) { 1457 mAccessibilityMgr.interrupt(); 1458 } 1459 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1460 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1461 getSelectedTimeInMillis()); 1462 } 1463 } 1464 } 1465 1466 @Override 1467 public boolean onKeyUp(int keyCode, KeyEvent event) { 1468 mScrolling = false; 1469 long duration = event.getEventTime() - event.getDownTime(); 1470 1471 switch (keyCode) { 1472 case KeyEvent.KEYCODE_DPAD_CENTER: 1473 if (mSelectionMode == SELECTION_HIDDEN) { 1474 // Don't do anything unless the selection is visible. 1475 break; 1476 } 1477 1478 if (mSelectionMode == SELECTION_PRESSED) { 1479 // This was the first press when there was nothing selected. 1480 // Change the selection from the "pressed" state to the 1481 // the "selected" state. We treat short-press and 1482 // long-press the same here because nothing was selected. 1483 mSelectionMode = SELECTION_SELECTED; 1484 invalidate(); 1485 break; 1486 } 1487 1488 // Check the duration to determine if this was a short press 1489 if (duration < ViewConfiguration.getLongPressTimeout()) { 1490 switchViews(true /* trackball */); 1491 } else { 1492 mSelectionMode = SELECTION_LONGPRESS; 1493 invalidate(); 1494 performLongClick(); 1495 } 1496 break; 1497 // case KeyEvent.KEYCODE_BACK: 1498 // if (event.isTracking() && !event.isCanceled()) { 1499 // mPopup.dismiss(); 1500 // mContext.finish(); 1501 // return true; 1502 // } 1503 // break; 1504 } 1505 return super.onKeyUp(keyCode, event); 1506 } 1507 1508 @Override 1509 public boolean onKeyDown(int keyCode, KeyEvent event) { 1510 if (mSelectionMode == SELECTION_HIDDEN) { 1511 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1512 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP 1513 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1514 // Display the selection box but don't move or select it 1515 // on this key press. 1516 mSelectionMode = SELECTION_SELECTED; 1517 invalidate(); 1518 return true; 1519 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 1520 // Display the selection box but don't select it 1521 // on this key press. 1522 mSelectionMode = SELECTION_PRESSED; 1523 invalidate(); 1524 return true; 1525 } 1526 } 1527 1528 mSelectionMode = SELECTION_SELECTED; 1529 mScrolling = false; 1530 boolean redraw; 1531 int selectionDay = mSelectionDay; 1532 1533 switch (keyCode) { 1534 case KeyEvent.KEYCODE_DEL: 1535 // Delete the selected event, if any 1536 Event selectedEvent = mSelectedEvent; 1537 if (selectedEvent == null) { 1538 return false; 1539 } 1540 mPopup.dismiss(); 1541 mLastPopupEventID = INVALID_EVENT_ID; 1542 1543 long begin = selectedEvent.startMillis; 1544 long end = selectedEvent.endMillis; 1545 long id = selectedEvent.id; 1546 mDeleteEventHelper.delete(begin, end, id, -1); 1547 return true; 1548 case KeyEvent.KEYCODE_ENTER: 1549 switchViews(true /* trackball or keyboard */); 1550 return true; 1551 case KeyEvent.KEYCODE_BACK: 1552 if (event.getRepeatCount() == 0) { 1553 event.startTracking(); 1554 return true; 1555 } 1556 return super.onKeyDown(keyCode, event); 1557 case KeyEvent.KEYCODE_DPAD_LEFT: 1558 if (mSelectedEvent != null) { 1559 setSelectedEvent(mSelectedEvent.nextLeft); 1560 } 1561 if (mSelectedEvent == null) { 1562 mLastPopupEventID = INVALID_EVENT_ID; 1563 selectionDay -= 1; 1564 } 1565 redraw = true; 1566 break; 1567 1568 case KeyEvent.KEYCODE_DPAD_RIGHT: 1569 if (mSelectedEvent != null) { 1570 setSelectedEvent(mSelectedEvent.nextRight); 1571 } 1572 if (mSelectedEvent == null) { 1573 mLastPopupEventID = INVALID_EVENT_ID; 1574 selectionDay += 1; 1575 } 1576 redraw = true; 1577 break; 1578 1579 case KeyEvent.KEYCODE_DPAD_UP: 1580 if (mSelectedEvent != null) { 1581 setSelectedEvent(mSelectedEvent.nextUp); 1582 } 1583 if (mSelectedEvent == null) { 1584 mLastPopupEventID = INVALID_EVENT_ID; 1585 if (!mSelectionAllday) { 1586 setSelectedHour(mSelectionHour - 1); 1587 adjustHourSelection(); 1588 mSelectedEvents.clear(); 1589 mComputeSelectedEvents = true; 1590 } 1591 } 1592 redraw = true; 1593 break; 1594 1595 case KeyEvent.KEYCODE_DPAD_DOWN: 1596 if (mSelectedEvent != null) { 1597 setSelectedEvent(mSelectedEvent.nextDown); 1598 } 1599 if (mSelectedEvent == null) { 1600 mLastPopupEventID = INVALID_EVENT_ID; 1601 if (mSelectionAllday) { 1602 mSelectionAllday = false; 1603 } else { 1604 setSelectedHour(mSelectionHour + 1); 1605 adjustHourSelection(); 1606 mSelectedEvents.clear(); 1607 mComputeSelectedEvents = true; 1608 } 1609 } 1610 redraw = true; 1611 break; 1612 1613 default: 1614 return super.onKeyDown(keyCode, event); 1615 } 1616 1617 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { 1618 DayView view = (DayView) mViewSwitcher.getNextView(); 1619 Time date = view.mBaseDate; 1620 date.set(mBaseDate); 1621 if (selectionDay < mFirstJulianDay) { 1622 date.monthDay -= mNumDays; 1623 } else { 1624 date.monthDay += mNumDays; 1625 } 1626 date.normalize(true /* ignore isDst */); 1627 view.setSelectedDay(selectionDay); 1628 1629 initView(view); 1630 1631 Time end = new Time(date); 1632 end.monthDay += mNumDays - 1; 1633 mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); 1634 return true; 1635 } 1636 if (mSelectionDay != selectionDay) { 1637 Time date = new Time(mBaseDate); 1638 date.setJulianDay(selectionDay); 1639 date.hour = mSelectionHour; 1640 mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); 1641 } 1642 setSelectedDay(selectionDay); 1643 mSelectedEvents.clear(); 1644 mComputeSelectedEvents = true; 1645 mUpdateToast = true; 1646 1647 if (redraw) { 1648 invalidate(); 1649 return true; 1650 } 1651 1652 return super.onKeyDown(keyCode, event); 1653 } 1654 1655 1656 @Override 1657 public boolean onHoverEvent(MotionEvent event) { 1658 if (DEBUG) { 1659 int action = event.getAction(); 1660 switch (action) { 1661 case MotionEvent.ACTION_HOVER_ENTER: 1662 Log.e(TAG, "ACTION_HOVER_ENTER"); 1663 break; 1664 case MotionEvent.ACTION_HOVER_MOVE: 1665 Log.e(TAG, "ACTION_HOVER_MOVE"); 1666 break; 1667 case MotionEvent.ACTION_HOVER_EXIT: 1668 Log.e(TAG, "ACTION_HOVER_EXIT"); 1669 break; 1670 default: 1671 Log.e(TAG, "Unknown hover event action. " + event); 1672 } 1673 } 1674 1675 // Mouse also generates hover events 1676 // Send accessibility events if accessibility and exploration are on. 1677 if (!mTouchExplorationEnabled) { 1678 return super.onHoverEvent(event); 1679 } 1680 if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { 1681 setSelectionFromPosition((int) event.getX(), (int) event.getY(), true); 1682 invalidate(); 1683 } 1684 return true; 1685 } 1686 1687 private boolean isTouchExplorationEnabled() { 1688 return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled(); 1689 } 1690 1691 private void sendAccessibilityEventAsNeeded(boolean speakEvents) { 1692 if (!mIsAccessibilityEnabled) { 1693 return; 1694 } 1695 boolean dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility; 1696 boolean hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility; 1697 if (dayChanged || hourChanged || 1698 mLastSelectedEventForAccessibility != mSelectedEventForAccessibility) { 1699 mLastSelectionDayForAccessibility = mSelectionDayForAccessibility; 1700 mLastSelectionHourForAccessibility = mSelectionHourForAccessibility; 1701 mLastSelectedEventForAccessibility = mSelectedEventForAccessibility; 1702 1703 StringBuilder b = new StringBuilder(); 1704 1705 // Announce only the changes i.e. day or hour or both 1706 if (dayChanged) { 1707 b.append(getSelectedTimeForAccessibility().format("%A ")); 1708 } 1709 if (hourChanged) { 1710 b.append(getSelectedTimeForAccessibility().format(mIs24HourFormat ? "%k" : "%l%p")); 1711 } 1712 if (dayChanged || hourChanged) { 1713 b.append(PERIOD_SPACE); 1714 } 1715 1716 if (speakEvents) { 1717 if (mEventCountTemplate == null) { 1718 mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); 1719 } 1720 1721 // Read out the relevant event(s) 1722 int numEvents = mSelectedEvents.size(); 1723 if (numEvents > 0) { 1724 if (mSelectedEventForAccessibility == null) { 1725 // Read out all the events 1726 int i = 1; 1727 for (Event calEvent : mSelectedEvents) { 1728 if (numEvents > 1) { 1729 // Read out x of numEvents if there are more than one event 1730 mStringBuilder.setLength(0); 1731 b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); 1732 b.append(" "); 1733 } 1734 appendEventAccessibilityString(b, calEvent); 1735 } 1736 } else { 1737 if (numEvents > 1) { 1738 // Read out x of numEvents if there are more than one event 1739 mStringBuilder.setLength(0); 1740 b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents 1741 .indexOf(mSelectedEventForAccessibility) + 1, numEvents)); 1742 b.append(" "); 1743 } 1744 appendEventAccessibilityString(b, mSelectedEventForAccessibility); 1745 } 1746 } else { 1747 b.append(mCreateNewEventString); 1748 } 1749 } 1750 1751 if (dayChanged || hourChanged || speakEvents) { 1752 AccessibilityEvent event = AccessibilityEvent 1753 .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1754 CharSequence msg = b.toString(); 1755 event.getText().add(msg); 1756 event.setAddedCount(msg.length()); 1757 sendAccessibilityEventUnchecked(event); 1758 } 1759 } 1760 } 1761 1762 /** 1763 * @param b 1764 * @param calEvent 1765 */ 1766 private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { 1767 b.append(calEvent.getTitleAndLocation()); 1768 b.append(PERIOD_SPACE); 1769 String when; 1770 int flags = DateUtils.FORMAT_SHOW_DATE; 1771 if (calEvent.allDay) { 1772 flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; 1773 } else { 1774 flags |= DateUtils.FORMAT_SHOW_TIME; 1775 if (DateFormat.is24HourFormat(mContext)) { 1776 flags |= DateUtils.FORMAT_24HOUR; 1777 } 1778 } 1779 when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); 1780 b.append(when); 1781 b.append(PERIOD_SPACE); 1782 } 1783 1784 private class GotoBroadcaster implements Animation.AnimationListener { 1785 private final int mCounter; 1786 private final Time mStart; 1787 private final Time mEnd; 1788 1789 public GotoBroadcaster(Time start, Time end) { 1790 mCounter = ++sCounter; 1791 mStart = start; 1792 mEnd = end; 1793 } 1794 1795 @Override 1796 public void onAnimationEnd(Animation animation) { 1797 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1798 view.mViewStartX = 0; 1799 view = (DayView) mViewSwitcher.getNextView(); 1800 view.mViewStartX = 0; 1801 1802 if (mCounter == sCounter) { 1803 mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, 1804 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 1805 } 1806 } 1807 1808 @Override 1809 public void onAnimationRepeat(Animation animation) { 1810 } 1811 1812 @Override 1813 public void onAnimationStart(Animation animation) { 1814 } 1815 } 1816 1817 private View switchViews(boolean forward, float xOffSet, float width, float velocity) { 1818 mAnimationDistance = width - xOffSet; 1819 if (DEBUG) { 1820 Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); 1821 } 1822 1823 float progress = Math.abs(xOffSet) / width; 1824 if (progress > 1.0f) { 1825 progress = 1.0f; 1826 } 1827 1828 float inFromXValue, inToXValue; 1829 float outFromXValue, outToXValue; 1830 if (forward) { 1831 inFromXValue = 1.0f - progress; 1832 inToXValue = 0.0f; 1833 outFromXValue = -progress; 1834 outToXValue = -1.0f; 1835 } else { 1836 inFromXValue = progress - 1.0f; 1837 inToXValue = 0.0f; 1838 outFromXValue = progress; 1839 outToXValue = 1.0f; 1840 } 1841 1842 final Time start = new Time(mBaseDate.timezone); 1843 start.set(mController.getTime()); 1844 if (forward) { 1845 start.monthDay += mNumDays; 1846 } else { 1847 start.monthDay -= mNumDays; 1848 } 1849 mController.setTime(start.normalize(true)); 1850 1851 Time newSelected = start; 1852 1853 if (mNumDays == 7) { 1854 newSelected = new Time(start); 1855 adjustToBeginningOfWeek(start); 1856 } 1857 1858 final Time end = new Time(start); 1859 end.monthDay += mNumDays - 1; 1860 1861 // We have to allocate these animation objects each time we switch views 1862 // because that is the only way to set the animation parameters. 1863 TranslateAnimation inAnimation = new TranslateAnimation( 1864 Animation.RELATIVE_TO_SELF, inFromXValue, 1865 Animation.RELATIVE_TO_SELF, inToXValue, 1866 Animation.ABSOLUTE, 0.0f, 1867 Animation.ABSOLUTE, 0.0f); 1868 1869 TranslateAnimation outAnimation = new TranslateAnimation( 1870 Animation.RELATIVE_TO_SELF, outFromXValue, 1871 Animation.RELATIVE_TO_SELF, outToXValue, 1872 Animation.ABSOLUTE, 0.0f, 1873 Animation.ABSOLUTE, 0.0f); 1874 1875 long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); 1876 inAnimation.setDuration(duration); 1877 inAnimation.setInterpolator(mHScrollInterpolator); 1878 outAnimation.setInterpolator(mHScrollInterpolator); 1879 outAnimation.setDuration(duration); 1880 outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); 1881 mViewSwitcher.setInAnimation(inAnimation); 1882 mViewSwitcher.setOutAnimation(outAnimation); 1883 1884 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1885 view.cleanup(); 1886 mViewSwitcher.showNext(); 1887 view = (DayView) mViewSwitcher.getCurrentView(); 1888 view.setSelected(newSelected, true, false); 1889 view.requestFocus(); 1890 view.reloadEvents(); 1891 view.updateTitle(); 1892 view.restartCurrentTimeUpdates(); 1893 1894 return view; 1895 } 1896 1897 // This is called after scrolling stops to move the selected hour 1898 // to the visible part of the screen. 1899 private void resetSelectedHour() { 1900 if (mSelectionHour < mFirstHour + 1) { 1901 setSelectedHour(mFirstHour + 1); 1902 setSelectedEvent(null); 1903 mSelectedEvents.clear(); 1904 mComputeSelectedEvents = true; 1905 } else if (mSelectionHour > mFirstHour + mNumHours - 3) { 1906 setSelectedHour(mFirstHour + mNumHours - 3); 1907 setSelectedEvent(null); 1908 mSelectedEvents.clear(); 1909 mComputeSelectedEvents = true; 1910 } 1911 } 1912 1913 private void initFirstHour() { 1914 mFirstHour = mSelectionHour - mNumHours / 5; 1915 if (mFirstHour < 0) { 1916 mFirstHour = 0; 1917 } else if (mFirstHour + mNumHours > 24) { 1918 mFirstHour = 24 - mNumHours; 1919 } 1920 } 1921 1922 /** 1923 * Recomputes the first full hour that is visible on screen after the 1924 * screen is scrolled. 1925 */ 1926 private void computeFirstHour() { 1927 // Compute the first full hour that is visible on screen 1928 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); 1929 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; 1930 } 1931 1932 private void adjustHourSelection() { 1933 if (mSelectionHour < 0) { 1934 setSelectedHour(0); 1935 if (mMaxAlldayEvents > 0) { 1936 mPrevSelectedEvent = null; 1937 mSelectionAllday = true; 1938 } 1939 } 1940 1941 if (mSelectionHour > 23) { 1942 setSelectedHour(23); 1943 } 1944 1945 // If the selected hour is at least 2 time slots from the top and 1946 // bottom of the screen, then don't scroll the view. 1947 if (mSelectionHour < mFirstHour + 1) { 1948 // If there are all-days events for the selected day but there 1949 // are no more normal events earlier in the day, then jump to 1950 // the all-day event area. 1951 // Exception 1: allow the user to scroll to 8am with the trackball 1952 // before jumping to the all-day event area. 1953 // Exception 2: if 12am is on screen, then allow the user to select 1954 // 12am before going up to the all-day event area. 1955 int daynum = mSelectionDay - mFirstJulianDay; 1956 if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1957 && mFirstHour > 0 && mFirstHour < 8) { 1958 mPrevSelectedEvent = null; 1959 mSelectionAllday = true; 1960 setSelectedHour(mFirstHour + 1); 1961 return; 1962 } 1963 1964 if (mFirstHour > 0) { 1965 mFirstHour -= 1; 1966 mViewStartY -= (mCellHeight + HOUR_GAP); 1967 if (mViewStartY < 0) { 1968 mViewStartY = 0; 1969 } 1970 return; 1971 } 1972 } 1973 1974 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1975 if (mFirstHour < 24 - mNumHours) { 1976 mFirstHour += 1; 1977 mViewStartY += (mCellHeight + HOUR_GAP); 1978 if (mViewStartY > mMaxViewStartY) { 1979 mViewStartY = mMaxViewStartY; 1980 } 1981 return; 1982 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1983 mViewStartY = mMaxViewStartY; 1984 } 1985 } 1986 } 1987 1988 void clearCachedEvents() { 1989 mLastReloadMillis = 0; 1990 } 1991 1992 private final Runnable mCancelCallback = new Runnable() { 1993 public void run() { 1994 clearCachedEvents(); 1995 } 1996 }; 1997 1998 /* package */ void reloadEvents() { 1999 // Protect against this being called before this view has been 2000 // initialized. 2001 // if (mContext == null) { 2002 // return; 2003 // } 2004 2005 // Make sure our time zones are up to date 2006 mTZUpdater.run(); 2007 2008 setSelectedEvent(null); 2009 mPrevSelectedEvent = null; 2010 mSelectedEvents.clear(); 2011 2012 // The start date is the beginning of the week at 12am 2013 Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); 2014 weekStart.set(mBaseDate); 2015 weekStart.hour = 0; 2016 weekStart.minute = 0; 2017 weekStart.second = 0; 2018 long millis = weekStart.normalize(true /* ignore isDst */); 2019 2020 // Avoid reloading events unnecessarily. 2021 if (millis == mLastReloadMillis) { 2022 return; 2023 } 2024 mLastReloadMillis = millis; 2025 2026 // load events in the background 2027 // mContext.startProgressSpinner(); 2028 final ArrayList<Event> events = new ArrayList<Event>(); 2029 mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { 2030 public void run() { 2031 boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay; 2032 mEvents = events; 2033 mLoadedFirstJulianDay = mFirstJulianDay; 2034 if (mAllDayEvents == null) { 2035 mAllDayEvents = new ArrayList<Event>(); 2036 } else { 2037 mAllDayEvents.clear(); 2038 } 2039 2040 // Create a shorter array for all day events 2041 for (Event e : events) { 2042 if (e.drawAsAllday()) { 2043 mAllDayEvents.add(e); 2044 } 2045 } 2046 2047 // New events, new layouts 2048 if (mLayouts == null || mLayouts.length < events.size()) { 2049 mLayouts = new StaticLayout[events.size()]; 2050 } else { 2051 Arrays.fill(mLayouts, null); 2052 } 2053 2054 if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { 2055 mAllDayLayouts = new StaticLayout[events.size()]; 2056 } else { 2057 Arrays.fill(mAllDayLayouts, null); 2058 } 2059 2060 computeEventRelations(); 2061 2062 mRemeasure = true; 2063 mComputeSelectedEvents = true; 2064 recalc(); 2065 2066 // Start animation to cross fade the events 2067 if (fadeinEvents) { 2068 if (mEventsCrossFadeAnimation == null) { 2069 mEventsCrossFadeAnimation = 2070 ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255); 2071 mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION); 2072 } 2073 mEventsCrossFadeAnimation.start(); 2074 } else{ 2075 invalidate(); 2076 } 2077 } 2078 }, mCancelCallback); 2079 } 2080 2081 public void setEventsAlpha(int alpha) { 2082 mEventsAlpha = alpha; 2083 invalidate(); 2084 } 2085 2086 public int getEventsAlpha() { 2087 return mEventsAlpha; 2088 } 2089 2090 public void stopEventsAnimation() { 2091 if (mEventsCrossFadeAnimation != null) { 2092 mEventsCrossFadeAnimation.cancel(); 2093 } 2094 mEventsAlpha = 255; 2095 } 2096 2097 private void computeEventRelations() { 2098 // Compute the layout relation between each event before measuring cell 2099 // width, as the cell width should be adjusted along with the relation. 2100 // 2101 // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) 2102 // We should mark them as "overwapped". Though they are not overwapped logically, but 2103 // minimum cell height implicitly expands the cell height of A and it should look like 2104 // (1:00pm - 1:15pm) after the cell height adjustment. 2105 2106 // Compute the space needed for the all-day events, if any. 2107 // Make a pass over all the events, and keep track of the maximum 2108 // number of all-day events in any one day. Also, keep track of 2109 // the earliest event in each day. 2110 int maxAllDayEvents = 0; 2111 final ArrayList<Event> events = mEvents; 2112 final int len = events.size(); 2113 // Num of all-day-events on each day. 2114 final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; 2115 Arrays.fill(eventsCount, 0); 2116 for (int ii = 0; ii < len; ii++) { 2117 Event event = events.get(ii); 2118 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { 2119 continue; 2120 } 2121 if (event.drawAsAllday()) { 2122 // Count all the events being drawn as allDay events 2123 final int firstDay = Math.max(event.startDay, mFirstJulianDay); 2124 final int lastDay = Math.min(event.endDay, mLastJulianDay); 2125 for (int day = firstDay; day <= lastDay; day++) { 2126 final int count = ++eventsCount[day - mFirstJulianDay]; 2127 if (maxAllDayEvents < count) { 2128 maxAllDayEvents = count; 2129 } 2130 } 2131 2132 int daynum = event.startDay - mFirstJulianDay; 2133 int durationDays = event.endDay - event.startDay + 1; 2134 if (daynum < 0) { 2135 durationDays += daynum; 2136 daynum = 0; 2137 } 2138 if (daynum + durationDays > mNumDays) { 2139 durationDays = mNumDays - daynum; 2140 } 2141 for (int day = daynum; durationDays > 0; day++, durationDays--) { 2142 mHasAllDayEvent[day] = true; 2143 } 2144 } else { 2145 int daynum = event.startDay - mFirstJulianDay; 2146 int hour = event.startTime / 60; 2147 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 2148 mEarliestStartHour[daynum] = hour; 2149 } 2150 2151 // Also check the end hour in case the event spans more than 2152 // one day. 2153 daynum = event.endDay - mFirstJulianDay; 2154 hour = event.endTime / 60; 2155 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 2156 mEarliestStartHour[daynum] = hour; 2157 } 2158 } 2159 } 2160 mMaxAlldayEvents = maxAllDayEvents; 2161 initAllDayHeights(); 2162 } 2163 2164 @Override 2165 protected void onDraw(Canvas canvas) { 2166 if (mRemeasure) { 2167 remeasure(getWidth(), getHeight()); 2168 mRemeasure = false; 2169 } 2170 canvas.save(); 2171 2172 float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; 2173 // offset canvas by the current drag and header position 2174 canvas.translate(-mViewStartX, yTranslate); 2175 // clip to everything below the allDay area 2176 Rect dest = mDestRect; 2177 dest.top = (int) (mFirstCell - yTranslate); 2178 dest.bottom = (int) (mViewHeight - yTranslate); 2179 dest.left = 0; 2180 dest.right = mViewWidth; 2181 canvas.save(); 2182 canvas.clipRect(dest); 2183 // Draw the movable part of the view 2184 doDraw(canvas); 2185 // restore to having no clip 2186 canvas.restore(); 2187 2188 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 2189 float xTranslate; 2190 if (mViewStartX > 0) { 2191 xTranslate = mViewWidth; 2192 } else { 2193 xTranslate = -mViewWidth; 2194 } 2195 // Move the canvas around to prep it for the next view 2196 // specifically, shift it by a screen and undo the 2197 // yTranslation which will be redone in the nextView's onDraw(). 2198 canvas.translate(xTranslate, -yTranslate); 2199 DayView nextView = (DayView) mViewSwitcher.getNextView(); 2200 2201 // Prevent infinite recursive calls to onDraw(). 2202 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 2203 2204 nextView.onDraw(canvas); 2205 // Move it back for this view 2206 canvas.translate(-xTranslate, 0); 2207 } else { 2208 // If we drew another view we already translated it back 2209 // If we didn't draw another view we should be at the edge of the 2210 // screen 2211 canvas.translate(mViewStartX, -yTranslate); 2212 } 2213 2214 // Draw the fixed areas (that don't scroll) directly to the canvas. 2215 drawAfterScroll(canvas); 2216 if (mComputeSelectedEvents && mUpdateToast) { 2217 updateEventDetails(); 2218 mUpdateToast = false; 2219 } 2220 mComputeSelectedEvents = false; 2221 2222 // Draw overscroll glow 2223 if (!mEdgeEffectTop.isFinished()) { 2224 if (DAY_HEADER_HEIGHT != 0) { 2225 canvas.translate(0, DAY_HEADER_HEIGHT); 2226 } 2227 if (mEdgeEffectTop.draw(canvas)) { 2228 invalidate(); 2229 } 2230 if (DAY_HEADER_HEIGHT != 0) { 2231 canvas.translate(0, -DAY_HEADER_HEIGHT); 2232 } 2233 } 2234 if (!mEdgeEffectBottom.isFinished()) { 2235 canvas.rotate(180, mViewWidth/2, mViewHeight/2); 2236 if (mEdgeEffectBottom.draw(canvas)) { 2237 invalidate(); 2238 } 2239 } 2240 canvas.restore(); 2241 } 2242 2243 private void drawAfterScroll(Canvas canvas) { 2244 Paint p = mPaint; 2245 Rect r = mRect; 2246 2247 drawAllDayHighlights(r, canvas, p); 2248 if (mMaxAlldayEvents != 0) { 2249 drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); 2250 drawUpperLeftCorner(r, canvas, p); 2251 } 2252 2253 drawScrollLine(r, canvas, p); 2254 drawDayHeaderLoop(r, canvas, p); 2255 2256 // Draw the AM and PM indicators if we're in 12 hour mode 2257 if (!mIs24HourFormat) { 2258 drawAmPm(canvas, p); 2259 } 2260 } 2261 2262 // This isn't really the upper-left corner. It's the square area just 2263 // below the upper-left corner, above the hours and to the left of the 2264 // all-day area. 2265 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 2266 setupHourTextPaint(p); 2267 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2268 // Draw the allDay expand/collapse icon 2269 if (mUseExpandIcon) { 2270 mExpandAlldayDrawable.setBounds(mExpandAllDayRect); 2271 mExpandAlldayDrawable.draw(canvas); 2272 } else { 2273 mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); 2274 mCollapseAlldayDrawable.draw(canvas); 2275 } 2276 } 2277 } 2278 2279 private void drawScrollLine(Rect r, Canvas canvas, Paint p) { 2280 final int right = computeDayLeftPosition(mNumDays); 2281 final int y = mFirstCell - 1; 2282 2283 p.setAntiAlias(false); 2284 p.setStyle(Style.FILL); 2285 2286 p.setColor(mCalendarGridLineInnerHorizontalColor); 2287 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2288 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 2289 p.setAntiAlias(true); 2290 } 2291 2292 // Computes the x position for the left side of the given day (base 0) 2293 private int computeDayLeftPosition(int day) { 2294 int effectiveWidth = mViewWidth - mHoursWidth; 2295 return day * effectiveWidth / mNumDays + mHoursWidth; 2296 } 2297 2298 private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { 2299 if (mFutureBgColor != 0) { 2300 // First, color the labels area light gray 2301 r.top = 0; 2302 r.bottom = DAY_HEADER_HEIGHT; 2303 r.left = 0; 2304 r.right = mViewWidth; 2305 p.setColor(mBgColor); 2306 p.setStyle(Style.FILL); 2307 canvas.drawRect(r, p); 2308 // and the area that says All day 2309 r.top = DAY_HEADER_HEIGHT; 2310 r.bottom = mFirstCell - 1; 2311 r.left = 0; 2312 r.right = mHoursWidth; 2313 canvas.drawRect(r, p); 2314 2315 int startIndex = -1; 2316 2317 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2318 if (todayIndex < 0) { 2319 // Future 2320 startIndex = 0; 2321 } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { 2322 // Multiday - tomorrow is visible. 2323 startIndex = todayIndex + 1; 2324 } 2325 2326 if (startIndex >= 0) { 2327 // Draw the future highlight 2328 r.top = 0; 2329 r.bottom = mFirstCell - 1; 2330 r.left = computeDayLeftPosition(startIndex) + 1; 2331 r.right = computeDayLeftPosition(mNumDays); 2332 p.setColor(mFutureBgColor); 2333 p.setStyle(Style.FILL); 2334 canvas.drawRect(r, p); 2335 } 2336 } 2337 2338 if (mSelectionAllday && mSelectionMode != SELECTION_HIDDEN) { 2339 // Draw the selection highlight on the selected all-day area 2340 mRect.top = DAY_HEADER_HEIGHT + 1; 2341 mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; 2342 int daynum = mSelectionDay - mFirstJulianDay; 2343 mRect.left = computeDayLeftPosition(daynum) + 1; 2344 mRect.right = computeDayLeftPosition(daynum + 1); 2345 p.setColor(mCalendarGridAreaSelected); 2346 canvas.drawRect(mRect, p); 2347 } 2348 } 2349 2350 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 2351 // Draw the horizontal day background banner 2352 // p.setColor(mCalendarDateBannerBackground); 2353 // r.top = 0; 2354 // r.bottom = DAY_HEADER_HEIGHT; 2355 // r.left = 0; 2356 // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 2357 // canvas.drawRect(r, p); 2358 // 2359 // Fill the extra space on the right side with the default background 2360 // r.left = r.right; 2361 // r.right = mViewWidth; 2362 // p.setColor(mCalendarGridAreaBackground); 2363 // canvas.drawRect(r, p); 2364 if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { 2365 return; 2366 } 2367 2368 p.setTypeface(mBold); 2369 p.setTextAlign(Paint.Align.RIGHT); 2370 int cell = mFirstJulianDay; 2371 2372 String[] dayNames; 2373 if (mDateStrWidth < mCellWidth) { 2374 dayNames = mDayStrs; 2375 } else { 2376 dayNames = mDayStrs2Letter; 2377 } 2378 2379 p.setAntiAlias(true); 2380 for (int day = 0; day < mNumDays; day++, cell++) { 2381 int dayOfWeek = day + mFirstVisibleDayOfWeek; 2382 if (dayOfWeek >= 14) { 2383 dayOfWeek -= 14; 2384 } 2385 2386 int color = mCalendarDateBannerTextColor; 2387 if (mNumDays == 1) { 2388 if (dayOfWeek == Time.SATURDAY) { 2389 color = mWeek_saturdayColor; 2390 } else if (dayOfWeek == Time.SUNDAY) { 2391 color = mWeek_sundayColor; 2392 } 2393 } else { 2394 final int column = day % 7; 2395 if (Utils.isSaturday(column, mFirstDayOfWeek)) { 2396 color = mWeek_saturdayColor; 2397 } else if (Utils.isSunday(column, mFirstDayOfWeek)) { 2398 color = mWeek_sundayColor; 2399 } 2400 } 2401 2402 p.setColor(color); 2403 drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); 2404 } 2405 p.setTypeface(null); 2406 } 2407 2408 private void drawAmPm(Canvas canvas, Paint p) { 2409 p.setColor(mCalendarAmPmLabel); 2410 p.setTextSize(AMPM_TEXT_SIZE); 2411 p.setTypeface(mBold); 2412 p.setAntiAlias(true); 2413 p.setTextAlign(Paint.Align.RIGHT); 2414 String text = mAmString; 2415 if (mFirstHour >= 12) { 2416 text = mPmString; 2417 } 2418 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 2419 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2420 2421 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 2422 // Also draw the "PM" 2423 text = mPmString; 2424 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 2425 + 2 * mHoursTextHeight + HOUR_GAP; 2426 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2427 } 2428 } 2429 2430 private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, 2431 Paint p) { 2432 r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2433 r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2434 2435 r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; 2436 r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); 2437 2438 mCurrentTimeLine.setBounds(r); 2439 mCurrentTimeLine.draw(canvas); 2440 if (mAnimateToday) { 2441 mCurrentTimeAnimateLine.setBounds(r); 2442 mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha); 2443 mCurrentTimeAnimateLine.draw(canvas); 2444 } 2445 } 2446 2447 private void doDraw(Canvas canvas) { 2448 Paint p = mPaint; 2449 Rect r = mRect; 2450 2451 if (mFutureBgColor != 0) { 2452 drawBgColors(r, canvas, p); 2453 } 2454 drawGridBackground(r, canvas, p); 2455 drawHours(r, canvas, p); 2456 2457 // Draw each day 2458 int cell = mFirstJulianDay; 2459 p.setAntiAlias(false); 2460 int alpha = p.getAlpha(); 2461 p.setAlpha(mEventsAlpha); 2462 for (int day = 0; day < mNumDays; day++, cell++) { 2463 // TODO Wow, this needs cleanup. drawEvents loop through all the 2464 // events on every call. 2465 drawEvents(cell, day, HOUR_GAP, canvas, p); 2466 // If this is today 2467 if (cell == mTodayJulianDay) { 2468 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2469 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2470 2471 // And the current time shows up somewhere on the screen 2472 if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 2473 drawCurrentTimeLine(r, day, lineY, canvas, p); 2474 } 2475 } 2476 } 2477 p.setAntiAlias(true); 2478 p.setAlpha(alpha); 2479 2480 drawSelectedRect(r, canvas, p); 2481 } 2482 2483 private void drawSelectedRect(Rect r, Canvas canvas, Paint p) { 2484 // Draw a highlight on the selected hour (if needed) 2485 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { 2486 int daynum = mSelectionDay - mFirstJulianDay; 2487 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2488 r.bottom = r.top + mCellHeight + HOUR_GAP; 2489 r.left = computeDayLeftPosition(daynum) + 1; 2490 r.right = computeDayLeftPosition(daynum + 1) + 1; 2491 2492 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 2493 2494 // Draw the highlight on the grid 2495 p.setColor(mCalendarGridAreaSelected); 2496 r.top += HOUR_GAP; 2497 r.right -= DAY_GAP; 2498 p.setAntiAlias(false); 2499 canvas.drawRect(r, p); 2500 2501 // Draw a "new event hint" on top of the highlight 2502 // For the week view, show a "+", for day view, show "+ New event" 2503 p.setColor(mNewEventHintColor); 2504 if (mNumDays > 1) { 2505 p.setStrokeWidth(NEW_EVENT_WIDTH); 2506 int width = r.right - r.left; 2507 int midX = r.left + width / 2; 2508 int midY = r.top + mCellHeight / 2; 2509 int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2; 2510 length = Math.min(length, NEW_EVENT_MAX_LENGTH); 2511 int verticalPadding = (mCellHeight - length) / 2; 2512 int horizontalPadding = (width - length) / 2; 2513 canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, 2514 midY, p); 2515 canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p); 2516 } else { 2517 p.setStyle(Paint.Style.FILL); 2518 p.setTextSize(NEW_EVENT_HINT_FONT_SIZE); 2519 p.setTextAlign(Paint.Align.LEFT); 2520 p.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 2521 canvas.drawText(mNewEventHintString, r.left + EVENT_TEXT_LEFT_MARGIN, 2522 r.top + Math.abs(p.getFontMetrics().ascent) + EVENT_TEXT_TOP_MARGIN , p); 2523 } 2524 } 2525 } 2526 2527 private void drawHours(Rect r, Canvas canvas, Paint p) { 2528 setupHourTextPaint(p); 2529 2530 int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; 2531 2532 for (int i = 0; i < 24; i++) { 2533 String time = mHourStrs[i]; 2534 canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); 2535 y += mCellHeight + HOUR_GAP; 2536 } 2537 } 2538 2539 private void setupHourTextPaint(Paint p) { 2540 p.setColor(mCalendarHourLabelColor); 2541 p.setTextSize(HOURS_TEXT_SIZE); 2542 p.setTypeface(Typeface.DEFAULT); 2543 p.setTextAlign(Paint.Align.RIGHT); 2544 p.setAntiAlias(true); 2545 } 2546 2547 private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { 2548 int dateNum = mFirstVisibleDate + day; 2549 int x; 2550 if (dateNum > mMonthLength) { 2551 dateNum -= mMonthLength; 2552 } 2553 p.setAntiAlias(true); 2554 2555 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2556 // Draw day of the month 2557 String dateNumStr = String.valueOf(dateNum); 2558 if (mNumDays > 1) { 2559 float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; 2560 2561 // Draw day of the month 2562 x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; 2563 p.setTextAlign(Align.RIGHT); 2564 p.setTextSize(DATE_HEADER_FONT_SIZE); 2565 2566 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2567 canvas.drawText(dateNumStr, x, y, p); 2568 2569 // Draw day of the week 2570 x -= p.measureText(" " + dateNumStr); 2571 p.setTextSize(DAY_HEADER_FONT_SIZE); 2572 p.setTypeface(Typeface.DEFAULT); 2573 canvas.drawText(dayStr, x, y, p); 2574 } else { 2575 float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; 2576 p.setTextAlign(Align.LEFT); 2577 2578 2579 // Draw day of the week 2580 x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; 2581 p.setTextSize(DAY_HEADER_FONT_SIZE); 2582 p.setTypeface(Typeface.DEFAULT); 2583 canvas.drawText(dayStr, x, y, p); 2584 2585 // Draw day of the month 2586 x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; 2587 p.setTextSize(DATE_HEADER_FONT_SIZE); 2588 p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); 2589 canvas.drawText(dateNumStr, x, y, p); 2590 } 2591 } 2592 2593 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 2594 Paint.Style savedStyle = p.getStyle(); 2595 2596 final float stopX = computeDayLeftPosition(mNumDays); 2597 float y = 0; 2598 final float deltaY = mCellHeight + HOUR_GAP; 2599 int linesIndex = 0; 2600 final float startY = 0; 2601 final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 2602 float x = mHoursWidth; 2603 2604 // Draw the inner horizontal grid lines 2605 p.setColor(mCalendarGridLineInnerHorizontalColor); 2606 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2607 p.setAntiAlias(false); 2608 y = 0; 2609 linesIndex = 0; 2610 for (int hour = 0; hour <= 24; hour++) { 2611 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2612 mLines[linesIndex++] = y; 2613 mLines[linesIndex++] = stopX; 2614 mLines[linesIndex++] = y; 2615 y += deltaY; 2616 } 2617 if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { 2618 canvas.drawLines(mLines, 0, linesIndex, p); 2619 linesIndex = 0; 2620 p.setColor(mCalendarGridLineInnerVerticalColor); 2621 } 2622 2623 // Draw the inner vertical grid lines 2624 for (int day = 0; day <= mNumDays; day++) { 2625 x = computeDayLeftPosition(day); 2626 mLines[linesIndex++] = x; 2627 mLines[linesIndex++] = startY; 2628 mLines[linesIndex++] = x; 2629 mLines[linesIndex++] = stopY; 2630 } 2631 canvas.drawLines(mLines, 0, linesIndex, p); 2632 2633 // Restore the saved style. 2634 p.setStyle(savedStyle); 2635 p.setAntiAlias(true); 2636 } 2637 2638 /** 2639 * @param r 2640 * @param canvas 2641 * @param p 2642 */ 2643 private void drawBgColors(Rect r, Canvas canvas, Paint p) { 2644 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2645 // Draw the hours background color 2646 r.top = mDestRect.top; 2647 r.bottom = mDestRect.bottom; 2648 r.left = 0; 2649 r.right = mHoursWidth; 2650 p.setColor(mBgColor); 2651 p.setStyle(Style.FILL); 2652 p.setAntiAlias(false); 2653 canvas.drawRect(r, p); 2654 2655 // Draw background for grid area 2656 if (mNumDays == 1 && todayIndex == 0) { 2657 // Draw a white background for the time later than current time 2658 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2659 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2660 if (lineY < mViewStartY + mViewHeight) { 2661 lineY = Math.max(lineY, mViewStartY); 2662 r.left = mHoursWidth; 2663 r.right = mViewWidth; 2664 r.top = lineY; 2665 r.bottom = mViewStartY + mViewHeight; 2666 p.setColor(mFutureBgColor); 2667 canvas.drawRect(r, p); 2668 } 2669 } else if (todayIndex >= 0 && todayIndex < mNumDays) { 2670 // Draw today with a white background for the time later than current time 2671 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2672 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2673 if (lineY < mViewStartY + mViewHeight) { 2674 lineY = Math.max(lineY, mViewStartY); 2675 r.left = computeDayLeftPosition(todayIndex) + 1; 2676 r.right = computeDayLeftPosition(todayIndex + 1); 2677 r.top = lineY; 2678 r.bottom = mViewStartY + mViewHeight; 2679 p.setColor(mFutureBgColor); 2680 canvas.drawRect(r, p); 2681 } 2682 2683 // Paint Tomorrow and later days with future color 2684 if (todayIndex + 1 < mNumDays) { 2685 r.left = computeDayLeftPosition(todayIndex + 1) + 1; 2686 r.right = computeDayLeftPosition(mNumDays); 2687 r.top = mDestRect.top; 2688 r.bottom = mDestRect.bottom; 2689 p.setColor(mFutureBgColor); 2690 canvas.drawRect(r, p); 2691 } 2692 } else if (todayIndex < 0) { 2693 // Future 2694 r.left = computeDayLeftPosition(0) + 1; 2695 r.right = computeDayLeftPosition(mNumDays); 2696 r.top = mDestRect.top; 2697 r.bottom = mDestRect.bottom; 2698 p.setColor(mFutureBgColor); 2699 canvas.drawRect(r, p); 2700 } 2701 p.setAntiAlias(true); 2702 } 2703 2704 Event getSelectedEvent() { 2705 if (mSelectedEvent == null) { 2706 // There is no event at the selected hour, so create a new event. 2707 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2708 getSelectedMinutesSinceMidnight()); 2709 } 2710 return mSelectedEvent; 2711 } 2712 2713 boolean isEventSelected() { 2714 return (mSelectedEvent != null); 2715 } 2716 2717 Event getNewEvent() { 2718 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2719 getSelectedMinutesSinceMidnight()); 2720 } 2721 2722 static Event getNewEvent(int julianDay, long utcMillis, 2723 int minutesSinceMidnight) { 2724 Event event = Event.newInstance(); 2725 event.startDay = julianDay; 2726 event.endDay = julianDay; 2727 event.startMillis = utcMillis; 2728 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 2729 event.startTime = minutesSinceMidnight; 2730 event.endTime = event.startTime + MINUTES_PER_HOUR; 2731 return event; 2732 } 2733 2734 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 2735 float maxWidthF = 0.0f; 2736 2737 int len = strings.length; 2738 for (int i = 0; i < len; i++) { 2739 float width = p.measureText(strings[i]); 2740 maxWidthF = Math.max(width, maxWidthF); 2741 } 2742 int maxWidth = (int) (maxWidthF + 0.5); 2743 if (maxWidth < currentMax) { 2744 maxWidth = currentMax; 2745 } 2746 return maxWidth; 2747 } 2748 2749 private void saveSelectionPosition(float left, float top, float right, float bottom) { 2750 mPrevBox.left = (int) left; 2751 mPrevBox.right = (int) right; 2752 mPrevBox.top = (int) top; 2753 mPrevBox.bottom = (int) bottom; 2754 } 2755 2756 private Rect getCurrentSelectionPosition() { 2757 Rect box = new Rect(); 2758 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2759 box.bottom = box.top + mCellHeight + HOUR_GAP; 2760 int daynum = mSelectionDay - mFirstJulianDay; 2761 box.left = computeDayLeftPosition(daynum) + 1; 2762 box.right = computeDayLeftPosition(daynum + 1); 2763 return box; 2764 } 2765 2766 private void setupTextRect(Rect r) { 2767 if (r.bottom <= r.top || r.right <= r.left) { 2768 r.bottom = r.top; 2769 r.right = r.left; 2770 return; 2771 } 2772 2773 if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { 2774 r.top += EVENT_TEXT_TOP_MARGIN; 2775 r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; 2776 } 2777 if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { 2778 r.left += EVENT_TEXT_LEFT_MARGIN; 2779 r.right -= EVENT_TEXT_RIGHT_MARGIN; 2780 } 2781 } 2782 2783 private void setupAllDayTextRect(Rect r) { 2784 if (r.bottom <= r.top || r.right <= r.left) { 2785 r.bottom = r.top; 2786 r.right = r.left; 2787 return; 2788 } 2789 2790 if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { 2791 r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; 2792 r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; 2793 } 2794 if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { 2795 r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2796 r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2797 } 2798 } 2799 2800 /** 2801 * Return the layout for a numbered event. Create it if not already existing 2802 */ 2803 private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, 2804 Rect r) { 2805 if (i < 0 || i >= layouts.length) { 2806 return null; 2807 } 2808 2809 StaticLayout layout = layouts[i]; 2810 // Check if we have already initialized the StaticLayout and that 2811 // the width hasn't changed (due to vertical resizing which causes 2812 // re-layout of events at min height) 2813 if (layout == null || r.width() != layout.getWidth()) { 2814 SpannableStringBuilder bob = new SpannableStringBuilder(); 2815 if (event.title != null) { 2816 // MAX - 1 since we add a space 2817 bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1)); 2818 bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); 2819 bob.append(' '); 2820 } 2821 if (event.location != null) { 2822 bob.append(drawTextSanitizer(event.location.toString(), 2823 MAX_EVENT_TEXT_LEN - bob.length())); 2824 } 2825 2826 switch (event.selfAttendeeStatus) { 2827 case Attendees.ATTENDEE_STATUS_INVITED: 2828 paint.setColor(event.color); 2829 break; 2830 case Attendees.ATTENDEE_STATUS_DECLINED: 2831 paint.setColor(mEventTextColor); 2832 paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA); 2833 break; 2834 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 2835 case Attendees.ATTENDEE_STATUS_ACCEPTED: 2836 case Attendees.ATTENDEE_STATUS_TENTATIVE: 2837 default: 2838 paint.setColor(mEventTextColor); 2839 break; 2840 } 2841 2842 // Leave a one pixel boundary on the left and right of the rectangle for the event 2843 layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), 2844 Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); 2845 2846 layouts[i] = layout; 2847 } 2848 layout.getPaint().setAlpha(mEventsAlpha); 2849 return layout; 2850 } 2851 2852 private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { 2853 2854 p.setTextSize(NORMAL_FONT_SIZE); 2855 p.setTextAlign(Paint.Align.LEFT); 2856 Paint eventTextPaint = mEventTextPaint; 2857 2858 final float startY = DAY_HEADER_HEIGHT; 2859 final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; 2860 float x = 0; 2861 int linesIndex = 0; 2862 2863 // Draw the inner vertical grid lines 2864 p.setColor(mCalendarGridLineInnerVerticalColor); 2865 x = mHoursWidth; 2866 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2867 // Line bounding the top of the all day area 2868 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2869 mLines[linesIndex++] = startY; 2870 mLines[linesIndex++] = computeDayLeftPosition(mNumDays); 2871 mLines[linesIndex++] = startY; 2872 2873 for (int day = 0; day <= mNumDays; day++) { 2874 x = computeDayLeftPosition(day); 2875 mLines[linesIndex++] = x; 2876 mLines[linesIndex++] = startY; 2877 mLines[linesIndex++] = x; 2878 mLines[linesIndex++] = stopY; 2879 } 2880 p.setAntiAlias(false); 2881 canvas.drawLines(mLines, 0, linesIndex, p); 2882 p.setStyle(Style.FILL); 2883 2884 int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 2885 int lastDay = firstDay + numDays - 1; 2886 final ArrayList<Event> events = mAllDayEvents; 2887 int numEvents = events.size(); 2888 // Whether or not we should draw the more events text 2889 boolean hasMoreEvents = false; 2890 // size of the allDay area 2891 float drawHeight = mAlldayHeight; 2892 // max number of events being drawn in one day of the allday area 2893 float numRectangles = mMaxAlldayEvents; 2894 // Where to cut off drawn allday events 2895 int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; 2896 // The number of events that weren't drawn in each day 2897 mSkippedAlldayEvents = new int[numDays]; 2898 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && 2899 mAnimateDayHeight == 0) { 2900 // We draw one fewer event than will fit so that more events text 2901 // can be drawn 2902 numRectangles = mMaxUnexpandedAlldayEventCount - 1; 2903 // We also clip the events above the more events text 2904 allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 2905 hasMoreEvents = true; 2906 } else if (mAnimateDayHeight != 0) { 2907 // clip at the end of the animating space 2908 allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; 2909 } 2910 2911 int alpha = eventTextPaint.getAlpha(); 2912 eventTextPaint.setAlpha(mEventsAlpha); 2913 for (int i = 0; i < numEvents; i++) { 2914 Event event = events.get(i); 2915 int startDay = event.startDay; 2916 int endDay = event.endDay; 2917 if (startDay > lastDay || endDay < firstDay) { 2918 continue; 2919 } 2920 if (startDay < firstDay) { 2921 startDay = firstDay; 2922 } 2923 if (endDay > lastDay) { 2924 endDay = lastDay; 2925 } 2926 int startIndex = startDay - firstDay; 2927 int endIndex = endDay - firstDay; 2928 float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : 2929 drawHeight / numRectangles; 2930 2931 // Prevent a single event from getting too big 2932 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 2933 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 2934 } 2935 2936 // Leave a one-pixel space between the vertical day lines and the 2937 // event rectangle. 2938 event.left = computeDayLeftPosition(startIndex); 2939 event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; 2940 event.top = y + height * event.getColumn(); 2941 event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; 2942 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2943 // check if we should skip this event. We skip if it starts 2944 // after the clip bound or ends after the skip bound and we're 2945 // not animating. 2946 if (event.top >= allDayEventClip) { 2947 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2948 continue; 2949 } else if (event.bottom > allDayEventClip) { 2950 if (hasMoreEvents) { 2951 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2952 continue; 2953 } 2954 event.bottom = allDayEventClip; 2955 } 2956 } 2957 Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, 2958 (int) event.bottom); 2959 setupAllDayTextRect(r); 2960 StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); 2961 drawEventText(layout, r, canvas, r.top, r.bottom, true); 2962 2963 // Check if this all-day event intersects the selected day 2964 if (mSelectionAllday && mComputeSelectedEvents) { 2965 if (startDay <= mSelectionDay && endDay >= mSelectionDay) { 2966 mSelectedEvents.add(event); 2967 } 2968 } 2969 } 2970 eventTextPaint.setAlpha(alpha); 2971 2972 if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { 2973 // If the more allday text should be visible, draw it. 2974 alpha = p.getAlpha(); 2975 p.setAlpha(mEventsAlpha); 2976 p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); 2977 for (int i = 0; i < mSkippedAlldayEvents.length; i++) { 2978 if (mSkippedAlldayEvents[i] > 0) { 2979 drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); 2980 } 2981 } 2982 p.setAlpha(alpha); 2983 } 2984 2985 if (mSelectionAllday) { 2986 // Compute the neighbors for the list of all-day events that 2987 // intersect the selected day. 2988 computeAllDayNeighbors(); 2989 2990 // Set the selection position to zero so that when we move down 2991 // to the normal event area, we will highlight the topmost event. 2992 saveSelectionPosition(0f, 0f, 0f, 0f); 2993 } 2994 } 2995 2996 // Helper method for counting the number of allday events skipped on each day 2997 private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { 2998 if (counts == null || startIndex < 0 || endIndex > counts.length) { 2999 return; 3000 } 3001 for (int i = startIndex; i <= endIndex; i++) { 3002 counts[i]++; 3003 } 3004 } 3005 3006 // Draws the "box +n" text for hidden allday events 3007 protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { 3008 int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 3009 int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f 3010 * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); 3011 Rect r = mRect; 3012 r.top = y; 3013 r.left = x; 3014 r.bottom = y + EVENT_SQUARE_WIDTH; 3015 r.right = x + EVENT_SQUARE_WIDTH; 3016 p.setColor(mMoreEventsTextColor); 3017 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 3018 p.setStyle(Style.STROKE); 3019 p.setAntiAlias(false); 3020 canvas.drawRect(r, p); 3021 p.setAntiAlias(true); 3022 p.setStyle(Style.FILL); 3023 p.setTextSize(EVENT_TEXT_FONT_SIZE); 3024 String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); 3025 y += EVENT_SQUARE_WIDTH; 3026 x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; 3027 canvas.drawText(String.format(text, remainingEvents), x, y, p); 3028 } 3029 3030 private void computeAllDayNeighbors() { 3031 int len = mSelectedEvents.size(); 3032 if (len == 0 || mSelectedEvent != null) { 3033 return; 3034 } 3035 3036 // First, clear all the links 3037 for (int ii = 0; ii < len; ii++) { 3038 Event ev = mSelectedEvents.get(ii); 3039 ev.nextUp = null; 3040 ev.nextDown = null; 3041 ev.nextLeft = null; 3042 ev.nextRight = null; 3043 } 3044 3045 // For each event in the selected event list "mSelectedEvents", find 3046 // its neighbors in the up and down directions. This could be done 3047 // more efficiently by sorting on the Event.getColumn() field, but 3048 // the list is expected to be very small. 3049 3050 // Find the event in the same row as the previously selected all-day 3051 // event, if any. 3052 int startPosition = -1; 3053 if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) { 3054 startPosition = mPrevSelectedEvent.getColumn(); 3055 } 3056 int maxPosition = -1; 3057 Event startEvent = null; 3058 Event maxPositionEvent = null; 3059 for (int ii = 0; ii < len; ii++) { 3060 Event ev = mSelectedEvents.get(ii); 3061 int position = ev.getColumn(); 3062 if (position == startPosition) { 3063 startEvent = ev; 3064 } else if (position > maxPosition) { 3065 maxPositionEvent = ev; 3066 maxPosition = position; 3067 } 3068 for (int jj = 0; jj < len; jj++) { 3069 if (jj == ii) { 3070 continue; 3071 } 3072 Event neighbor = mSelectedEvents.get(jj); 3073 int neighborPosition = neighbor.getColumn(); 3074 if (neighborPosition == position - 1) { 3075 ev.nextUp = neighbor; 3076 } else if (neighborPosition == position + 1) { 3077 ev.nextDown = neighbor; 3078 } 3079 } 3080 } 3081 if (startEvent != null) { 3082 setSelectedEvent(startEvent); 3083 } else { 3084 setSelectedEvent(maxPositionEvent); 3085 } 3086 } 3087 3088 private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { 3089 Paint eventTextPaint = mEventTextPaint; 3090 int left = computeDayLeftPosition(dayIndex) + 1; 3091 int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; 3092 int cellHeight = mCellHeight; 3093 3094 // Use the selected hour as the selection region 3095 Rect selectionArea = mSelectionRect; 3096 selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); 3097 selectionArea.bottom = selectionArea.top + cellHeight; 3098 selectionArea.left = left; 3099 selectionArea.right = selectionArea.left + cellWidth; 3100 3101 final ArrayList<Event> events = mEvents; 3102 int numEvents = events.size(); 3103 EventGeometry geometry = mEventGeometry; 3104 3105 final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; 3106 3107 int alpha = eventTextPaint.getAlpha(); 3108 eventTextPaint.setAlpha(mEventsAlpha); 3109 for (int i = 0; i < numEvents; i++) { 3110 Event event = events.get(i); 3111 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 3112 continue; 3113 } 3114 3115 // Don't draw it if it is not visible 3116 if (event.bottom < mViewStartY || event.top > viewEndY) { 3117 continue; 3118 } 3119 3120 if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents 3121 && geometry.eventIntersectsSelection(event, selectionArea)) { 3122 mSelectedEvents.add(event); 3123 } 3124 3125 Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); 3126 setupTextRect(r); 3127 3128 // Don't draw text if it is not visible 3129 if (r.top > viewEndY || r.bottom < mViewStartY) { 3130 continue; 3131 } 3132 StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); 3133 // TODO: not sure why we are 4 pixels off 3134 drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight 3135 - DAY_HEADER_HEIGHT - mAlldayHeight, false); 3136 } 3137 eventTextPaint.setAlpha(alpha); 3138 3139 if (date == mSelectionDay && !mSelectionAllday && isFocused() 3140 && mSelectionMode != SELECTION_HIDDEN) { 3141 computeNeighbors(); 3142 } 3143 } 3144 3145 // Computes the "nearest" neighbor event in four directions (left, right, 3146 // up, down) for each of the events in the mSelectedEvents array. 3147 private void computeNeighbors() { 3148 int len = mSelectedEvents.size(); 3149 if (len == 0 || mSelectedEvent != null) { 3150 return; 3151 } 3152 3153 // First, clear all the links 3154 for (int ii = 0; ii < len; ii++) { 3155 Event ev = mSelectedEvents.get(ii); 3156 ev.nextUp = null; 3157 ev.nextDown = null; 3158 ev.nextLeft = null; 3159 ev.nextRight = null; 3160 } 3161 3162 Event startEvent = mSelectedEvents.get(0); 3163 int startEventDistance1 = 100000; // any large number 3164 int startEventDistance2 = 100000; // any large number 3165 int prevLocation = FROM_NONE; 3166 int prevTop; 3167 int prevBottom; 3168 int prevLeft; 3169 int prevRight; 3170 int prevCenter = 0; 3171 Rect box = getCurrentSelectionPosition(); 3172 if (mPrevSelectedEvent != null) { 3173 prevTop = (int) mPrevSelectedEvent.top; 3174 prevBottom = (int) mPrevSelectedEvent.bottom; 3175 prevLeft = (int) mPrevSelectedEvent.left; 3176 prevRight = (int) mPrevSelectedEvent.right; 3177 // Check if the previously selected event intersects the previous 3178 // selection box. (The previously selected event may be from a 3179 // much older selection box.) 3180 if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top 3181 || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { 3182 mPrevSelectedEvent = null; 3183 prevTop = mPrevBox.top; 3184 prevBottom = mPrevBox.bottom; 3185 prevLeft = mPrevBox.left; 3186 prevRight = mPrevBox.right; 3187 } else { 3188 // Clip the top and bottom to the previous selection box. 3189 if (prevTop < mPrevBox.top) { 3190 prevTop = mPrevBox.top; 3191 } 3192 if (prevBottom > mPrevBox.bottom) { 3193 prevBottom = mPrevBox.bottom; 3194 } 3195 } 3196 } else { 3197 // Just use the previously drawn selection box 3198 prevTop = mPrevBox.top; 3199 prevBottom = mPrevBox.bottom; 3200 prevLeft = mPrevBox.left; 3201 prevRight = mPrevBox.right; 3202 } 3203 3204 // Figure out where we came from and compute the center of that area. 3205 if (prevLeft >= box.right) { 3206 // The previously selected event was to the right of us. 3207 prevLocation = FROM_RIGHT; 3208 prevCenter = (prevTop + prevBottom) / 2; 3209 } else if (prevRight <= box.left) { 3210 // The previously selected event was to the left of us. 3211 prevLocation = FROM_LEFT; 3212 prevCenter = (prevTop + prevBottom) / 2; 3213 } else if (prevBottom <= box.top) { 3214 // The previously selected event was above us. 3215 prevLocation = FROM_ABOVE; 3216 prevCenter = (prevLeft + prevRight) / 2; 3217 } else if (prevTop >= box.bottom) { 3218 // The previously selected event was below us. 3219 prevLocation = FROM_BELOW; 3220 prevCenter = (prevLeft + prevRight) / 2; 3221 } 3222 3223 // For each event in the selected event list "mSelectedEvents", search 3224 // all the other events in that list for the nearest neighbor in 4 3225 // directions. 3226 for (int ii = 0; ii < len; ii++) { 3227 Event ev = mSelectedEvents.get(ii); 3228 3229 int startTime = ev.startTime; 3230 int endTime = ev.endTime; 3231 int left = (int) ev.left; 3232 int right = (int) ev.right; 3233 int top = (int) ev.top; 3234 if (top < box.top) { 3235 top = box.top; 3236 } 3237 int bottom = (int) ev.bottom; 3238 if (bottom > box.bottom) { 3239 bottom = box.bottom; 3240 } 3241 // if (false) { 3242 // int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL 3243 // | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3244 // if (DateFormat.is24HourFormat(mContext)) { 3245 // flags |= DateUtils.FORMAT_24HOUR; 3246 // } 3247 // String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis, 3248 // ev.endMillis, flags); 3249 // Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: " 3250 // + bottom + " ev: " + timeRange + " " + ev.title); 3251 // } 3252 int upDistanceMin = 10000; // any large number 3253 int downDistanceMin = 10000; // any large number 3254 int leftDistanceMin = 10000; // any large number 3255 int rightDistanceMin = 10000; // any large number 3256 Event upEvent = null; 3257 Event downEvent = null; 3258 Event leftEvent = null; 3259 Event rightEvent = null; 3260 3261 // Pick the starting event closest to the previously selected event, 3262 // if any. distance1 takes precedence over distance2. 3263 int distance1 = 0; 3264 int distance2 = 0; 3265 if (prevLocation == FROM_ABOVE) { 3266 if (left >= prevCenter) { 3267 distance1 = left - prevCenter; 3268 } else if (right <= prevCenter) { 3269 distance1 = prevCenter - right; 3270 } 3271 distance2 = top - prevBottom; 3272 } else if (prevLocation == FROM_BELOW) { 3273 if (left >= prevCenter) { 3274 distance1 = left - prevCenter; 3275 } else if (right <= prevCenter) { 3276 distance1 = prevCenter - right; 3277 } 3278 distance2 = prevTop - bottom; 3279 } else if (prevLocation == FROM_LEFT) { 3280 if (bottom <= prevCenter) { 3281 distance1 = prevCenter - bottom; 3282 } else if (top >= prevCenter) { 3283 distance1 = top - prevCenter; 3284 } 3285 distance2 = left - prevRight; 3286 } else if (prevLocation == FROM_RIGHT) { 3287 if (bottom <= prevCenter) { 3288 distance1 = prevCenter - bottom; 3289 } else if (top >= prevCenter) { 3290 distance1 = top - prevCenter; 3291 } 3292 distance2 = prevLeft - right; 3293 } 3294 if (distance1 < startEventDistance1 3295 || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { 3296 startEvent = ev; 3297 startEventDistance1 = distance1; 3298 startEventDistance2 = distance2; 3299 } 3300 3301 // For each neighbor, figure out if it is above or below or left 3302 // or right of me and compute the distance. 3303 for (int jj = 0; jj < len; jj++) { 3304 if (jj == ii) { 3305 continue; 3306 } 3307 Event neighbor = mSelectedEvents.get(jj); 3308 int neighborLeft = (int) neighbor.left; 3309 int neighborRight = (int) neighbor.right; 3310 if (neighbor.endTime <= startTime) { 3311 // This neighbor is entirely above me. 3312 // If we overlap the same column, then compute the distance. 3313 if (neighborLeft < right && neighborRight > left) { 3314 int distance = startTime - neighbor.endTime; 3315 if (distance < upDistanceMin) { 3316 upDistanceMin = distance; 3317 upEvent = neighbor; 3318 } else if (distance == upDistanceMin) { 3319 int center = (left + right) / 2; 3320 int currentDistance = 0; 3321 int currentLeft = (int) upEvent.left; 3322 int currentRight = (int) upEvent.right; 3323 if (currentRight <= center) { 3324 currentDistance = center - currentRight; 3325 } else if (currentLeft >= center) { 3326 currentDistance = currentLeft - center; 3327 } 3328 3329 int neighborDistance = 0; 3330 if (neighborRight <= center) { 3331 neighborDistance = center - neighborRight; 3332 } else if (neighborLeft >= center) { 3333 neighborDistance = neighborLeft - center; 3334 } 3335 if (neighborDistance < currentDistance) { 3336 upDistanceMin = distance; 3337 upEvent = neighbor; 3338 } 3339 } 3340 } 3341 } else if (neighbor.startTime >= endTime) { 3342 // This neighbor is entirely below me. 3343 // If we overlap the same column, then compute the distance. 3344 if (neighborLeft < right && neighborRight > left) { 3345 int distance = neighbor.startTime - endTime; 3346 if (distance < downDistanceMin) { 3347 downDistanceMin = distance; 3348 downEvent = neighbor; 3349 } else if (distance == downDistanceMin) { 3350 int center = (left + right) / 2; 3351 int currentDistance = 0; 3352 int currentLeft = (int) downEvent.left; 3353 int currentRight = (int) downEvent.right; 3354 if (currentRight <= center) { 3355 currentDistance = center - currentRight; 3356 } else if (currentLeft >= center) { 3357 currentDistance = currentLeft - center; 3358 } 3359 3360 int neighborDistance = 0; 3361 if (neighborRight <= center) { 3362 neighborDistance = center - neighborRight; 3363 } else if (neighborLeft >= center) { 3364 neighborDistance = neighborLeft - center; 3365 } 3366 if (neighborDistance < currentDistance) { 3367 downDistanceMin = distance; 3368 downEvent = neighbor; 3369 } 3370 } 3371 } 3372 } 3373 3374 if (neighborLeft >= right) { 3375 // This neighbor is entirely to the right of me. 3376 // Take the closest neighbor in the y direction. 3377 int center = (top + bottom) / 2; 3378 int distance = 0; 3379 int neighborBottom = (int) neighbor.bottom; 3380 int neighborTop = (int) neighbor.top; 3381 if (neighborBottom <= center) { 3382 distance = center - neighborBottom; 3383 } else if (neighborTop >= center) { 3384 distance = neighborTop - center; 3385 } 3386 if (distance < rightDistanceMin) { 3387 rightDistanceMin = distance; 3388 rightEvent = neighbor; 3389 } else if (distance == rightDistanceMin) { 3390 // Pick the closest in the x direction 3391 int neighborDistance = neighborLeft - right; 3392 int currentDistance = (int) rightEvent.left - right; 3393 if (neighborDistance < currentDistance) { 3394 rightDistanceMin = distance; 3395 rightEvent = neighbor; 3396 } 3397 } 3398 } else if (neighborRight <= left) { 3399 // This neighbor is entirely to the left of me. 3400 // Take the closest neighbor in the y direction. 3401 int center = (top + bottom) / 2; 3402 int distance = 0; 3403 int neighborBottom = (int) neighbor.bottom; 3404 int neighborTop = (int) neighbor.top; 3405 if (neighborBottom <= center) { 3406 distance = center - neighborBottom; 3407 } else if (neighborTop >= center) { 3408 distance = neighborTop - center; 3409 } 3410 if (distance < leftDistanceMin) { 3411 leftDistanceMin = distance; 3412 leftEvent = neighbor; 3413 } else if (distance == leftDistanceMin) { 3414 // Pick the closest in the x direction 3415 int neighborDistance = left - neighborRight; 3416 int currentDistance = left - (int) leftEvent.right; 3417 if (neighborDistance < currentDistance) { 3418 leftDistanceMin = distance; 3419 leftEvent = neighbor; 3420 } 3421 } 3422 } 3423 } 3424 ev.nextUp = upEvent; 3425 ev.nextDown = downEvent; 3426 ev.nextLeft = leftEvent; 3427 ev.nextRight = rightEvent; 3428 } 3429 setSelectedEvent(startEvent); 3430 } 3431 3432 private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, 3433 int visibleTop, int visibleBot) { 3434 // Draw the Event Rect 3435 Rect r = mRect; 3436 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); 3437 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); 3438 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3439 r.right = (int) event.right; 3440 3441 int color; 3442 if (event == mClickedEvent) { 3443 color = mClickedColor; 3444 } else { 3445 color = event.color; 3446 } 3447 3448 switch (event.selfAttendeeStatus) { 3449 case Attendees.ATTENDEE_STATUS_INVITED: 3450 if (event != mClickedEvent) { 3451 p.setStyle(Style.STROKE); 3452 } 3453 break; 3454 case Attendees.ATTENDEE_STATUS_DECLINED: 3455 if (event != mClickedEvent) { 3456 color = Utils.getDeclinedColorFromColor(color); 3457 } 3458 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 3459 case Attendees.ATTENDEE_STATUS_ACCEPTED: 3460 case Attendees.ATTENDEE_STATUS_TENTATIVE: 3461 default: 3462 p.setStyle(Style.FILL_AND_STROKE); 3463 break; 3464 } 3465 3466 p.setAntiAlias(false); 3467 3468 int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); 3469 int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); 3470 r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); 3471 r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, 3472 visibleBot); 3473 r.left += floorHalfStroke; 3474 r.right -= ceilHalfStroke; 3475 p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); 3476 p.setColor(color); 3477 int alpha = p.getAlpha(); 3478 p.setAlpha(mEventsAlpha); 3479 canvas.drawRect(r, p); 3480 p.setAlpha(alpha); 3481 p.setStyle(Style.FILL); 3482 3483 // If this event is selected, then use the selection color 3484 if (mSelectedEvent == event && mClickedEvent != null) { 3485 boolean paintIt = false; 3486 color = 0; 3487 if (mSelectionMode == SELECTION_PRESSED) { 3488 // Also, remember the last selected event that we drew 3489 mPrevSelectedEvent = event; 3490 color = mPressedColor; 3491 paintIt = true; 3492 } else if (mSelectionMode == SELECTION_SELECTED) { 3493 // Also, remember the last selected event that we drew 3494 mPrevSelectedEvent = event; 3495 color = mPressedColor; 3496 paintIt = true; 3497 } 3498 3499 if (paintIt) { 3500 p.setColor(color); 3501 canvas.drawRect(r, p); 3502 } 3503 p.setAntiAlias(true); 3504 } 3505 3506 // Draw cal color square border 3507 // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; 3508 // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; 3509 // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; 3510 // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; 3511 // p.setColor(0xFFFFFFFF); 3512 // canvas.drawRect(r, p); 3513 3514 // Draw cal color 3515 // r.top++; 3516 // r.left++; 3517 // r.bottom--; 3518 // r.right--; 3519 // p.setColor(event.color); 3520 // canvas.drawRect(r, p); 3521 3522 // Setup rect for drawEventText which follows 3523 r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; 3524 r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; 3525 r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; 3526 r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; 3527 return r; 3528 } 3529 3530 private final Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); 3531 3532 // Sanitize a string before passing it to drawText or else we get little 3533 // squares. For newlines and tabs before a comma, delete the character. 3534 // Otherwise, just replace them with a space. 3535 private String drawTextSanitizer(String string, int maxEventTextLen) { 3536 Matcher m = drawTextSanitizerFilter.matcher(string); 3537 string = m.replaceAll(","); 3538 3539 int len = string.length(); 3540 if (maxEventTextLen <= 0) { 3541 string = ""; 3542 len = 0; 3543 } else if (len > maxEventTextLen) { 3544 string = string.substring(0, maxEventTextLen); 3545 len = maxEventTextLen; 3546 } 3547 3548 return string.replace('\n', ' '); 3549 } 3550 3551 private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, 3552 int bottom, boolean center) { 3553 // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging 3554 3555 int width = rect.right - rect.left; 3556 int height = rect.bottom - rect.top; 3557 3558 // If the rectangle is too small for text, then return 3559 if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { 3560 return; 3561 } 3562 3563 int totalLineHeight = 0; 3564 int lineCount = eventLayout.getLineCount(); 3565 for (int i = 0; i < lineCount; i++) { 3566 int lineBottom = eventLayout.getLineBottom(i); 3567 if (lineBottom <= height) { 3568 totalLineHeight = lineBottom; 3569 } else { 3570 break; 3571 } 3572 } 3573 3574 if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { 3575 return; 3576 } 3577 3578 // Use a StaticLayout to format the string. 3579 canvas.save(); 3580 // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); 3581 int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0; 3582 canvas.translate(rect.left, rect.top + padding); 3583 rect.left = 0; 3584 rect.right = width; 3585 rect.top = 0; 3586 rect.bottom = totalLineHeight; 3587 3588 // There's a bug somewhere. If this rect is outside of a previous 3589 // cliprect, this becomes a no-op. What happens is that the text draw 3590 // past the event rect. The current fix is to not draw the staticLayout 3591 // at all if it is completely out of bound. 3592 canvas.clipRect(rect); 3593 eventLayout.draw(canvas); 3594 canvas.restore(); 3595 } 3596 3597 // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it 3598 // doesn't work well with hardware acceleration 3599 // private void drawEmptyRect(Canvas canvas, Rect r, int color) { 3600 // int linesIndex = 0; 3601 // mLines[linesIndex++] = r.left; 3602 // mLines[linesIndex++] = r.top; 3603 // mLines[linesIndex++] = r.right; 3604 // mLines[linesIndex++] = r.top; 3605 // 3606 // mLines[linesIndex++] = r.left; 3607 // mLines[linesIndex++] = r.bottom; 3608 // mLines[linesIndex++] = r.right; 3609 // mLines[linesIndex++] = r.bottom; 3610 // 3611 // mLines[linesIndex++] = r.left; 3612 // mLines[linesIndex++] = r.top; 3613 // mLines[linesIndex++] = r.left; 3614 // mLines[linesIndex++] = r.bottom; 3615 // 3616 // mLines[linesIndex++] = r.right; 3617 // mLines[linesIndex++] = r.top; 3618 // mLines[linesIndex++] = r.right; 3619 // mLines[linesIndex++] = r.bottom; 3620 // mPaint.setColor(color); 3621 // canvas.drawLines(mLines, 0, linesIndex, mPaint); 3622 // } 3623 3624 private void updateEventDetails() { 3625 if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN 3626 || mSelectionMode == SELECTION_LONGPRESS) { 3627 mPopup.dismiss(); 3628 return; 3629 } 3630 if (mLastPopupEventID == mSelectedEvent.id) { 3631 return; 3632 } 3633 3634 mLastPopupEventID = mSelectedEvent.id; 3635 3636 // Remove any outstanding callbacks to dismiss the popup. 3637 mHandler.removeCallbacks(mDismissPopup); 3638 3639 Event event = mSelectedEvent; 3640 TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); 3641 titleView.setText(event.title); 3642 3643 ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); 3644 imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); 3645 3646 imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); 3647 imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); 3648 3649 int flags; 3650 if (event.allDay) { 3651 flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE 3652 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; 3653 } else { 3654 flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE 3655 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL 3656 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; 3657 } 3658 if (DateFormat.is24HourFormat(mContext)) { 3659 flags |= DateUtils.FORMAT_24HOUR; 3660 } 3661 String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis, 3662 flags); 3663 TextView timeView = (TextView) mPopupView.findViewById(R.id.time); 3664 timeView.setText(timeRange); 3665 3666 TextView whereView = (TextView) mPopupView.findViewById(R.id.where); 3667 final boolean empty = TextUtils.isEmpty(event.location); 3668 whereView.setVisibility(empty ? View.GONE : View.VISIBLE); 3669 if (!empty) whereView.setText(event.location); 3670 3671 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); 3672 mHandler.postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); 3673 } 3674 3675 // The following routines are called from the parent activity when certain 3676 // touch events occur. 3677 private void doDown(MotionEvent ev) { 3678 mTouchMode = TOUCH_MODE_DOWN; 3679 mViewStartX = 0; 3680 mOnFlingCalled = false; 3681 mHandler.removeCallbacks(mContinueScroll); 3682 int x = (int) ev.getX(); 3683 int y = (int) ev.getY(); 3684 3685 // Save selection information: we use setSelectionFromPosition to find the selected event 3686 // in order to show the "clicked" color. But since it is also setting the selected info 3687 // for new events, we need to restore the old info after calling the function. 3688 Event oldSelectedEvent = mSelectedEvent; 3689 int oldSelectionDay = mSelectionDay; 3690 int oldSelectionHour = mSelectionHour; 3691 if (setSelectionFromPosition(x, y, false)) { 3692 // If a time was selected (a blue selection box is visible) and the click location 3693 // is in the selected time, do not show a click on an event to prevent a situation 3694 // of both a selection and an event are clicked when they overlap. 3695 boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN) 3696 && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour; 3697 if (!pressedSelected && mSelectedEvent != null) { 3698 mSavedClickedEvent = mSelectedEvent; 3699 mDownTouchTime = System.currentTimeMillis(); 3700 postDelayed (mSetClick,mOnDownDelay); 3701 } else { 3702 eventClickCleanup(); 3703 } 3704 } 3705 mSelectedEvent = oldSelectedEvent; 3706 mSelectionDay = oldSelectionDay; 3707 mSelectionHour = oldSelectionHour; 3708 invalidate(); 3709 } 3710 3711 // Kicks off all the animations when the expand allday area is tapped 3712 private void doExpandAllDayClick() { 3713 mShowAllAllDayEvents = !mShowAllAllDayEvents; 3714 3715 ObjectAnimator.setFrameDelay(0); 3716 3717 // Determine the starting height 3718 if (mAnimateDayHeight == 0) { 3719 mAnimateDayHeight = mShowAllAllDayEvents ? 3720 mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; 3721 } 3722 // Cancel current animations 3723 mCancellingAnimations = true; 3724 if (mAlldayAnimator != null) { 3725 mAlldayAnimator.cancel(); 3726 } 3727 if (mAlldayEventAnimator != null) { 3728 mAlldayEventAnimator.cancel(); 3729 } 3730 if (mMoreAlldayEventsAnimator != null) { 3731 mMoreAlldayEventsAnimator.cancel(); 3732 } 3733 mCancellingAnimations = false; 3734 // get new animators 3735 mAlldayAnimator = getAllDayAnimator(); 3736 mAlldayEventAnimator = getAllDayEventAnimator(); 3737 mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, 3738 "moreAllDayEventsTextAlpha", 3739 mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, 3740 mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); 3741 3742 // Set up delays and start the animators 3743 mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3744 mAlldayAnimator.start(); 3745 mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); 3746 mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); 3747 mMoreAlldayEventsAnimator.start(); 3748 if (mAlldayEventAnimator != null) { 3749 // This is the only animator that can return null, so check it 3750 mAlldayEventAnimator 3751 .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); 3752 mAlldayEventAnimator.start(); 3753 } 3754 } 3755 3756 /** 3757 * Figures out the initial heights for allDay events and space when 3758 * a view is being set up. 3759 */ 3760 public void initAllDayHeights() { 3761 if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { 3762 return; 3763 } 3764 if (mShowAllAllDayEvents) { 3765 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3766 maxADHeight = Math.min(maxADHeight, 3767 (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3768 mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; 3769 } else { 3770 mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3771 } 3772 } 3773 3774 // Sets up an animator for changing the height of allday events 3775 private ObjectAnimator getAllDayEventAnimator() { 3776 // First calculate the absolute max height 3777 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3778 // Now expand to fit but not beyond the absolute max 3779 maxADHeight = 3780 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3781 // calculate the height of individual events in order to fit 3782 int fitHeight = maxADHeight / mMaxAlldayEvents; 3783 int currentHeight = mAnimateDayEventHeight; 3784 int desiredHeight = 3785 mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 3786 // if there's nothing to animate just return 3787 if (currentHeight == desiredHeight) { 3788 return null; 3789 } 3790 3791 // Set up the animator with the calculated values 3792 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", 3793 currentHeight, desiredHeight); 3794 animator.setDuration(ANIMATION_DURATION); 3795 return animator; 3796 } 3797 3798 // Sets up an animator for changing the height of the allday area 3799 private ObjectAnimator getAllDayAnimator() { 3800 // Calculate the absolute max height 3801 int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 3802 // Find the desired height but don't exceed abs max 3803 maxADHeight = 3804 Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); 3805 // calculate the current and desired heights 3806 int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; 3807 int desiredHeight = mShowAllAllDayEvents ? maxADHeight : 3808 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); 3809 3810 // Set up the animator with the calculated values 3811 ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", 3812 currentHeight, desiredHeight); 3813 animator.setDuration(ANIMATION_DURATION); 3814 3815 animator.addListener(new AnimatorListenerAdapter() { 3816 @Override 3817 public void onAnimationEnd(Animator animation) { 3818 if (!mCancellingAnimations) { 3819 // when finished, set this to 0 to signify not animating 3820 mAnimateDayHeight = 0; 3821 mUseExpandIcon = !mShowAllAllDayEvents; 3822 } 3823 mRemeasure = true; 3824 invalidate(); 3825 } 3826 }); 3827 return animator; 3828 } 3829 3830 // setter for the 'box +n' alpha text used by the animator 3831 public void setMoreAllDayEventsTextAlpha(int alpha) { 3832 mMoreAlldayEventsTextAlpha = alpha; 3833 invalidate(); 3834 } 3835 3836 // setter for the height of the allday area used by the animator 3837 public void setAnimateDayHeight(int height) { 3838 mAnimateDayHeight = height; 3839 mRemeasure = true; 3840 invalidate(); 3841 } 3842 3843 // setter for the height of allday events used by the animator 3844 public void setAnimateDayEventHeight(int height) { 3845 mAnimateDayEventHeight = height; 3846 mRemeasure = true; 3847 invalidate(); 3848 } 3849 3850 private void doSingleTapUp(MotionEvent ev) { 3851 if (!mHandleActionUp || mScrolling) { 3852 return; 3853 } 3854 3855 int x = (int) ev.getX(); 3856 int y = (int) ev.getY(); 3857 int selectedDay = mSelectionDay; 3858 int selectedHour = mSelectionHour; 3859 3860 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 3861 // check if the tap was in the allday expansion area 3862 int bottom = mFirstCell; 3863 if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) 3864 || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && 3865 y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { 3866 doExpandAllDayClick(); 3867 return; 3868 } 3869 } 3870 3871 boolean validPosition = setSelectionFromPosition(x, y, false); 3872 if (!validPosition) { 3873 if (y < DAY_HEADER_HEIGHT) { 3874 Time selectedTime = new Time(mBaseDate); 3875 selectedTime.setJulianDay(mSelectionDay); 3876 selectedTime.hour = mSelectionHour; 3877 selectedTime.normalize(true /* ignore isDst */); 3878 mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, 3879 ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); 3880 } 3881 return; 3882 } 3883 3884 boolean hasSelection = mSelectionMode != SELECTION_HIDDEN; 3885 boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) 3886 && selectedDay == mSelectionDay && selectedHour == mSelectionHour; 3887 3888 if (pressedSelected && mSavedClickedEvent == null) { 3889 // If the tap is on an already selected hour slot, then create a new 3890 // event 3891 long extraLong = 0; 3892 if (mSelectionAllday) { 3893 extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; 3894 } 3895 mSelectionMode = SELECTION_SELECTED; 3896 mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, 3897 getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), 3898 extraLong, -1); 3899 } else if (mSelectedEvent != null) { 3900 // If the tap is on an event, launch the "View event" view 3901 if (mIsAccessibilityEnabled) { 3902 mAccessibilityMgr.interrupt(); 3903 } 3904 3905 mSelectionMode = SELECTION_HIDDEN; 3906 3907 int yLocation = 3908 (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2); 3909 // Y location is affected by the position of the event in the scrolling 3910 // view (mViewStartY) and the presence of all day events (mFirstCell) 3911 if (!mSelectedEvent.allDay) { 3912 yLocation += (mFirstCell - mViewStartY); 3913 } 3914 mClickedYLocation = yLocation; 3915 long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) - 3916 (System.currentTimeMillis() - mDownTouchTime); 3917 if (clearDelay > 0) { 3918 this.postDelayed(mClearClick, clearDelay); 3919 } else { 3920 this.post(mClearClick); 3921 } 3922 } else { 3923 // Select time 3924 Time startTime = new Time(mBaseDate); 3925 startTime.setJulianDay(mSelectionDay); 3926 startTime.hour = mSelectionHour; 3927 startTime.normalize(true /* ignore isDst */); 3928 3929 Time endTime = new Time(startTime); 3930 endTime.hour++; 3931 3932 mSelectionMode = SELECTION_SELECTED; 3933 mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, 3934 CalendarController.EXTRA_GOTO_TIME, null, null); 3935 } 3936 invalidate(); 3937 } 3938 3939 private void doLongPress(MotionEvent ev) { 3940 eventClickCleanup(); 3941 if (mScrolling) { 3942 return; 3943 } 3944 3945 // Scale gesture in progress 3946 if (mStartingSpanY != 0) { 3947 return; 3948 } 3949 3950 int x = (int) ev.getX(); 3951 int y = (int) ev.getY(); 3952 3953 boolean validPosition = setSelectionFromPosition(x, y, false); 3954 if (!validPosition) { 3955 // return if the touch wasn't on an area of concern 3956 return; 3957 } 3958 3959 mSelectionMode = SELECTION_LONGPRESS; 3960 invalidate(); 3961 performLongClick(); 3962 } 3963 3964 private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { 3965 cancelAnimation(); 3966 if (mStartingScroll) { 3967 mInitialScrollX = 0; 3968 mInitialScrollY = 0; 3969 mStartingScroll = false; 3970 } 3971 3972 mInitialScrollX += deltaX; 3973 mInitialScrollY += deltaY; 3974 int distanceX = (int) mInitialScrollX; 3975 int distanceY = (int) mInitialScrollY; 3976 3977 // If we haven't figured out the predominant scroll direction yet, 3978 // then do it now. 3979 if (mTouchMode == TOUCH_MODE_DOWN) { 3980 int absDistanceX = Math.abs(distanceX); 3981 int absDistanceY = Math.abs(distanceY); 3982 mScrollStartY = mViewStartY; 3983 mPreviousDirection = 0; 3984 3985 if (absDistanceX > absDistanceY) { 3986 mTouchMode = TOUCH_MODE_HSCROLL; 3987 mViewStartX = distanceX; 3988 initNextView(-mViewStartX); 3989 } else { 3990 mTouchMode = TOUCH_MODE_VSCROLL; 3991 } 3992 } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 3993 // We are already scrolling horizontally, so check if we 3994 // changed the direction of scrolling so that the other week 3995 // is now visible. 3996 mViewStartX = distanceX; 3997 if (distanceX != 0) { 3998 int direction = (distanceX > 0) ? 1 : -1; 3999 if (direction != mPreviousDirection) { 4000 // The user has switched the direction of scrolling 4001 // so re-init the next view 4002 initNextView(-mViewStartX); 4003 mPreviousDirection = direction; 4004 } 4005 } 4006 } 4007 4008 if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { 4009 mViewStartY = mScrollStartY + distanceY; 4010 4011 // If dragging while already at the end, do a glow 4012 final int pulledToY = (int) (mScrollStartY + deltaY); 4013 if (pulledToY < 0) { 4014 mEdgeEffectTop.onPull(deltaY / mViewHeight); 4015 if (!mEdgeEffectBottom.isFinished()) { 4016 mEdgeEffectBottom.onRelease(); 4017 } 4018 } else if (pulledToY > mMaxViewStartY) { 4019 mEdgeEffectBottom.onPull(deltaY / mViewHeight); 4020 if (!mEdgeEffectTop.isFinished()) { 4021 mEdgeEffectTop.onRelease(); 4022 } 4023 } 4024 4025 if (mViewStartY < 0) { 4026 mViewStartY = 0; 4027 } else if (mViewStartY > mMaxViewStartY) { 4028 mViewStartY = mMaxViewStartY; 4029 } 4030 computeFirstHour(); 4031 } 4032 4033 mScrolling = true; 4034 4035 mSelectionMode = SELECTION_HIDDEN; 4036 invalidate(); 4037 } 4038 4039 private void cancelAnimation() { 4040 Animation in = mViewSwitcher.getInAnimation(); 4041 if (in != null) { 4042 // cancel() doesn't terminate cleanly. 4043 in.scaleCurrentDuration(0); 4044 } 4045 Animation out = mViewSwitcher.getOutAnimation(); 4046 if (out != null) { 4047 // cancel() doesn't terminate cleanly. 4048 out.scaleCurrentDuration(0); 4049 } 4050 } 4051 4052 private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 4053 cancelAnimation(); 4054 4055 mSelectionMode = SELECTION_HIDDEN; 4056 eventClickCleanup(); 4057 4058 mOnFlingCalled = true; 4059 4060 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 4061 // Horizontal fling. 4062 // initNextView(deltaX); 4063 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4064 if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); 4065 int deltaX = (int) e2.getX() - (int) e1.getX(); 4066 switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); 4067 mViewStartX = 0; 4068 return; 4069 } 4070 4071 if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { 4072 if (DEBUG) Log.d(TAG, "doFling: no fling"); 4073 return; 4074 } 4075 4076 // Vertical fling. 4077 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4078 mViewStartX = 0; 4079 4080 if (DEBUG) { 4081 Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); 4082 } 4083 4084 // Continue scrolling vertically 4085 mScrolling = true; 4086 mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, 4087 (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, 4088 mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); 4089 4090 // When flinging down, show a glow when it hits the end only if it 4091 // wasn't started at the top 4092 if (velocityY > 0 && mViewStartY != 0) { 4093 mCallEdgeEffectOnAbsorb = true; 4094 } 4095 // When flinging up, show a glow when it hits the end only if it wasn't 4096 // started at the bottom 4097 else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { 4098 mCallEdgeEffectOnAbsorb = true; 4099 } 4100 mHandler.post(mContinueScroll); 4101 } 4102 4103 private boolean initNextView(int deltaX) { 4104 // Change the view to the previous day or week 4105 DayView view = (DayView) mViewSwitcher.getNextView(); 4106 Time date = view.mBaseDate; 4107 date.set(mBaseDate); 4108 boolean switchForward; 4109 if (deltaX > 0) { 4110 date.monthDay -= mNumDays; 4111 view.setSelectedDay(mSelectionDay - mNumDays); 4112 switchForward = false; 4113 } else { 4114 date.monthDay += mNumDays; 4115 view.setSelectedDay(mSelectionDay + mNumDays); 4116 switchForward = true; 4117 } 4118 date.normalize(true /* ignore isDst */); 4119 initView(view); 4120 view.layout(getLeft(), getTop(), getRight(), getBottom()); 4121 view.reloadEvents(); 4122 return switchForward; 4123 } 4124 4125 // ScaleGestureDetector.OnScaleGestureListener 4126 public boolean onScaleBegin(ScaleGestureDetector detector) { 4127 mHandleActionUp = false; 4128 float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 4129 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); 4130 4131 mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 4132 mCellHeightBeforeScaleGesture = mCellHeight; 4133 4134 if (DEBUG_SCALING) { 4135 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 4136 Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour 4137 + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY 4138 + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 4139 } 4140 4141 return true; 4142 } 4143 4144 // ScaleGestureDetector.OnScaleGestureListener 4145 public boolean onScale(ScaleGestureDetector detector) { 4146 float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); 4147 4148 mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); 4149 4150 if (mCellHeight < mMinCellHeight) { 4151 // If mStartingSpanY is too small, even a small increase in the 4152 // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT 4153 mStartingSpanY = spanY; 4154 mCellHeight = mMinCellHeight; 4155 mCellHeightBeforeScaleGesture = mMinCellHeight; 4156 } else if (mCellHeight > MAX_CELL_HEIGHT) { 4157 mStartingSpanY = spanY; 4158 mCellHeight = MAX_CELL_HEIGHT; 4159 mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; 4160 } 4161 4162 int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; 4163 mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; 4164 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 4165 4166 if (DEBUG_SCALING) { 4167 float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); 4168 Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " 4169 + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" 4170 + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); 4171 } 4172 4173 if (mViewStartY < 0) { 4174 mViewStartY = 0; 4175 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 4176 / (float) (mCellHeight + DAY_GAP); 4177 } else if (mViewStartY > mMaxViewStartY) { 4178 mViewStartY = mMaxViewStartY; 4179 mGestureCenterHour = (mViewStartY + gestureCenterInPixels) 4180 / (float) (mCellHeight + DAY_GAP); 4181 } 4182 computeFirstHour(); 4183 4184 mRemeasure = true; 4185 invalidate(); 4186 return true; 4187 } 4188 4189 // ScaleGestureDetector.OnScaleGestureListener 4190 public void onScaleEnd(ScaleGestureDetector detector) { 4191 mScrollStartY = mViewStartY; 4192 mInitialScrollY = 0; 4193 mInitialScrollX = 0; 4194 mStartingSpanY = 0; 4195 } 4196 4197 @Override 4198 public boolean onTouchEvent(MotionEvent ev) { 4199 int action = ev.getAction(); 4200 if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); 4201 4202 if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { 4203 mScaleGestureDetector.onTouchEvent(ev); 4204 if (mScaleGestureDetector.isInProgress()) { 4205 return true; 4206 } 4207 } 4208 4209 switch (action) { 4210 case MotionEvent.ACTION_DOWN: 4211 mStartingScroll = true; 4212 if (DEBUG) { 4213 Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" 4214 + ev.getPointerCount()); 4215 } 4216 4217 int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 4218 if (ev.getY() < bottom) { 4219 mTouchStartedInAlldayArea = true; 4220 } else { 4221 mTouchStartedInAlldayArea = false; 4222 } 4223 mHandleActionUp = true; 4224 mGestureDetector.onTouchEvent(ev); 4225 return true; 4226 4227 case MotionEvent.ACTION_MOVE: 4228 if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); 4229 mGestureDetector.onTouchEvent(ev); 4230 return true; 4231 4232 case MotionEvent.ACTION_UP: 4233 if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); 4234 mEdgeEffectTop.onRelease(); 4235 mEdgeEffectBottom.onRelease(); 4236 mStartingScroll = false; 4237 mGestureDetector.onTouchEvent(ev); 4238 if (!mHandleActionUp) { 4239 mHandleActionUp = true; 4240 mViewStartX = 0; 4241 invalidate(); 4242 return true; 4243 } 4244 4245 if (mOnFlingCalled) { 4246 return true; 4247 } 4248 4249 // If we were scrolling, then reset the selected hour so that it 4250 // is visible. 4251 if (mScrolling) { 4252 mScrolling = false; 4253 resetSelectedHour(); 4254 invalidate(); 4255 } 4256 4257 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 4258 mTouchMode = TOUCH_MODE_INITIAL_STATE; 4259 if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { 4260 // The user has gone beyond the threshold so switch views 4261 if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); 4262 switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); 4263 mViewStartX = 0; 4264 return true; 4265 } else { 4266 // Not beyond the threshold so invalidate which will cause 4267 // the view to snap back. Also call recalc() to ensure 4268 // that we have the correct starting date and title. 4269 if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); 4270 recalc(); 4271 invalidate(); 4272 mViewStartX = 0; 4273 } 4274 } 4275 4276 return true; 4277 4278 // This case isn't expected to happen. 4279 case MotionEvent.ACTION_CANCEL: 4280 if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); 4281 mGestureDetector.onTouchEvent(ev); 4282 mScrolling = false; 4283 resetSelectedHour(); 4284 return true; 4285 4286 default: 4287 if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); 4288 if (mGestureDetector.onTouchEvent(ev)) { 4289 return true; 4290 } 4291 return super.onTouchEvent(ev); 4292 } 4293 } 4294 4295 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 4296 MenuItem item; 4297 4298 // If the trackball is held down, then the context menu pops up and 4299 // we never get onKeyUp() for the long-press. So check for it here 4300 // and change the selection to the long-press state. 4301 if (mSelectionMode != SELECTION_LONGPRESS) { 4302 mSelectionMode = SELECTION_LONGPRESS; 4303 invalidate(); 4304 } 4305 4306 final long startMillis = getSelectedTimeInMillis(); 4307 int flags = DateUtils.FORMAT_SHOW_TIME 4308 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT 4309 | DateUtils.FORMAT_SHOW_WEEKDAY; 4310 final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); 4311 menu.setHeaderTitle(title); 4312 4313 int numSelectedEvents = mSelectedEvents.size(); 4314 if (mNumDays == 1) { 4315 // Day view. 4316 4317 // If there is a selected event, then allow it to be viewed and 4318 // edited. 4319 if (numSelectedEvents >= 1) { 4320 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4321 item.setOnMenuItemClickListener(mContextMenuHandler); 4322 item.setIcon(android.R.drawable.ic_menu_info_details); 4323 4324 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4325 if (accessLevel == ACCESS_LEVEL_EDIT) { 4326 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4327 item.setOnMenuItemClickListener(mContextMenuHandler); 4328 item.setIcon(android.R.drawable.ic_menu_edit); 4329 item.setAlphabeticShortcut('e'); 4330 } 4331 4332 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4333 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4334 item.setOnMenuItemClickListener(mContextMenuHandler); 4335 item.setIcon(android.R.drawable.ic_menu_delete); 4336 } 4337 4338 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4339 item.setOnMenuItemClickListener(mContextMenuHandler); 4340 item.setIcon(android.R.drawable.ic_menu_add); 4341 item.setAlphabeticShortcut('n'); 4342 } else { 4343 // Otherwise, if the user long-pressed on a blank hour, allow 4344 // them to create an event. They can also do this by tapping. 4345 item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); 4346 item.setOnMenuItemClickListener(mContextMenuHandler); 4347 item.setIcon(android.R.drawable.ic_menu_add); 4348 item.setAlphabeticShortcut('n'); 4349 } 4350 } else { 4351 // Week view. 4352 4353 // If there is a selected event, then allow it to be viewed and 4354 // edited. 4355 if (numSelectedEvents >= 1) { 4356 item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); 4357 item.setOnMenuItemClickListener(mContextMenuHandler); 4358 item.setIcon(android.R.drawable.ic_menu_info_details); 4359 4360 int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); 4361 if (accessLevel == ACCESS_LEVEL_EDIT) { 4362 item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); 4363 item.setOnMenuItemClickListener(mContextMenuHandler); 4364 item.setIcon(android.R.drawable.ic_menu_edit); 4365 item.setAlphabeticShortcut('e'); 4366 } 4367 4368 if (accessLevel >= ACCESS_LEVEL_DELETE) { 4369 item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); 4370 item.setOnMenuItemClickListener(mContextMenuHandler); 4371 item.setIcon(android.R.drawable.ic_menu_delete); 4372 } 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 4380 item = menu.add(0, MENU_DAY, 0, R.string.show_day_view); 4381 item.setOnMenuItemClickListener(mContextMenuHandler); 4382 item.setIcon(android.R.drawable.ic_menu_day); 4383 item.setAlphabeticShortcut('d'); 4384 } 4385 4386 mPopup.dismiss(); 4387 } 4388 4389 private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { 4390 public boolean onMenuItemClick(MenuItem item) { 4391 switch (item.getItemId()) { 4392 case MENU_EVENT_VIEW: { 4393 if (mSelectedEvent != null) { 4394 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, 4395 mSelectedEvent.id, mSelectedEvent.startMillis, 4396 mSelectedEvent.endMillis, 0, 0, -1); 4397 } 4398 break; 4399 } 4400 case MENU_EVENT_EDIT: { 4401 if (mSelectedEvent != null) { 4402 mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, 4403 mSelectedEvent.id, mSelectedEvent.startMillis, 4404 mSelectedEvent.endMillis, 0, 0, -1); 4405 } 4406 break; 4407 } 4408 case MENU_DAY: { 4409 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4410 ViewType.DAY); 4411 break; 4412 } 4413 case MENU_AGENDA: { 4414 mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, 4415 ViewType.AGENDA); 4416 break; 4417 } 4418 case MENU_EVENT_CREATE: { 4419 long startMillis = getSelectedTimeInMillis(); 4420 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 4421 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 4422 startMillis, endMillis, 0, 0, -1); 4423 break; 4424 } 4425 case MENU_EVENT_DELETE: { 4426 if (mSelectedEvent != null) { 4427 Event selectedEvent = mSelectedEvent; 4428 long begin = selectedEvent.startMillis; 4429 long end = selectedEvent.endMillis;