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