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