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