DayView.java revision b5e3c17201b2c08ca9f3fd33f4585e1ec8fa71b1
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.Iterator; 91import java.util.Locale; 92import java.util.regex.Matcher; 93import java.util.regex.Pattern; 94 95/** 96 * View for multi-day view. So far only 1 and 7 day have been tested. 97 */ 98public class DayView extends View implements View.OnCreateContextMenuListener, 99 ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener 100 { 101 private static String TAG = "DayView"; 102 private static boolean DEBUG = false; 103 private static final String HOUR_FORMAT_12H = "%A %I%p"; 104 private static final String HOUR_FORMAT_24H = "%A %H"; 105 private static final String PERIOD_SPACE = ". "; 106 107 private static float mScale = 0; // Used for supporting different screen densities 108 private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event 109 // Duration of the allday expansion 110 private static final long ANIMATION_DURATION = 400; 111 // duration of the more allday event text fade 112 private static final long ANIMATION_SECONDARY_DURATION = 200; 113 114 private static final int MENU_AGENDA = 2; 115 private static final int MENU_DAY = 3; 116 private static final int MENU_EVENT_VIEW = 5; 117 private static final int MENU_EVENT_CREATE = 6; 118 private static final int MENU_EVENT_EDIT = 7; 119 private static final int MENU_EVENT_DELETE = 8; 120 121 private static int DEFAULT_CELL_HEIGHT = 64; 122 private static int MAX_CELL_HEIGHT = 150; 123 private static int MIN_Y_SPAN = 100; 124 125 private boolean mOnFlingCalled; 126 private boolean mStartingScroll = false; 127 /** 128 * ID of the last event which was displayed with the toast popup. 129 * 130 * This is used to prevent popping up multiple quick views for the same event, especially 131 * during calendar syncs. This becomes valid when an event is selected, either by default 132 * on starting calendar or by scrolling to an event. It becomes invalid when the user 133 * explicitly scrolls to an empty time slot, changes views, or deletes the event. 134 */ 135 private long mLastPopupEventID; 136 137 protected Context mContext; 138 139 private static final String[] CALENDARS_PROJECTION = new String[] { 140 Calendars._ID, // 0 141 Calendars.CALENDAR_ACCESS_LEVEL, // 1 142 Calendars.OWNER_ACCOUNT, // 2 143 }; 144 private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; 145 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 146 private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; 147 148 private static final String[] ATTENDEES_PROJECTION = new String[] { 149 Attendees._ID, // 0 150 Attendees.ATTENDEE_RELATIONSHIP, // 1 151 }; 152 private static final int ATTENDEES_INDEX_RELATIONSHIP = 1; 153 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d"; 154 155 private static final int FROM_NONE = 0; 156 private static final int FROM_ABOVE = 1; 157 private static final int FROM_BELOW = 2; 158 private static final int FROM_LEFT = 4; 159 private static final int FROM_RIGHT = 8; 160 161 private static final int ACCESS_LEVEL_NONE = 0; 162 private static final int ACCESS_LEVEL_DELETE = 1; 163 private static final int ACCESS_LEVEL_EDIT = 2; 164 165 private static int mHorizontalSnapBackThreshold = 128; 166 167 private ContinueScroll mContinueScroll = new ContinueScroll(); 168 169 // Make this visible within the package for more informative debugging 170 Time mBaseDate; 171 private Time mCurrentTime; 172 //Update the current time line every five minutes if the window is left open that long 173 private static final int UPDATE_CURRENT_TIME_DELAY = 300000; 174 private UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); 175 private int mTodayJulianDay; 176 177 private Typeface mBold = Typeface.DEFAULT_BOLD; 178 private int mFirstJulianDay; 179 private int mLastJulianDay; 180 181 private int mMonthLength; 182 private int mFirstVisibleDate; 183 private int mFirstVisibleDayOfWeek; 184 private int[] mEarliestStartHour; // indexed by the week day offset 185 private boolean[] mHasAllDayEvent; // indexed by the week day offset 186 private String mAllDayString; 187 private String mEventCountTemplate; 188 189 protected static StringBuilder mStringBuilder = new StringBuilder(50); 190 // TODO recreate formatter when locale changes 191 protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 192 193 private Runnable mTZUpdater = new Runnable() { 194 @Override 195 public void run() { 196 String tz = Utils.getTimeZone(mContext, this); 197 mBaseDate.timezone = tz; 198 mBaseDate.normalize(true); 199 mCurrentTime.switchTimezone(tz); 200 invalidate(); 201 } 202 }; 203 204 AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { 205 @Override 206 public void onAnimationStart(Animator animation) { 207 mScrolling = true; 208 } 209 210 @Override 211 public void onAnimationCancel(Animator animation) { 212 mScrolling = false; 213 } 214 215 @Override 216 public void onAnimationEnd(Animator animation) { 217 mScrolling = false; 218 resetSelectedHour(); 219 invalidate(); 220 } 221 }; 222 223 /** 224 * This variable helps to avoid unnecessarily reloading events by keeping 225 * track of the start millis parameter used for the most recent loading 226 * of events. If the next reload matches this, then the events are not 227 * reloaded. To force a reload, set this to zero (this is set to zero 228 * in the method clearCachedEvents()). 229 */ 230 private long mLastReloadMillis; 231 232 private ArrayList<Event> mEvents = new ArrayList<Event>(); 233 private ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); 234 private StaticLayout[] mLayouts = null; 235 private StaticLayout[] mAllDayLayouts = null; 236 private StaticLayout mAllDayTextLayout = null; 237 private int mSelectionDay; // Julian day 238 private int mSelectionHour; 239 240 boolean mSelectionAllday; 241 242 /** Width of a day or non-conflicting event */ 243 private int mCellWidth; 244 245 // Pre-allocate these objects and re-use them 246 private Rect mRect = new Rect(); 247 private Rect mDestRect = new Rect(); 248 private Rect mSelectionRect = new Rect(); 249 // This encloses the more allDay events icon 250 private Rect mExpandAllDayRect = new Rect(); 251 // TODO Clean up paint usage 252 private Paint mPaint = new Paint(); 253 private Paint mEventTextPaint = new Paint(); 254 private Paint mSelectionPaint = new Paint(); 255 private float[] mLines; 256 257 private int mFirstDayOfWeek; // First day of the week 258 259 private PopupWindow mPopup; 260 private View mPopupView; 261 262 // The number of milliseconds to show the popup window 263 private static final int POPUP_DISMISS_DELAY = 3000; 264 private DismissPopup mDismissPopup = new DismissPopup(); 265 266 private boolean mRemeasure = true; 267 268 private final EventLoader mEventLoader; 269 protected final EventGeometry mEventGeometry; 270 271 private static float GRID_LINE_LEFT_MARGIN = 0; 272 private static final float GRID_LINE_INNER_WIDTH = 1; 273 274 private static final int DAY_GAP = 1; 275 private static final int HOUR_GAP = 1; 276 // This is the standard height of an allday event with no restrictions 277 private static int SINGLE_ALLDAY_HEIGHT = 34; 278 /** 279 * This is the minimum desired height of a allday event. 280 * When unexpanded, allday events will use this height. 281 * When expanded allDay events will attempt to grow to fit all 282 * events at this height. 283 */ 284 private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels 285 /** 286 * This is how big the unexpanded allday height is allowed to be. 287 * It will get adjusted based on screen size 288 */ 289 private static int MAX_UNEXPANDED_ALLDAY_HEIGHT = 290 (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); 291 /** 292 * This is the minimum size reserved for displaying regular events. 293 * The expanded allDay region can't expand into this. 294 */ 295 private static int MIN_HOURS_HEIGHT = 180; 296 private static int ALLDAY_TOP_MARGIN = 1; 297 // The largest a single allDay event will become. 298 private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; 299 300 private static int HOURS_TOP_MARGIN = 2; 301 private static int HOURS_LEFT_MARGIN = 2; 302 private static int HOURS_RIGHT_MARGIN = 4; 303 private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 304 305 private static int CURRENT_TIME_LINE_HEIGHT = 2; 306 private static int CURRENT_TIME_LINE_BORDER_WIDTH = 1; 307 private static final int CURRENT_TIME_LINE_SIDE_BUFFER = 2; 308 309 /* package */ static final int MINUTES_PER_HOUR = 60; 310 /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; 311 /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; 312 /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); 313 /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 314 315 private static final int DECLINED_ALPHA = 0x66000000; 316 private static final int DATE_HEADER_ALPHA = 0x26000000; 317 private static final int DATE_HEADER_TODAY_ALPHA = 0x99000000; 318 // More events text will transition between invisible and this alpha 319 private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; 320 private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; 321 private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; 322 private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; 323 private static int DAY_HEADER_LEFT_MARGIN = 5; 324 private static int DAY_HEADER_RIGHT_MARGIN = 4; 325 private static int DAY_HEADER_BOTTOM_MARGIN = 3; 326 private static float DAY_HEADER_FONT_SIZE = 14; 327 private static float DATE_HEADER_FONT_SIZE = 32; 328 private static float NORMAL_FONT_SIZE = 12; 329 private static float EVENT_TEXT_FONT_SIZE = 12; 330 private static float HOURS_TEXT_SIZE = 12; 331 private static float ALLDAY_TEXT_SIZE = 12; 332 private static float AMPM_TEXT_SIZE = 9; 333 private static int MIN_HOURS_WIDTH = 96; 334 private static int MIN_CELL_WIDTH_FOR_TEXT = 27; 335 private static final int MAX_EVENT_TEXT_LEN = 500; 336 // smallest height to draw an event with 337 private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels 338 private static int CALENDAR_COLOR_SQUARE_SIZE = 10; 339 private static int EVENT_RECT_TOP_MARGIN = 1; 340 private static int EVENT_RECT_BOTTOM_MARGIN = 1; 341 private static int EVENT_RECT_LEFT_MARGIN = 2; 342 private static int EVENT_RECT_RIGHT_MARGIN = 1; 343 private static int EVENT_RECT_STROKE_WIDTH = 2; 344 private static int EVENT_TEXT_TOP_MARGIN = 2; 345 private static int EVENT_TEXT_BOTTOM_MARGIN = 2; 346 private static int EVENT_TEXT_LEFT_MARGIN = 2; 347 private static int EVENT_TEXT_RIGHT_MARGIN = 2; 348 private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; 349 private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; 350 private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; 351 private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; 352 private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; 353 // margins and sizing for the expand allday icon 354 private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; 355 // sizing for "box +n" in allDay events 356 private static int EVENT_SQUARE_WIDTH = 10; 357 private static int EVENT_LINE_PADDING = 4; 358 359 private static int mPressedColor; 360 private static int mEventTextColor; 361 private static int mMoreEventsTextColor; 362 363 private static int mWeek_saturdayColor; 364 private static int mWeek_sundayColor; 365 private static int mCalendarDateBannerTextColor; 366 private static int mCalendarAmPmLabel; 367 private static int mCalendarGridAreaSelected; 368 private static int mCalendarGridLineInnerHorizontalColor; 369 private static int mCalendarGridLineInnerVerticalColor; 370 private static int mTodayBgColor; 371 private static int mBgColor; 372 private static int mCalendarHourLabelColor; 373 private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; 374 375 private float mAnimationDistance = 0; 376 private int mViewStartX; 377 private int mViewStartY; 378 private int mMaxViewStartY; 379 private int mViewHeight; 380 private int mViewWidth; 381 private int mGridAreaHeight = -1; 382 private static int mCellHeight = 0; // shared among all DayViews 383 private static int mMinCellHeight = 32; 384 private int mScrollStartY; 385 private int mPreviousDirection; 386 387 /** 388 * Vertical distance or span between the two touch points at the start of a 389 * scaling gesture 390 */ 391 private float mStartingSpanY = 0; 392 /** Height of 1 hour in pixels at the start of a scaling gesture */ 393 private int mCellHeightBeforeScaleGesture; 394 /** The hour at the center two touch points */ 395 private float mGestureCenterHour = 0; 396 /** 397 * Flag to decide whether to handle the up event. Cases where up events 398 * should be ignored are 1) right after a scale gesture and 2) finger was 399 * down before app launch 400 */ 401 private boolean mHandleActionUp = true; 402 403 private int mHoursTextHeight; 404 /** 405 * The height of the area used for allday events 406 */ 407 private int mAlldayHeight; 408 /** 409 * The height of the allday event area used during animation 410 */ 411 private int mAnimateDayHeight = 0; 412 /** 413 * The height of an individual allday event during animation 414 */ 415 private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 416 /** 417 * Whether to use the expand or collapse icon. 418 */ 419 private static boolean mUseExpandIcon = true; 420 /** 421 * The height of the day names/numbers 422 */ 423 private static int DAY_HEADER_HEIGHT = 45; 424 /** 425 * The height of the day names/numbers for multi-day views 426 */ 427 private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 428 /** 429 * The height of the day names/numbers when viewing a single day 430 */ 431 private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; 432 /** 433 * Max of all day events in a given day in this view. 434 */ 435 private int mMaxAlldayEvents; 436 /** 437 * A count of the number of allday events that were not drawn for each day 438 */ 439 private int[] mSkippedAlldayEvents; 440 /** 441 * The number of allDay events at which point we start hiding allDay events. 442 */ 443 private int mMaxUnexpandedAlldayEventCount = 4; 444 /** 445 * Whether or not to expand the allDay area to fill the screen 446 */ 447 private static boolean mShowAllAllDayEvents = false; 448 449 protected int mNumDays = 7; 450 private int mNumHours = 10; 451 452 /** Width of the time line (list of hours) to the left. */ 453 private int mHoursWidth; 454 private int mDateStrWidth; 455 /** Top of the scrollable region i.e. below date labels and all day events */ 456 private int mFirstCell; 457 /** First fully visibile hour */ 458 private int mFirstHour = -1; 459 /** Distance between the mFirstCell and the top of first fully visible hour. */ 460 private int mFirstHourOffset; 461 private String[] mHourStrs; 462 private String[] mDayStrs; 463 private String[] mDayStrs2Letter; 464 private boolean mIs24HourFormat; 465 466 private ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); 467 private boolean mComputeSelectedEvents; 468 private boolean mUpdateToast; 469 private Event mSelectedEvent; 470 private Event mPrevSelectedEvent; 471 private Rect mPrevBox = new Rect(); 472 protected final Resources mResources; 473 protected final Drawable mCurrentTimeLine; 474 protected final Drawable mTodayHeaderDrawable; 475 protected final Drawable mExpandAlldayDrawable; 476 protected final Drawable mCollapseAlldayDrawable; 477 protected Drawable mAcceptedOrTentativeEventBoxDrawable; 478 protected BitmapDrawable mDeclinedBgDrawable; 479 private String mAmString; 480 private String mPmString; 481 private DeleteEventHelper mDeleteEventHelper; 482 private static int sCounter = 0; 483 484 private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); 485 486 ScaleGestureDetector mScaleGestureDetector; 487 488 /** 489 * The initial state of the touch mode when we enter this view. 490 */ 491 private static final int TOUCH_MODE_INITIAL_STATE = 0; 492 493 /** 494 * Indicates we just received the touch event and we are waiting to see if 495 * it is a tap or a scroll gesture. 496 */ 497 private static final int TOUCH_MODE_DOWN = 1; 498 499 /** 500 * Indicates the touch gesture is a vertical scroll 501 */ 502 private static final int TOUCH_MODE_VSCROLL = 0x20; 503 504 /** 505 * Indicates the touch gesture is a horizontal scroll 506 */ 507 private static final int TOUCH_MODE_HSCROLL = 0x40; 508 509 private int mTouchMode = TOUCH_MODE_INITIAL_STATE; 510 511 /** 512 * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. 513 */ 514 private static final int SELECTION_HIDDEN = 0; 515 private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet 516 private static final int SELECTION_SELECTED = 2; 517 private static final int SELECTION_LONGPRESS = 3; 518 519 private int mSelectionMode = SELECTION_HIDDEN; 520 521 private boolean mScrolling = false; 522 523 private float mInitialScrollX; 524 private float mInitialScrollY; 525 526 // Animates the height of the allday region 527 ObjectAnimator mAlldayAnimator; 528 // Animates the height of events in the allday region 529 ObjectAnimator mAlldayEventAnimator; 530 // Animates the transparency of the more events text 531 ObjectAnimator mMoreAlldayEventsAnimator; 532 // whether or not an event is stopping because it was cancelled 533 private boolean mCancellingAnimations = false; 534 // tracks whether a touch originated in the allday area 535 private boolean mTouchStartedInAlldayArea = false; 536 537 private CalendarController mController; 538 private ViewSwitcher mViewSwitcher; 539 private GestureDetector mGestureDetector; 540 private OverScroller mScroller; 541 private ScrollInterpolator mHScrollInterpolator; 542 543 public DayView(Context context, CalendarController controller, 544 ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { 545 super(context); 546 mResources = context.getResources(); 547 548 DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); 549 DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); 550 ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); 551 DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); 552 EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); 553 HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); 554 AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); 555 ALLDAY_TEXT_SIZE = (int) mResources.getDimension(R.dimen.allday_text_size); 556 MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); 557 HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); 558 HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); 559 MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); 560 561 if (mScale == 0) { 562 563 mScale = mResources.getDisplayMetrics().density; 564 if (mScale != 1) { 565 SINGLE_ALLDAY_HEIGHT *= mScale; 566 ALLDAY_TOP_MARGIN *= mScale; 567 MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; 568 569 NORMAL_FONT_SIZE *= mScale; 570 EVENT_TEXT_FONT_SIZE *= mScale; 571 GRID_LINE_LEFT_MARGIN *= mScale; 572 HOURS_TOP_MARGIN *= mScale; 573 MIN_CELL_WIDTH_FOR_TEXT *= mScale; 574 MIN_EVENT_HEIGHT *= mScale; 575 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT *= mScale; 576 MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; 577 mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 578 579 CURRENT_TIME_LINE_HEIGHT *= mScale; 580 CURRENT_TIME_LINE_BORDER_WIDTH *= mScale; 581 582 MIN_Y_SPAN *= mScale; 583 MAX_CELL_HEIGHT *= mScale; 584 DEFAULT_CELL_HEIGHT *= mScale; 585 DAY_HEADER_HEIGHT *= mScale; 586 DAY_HEADER_LEFT_MARGIN *= mScale; 587 DAY_HEADER_RIGHT_MARGIN *= mScale; 588 DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; 589 DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; 590 DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; 591 CALENDAR_COLOR_SQUARE_SIZE *= mScale; 592 EVENT_TEXT_TOP_MARGIN *= mScale; 593 EVENT_TEXT_BOTTOM_MARGIN *= mScale; 594 EVENT_TEXT_LEFT_MARGIN *= mScale; 595 EVENT_TEXT_RIGHT_MARGIN *= mScale; 596 EVENT_ALL_DAY_TEXT_TOP_MARGIN *= mScale; 597 EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN *= mScale; 598 EVENT_ALL_DAY_TEXT_LEFT_MARGIN *= mScale; 599 EVENT_ALL_DAY_TEXT_RIGHT_MARGIN *= mScale; 600 EVENT_RECT_TOP_MARGIN *= mScale; 601 EVENT_RECT_BOTTOM_MARGIN *= mScale; 602 ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; 603 EVENT_RECT_LEFT_MARGIN *= mScale; 604 EVENT_RECT_RIGHT_MARGIN *= mScale; 605 EVENT_RECT_STROKE_WIDTH *= mScale; 606 EXPAND_ALL_DAY_BOTTOM_MARGIN *= mScale; 607 EVENT_SQUARE_WIDTH *= mScale; 608 EVENT_LINE_PADDING *= mScale; 609 } 610 } 611 HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; 612 DAY_HEADER_HEIGHT = numDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; 613 614 mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_week_holo_light); 615 mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); 616 mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_expand_holo_light); 617 mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_allday_collapse_holo_light); 618 mAcceptedOrTentativeEventBoxDrawable = mResources 619 .getDrawable(R.drawable.panel_month_event_holo_light); 620 621 mDeclinedBgDrawable = new BitmapDrawable(mResources, BitmapFactory.decodeResource( 622 mResources, R.drawable.event_bg_declined)); 623 mDeclinedBgDrawable.setTileModeXY(TileMode.REPEAT, TileMode.REPEAT); 624 625 mEventLoader = eventLoader; 626 mEventGeometry = new EventGeometry(); 627 mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); 628 mEventGeometry.setHourGap(HOUR_GAP); 629 mEventGeometry.setCellMargin(DAY_GAP); 630 mContext = context; 631 mAllDayString = mContext.getString(R.string.edit_event_all_day_label); 632 mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); 633 mLastPopupEventID = INVALID_EVENT_ID; 634 mController = controller; 635 mViewSwitcher = viewSwitcher; 636 mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); 637 mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); 638 mNumDays = numDays; 639 if (mCellHeight == 0) { 640 mCellHeight = Utils.getSharedPreference(mContext, 641 GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); 642 } 643 mScroller = new OverScroller(context); 644 mHScrollInterpolator = new ScrollInterpolator(); 645 init(context); 646 } 647 648 private void init(Context context) { 649 setFocusable(true); 650 651 // Allow focus in touch mode so that we can do keyboard shortcuts 652 // even after we've entered touch mode. 653 setFocusableInTouchMode(true); 654 setClickable(true); 655 setOnCreateContextMenuListener(this); 656 657 mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); 658 659 mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); 660 long currentTime = System.currentTimeMillis(); 661 mCurrentTime.set(currentTime); 662 //The % makes it go off at the next increment of 5 minutes. 663 postDelayed(mUpdateCurrentTime, 664 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 665 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 666 667 mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); 668 mWeek_sundayColor = mResources.getColor(R.color.week_sunday); 669 mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); 670 mTodayBgColor = mResources.getColor(R.color.calendar_today_bg_color); 671 mBgColor = mResources.getColor(R.color.calendar_hour_background); 672 mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); 673 mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); 674 mCalendarGridLineInnerHorizontalColor = mResources 675 .getColor(R.color.calendar_grid_line_inner_horizontal_color); 676 mCalendarGridLineInnerVerticalColor = mResources 677 .getColor(R.color.calendar_grid_line_inner_vertical_color); 678 mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); 679 mPressedColor = mResources.getColor(R.color.pressed); 680 mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); 681 mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color); 682 683 mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); 684 mEventTextPaint.setTextAlign(Paint.Align.LEFT); 685 mEventTextPaint.setAntiAlias(true); 686 687 int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); 688 Paint p = mSelectionPaint; 689 p.setColor(gridLineColor); 690 p.setStyle(Style.FILL); 691 p.setAntiAlias(false); 692 693 p = mPaint; 694 p.setAntiAlias(true); 695 696 // Allocate space for 2 weeks worth of weekday names so that we can 697 // easily start the week display at any week day. 698 mDayStrs = new String[14]; 699 700 // Also create an array of 2-letter abbreviations. 701 mDayStrs2Letter = new String[14]; 702 703 for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 704 int index = i - Calendar.SUNDAY; 705 // e.g. Tue for Tuesday 706 mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM) 707 .toUpperCase(); 708 mDayStrs[index + 7] = mDayStrs[index]; 709 // e.g. Tu for Tuesday 710 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) 711 .toUpperCase(); 712 713 // If we don't have 2-letter day strings, fall back to 1-letter. 714 if (mDayStrs2Letter[index].equals(mDayStrs[index])) { 715 mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); 716 } 717 718 mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; 719 } 720 721 // Figure out how much space we need for the 3-letter abbrev names 722 // in the worst case. 723 p.setTextSize(DATE_HEADER_FONT_SIZE); 724 p.setTypeface(mBold); 725 String[] dateStrs = {" 28", " 30"}; 726 mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); 727 p.setTextSize(DAY_HEADER_FONT_SIZE); 728 mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); 729 730 p.setTextSize(HOURS_TEXT_SIZE); 731 p.setTypeface(null); 732 handleOnResume(); 733 734 mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); 735 mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); 736 String[] ampm = {mAmString, mPmString}; 737 p.setTextSize(AMPM_TEXT_SIZE); 738 mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) 739 + HOURS_RIGHT_MARGIN); 740 mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); 741 742 LayoutInflater inflater; 743 inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 744 mPopupView = inflater.inflate(R.layout.bubble_event, null); 745 mPopupView.setLayoutParams(new ViewGroup.LayoutParams( 746 ViewGroup.LayoutParams.MATCH_PARENT, 747 ViewGroup.LayoutParams.WRAP_CONTENT)); 748 mPopup = new PopupWindow(context); 749 mPopup.setContentView(mPopupView); 750 Resources.Theme dialogTheme = getResources().newTheme(); 751 dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); 752 TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { 753 android.R.attr.windowBackground }); 754 mPopup.setBackgroundDrawable(ta.getDrawable(0)); 755 ta.recycle(); 756 757 // Enable touching the popup window 758 mPopupView.setOnClickListener(this); 759 // Catch long clicks for creating a new event 760 setOnLongClickListener(this); 761 762 mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); 763 long millis = System.currentTimeMillis(); 764 mBaseDate.set(millis); 765 766 mEarliestStartHour = new int[mNumDays]; 767 mHasAllDayEvent = new boolean[mNumDays]; 768 769 // mLines is the array of points used with Canvas.drawLines() in 770 // drawGridBackground() and drawAllDayEvents(). Its size depends 771 // on the max number of lines that can ever be drawn by any single 772 // drawLines() call in either of those methods. 773 final int maxGridLines = (24 + 1) // max horizontal lines we might draw 774 + (mNumDays + 1); // max vertical lines we might draw 775 mLines = new float[maxGridLines * 4]; 776 } 777 778 /** 779 * This is called when the popup window is pressed. 780 */ 781 public void onClick(View v) { 782 if (v == mPopupView) { 783 // Pretend it was a trackball click because that will always 784 // jump to the "View event" screen. 785 switchViews(true /* trackball */); 786 } 787 } 788 789 public void handleOnResume() { 790 mIs24HourFormat = DateFormat.is24HourFormat(mContext); 791 mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; 792 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 793 } 794 795 /** 796 * Returns the start of the selected time in milliseconds since the epoch. 797 * 798 * @return selected time in UTC milliseconds since the epoch. 799 */ 800 long getSelectedTimeInMillis() { 801 Time time = new Time(mBaseDate); 802 time.setJulianDay(mSelectionDay); 803 time.hour = mSelectionHour; 804 805 // We ignore the "isDst" field because we want normalize() to figure 806 // out the correct DST value and not adjust the selected time based 807 // on the current setting of DST. 808 return time.normalize(true /* ignore isDst */); 809 } 810 811 Time getSelectedTime() { 812 Time time = new Time(mBaseDate); 813 time.setJulianDay(mSelectionDay); 814 time.hour = mSelectionHour; 815 816 // We ignore the "isDst" field because we want normalize() to figure 817 // out the correct DST value and not adjust the selected time based 818 // on the current setting of DST. 819 time.normalize(true /* ignore isDst */); 820 return time; 821 } 822 823 /** 824 * Returns the start of the selected time in minutes since midnight, 825 * local time. The derived class must ensure that this is consistent 826 * with the return value from getSelectedTimeInMillis(). 827 */ 828 int getSelectedMinutesSinceMidnight() { 829 return mSelectionHour * MINUTES_PER_HOUR; 830 } 831 832 int getFirstVisibleHour() { 833 return mFirstHour; 834 } 835 836 void setFirstVisibleHour(int firstHour) { 837 mFirstHour = firstHour; 838 mFirstHourOffset = 0; 839 } 840 841 public void setSelected(Time time, boolean ignoreTime) { 842 mBaseDate.set(time); 843 mSelectionHour = mBaseDate.hour; 844 mSelectedEvent = null; 845 mPrevSelectedEvent = null; 846 long millis = mBaseDate.toMillis(false /* use isDst */); 847 mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff); 848 mSelectedEvents.clear(); 849 mComputeSelectedEvents = true; 850 851 int gotoY = Integer.MIN_VALUE; 852 853 if (!ignoreTime && mGridAreaHeight != -1) { 854 int lastHour = 0; 855 856 if (mBaseDate.hour < mFirstHour) { 857 // Above visible region 858 gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); 859 } else { 860 lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) 861 + mFirstHour; 862 863 if (mBaseDate.hour >= lastHour) { 864 // Below visible region 865 866 // target hour + 1 (to give it room to see the event) - 867 // grid height (to get the y of the top of the visible 868 // region) 869 gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) 870 * (mCellHeight + HOUR_GAP) - mGridAreaHeight); 871 } 872 } 873 874 if (DEBUG) { 875 Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " 876 + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight 877 + " ymax " + mMaxViewStartY); 878 } 879 880 if (gotoY > mMaxViewStartY) { 881 gotoY = mMaxViewStartY; 882 } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { 883 gotoY = 0; 884 } 885 } 886 887 recalc(); 888 889 // Don't draw the selection box if we are going to the "current" time 890 long currMillis = System.currentTimeMillis(); 891 boolean recent = (currMillis - 10000) < millis && millis < currMillis; 892 mSelectionMode = (recent || ignoreTime) ? SELECTION_HIDDEN : SELECTION_SELECTED; 893 mRemeasure = true; 894 invalidate(); 895 896 if (gotoY != Integer.MIN_VALUE) { 897 TypeEvaluator evaluator = new TypeEvaluator() { 898 @Override 899 public Object evaluate(float fraction, Object startValue, Object endValue) { 900 int start = (Integer) startValue; 901 int end = (Integer) endValue; 902 final int newValue = (int) ((end - start) * fraction + start); 903 setViewStartY(newValue); 904 return new Integer(newValue); 905 } 906 }; 907 ValueAnimator scrollAnim = ObjectAnimator.ofObject(evaluator, new Integer(mViewStartY), 908 new Integer(gotoY)); 909// TODO The following line is supposed to replace the two statements above. 910// Need to investigate why it's not working. 911 912// ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); 913 scrollAnim.setDuration(200); 914 scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 915 scrollAnim.addListener(mAnimatorListener); 916 scrollAnim.start(); 917 } 918 } 919 920 public void setViewStartY(int viewStartY) { 921 if (viewStartY > mMaxViewStartY) { 922 viewStartY = mMaxViewStartY; 923 } 924 925 mViewStartY = viewStartY; 926 927 computeFirstHour(); 928 invalidate(); 929 } 930 931 public Time getSelectedDay() { 932 Time time = new Time(mBaseDate); 933 time.setJulianDay(mSelectionDay); 934 time.hour = mSelectionHour; 935 936 // We ignore the "isDst" field because we want normalize() to figure 937 // out the correct DST value and not adjust the selected time based 938 // on the current setting of DST. 939 time.normalize(true /* ignore isDst */); 940 return time; 941 } 942 943 public void updateTitle() { 944 Time start = new Time(mBaseDate); 945 start.normalize(true); 946 Time end = new Time(start); 947 end.monthDay += mNumDays - 1; 948 // Move it forward one minute so the formatter doesn't lose a day 949 end.minute += 1; 950 end.normalize(true); 951 952 long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 953 if (mNumDays != 1) { 954 // Don't show day of the month if for multi-day view 955 formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; 956 957 // Abbreviate the month if showing multiple months 958 if (start.month != end.month) { 959 formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; 960 } 961 } 962 963 mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, 964 formatFlags, null, null); 965 } 966 967 /** 968 * return a negative number if "time" is comes before the visible time 969 * range, a positive number if "time" is after the visible time range, and 0 970 * if it is in the visible time range. 971 */ 972 public int compareToVisibleTimeRange(Time time) { 973 974 int savedHour = mBaseDate.hour; 975 int savedMinute = mBaseDate.minute; 976 int savedSec = mBaseDate.second; 977 978 mBaseDate.hour = 0; 979 mBaseDate.minute = 0; 980 mBaseDate.second = 0; 981 982 if (DEBUG) { 983 Log.d(TAG, "Begin " + mBaseDate.toString()); 984 Log.d(TAG, "Diff " + time.toString()); 985 } 986 987 // Compare beginning of range 988 int diff = Time.compare(time, mBaseDate); 989 if (diff > 0) { 990 // Compare end of range 991 mBaseDate.monthDay += mNumDays; 992 mBaseDate.normalize(true); 993 diff = Time.compare(time, mBaseDate); 994 995 if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); 996 997 mBaseDate.monthDay -= mNumDays; 998 mBaseDate.normalize(true); 999 if (diff < 0) { 1000 // in visible time 1001 diff = 0; 1002 } else if (diff == 0) { 1003 // Midnight of following day 1004 diff = 1; 1005 } 1006 } 1007 1008 if (DEBUG) Log.d(TAG, "Diff: " + diff); 1009 1010 mBaseDate.hour = savedHour; 1011 mBaseDate.minute = savedMinute; 1012 mBaseDate.second = savedSec; 1013 return diff; 1014 } 1015 1016 private void recalc() { 1017 // Set the base date to the beginning of the week if we are displaying 1018 // 7 days at a time. 1019 if (mNumDays == 7) { 1020 adjustToBeginningOfWeek(mBaseDate); 1021 } 1022 1023 final long start = mBaseDate.toMillis(false /* use isDst */); 1024 mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); 1025 mLastJulianDay = mFirstJulianDay + mNumDays - 1; 1026 1027 mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); 1028 mFirstVisibleDate = mBaseDate.monthDay; 1029 mFirstVisibleDayOfWeek = mBaseDate.weekDay; 1030 } 1031 1032 private void adjustToBeginningOfWeek(Time time) { 1033 int dayOfWeek = time.weekDay; 1034 int diff = dayOfWeek - mFirstDayOfWeek; 1035 if (diff != 0) { 1036 if (diff < 0) { 1037 diff += 7; 1038 } 1039 time.monthDay -= diff; 1040 time.normalize(true /* ignore isDst */); 1041 } 1042 } 1043 1044 @Override 1045 protected void onSizeChanged(int width, int height, int oldw, int oldh) { 1046 mViewWidth = width; 1047 mViewHeight = height; 1048 int gridAreaWidth = width - mHoursWidth; 1049 mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; 1050 1051 // This would be about 1 day worth in a 7 day view 1052 mHorizontalSnapBackThreshold = width / 7; 1053 1054 Paint p = new Paint(); 1055 p.setTextSize(HOURS_TEXT_SIZE); 1056 mHoursTextHeight = (int) Math.abs(p.ascent()); 1057 remeasure(width, height); 1058 } 1059 1060 /** 1061 * Measures the space needed for various parts of the view after 1062 * loading new events. This can change if there are all-day events. 1063 */ 1064 private void remeasure(int width, int height) { 1065 // Shrink to fit available space but make sure we can display at least two events 1066 MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); 1067 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); 1068 MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, 1069 (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); 1070 mMaxUnexpandedAlldayEventCount = 1071 (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1072 1073 // First, clear the array of earliest start times, and the array 1074 // indicating presence of an all-day event. 1075 for (int day = 0; day < mNumDays; day++) { 1076 mEarliestStartHour[day] = 25; // some big number 1077 mHasAllDayEvent[day] = false; 1078 } 1079 1080 int maxAllDayEvents = mMaxAlldayEvents; 1081 1082 // The min is where 24 hours cover the entire visible area 1083 mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); 1084 if (mCellHeight < mMinCellHeight) { 1085 mCellHeight = mMinCellHeight; 1086 } 1087 1088 // Calculate mAllDayHeight 1089 mFirstCell = DAY_HEADER_HEIGHT; 1090 int allDayHeight = 0; 1091 if (maxAllDayEvents > 0) { 1092 int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; 1093 // If there is at most one all-day event per day, then use less 1094 // space (but more than the space for a single event). 1095 if (maxAllDayEvents == 1) { 1096 allDayHeight = SINGLE_ALLDAY_HEIGHT; 1097 } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ 1098 // Allow the all-day area to grow in height depending on the 1099 // number of all-day events we need to show, up to a limit. 1100 allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 1101 if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1102 allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; 1103 } 1104 } else { 1105 // if we have more than the magic number, check if we're animating 1106 // and if not adjust the sizes appropriately 1107 if (mAnimateDayHeight != 0) { 1108 // Don't shrink the space past the final allDay space. The animation 1109 // continues to hide the last event so the more events text can 1110 // fade in. 1111 allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); 1112 } else { 1113 // Try to fit all the events in 1114 allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1115 // But clip the area depending on which mode we're in 1116 if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { 1117 allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * 1118 MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); 1119 } else if (allDayHeight > maxAllAllDayHeight) { 1120 allDayHeight = maxAllAllDayHeight; 1121 } 1122 } 1123 } 1124 mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; 1125 } else { 1126 mSelectionAllday = false; 1127 } 1128 mAlldayHeight = allDayHeight; 1129 1130 mGridAreaHeight = height - mFirstCell; 1131 1132 // Set up the expand icon position 1133 int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); 1134 mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, 1135 EVENT_ALL_DAY_TEXT_LEFT_MARGIN); 1136 mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth 1137 - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); 1138 mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; 1139 mExpandAllDayRect.top = mExpandAllDayRect.bottom 1140 - mExpandAlldayDrawable.getIntrinsicHeight(); 1141 1142 // Cause the allDay text to be relaid out on the next draw pass 1143 mAllDayTextLayout = null; 1144 1145 1146 mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); 1147 mEventGeometry.setHourHeight(mCellHeight); 1148 1149 final long minimumDurationMillis = (long) 1150 (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); 1151 Event.computePositions(mEvents, minimumDurationMillis); 1152 1153 // Compute the top of our reachable view 1154 mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; 1155 if (DEBUG) { 1156 Log.e(TAG, "mViewStartY: " + mViewStartY); 1157 Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); 1158 } 1159 if (mViewStartY > mMaxViewStartY) { 1160 mViewStartY = mMaxViewStartY; 1161 computeFirstHour(); 1162 } 1163 1164 if (mFirstHour == -1) { 1165 initFirstHour(); 1166 mFirstHourOffset = 0; 1167 } 1168 1169 // When we change the base date, the number of all-day events may 1170 // change and that changes the cell height. When we switch dates, 1171 // we use the mFirstHourOffset from the previous view, but that may 1172 // be too large for the new view if the cell height is smaller. 1173 if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { 1174 mFirstHourOffset = mCellHeight + HOUR_GAP - 1; 1175 } 1176 mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; 1177 1178 final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); 1179 //When we get new events we don't want to dismiss the popup unless the event changes 1180 if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { 1181 mPopup.dismiss(); 1182 } 1183 mPopup.setWidth(eventAreaWidth - 20); 1184 mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 1185 } 1186 1187 /** 1188 * Initialize the state for another view. The given view is one that has 1189 * its own bitmap and will use an animation to replace the current view. 1190 * The current view and new view are either both Week views or both Day 1191 * views. They differ in their base date. 1192 * 1193 * @param view the view to initialize. 1194 */ 1195 private void initView(DayView view) { 1196 view.mSelectionHour = mSelectionHour; 1197 view.mSelectedEvents.clear(); 1198 view.mComputeSelectedEvents = true; 1199 view.mFirstHour = mFirstHour; 1200 view.mFirstHourOffset = mFirstHourOffset; 1201 view.remeasure(getWidth(), getHeight()); 1202 view.initAllDayHeights(); 1203 1204 view.mSelectedEvent = null; 1205 view.mPrevSelectedEvent = null; 1206 view.mFirstDayOfWeek = mFirstDayOfWeek; 1207 if (view.mEvents.size() > 0) { 1208 view.mSelectionAllday = mSelectionAllday; 1209 } else { 1210 view.mSelectionAllday = false; 1211 } 1212 1213 // Redraw the screen so that the selection box will be redrawn. We may 1214 // have scrolled to a different part of the day in some other view 1215 // so the selection box in this view may no longer be visible. 1216 view.recalc(); 1217 } 1218 1219 /** 1220 * Switch to another view based on what was selected (an event or a free 1221 * slot) and how it was selected (by touch or by trackball). 1222 * 1223 * @param trackBallSelection true if the selection was made using the 1224 * trackball. 1225 */ 1226 private void switchViews(boolean trackBallSelection) { 1227 Event selectedEvent = mSelectedEvent; 1228 1229 mPopup.dismiss(); 1230 mLastPopupEventID = INVALID_EVENT_ID; 1231 if (mNumDays > 1) { 1232 // This is the Week view. 1233 // With touch, we always switch to Day/Agenda View 1234 // With track ball, if we selected a free slot, then create an event. 1235 // If we selected a specific event, switch to EventInfo view. 1236 if (trackBallSelection) { 1237 if (selectedEvent == null) { 1238 // Switch to the EditEvent view 1239 long startMillis = getSelectedTimeInMillis(); 1240 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1241 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 1242 startMillis, endMillis, 0, 0, -1); 1243 } else { 1244 // Switch to the EventInfo view 1245 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1246 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1247 getSelectedTimeInMillis()); 1248 } 1249 } else { 1250 // This was a touch selection. If the touch selected a single 1251 // unambiguous event, then view that event. Otherwise go to 1252 // Day/Agenda view. 1253 if (mSelectedEvents.size() == 1) { 1254 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1255 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1256 getSelectedTimeInMillis()); 1257 } 1258 } 1259 } else { 1260 // This is the Day view. 1261 // If we selected a free slot, then create an event. 1262 // If we selected an event, then go to the EventInfo view. 1263 if (selectedEvent == null) { 1264 // Switch to the EditEvent view 1265 long startMillis = getSelectedTimeInMillis(); 1266 long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; 1267 1268 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, startMillis, 1269 endMillis, 0, 0, -1); 1270 } else { 1271 mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, 1272 selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, 1273 getSelectedTimeInMillis()); 1274 } 1275 } 1276 } 1277 1278 @Override 1279 public boolean onKeyUp(int keyCode, KeyEvent event) { 1280 mScrolling = false; 1281 long duration = event.getEventTime() - event.getDownTime(); 1282 1283 switch (keyCode) { 1284 case KeyEvent.KEYCODE_DPAD_CENTER: 1285 if (mSelectionMode == SELECTION_HIDDEN) { 1286 // Don't do anything unless the selection is visible. 1287 break; 1288 } 1289 1290 if (mSelectionMode == SELECTION_PRESSED) { 1291 // This was the first press when there was nothing selected. 1292 // Change the selection from the "pressed" state to the 1293 // the "selected" state. We treat short-press and 1294 // long-press the same here because nothing was selected. 1295 mSelectionMode = SELECTION_SELECTED; 1296 invalidate(); 1297 break; 1298 } 1299 1300 // Check the duration to determine if this was a short press 1301 if (duration < ViewConfiguration.getLongPressTimeout()) { 1302 switchViews(true /* trackball */); 1303 } else { 1304 mSelectionMode = SELECTION_LONGPRESS; 1305 invalidate(); 1306 performLongClick(); 1307 } 1308 break; 1309// case KeyEvent.KEYCODE_BACK: 1310// if (event.isTracking() && !event.isCanceled()) { 1311// mPopup.dismiss(); 1312// mContext.finish(); 1313// return true; 1314// } 1315// break; 1316 } 1317 return super.onKeyUp(keyCode, event); 1318 } 1319 1320 @Override 1321 public boolean onKeyDown(int keyCode, KeyEvent event) { 1322 if (mSelectionMode == SELECTION_HIDDEN) { 1323 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1324 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP 1325 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 1326 // Display the selection box but don't move or select it 1327 // on this key press. 1328 mSelectionMode = SELECTION_SELECTED; 1329 invalidate(); 1330 return true; 1331 } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 1332 // Display the selection box but don't select it 1333 // on this key press. 1334 mSelectionMode = SELECTION_PRESSED; 1335 invalidate(); 1336 return true; 1337 } 1338 } 1339 1340 mSelectionMode = SELECTION_SELECTED; 1341 mScrolling = false; 1342 boolean redraw; 1343 int selectionDay = mSelectionDay; 1344 1345 switch (keyCode) { 1346 case KeyEvent.KEYCODE_DEL: 1347 // Delete the selected event, if any 1348 Event selectedEvent = mSelectedEvent; 1349 if (selectedEvent == null) { 1350 return false; 1351 } 1352 mPopup.dismiss(); 1353 mLastPopupEventID = INVALID_EVENT_ID; 1354 1355 long begin = selectedEvent.startMillis; 1356 long end = selectedEvent.endMillis; 1357 long id = selectedEvent.id; 1358 mDeleteEventHelper.delete(begin, end, id, -1); 1359 return true; 1360 case KeyEvent.KEYCODE_ENTER: 1361 switchViews(true /* trackball or keyboard */); 1362 return true; 1363 case KeyEvent.KEYCODE_BACK: 1364 if (event.getRepeatCount() == 0) { 1365 event.startTracking(); 1366 return true; 1367 } 1368 return super.onKeyDown(keyCode, event); 1369 case KeyEvent.KEYCODE_DPAD_LEFT: 1370 if (mSelectedEvent != null) { 1371 mSelectedEvent = mSelectedEvent.nextLeft; 1372 } 1373 if (mSelectedEvent == null) { 1374 mLastPopupEventID = INVALID_EVENT_ID; 1375 selectionDay -= 1; 1376 } 1377 redraw = true; 1378 break; 1379 1380 case KeyEvent.KEYCODE_DPAD_RIGHT: 1381 if (mSelectedEvent != null) { 1382 mSelectedEvent = mSelectedEvent.nextRight; 1383 } 1384 if (mSelectedEvent == null) { 1385 mLastPopupEventID = INVALID_EVENT_ID; 1386 selectionDay += 1; 1387 } 1388 redraw = true; 1389 break; 1390 1391 case KeyEvent.KEYCODE_DPAD_UP: 1392 if (mSelectedEvent != null) { 1393 mSelectedEvent = mSelectedEvent.nextUp; 1394 } 1395 if (mSelectedEvent == null) { 1396 mLastPopupEventID = INVALID_EVENT_ID; 1397 if (!mSelectionAllday) { 1398 mSelectionHour -= 1; 1399 adjustHourSelection(); 1400 mSelectedEvents.clear(); 1401 mComputeSelectedEvents = true; 1402 } 1403 } 1404 redraw = true; 1405 break; 1406 1407 case KeyEvent.KEYCODE_DPAD_DOWN: 1408 if (mSelectedEvent != null) { 1409 mSelectedEvent = mSelectedEvent.nextDown; 1410 } 1411 if (mSelectedEvent == null) { 1412 mLastPopupEventID = INVALID_EVENT_ID; 1413 if (mSelectionAllday) { 1414 mSelectionAllday = false; 1415 } else { 1416 mSelectionHour++; 1417 adjustHourSelection(); 1418 mSelectedEvents.clear(); 1419 mComputeSelectedEvents = true; 1420 } 1421 } 1422 redraw = true; 1423 break; 1424 1425 default: 1426 return super.onKeyDown(keyCode, event); 1427 } 1428 1429 if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { 1430 DayView view = (DayView) mViewSwitcher.getNextView(); 1431 Time date = view.mBaseDate; 1432 date.set(mBaseDate); 1433 if (selectionDay < mFirstJulianDay) { 1434 date.monthDay -= mNumDays; 1435 } else { 1436 date.monthDay += mNumDays; 1437 } 1438 date.normalize(true /* ignore isDst */); 1439 view.mSelectionDay = selectionDay; 1440 1441 initView(view); 1442 1443 Time end = new Time(date); 1444 end.monthDay += mNumDays - 1; 1445 mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); 1446 return true; 1447 } 1448 if (mSelectionDay != selectionDay) { 1449 Time date = new Time(mBaseDate); 1450 date.setJulianDay(selectionDay); 1451 date.hour = mSelectionHour; 1452 mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); 1453 } 1454 mSelectionDay = selectionDay; 1455 mSelectedEvents.clear(); 1456 mComputeSelectedEvents = true; 1457 mUpdateToast = true; 1458 1459 if (redraw) { 1460 invalidate(); 1461 return true; 1462 } 1463 1464 return super.onKeyDown(keyCode, event); 1465 } 1466 1467 private class AccessibilityRunnable implements Runnable { 1468 int mEventType = AccessibilityEvent.TYPE_VIEW_SELECTED; 1469 @Override 1470 public void run() { 1471 sendAccessibilityEvent(mEventType); 1472 } 1473 } 1474 1475 private AccessibilityRunnable mDispatchAccessibilityEventRunnable = new AccessibilityRunnable(); 1476 1477 @Override 1478 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1479 if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SELECTED && 1480 event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) { 1481 return false; 1482 } 1483 StringBuilder b = new StringBuilder(getSelectedTime() 1484 .format(mIs24HourFormat ? HOUR_FORMAT_24H : HOUR_FORMAT_12H)); 1485 b.append(PERIOD_SPACE); 1486 int numEvents = mSelectedEvents.size(); 1487 if (mEventCountTemplate == null) { 1488 mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); 1489 } 1490 switch (event.getEventType()) { 1491 // When a new hour is selected we sent this event 1492 case AccessibilityEvent.TYPE_VIEW_SELECTED: 1493 int i = 1; 1494 for (Event calEvent : mSelectedEvents) { 1495 if (numEvents > 1) { 1496 mStringBuilder.setLength(0); 1497 b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); 1498 b.append(" "); 1499 } 1500 appendEventAccessibilityString(b, calEvent); 1501 } 1502 break; 1503 // When a different event is selected we send this event 1504 case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: 1505 if (mSelectedEvent != null) { 1506 if (numEvents > 1) { 1507 mStringBuilder.setLength(0); 1508 b.append(mFormatter.format(mEventCountTemplate, 1509 mSelectedEvents.indexOf(mSelectedEvent) + 1, numEvents)); 1510 b.append(" "); 1511 } 1512 appendEventAccessibilityString(b, mSelectedEvent); 1513 } 1514 break; 1515 default: 1516 break; 1517 } 1518 CharSequence msg = b.toString(); 1519 event.getText().add(msg); 1520 event.setAddedCount(msg.length()); 1521 return true; 1522 } 1523 1524 /** 1525 * @param b 1526 * @param calEvent 1527 */ 1528 private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { 1529 b.append(calEvent.getTitleAndLocation()); 1530 b.append(PERIOD_SPACE); 1531 String when; 1532 int flags = DateUtils.FORMAT_SHOW_DATE; 1533 if (calEvent.allDay) { 1534 flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; 1535 } else { 1536 flags |= DateUtils.FORMAT_SHOW_TIME; 1537 if (DateFormat.is24HourFormat(mContext)) { 1538 flags |= DateUtils.FORMAT_24HOUR; 1539 } 1540 } 1541 when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); 1542 b.append(when); 1543 b.append(PERIOD_SPACE); 1544 } 1545 1546 private class GotoBroadcaster implements Animation.AnimationListener { 1547 private final int mCounter; 1548 private final Time mStart; 1549 private final Time mEnd; 1550 1551 public GotoBroadcaster(Time start, Time end) { 1552 mCounter = ++sCounter; 1553 mStart = start; 1554 mEnd = end; 1555 } 1556 1557 @Override 1558 public void onAnimationEnd(Animation animation) { 1559 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1560 view.mViewStartX = 0; 1561 view = (DayView) mViewSwitcher.getNextView(); 1562 view.mViewStartX = 0; 1563 1564 if (mCounter == sCounter) { 1565 mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, 1566 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 1567 } 1568 } 1569 1570 @Override 1571 public void onAnimationRepeat(Animation animation) { 1572 } 1573 1574 @Override 1575 public void onAnimationStart(Animation animation) { 1576 } 1577 } 1578 1579 private View switchViews(boolean forward, float xOffSet, float width, float velocity) { 1580 mAnimationDistance = width - xOffSet; 1581 if (DEBUG) { 1582 Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); 1583 } 1584 1585 float progress = Math.abs(xOffSet) / width; 1586 if (progress > 1.0f) { 1587 progress = 1.0f; 1588 } 1589 1590 float inFromXValue, inToXValue; 1591 float outFromXValue, outToXValue; 1592 if (forward) { 1593 inFromXValue = 1.0f - progress; 1594 inToXValue = 0.0f; 1595 outFromXValue = -progress; 1596 outToXValue = -1.0f; 1597 } else { 1598 inFromXValue = progress - 1.0f; 1599 inToXValue = 0.0f; 1600 outFromXValue = progress; 1601 outToXValue = 1.0f; 1602 } 1603 1604 final Time start = new Time(mBaseDate.timezone); 1605 start.set(mController.getTime()); 1606 if (forward) { 1607 start.monthDay += mNumDays; 1608 } else { 1609 start.monthDay -= mNumDays; 1610 } 1611 mController.setTime(start.normalize(true)); 1612 1613 Time newSelected = start; 1614 1615 if (mNumDays == 7) { 1616 newSelected = new Time(start); 1617 adjustToBeginningOfWeek(start); 1618 } 1619 1620 final Time end = new Time(start); 1621 end.monthDay += mNumDays - 1; 1622 1623 // We have to allocate these animation objects each time we switch views 1624 // because that is the only way to set the animation parameters. 1625 TranslateAnimation inAnimation = new TranslateAnimation( 1626 Animation.RELATIVE_TO_SELF, inFromXValue, 1627 Animation.RELATIVE_TO_SELF, inToXValue, 1628 Animation.ABSOLUTE, 0.0f, 1629 Animation.ABSOLUTE, 0.0f); 1630 1631 TranslateAnimation outAnimation = new TranslateAnimation( 1632 Animation.RELATIVE_TO_SELF, outFromXValue, 1633 Animation.RELATIVE_TO_SELF, outToXValue, 1634 Animation.ABSOLUTE, 0.0f, 1635 Animation.ABSOLUTE, 0.0f); 1636 1637 long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); 1638 inAnimation.setDuration(duration); 1639 inAnimation.setInterpolator(mHScrollInterpolator); 1640 outAnimation.setInterpolator(mHScrollInterpolator); 1641 outAnimation.setDuration(duration); 1642 outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); 1643 mViewSwitcher.setInAnimation(inAnimation); 1644 mViewSwitcher.setOutAnimation(outAnimation); 1645 1646 DayView view = (DayView) mViewSwitcher.getCurrentView(); 1647 view.cleanup(); 1648 mViewSwitcher.showNext(); 1649 view = (DayView) mViewSwitcher.getCurrentView(); 1650 view.setSelected(newSelected, true); 1651 view.requestFocus(); 1652 view.reloadEvents(); 1653 view.updateTitle(); 1654 1655 return view; 1656 } 1657 1658 // This is called after scrolling stops to move the selected hour 1659 // to the visible part of the screen. 1660 private void resetSelectedHour() { 1661 if (mSelectionHour < mFirstHour + 1) { 1662 mSelectionHour = mFirstHour + 1; 1663 mSelectedEvent = null; 1664 mSelectedEvents.clear(); 1665 mComputeSelectedEvents = true; 1666 } else if (mSelectionHour > mFirstHour + mNumHours - 3) { 1667 mSelectionHour = mFirstHour + mNumHours - 3; 1668 mSelectedEvent = null; 1669 mSelectedEvents.clear(); 1670 mComputeSelectedEvents = true; 1671 } 1672 } 1673 1674 private void initFirstHour() { 1675 mFirstHour = mSelectionHour - mNumHours / 5; 1676 if (mFirstHour < 0) { 1677 mFirstHour = 0; 1678 } else if (mFirstHour + mNumHours > 24) { 1679 mFirstHour = 24 - mNumHours; 1680 } 1681 } 1682 1683 /** 1684 * Recomputes the first full hour that is visible on screen after the 1685 * screen is scrolled. 1686 */ 1687 private void computeFirstHour() { 1688 // Compute the first full hour that is visible on screen 1689 mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); 1690 mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; 1691 } 1692 1693 private void adjustHourSelection() { 1694 if (mSelectionHour < 0) { 1695 mSelectionHour = 0; 1696 if (mMaxAlldayEvents > 0) { 1697 mPrevSelectedEvent = null; 1698 mSelectionAllday = true; 1699 } 1700 } 1701 1702 if (mSelectionHour > 23) { 1703 mSelectionHour = 23; 1704 } 1705 1706 // If the selected hour is at least 2 time slots from the top and 1707 // bottom of the screen, then don't scroll the view. 1708 if (mSelectionHour < mFirstHour + 1) { 1709 // If there are all-days events for the selected day but there 1710 // are no more normal events earlier in the day, then jump to 1711 // the all-day event area. 1712 // Exception 1: allow the user to scroll to 8am with the trackball 1713 // before jumping to the all-day event area. 1714 // Exception 2: if 12am is on screen, then allow the user to select 1715 // 12am before going up to the all-day event area. 1716 int daynum = mSelectionDay - mFirstJulianDay; 1717 if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour 1718 && mFirstHour > 0 && mFirstHour < 8) { 1719 mPrevSelectedEvent = null; 1720 mSelectionAllday = true; 1721 mSelectionHour = mFirstHour + 1; 1722 return; 1723 } 1724 1725 if (mFirstHour > 0) { 1726 mFirstHour -= 1; 1727 mViewStartY -= (mCellHeight + HOUR_GAP); 1728 if (mViewStartY < 0) { 1729 mViewStartY = 0; 1730 } 1731 return; 1732 } 1733 } 1734 1735 if (mSelectionHour > mFirstHour + mNumHours - 3) { 1736 if (mFirstHour < 24 - mNumHours) { 1737 mFirstHour += 1; 1738 mViewStartY += (mCellHeight + HOUR_GAP); 1739 if (mViewStartY > mMaxViewStartY) { 1740 mViewStartY = mMaxViewStartY; 1741 } 1742 return; 1743 } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { 1744 mViewStartY = mMaxViewStartY; 1745 } 1746 } 1747 } 1748 1749 void clearCachedEvents() { 1750 mLastReloadMillis = 0; 1751 } 1752 1753 private Runnable mCancelCallback = new Runnable() { 1754 public void run() { 1755 clearCachedEvents(); 1756 } 1757 }; 1758 1759 /* package */ void reloadEvents() { 1760 // Protect against this being called before this view has been 1761 // initialized. 1762// if (mContext == null) { 1763// return; 1764// } 1765 1766 // Make sure our time zones are up to date 1767 mTZUpdater.run(); 1768 1769 mSelectedEvent = null; 1770 mPrevSelectedEvent = null; 1771 mSelectedEvents.clear(); 1772 1773 // The start date is the beginning of the week at 12am 1774 Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); 1775 weekStart.set(mBaseDate); 1776 weekStart.hour = 0; 1777 weekStart.minute = 0; 1778 weekStart.second = 0; 1779 long millis = weekStart.normalize(true /* ignore isDst */); 1780 1781 // Avoid reloading events unnecessarily. 1782 if (millis == mLastReloadMillis) { 1783 return; 1784 } 1785 mLastReloadMillis = millis; 1786 1787 // load events in the background 1788// mContext.startProgressSpinner(); 1789 final ArrayList<Event> events = new ArrayList<Event>(); 1790 mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { 1791 public void run() { 1792 mEvents = events; 1793 if (mAllDayEvents == null) { 1794 mAllDayEvents = new ArrayList<Event>(); 1795 } else { 1796 mAllDayEvents.clear(); 1797 } 1798 1799 // Create a shorter array for all day events 1800 for (Event e : events) { 1801 if (e.drawAsAllday()) { 1802 mAllDayEvents.add(e); 1803 } 1804 } 1805 1806 // New events, new layouts 1807 if (mLayouts == null || mLayouts.length < events.size()) { 1808 mLayouts = new StaticLayout[events.size()]; 1809 } else { 1810 Arrays.fill(mLayouts, null); 1811 } 1812 1813 if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { 1814 mAllDayLayouts = new StaticLayout[events.size()]; 1815 } else { 1816 Arrays.fill(mAllDayLayouts, null); 1817 } 1818 1819 computeEventRelations(); 1820 1821 mRemeasure = true; 1822 mComputeSelectedEvents = true; 1823 recalc(); 1824// mContext.stopProgressSpinner(); 1825 invalidate(); 1826 } 1827 }, mCancelCallback); 1828 } 1829 1830 private void computeEventRelations() { 1831 // Compute the layout relation between each event before measuring cell 1832 // width, as the cell width should be adjusted along with the relation. 1833 // 1834 // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) 1835 // We should mark them as "overwapped". Though they are not overwapped logically, but 1836 // minimum cell height implicitly expands the cell height of A and it should look like 1837 // (1:00pm - 1:15pm) after the cell height adjustment. 1838 1839 // Compute the space needed for the all-day events, if any. 1840 // Make a pass over all the events, and keep track of the maximum 1841 // number of all-day events in any one day. Also, keep track of 1842 // the earliest event in each day. 1843 int maxAllDayEvents = 0; 1844 final ArrayList<Event> events = mEvents; 1845 final int len = events.size(); 1846 // Num of all-day-events on each day. 1847 final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; 1848 Arrays.fill(eventsCount, 0); 1849 for (int ii = 0; ii < len; ii++) { 1850 Event event = events.get(ii); 1851 if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { 1852 continue; 1853 } 1854 if (event.drawAsAllday()) { 1855 // Count all the events being drawn as allDay events 1856 final int firstDay = Math.max(event.startDay, mFirstJulianDay); 1857 final int lastDay = Math.min(event.endDay, mLastJulianDay); 1858 for (int day = firstDay; day <= lastDay; day++) { 1859 final int count = ++eventsCount[day - mFirstJulianDay]; 1860 if (maxAllDayEvents < count) { 1861 maxAllDayEvents = count; 1862 } 1863 } 1864 1865 int daynum = event.startDay - mFirstJulianDay; 1866 int durationDays = event.endDay - event.startDay + 1; 1867 if (daynum < 0) { 1868 durationDays += daynum; 1869 daynum = 0; 1870 } 1871 if (daynum + durationDays > mNumDays) { 1872 durationDays = mNumDays - daynum; 1873 } 1874 for (int day = daynum; durationDays > 0; day++, durationDays--) { 1875 mHasAllDayEvent[day] = true; 1876 } 1877 } else { 1878 int daynum = event.startDay - mFirstJulianDay; 1879 int hour = event.startTime / 60; 1880 if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { 1881 mEarliestStartHour[daynum] = hour; 1882 } 1883 1884 // Also check the end hour in case the event spans more than 1885 // one day. 1886 daynum = event.endDay - mFirstJulianDay; 1887 hour = event.endTime / 60; 1888 if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { 1889 mEarliestStartHour[daynum] = hour; 1890 } 1891 } 1892 } 1893 mMaxAlldayEvents = maxAllDayEvents; 1894 initAllDayHeights(); 1895 } 1896 1897 @Override 1898 protected void onDraw(Canvas canvas) { 1899 if (mRemeasure) { 1900 remeasure(getWidth(), getHeight()); 1901 mRemeasure = false; 1902 } 1903 canvas.save(); 1904 1905 float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; 1906 // offset canvas by the current drag and header position 1907 canvas.translate(-mViewStartX, yTranslate); 1908 // clip to everything below the allDay area 1909 Rect dest = mDestRect; 1910 dest.top = (int) (mFirstCell - yTranslate); 1911 dest.bottom = (int) (mViewHeight - yTranslate); 1912 dest.left = 0; 1913 dest.right = mViewWidth; 1914 canvas.save(); 1915 canvas.clipRect(dest); 1916 // Draw the movable part of the view 1917 doDraw(canvas); 1918 // restore to having no clip 1919 canvas.restore(); 1920 1921 if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { 1922 float xTranslate; 1923 if (mViewStartX > 0) { 1924 xTranslate = mViewWidth; 1925 } else { 1926 xTranslate = -mViewWidth; 1927 } 1928 // Move the canvas around to prep it for the next view 1929 // specifically, shift it by a screen and undo the 1930 // yTranslation which will be redone in the nextView's onDraw(). 1931 canvas.translate(xTranslate, -yTranslate); 1932 DayView nextView = (DayView) mViewSwitcher.getNextView(); 1933 1934 // Prevent infinite recursive calls to onDraw(). 1935 nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; 1936 1937 nextView.onDraw(canvas); 1938 // Move it back for this view 1939 canvas.translate(-xTranslate, 0); 1940 } else { 1941 // If we drew another view we already translated it back 1942 // If we didn't draw another view we should be at the edge of the 1943 // screen 1944 canvas.translate(mViewStartX, -yTranslate); 1945 } 1946 1947 // Draw the fixed areas (that don't scroll) directly to the canvas. 1948 drawAfterScroll(canvas); 1949 if (mComputeSelectedEvents && mUpdateToast) { 1950 updateEventDetails(); 1951 mUpdateToast = false; 1952 } 1953 mComputeSelectedEvents = false; 1954 canvas.restore(); 1955 } 1956 1957 private void drawAfterScroll(Canvas canvas) { 1958 Paint p = mPaint; 1959 Rect r = mRect; 1960 1961 drawAllDayHighlights(r, canvas, p); 1962 if (mMaxAlldayEvents != 0) { 1963 drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); 1964 drawUpperLeftCorner(r, canvas, p); 1965 } 1966 1967 drawScrollLine(r, canvas, p); 1968 drawDayHeaderLoop(r, canvas, p); 1969 1970 // Draw the AM and PM indicators if we're in 12 hour mode 1971 if (!mIs24HourFormat) { 1972 drawAmPm(canvas, p); 1973 } 1974 } 1975 1976 // This isn't really the upper-left corner. It's the square area just 1977 // below the upper-left corner, above the hours and to the left of the 1978 // all-day area. 1979 private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { 1980 // StaticLayout is wonky and doesn't do multi-line center text 1981 // correctly. In order to make it work set the paint to Align.CENTER and 1982 // the Layout to Alignment.ALIGN_NORMAL. 1983 p.setTextAlign(Align.CENTER); 1984 p.setTextSize(ALLDAY_TEXT_SIZE); 1985 p.setColor(mCalendarHourLabelColor); 1986 int translateY = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 1987 r.left = -mHoursWidth / 2; 1988 r.right = mHoursWidth / 2; 1989 r.top = 0; 1990 r.bottom = mFirstCell - translateY; 1991 canvas.save(); 1992 canvas.translate(mHoursWidth / 2, translateY); 1993 canvas.clipRect(r); 1994 StaticLayout allDayLayout = getOrCreateAllDayTextLayout(p); 1995 allDayLayout.draw(canvas); 1996 canvas.restore(); 1997 1998 setupHourTextPaint(p); 1999 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2000 // Draw the allDay expand/collapse icon 2001 if (mUseExpandIcon) { 2002 mExpandAlldayDrawable.setBounds(mExpandAllDayRect); 2003 mExpandAlldayDrawable.draw(canvas); 2004 } else { 2005 mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); 2006 mCollapseAlldayDrawable.draw(canvas); 2007 } 2008 } 2009 } 2010 2011 private void drawScrollLine(Rect r, Canvas canvas, Paint p) { 2012 final int right = computeDayLeftPosition(mNumDays + 1); 2013 final int y = mFirstCell - 1; 2014 2015 p.setAntiAlias(false); 2016 p.setStyle(Style.FILL); 2017 2018 p.setColor(mCalendarGridLineInnerHorizontalColor); 2019 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2020 canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); 2021 p.setAntiAlias(true); 2022 } 2023 2024 // Computes the x position for the left side of the given day 2025 private int computeDayLeftPosition(int day) { 2026 int effectiveWidth = mViewWidth - mHoursWidth; 2027 return day * effectiveWidth / mNumDays + mHoursWidth; 2028 } 2029 2030 private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { 2031 // First, color the labels area light gray 2032 r.top = 0; 2033 r.bottom = DAY_HEADER_HEIGHT; 2034 r.left = 0; 2035 r.right = mViewWidth; 2036 p.setColor(mBgColor); 2037 p.setStyle(Style.FILL); 2038 canvas.drawRect(r, p); 2039 // and the area that says All day 2040 r.top = DAY_HEADER_HEIGHT; 2041 r.bottom = mFirstCell - 1; 2042 r.left = 0; 2043 r.right = mHoursWidth; 2044 canvas.drawRect(r, p); 2045 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2046 if (mNumDays > 1 && todayIndex >= 0 && todayIndex < mNumDays) { 2047 // Draw the today highlight 2048 r.top = 0; 2049 r.bottom = mFirstCell - 1; 2050 r.left = computeDayLeftPosition(todayIndex) + 1; 2051 r.right = computeDayLeftPosition(todayIndex + 1); 2052 p.setColor(mTodayBgColor); 2053 p.setStyle(Style.FILL); 2054 canvas.drawRect(r, p); 2055 } 2056 if (mSelectionAllday) { 2057 // Draw the selection highlight on the selected all-day area 2058 mRect.top = DAY_HEADER_HEIGHT + 1; 2059 mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; 2060 int daynum = mSelectionDay - mFirstJulianDay; 2061 mRect.left = computeDayLeftPosition(daynum) + 1; 2062 mRect.right = computeDayLeftPosition(daynum + 1); 2063 p.setColor(mCalendarGridAreaSelected); 2064 canvas.drawRect(mRect, p); 2065 } 2066 } 2067 2068 private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { 2069 // Draw the horizontal day background banner 2070 // p.setColor(mCalendarDateBannerBackground); 2071 // r.top = 0; 2072 // r.bottom = DAY_HEADER_HEIGHT; 2073 // r.left = 0; 2074 // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); 2075 // canvas.drawRect(r, p); 2076 // 2077 // Fill the extra space on the right side with the default background 2078 // r.left = r.right; 2079 // r.right = mViewWidth; 2080 // p.setColor(mCalendarGridAreaBackground); 2081 // canvas.drawRect(r, p); 2082 if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { 2083 return; 2084 } 2085 int todayNum = mTodayJulianDay - mFirstJulianDay; 2086 2087 p.setTypeface(mBold); 2088 p.setTextAlign(Paint.Align.RIGHT); 2089 int deltaX = mCellWidth + DAY_GAP; 2090 int cell = mFirstJulianDay; 2091 2092 String[] dayNames; 2093 if (mDateStrWidth < mCellWidth) { 2094 dayNames = mDayStrs; 2095 } else { 2096 dayNames = mDayStrs2Letter; 2097 } 2098 2099 p.setAntiAlias(true); 2100 for (int day = 0; day < mNumDays; day++, cell++) { 2101 int dayOfWeek = day + mFirstVisibleDayOfWeek; 2102 if (dayOfWeek >= 14) { 2103 dayOfWeek -= 14; 2104 } 2105 2106 int color = mCalendarDateBannerTextColor; 2107 if (mNumDays == 1) { 2108 if (dayOfWeek == Time.SATURDAY) { 2109 color = mWeek_saturdayColor; 2110 } else if (dayOfWeek == Time.SUNDAY) { 2111 color = mWeek_sundayColor; 2112 } 2113 } else { 2114 final int column = day % 7; 2115 if (Utils.isSaturday(column, mFirstDayOfWeek)) { 2116 color = mWeek_saturdayColor; 2117 } else if (Utils.isSunday(column, mFirstDayOfWeek)) { 2118 color = mWeek_sundayColor; 2119 } 2120 } 2121 2122 p.setColor(color); 2123 drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); 2124 } 2125 p.setTypeface(null); 2126 } 2127 2128 private void drawAmPm(Canvas canvas, Paint p) { 2129 p.setColor(mCalendarAmPmLabel); 2130 p.setTextSize(AMPM_TEXT_SIZE); 2131 p.setTypeface(mBold); 2132 p.setAntiAlias(true); 2133 p.setTextAlign(Paint.Align.RIGHT); 2134 String text = mAmString; 2135 if (mFirstHour >= 12) { 2136 text = mPmString; 2137 } 2138 int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; 2139 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2140 2141 if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { 2142 // Also draw the "PM" 2143 text = mPmString; 2144 y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) 2145 + 2 * mHoursTextHeight + HOUR_GAP; 2146 canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); 2147 } 2148 } 2149 2150 private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, 2151 Paint p) { 2152 r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; 2153 r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER; 2154 2155 r.top = top - mCurrentTimeLine.getIntrinsicHeight() / 2; 2156 r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); 2157 2158 mCurrentTimeLine.setBounds(r); 2159 mCurrentTimeLine.draw(canvas); 2160 } 2161 2162 private void doDraw(Canvas canvas) { 2163 Paint p = mPaint; 2164 Rect r = mRect; 2165 2166 drawBgColors(r, canvas, p); 2167 drawGridBackground(r, canvas, p); 2168 drawHours(r, canvas, p); 2169 2170 // Draw each day 2171 int cell = mFirstJulianDay; 2172 p.setAntiAlias(false); 2173 for (int day = 0; day < mNumDays; day++, cell++) { 2174 // TODO Wow, this needs cleanup. drawEvents loop through all the 2175 // events on every call. 2176 drawEvents(cell, day, HOUR_GAP, canvas, p); 2177 // If this is today 2178 if (cell == mTodayJulianDay) { 2179 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2180 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2181 2182 // And the current time shows up somewhere on the screen 2183 if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { 2184 drawCurrentTimeLine(r, day, lineY, canvas, p); 2185 } 2186 } 2187 } 2188 p.setAntiAlias(true); 2189 } 2190 2191 private void drawHours(Rect r, Canvas canvas, Paint p) { 2192 // Comment out as the background will be a drawable 2193 2194 // Draw a highlight on the selected hour (if needed) 2195 if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { 2196 // p.setColor(mCalendarHourSelected); 2197 int daynum = mSelectionDay - mFirstJulianDay; 2198 r.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2199 r.bottom = r.top + mCellHeight + HOUR_GAP; 2200 r.left = computeDayLeftPosition(daynum); 2201 r.right = computeDayLeftPosition(daynum + 1); 2202 2203 // Draw a border around the highlighted grid hour. 2204 // drawEmptyRect(canvas, r, mSelectionPaint.getColor()); 2205 saveSelectionPosition(r.left, r.top, r.right, r.bottom); 2206 2207 // Also draw the highlight on the grid 2208 p.setColor(mCalendarGridAreaSelected); 2209 r.top += HOUR_GAP; 2210 r.right -= DAY_GAP; 2211 p.setAntiAlias(false); 2212 canvas.drawRect(r, p); 2213 } 2214 2215 setupHourTextPaint(p); 2216 2217 int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; 2218 2219 for (int i = 0; i < 24; i++) { 2220 String time = mHourStrs[i]; 2221 canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); 2222 y += mCellHeight + HOUR_GAP; 2223 } 2224 } 2225 2226 private void setupHourTextPaint(Paint p) { 2227 p.setColor(mCalendarHourLabelColor); 2228 p.setTextSize(HOURS_TEXT_SIZE); 2229 p.setTypeface(Typeface.DEFAULT); 2230 p.setTextAlign(Paint.Align.RIGHT); 2231 p.setAntiAlias(true); 2232 } 2233 2234 private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { 2235 int dateNum = mFirstVisibleDate + day; 2236 int x; 2237 if (dateNum > mMonthLength) { 2238 dateNum -= mMonthLength; 2239 } 2240 p.setAntiAlias(true); 2241 2242 // Draw day of the month 2243 String dateNumStr = String.valueOf(dateNum); 2244 if (mNumDays > 1) { 2245 float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; 2246 2247 // Draw day of the month 2248 x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; 2249 p.setTextAlign(Align.RIGHT); 2250 p.setTextSize(DATE_HEADER_FONT_SIZE); 2251 p.setTypeface(mBold); 2252 canvas.drawText(dateNumStr, x, y, p); 2253 2254 // Draw day of the week 2255 x -= p.measureText(" " + dateNumStr); 2256 p.setTextSize(DAY_HEADER_FONT_SIZE); 2257 p.setTypeface(Typeface.DEFAULT); 2258 canvas.drawText(dayStr, x, y, p); 2259 } else { 2260 float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; 2261 p.setTextAlign(Align.LEFT); 2262 2263 2264 // Draw day of the week 2265 x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; 2266 p.setTextSize(DAY_HEADER_FONT_SIZE); 2267 p.setTypeface(Typeface.DEFAULT); 2268 canvas.drawText(dayStr, x, y, p); 2269 2270 // Draw day of the month 2271 x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; 2272 p.setTextSize(DATE_HEADER_FONT_SIZE); 2273 p.setTypeface(mBold); 2274 canvas.drawText(dateNumStr, x, y, p); 2275 } 2276 } 2277 2278 private void drawGridBackground(Rect r, Canvas canvas, Paint p) { 2279 Paint.Style savedStyle = p.getStyle(); 2280 2281 final float stopX = computeDayLeftPosition(mNumDays + 1); 2282 float y = 0; 2283 final float deltaY = mCellHeight + HOUR_GAP; 2284 int linesIndex = 0; 2285 final float startY = 0; 2286 final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); 2287 float x = mHoursWidth; 2288 2289 // Draw the inner horizontal grid lines 2290 p.setColor(mCalendarGridLineInnerHorizontalColor); 2291 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2292 p.setAntiAlias(false); 2293 y = 0; 2294 linesIndex = 0; 2295 for (int hour = 0; hour <= 24; hour++) { 2296 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2297 mLines[linesIndex++] = y; 2298 mLines[linesIndex++] = stopX; 2299 mLines[linesIndex++] = y; 2300 y += deltaY; 2301 } 2302 if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { 2303 canvas.drawLines(mLines, 0, linesIndex, p); 2304 linesIndex = 0; 2305 p.setColor(mCalendarGridLineInnerVerticalColor); 2306 } 2307 2308 // Draw the inner vertical grid lines 2309 for (int day = 0; day <= mNumDays; day++) { 2310 x = computeDayLeftPosition(day); 2311 mLines[linesIndex++] = x; 2312 mLines[linesIndex++] = startY; 2313 mLines[linesIndex++] = x; 2314 mLines[linesIndex++] = stopY; 2315 } 2316 canvas.drawLines(mLines, 0, linesIndex, p); 2317 2318 // Restore the saved style. 2319 p.setStyle(savedStyle); 2320 p.setAntiAlias(true); 2321 } 2322 2323 /** 2324 * @param r 2325 * @param canvas 2326 * @param p 2327 */ 2328 private void drawBgColors(Rect r, Canvas canvas, Paint p) { 2329 int todayIndex = mTodayJulianDay - mFirstJulianDay; 2330 // Draw the hours background color 2331 r.top = mDestRect.top; 2332 r.bottom = mDestRect.bottom; 2333 r.left = 0; 2334 r.right = mHoursWidth; 2335 p.setColor(mBgColor); 2336 p.setStyle(Style.FILL); 2337 p.setAntiAlias(false); 2338 canvas.drawRect(r, p); 2339 if (mNumDays == 1 && todayIndex == 0) { 2340 // Draw a white background for the time later than current time 2341 int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) 2342 + ((mCurrentTime.minute * mCellHeight) / 60) + 1; 2343 if (lineY < mViewStartY + mViewHeight) { 2344 lineY = Math.max(lineY, mViewStartY); 2345 r.left = mHoursWidth; 2346 r.right = mViewWidth; 2347 r.top = lineY; 2348 r.bottom = mViewStartY + mViewHeight; 2349 p.setColor(mTodayBgColor); 2350 canvas.drawRect(r, p); 2351 } 2352 } else if (todayIndex >= 0 && todayIndex < mNumDays) { 2353 r.left = computeDayLeftPosition(todayIndex) + 1; 2354 r.right = computeDayLeftPosition(todayIndex + 1); 2355 r.top = mDestRect.top; 2356 r.bottom = mDestRect.bottom; 2357 p.setColor(mTodayBgColor); 2358 canvas.drawRect(r, p); 2359 } 2360 p.setAntiAlias(true); 2361 } 2362 2363 Event getSelectedEvent() { 2364 if (mSelectedEvent == null) { 2365 // There is no event at the selected hour, so create a new event. 2366 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2367 getSelectedMinutesSinceMidnight()); 2368 } 2369 return mSelectedEvent; 2370 } 2371 2372 boolean isEventSelected() { 2373 return (mSelectedEvent != null); 2374 } 2375 2376 Event getNewEvent() { 2377 return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), 2378 getSelectedMinutesSinceMidnight()); 2379 } 2380 2381 static Event getNewEvent(int julianDay, long utcMillis, 2382 int minutesSinceMidnight) { 2383 Event event = Event.newInstance(); 2384 event.startDay = julianDay; 2385 event.endDay = julianDay; 2386 event.startMillis = utcMillis; 2387 event.endMillis = event.startMillis + MILLIS_PER_HOUR; 2388 event.startTime = minutesSinceMidnight; 2389 event.endTime = event.startTime + MINUTES_PER_HOUR; 2390 return event; 2391 } 2392 2393 private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { 2394 float maxWidthF = 0.0f; 2395 2396 int len = strings.length; 2397 for (int i = 0; i < len; i++) { 2398 float width = p.measureText(strings[i]); 2399 maxWidthF = Math.max(width, maxWidthF); 2400 } 2401 int maxWidth = (int) (maxWidthF + 0.5); 2402 if (maxWidth < currentMax) { 2403 maxWidth = currentMax; 2404 } 2405 return maxWidth; 2406 } 2407 2408 private void saveSelectionPosition(float left, float top, float right, float bottom) { 2409 mPrevBox.left = (int) left; 2410 mPrevBox.right = (int) right; 2411 mPrevBox.top = (int) top; 2412 mPrevBox.bottom = (int) bottom; 2413 } 2414 2415 private Rect getCurrentSelectionPosition() { 2416 Rect box = new Rect(); 2417 box.top = mSelectionHour * (mCellHeight + HOUR_GAP); 2418 box.bottom = box.top + mCellHeight + HOUR_GAP; 2419 int daynum = mSelectionDay - mFirstJulianDay; 2420 box.left = computeDayLeftPosition(daynum) + 1; 2421 box.right = computeDayLeftPosition(daynum + 1); 2422 return box; 2423 } 2424 2425 private void setupTextRect(Rect r) { 2426 if (r.bottom <= r.top || r.right <= r.left) { 2427 r.bottom = r.top; 2428 r.right = r.left; 2429 return; 2430 } 2431 2432 if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { 2433 r.top += EVENT_TEXT_TOP_MARGIN; 2434 r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; 2435 } 2436 if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { 2437 r.left += EVENT_TEXT_LEFT_MARGIN; 2438 r.right -= EVENT_TEXT_RIGHT_MARGIN; 2439 } 2440 } 2441 2442 private void setupAllDayTextRect(Rect r) { 2443 if (r.bottom <= r.top || r.right <= r.left) { 2444 r.bottom = r.top; 2445 r.right = r.left; 2446 return; 2447 } 2448 2449 if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { 2450 r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; 2451 r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; 2452 } 2453 if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { 2454 r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; 2455 r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2456 } 2457 } 2458 2459 private StaticLayout getOrCreateAllDayTextLayout(Paint p) { 2460 2461 if (mAllDayTextLayout == null) { 2462 int allDayWidth = mHoursWidth - EVENT_ALL_DAY_TEXT_LEFT_MARGIN 2463 - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; 2464 mAllDayTextLayout = new StaticLayout(mAllDayString, 0, mAllDayString.length(), 2465 new TextPaint(p), allDayWidth, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, 2466 allDayWidth); 2467 } 2468 return mAllDayTextLayout; 2469 } 2470 2471 /** 2472 * Return the layout for a numbered event. Create it if not already existing 2473 */ 2474 private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, 2475 Rect r) { 2476 if (i < 0 || i >= layouts.length) { 2477 return null; 2478 } 2479 2480 StaticLayout layout = layouts[i]; 2481 // Check if we have already initialized the StaticLayout and that 2482 // the width hasn't changed (due to vertical resizing which causes 2483 // re-layout of events at min height) 2484 if (layout == null || r.width() != layout.getWidth()) { 2485 SpannableStringBuilder bob = new SpannableStringBuilder(); 2486 if (event.title != null) { 2487 bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN)); 2488 bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); 2489 bob.append(' '); 2490 } 2491 if (event.location != null) { 2492 bob.append(drawTextSanitizer(event.location.toString(), 2493 MAX_EVENT_TEXT_LEN - bob.length())); 2494 } 2495 2496 switch (event.selfAttendeeStatus) { 2497 case Attendees.ATTENDEE_STATUS_INVITED: 2498 paint.setColor(event.color); 2499 break; 2500 case Attendees.ATTENDEE_STATUS_DECLINED: 2501 case Attendees.ATTENDEE_STATUS_NONE: // Your own events 2502 case Attendees.ATTENDEE_STATUS_ACCEPTED: 2503 case Attendees.ATTENDEE_STATUS_TENTATIVE: 2504 default: 2505 paint.setColor(mEventTextColor); 2506 break; 2507 } 2508 2509 // Leave a one pixel boundary on the left and right of the rectangle for the event 2510 layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), 2511 Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); 2512 2513 layouts[i] = layout; 2514 } 2515 2516 return layout; 2517 } 2518 2519 private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { 2520 2521 p.setTextSize(NORMAL_FONT_SIZE); 2522 p.setTextAlign(Paint.Align.LEFT); 2523 Paint eventTextPaint = mEventTextPaint; 2524 2525 final float startY = DAY_HEADER_HEIGHT; 2526 final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; 2527 float x = 0; 2528 int linesIndex = 0; 2529 2530 // Draw the inner vertical grid lines 2531 p.setColor(mCalendarGridLineInnerVerticalColor); 2532 x = mHoursWidth; 2533 p.setStrokeWidth(GRID_LINE_INNER_WIDTH); 2534 // Line bounding the top of the all day area 2535 mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; 2536 mLines[linesIndex++] = startY; 2537 mLines[linesIndex++] = computeDayLeftPosition(mNumDays + 1); 2538 mLines[linesIndex++] = startY; 2539 2540 for (int day = 0; day <= mNumDays; day++) { 2541 x = computeDayLeftPosition(day); 2542 mLines[linesIndex++] = x; 2543 mLines[linesIndex++] = startY; 2544 mLines[linesIndex++] = x; 2545 mLines[linesIndex++] = stopY; 2546 } 2547 p.setAntiAlias(false); 2548 canvas.drawLines(mLines, 0, linesIndex, p); 2549 p.setStyle(Style.FILL); 2550 2551 int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; 2552 float left = mHoursWidth; 2553 int lastDay = firstDay + numDays - 1; 2554 final ArrayList<Event> events = mAllDayEvents; 2555 int numEvents = events.size(); 2556 // Whether or not we should draw the more events text 2557 boolean hasMoreEvents = false; 2558 // size of the allDay area 2559 float drawHeight = mAlldayHeight; 2560 // max number of events being drawn in one day of the allday area 2561 float numRectangles = mMaxAlldayEvents; 2562 // Where to cut off drawn allday events 2563 int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; 2564 // The number of events that weren't drawn in each day 2565 mSkippedAlldayEvents = new int[numDays]; 2566 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && 2567 mAnimateDayHeight == 0) { 2568 // We draw one fewer event than will fit so that more events text 2569 // can be drawn 2570 numRectangles = mMaxUnexpandedAlldayEventCount - 1; 2571 // We also clip the events above the more events text 2572 allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; 2573 hasMoreEvents = true; 2574 } else if (mAnimateDayHeight != 0) { 2575 // clip at the end of the animating space 2576 allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; 2577 } 2578 for (int i = 0; i < numEvents; i++) { 2579 Event event = events.get(i); 2580 int startDay = event.startDay; 2581 int endDay = event.endDay; 2582 if (startDay > lastDay || endDay < firstDay) { 2583 continue; 2584 } 2585 if (startDay < firstDay) { 2586 startDay = firstDay; 2587 } 2588 if (endDay > lastDay) { 2589 endDay = lastDay; 2590 } 2591 int startIndex = startDay - firstDay; 2592 int endIndex = endDay - firstDay; 2593 float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : 2594 drawHeight / numRectangles; 2595 2596 // Prevent a single event from getting too big 2597 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 2598 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 2599 } 2600 2601 // Leave a one-pixel space between the vertical day lines and the 2602 // event rectangle. 2603 event.left = computeDayLeftPosition(startIndex); 2604 event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; 2605 event.top = y + height * event.getColumn(); 2606 event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; 2607 if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { 2608 // check if we should skip this event. We skip if it starts 2609 // after the clip bound or ends after the skip bound and we're 2610 // not animating. 2611 if (event.top >= allDayEventClip) { 2612 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2613 continue; 2614 } else if (event.bottom > allDayEventClip) { 2615 if (hasMoreEvents) { 2616 incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); 2617 continue; 2618 } 2619 event.bottom = allDayEventClip; 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 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 events = mAllDayEvents; 4168 numEvents = events.size(); 4169 for (int i = 0; i < numEvents; i++) { 4170 Event event = events.get(i); 4171 if (!event.drawAsAllday() || 4172 (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { 4173 // Don't check non-allday events or events that aren't shown 4174 continue; 4175 } 4176 4177 if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { 4178 float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents 4179 : mMaxUnexpandedAlldayEventCount; 4180 float height = drawHeight / numRectangles; 4181 if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { 4182 height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; 4183 } 4184 float eventTop = yOffset + height * event.getColumn(); 4185 float eventBottom = eventTop + height; 4186 if (eventTop < y && eventBottom > y) { 4187 // If the touch is inside the event rectangle, then 4188 // add the event. 4189 mSelectedEvents.add(event); 4190 closestEvent = event; 4191 break; 4192 } else { 4193 // Find the closest event 4194 if (eventTop >= y) { 4195 yDistance = eventTop - y; 4196 } else { 4197 yDistance = y - eventBottom; 4198 } 4199 if (yDistance < minYdistance) { 4200 minYdistance = yDistance; 4201 closestEvent = event; 4202 } 4203 } 4204 } 4205 } 4206 mSelectedEvent = closestEvent; 4207 return; 4208 } 4209 4210 // Adjust y for the scrollable bitmap 4211 y += mViewStartY - mFirstCell; 4212 4213 // Use a region around (x,y) for the selection region 4214 Rect region = mRect; 4215 region.left = x - 10; 4216 region.right = x + 10; 4217 region.top = y - 10; 4218 region.bottom = y + 10; 4219 4220 EventGeometry geometry = mEventGeometry; 4221 4222 for (int i = 0; i < numEvents; i++) { 4223 Event event = events.get(i); 4224 // Compute the event rectangle. 4225 if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { 4226 continue; 4227 } 4228 4229 // If the event intersects the selection region, then add it to 4230 // mSelectedEvents. 4231 if (geometry.eventIntersectsSelection(event, region)) { 4232 mSelectedEvents.add(event); 4233 } 4234 } 4235 4236 // If there are any events in the selected region, then assign the 4237 // closest one to mSelectedEvent. 4238 if (mSelectedEvents.size() > 0) { 4239 int len = mSelectedEvents.size(); 4240 Event closestEvent = null; 4241 float minDist = mViewWidth + mViewHeight; // some large distance 4242 for (int index = 0; index < len; index++) { 4243 Event ev = mSelectedEvents.get(index); 4244 float dist = geometry.pointToEvent(x, y, ev); 4245 if (dist < minDist) { 4246 minDist = dist; 4247 closestEvent = ev; 4248 } 4249 } 4250 mSelectedEvent = closestEvent; 4251 4252 // Keep the selected hour and day consistent with the selected 4253 // event. They could be different if we touched on an empty hour 4254 // slot very close to an event in the previous hour slot. In 4255 // that case we will select the nearby event. 4256 int startDay = mSelectedEvent.startDay; 4257 int endDay = mSelectedEvent.endDay; 4258 if (mSelectionDay < startDay) { 4259 mSelectionDay = startDay; 4260 } else if (mSelectionDay > endDay) { 4261 mSelectionDay = endDay; 4262 } 4263 4264 int startHour = mSelectedEvent.startTime / 60; 4265 int endHour; 4266 if (mSelectedEvent.startTime < mSelectedEvent.endTime) { 4267 endHour = (mSelectedEvent.endTime - 1) / 60; 4268 } else { 4269 endHour = mSelectedEvent.endTime / 60; 4270 } 4271 4272 if (mSelectionHour < startHour && mSelectionDay == startDay) { 4273 mSelectionHour = startHour; 4274 } else if (mSelectionHour > endHour && mSelectionDay == endDay) { 4275 mSelectionHour = endHour; 4276 } 4277 } 4278 } 4279 4280 // Encapsulates the code to continue the scrolling after the 4281 // finger is lifted. Instead of stopping the scroll immediately, 4282 // the scroll continues to "free spin" and gradually slows down. 4283 private class ContinueScroll implements Runnable { 4284 public void run() { 4285 mScrolling = mScrolling && mScroller.computeScrollOffset(); 4286 if (!mScrolling) { 4287 resetSelectedHour(); 4288 invalidate(); 4289 return; 4290 } 4291 4292 mViewStartY = mScroller.getCurrY(); 4293 4294 if (mViewStartY < 0) { 4295 mViewStartY = 0; 4296 } else if (mViewStartY > mMaxViewStartY) { 4297 mViewStartY = mMaxViewStartY; 4298 } 4299 4300 computeFirstHour(); 4301 post(this); 4302 invalidate(); 4303 } 4304 } 4305 4306 /** 4307 * Cleanup the pop-up and timers. 4308 */ 4309 public void cleanup() { 4310 // Protect against null-pointer exceptions 4311 if (mPopup != null) { 4312 mPopup.dismiss(); 4313 } 4314 mLastPopupEventID = INVALID_EVENT_ID; 4315 Handler handler = getHandler(); 4316 if (handler != null) { 4317 handler.removeCallbacks(mDismissPopup); 4318 handler.removeCallbacks(mUpdateCurrentTime); 4319 } 4320 4321 Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, 4322 mCellHeight); 4323 4324 // Turn off redraw 4325 mRemeasure = false; 4326 } 4327 4328 /** 4329 * Restart the update timer 4330 */ 4331 public void restartCurrentTimeUpdates() { 4332 post(mUpdateCurrentTime); 4333 } 4334 4335 @Override 4336 protected void onDetachedFromWindow() { 4337 cleanup(); 4338 super.onDetachedFromWindow(); 4339 } 4340 4341 class DismissPopup implements Runnable { 4342 public void run() { 4343 // Protect against null-pointer exceptions 4344 if (mPopup != null) { 4345 mPopup.dismiss(); 4346 } 4347 } 4348 } 4349 4350 class UpdateCurrentTime implements Runnable { 4351 public void run() { 4352 long currentTime = System.currentTimeMillis(); 4353 mCurrentTime.set(currentTime); 4354 //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) 4355 postDelayed(mUpdateCurrentTime, 4356 UPDATE_CURRENT_TIME_DELAY - (currentTime % UPDATE_CURRENT_TIME_DELAY)); 4357 mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); 4358 invalidate(); 4359 } 4360 } 4361 4362 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 4363 @Override 4364 public boolean onSingleTapUp(MotionEvent ev) { 4365 DayView.this.doSingleTapUp(ev); 4366 return true; 4367 } 4368 4369 @Override 4370 public void onLongPress(MotionEvent ev) { 4371 DayView.this.doLongPress(ev); 4372 } 4373 4374 @Override 4375 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 4376 if (mTouchStartedInAlldayArea) { 4377 if (Math.abs(distanceX) < Math.abs(distanceY)) { 4378 return false; 4379 } 4380 // don't scroll vertically if this started in the allday area 4381 distanceY = 0; 4382 } 4383 DayView.this.doScroll(e1, e2, distanceX, distanceY); 4384 return true; 4385 } 4386 4387 @Override 4388 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 4389 if (mTouchStartedInAlldayArea) { 4390 if (Math.abs(velocityX) < Math.abs(velocityY)) { 4391 return false; 4392 } 4393 // don't fling vertically if this started in the allday area 4394 velocityY = 0; 4395 } 4396 DayView.this.doFling(e1, e2, velocityX, velocityY); 4397 return true; 4398 } 4399 4400 @Override 4401 public boolean onDown(MotionEvent ev) { 4402 DayView.this.doDown(ev); 4403 return true; 4404 } 4405 } 4406 4407 @Override 4408 public boolean onLongClick(View v) { 4409 mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, 4410 getSelectedTimeInMillis(), 0, -1, -1, -1); 4411 return true; 4412 } 4413 4414 // The rest of this file was borrowed from Launcher2 - PagedView.java 4415 private static final int MINIMUM_SNAP_VELOCITY = 2200; 4416 4417 private class ScrollInterpolator implements Interpolator { 4418 public ScrollInterpolator() { 4419 } 4420 4421 public float getInterpolation(float t) { 4422 t -= 1.0f; 4423 t = t * t * t * t * t + 1; 4424 4425 if ((1 - t) * mAnimationDistance < 1) { 4426 cancelAnimation(); 4427 } 4428 4429 return t; 4430 } 4431 } 4432 4433 private long calculateDuration(float delta, float width, float velocity) { 4434 /* 4435 * Here we compute a "distance" that will be used in the computation of 4436 * the overall snap duration. This is a function of the actual distance 4437 * that needs to be traveled; we keep this value close to half screen 4438 * size in order to reduce the variance in snap duration as a function 4439 * of the distance the page needs to travel. 4440 */ 4441 final float halfScreenSize = width / 2; 4442 float distanceRatio = delta / width; 4443 float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); 4444 float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; 4445 4446 velocity = Math.abs(velocity); 4447 velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); 4448 4449 /* 4450 * we want the page's snap velocity to approximately match the velocity 4451 * at which the user flings, so we scale the duration by a value near to 4452 * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to 4453 * make it a little slower. 4454 */ 4455 long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); 4456 if (DEBUG) { 4457 Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" 4458 + distanceRatio + " distance:" + distance + " velocity:" + velocity 4459 + " duration:" + duration + " distanceInfluenceForSnapDuration:" 4460 + distanceInfluenceForSnapDuration); 4461 } 4462 return duration; 4463 } 4464 4465 /* 4466 * We want the duration of the page snap animation to be influenced by the 4467 * distance that the screen has to travel, however, we don't want this 4468 * duration to be effected in a purely linear fashion. Instead, we use this 4469 * method to moderate the effect that the distance of travel has on the 4470 * overall snap duration. 4471 */ 4472 private float distanceInfluenceForSnapDuration(float f) { 4473 f -= 0.5f; // center the values about 0. 4474 f *= 0.3f * Math.PI / 2.0f; 4475 return (float) Math.sin(f); 4476 } 4477} 4478