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