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