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