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