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