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