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