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