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