summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2021-07-08 12:17:54 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2021-07-08 12:17:54 +0000
commit9b3c5ce1f330b40bf072cf78069082ded3813cb3 (patch)
tree0980db5a0c3c85b7672caeb738a0d07460147694
parentaa98535cdb1e318f35aa3247a6819bc53a577367 (diff)
parent805e4e0e89fea65ba11abbeedced440ef1333ad7 (diff)
downloadCalendar-9b3c5ce1f330b40bf072cf78069082ded3813cb3.tar.gz
Merge changes from topic "DayView"
* changes: AOSP/Calendar - DayView fully converted with bp file AOSP/Calendar - Initial Conversion of DayView AOSP/Calendar - Copy of DayView.java
-rw-r--r--Android.bp1
-rw-r--r--src/com/android/calendar/DayFragment.kt6
-rw-r--r--src/com/android/calendar/DayView.kt3990
3 files changed, 3994 insertions, 3 deletions
diff --git a/Android.bp b/Android.bp
index af827ed4..fd318cc3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -34,6 +34,7 @@ exclude_srcsd = [
"src/**/calendar/AllInOneActivity.java",
"src/**/calendar/CalendarController.java",
"src/**/calendar/DayOfMonthDrawable.java",
+ "src/**/calendar/DayView.java",
"src/**/calendar/Event.java",
"src/**/calendar/EventInfoActivity.java",
"src/**/calendar/StickyHeaderListView.java",
diff --git a/src/com/android/calendar/DayFragment.kt b/src/com/android/calendar/DayFragment.kt
index 63800951..39e92f5b 100644
--- a/src/com/android/calendar/DayFragment.kt
+++ b/src/com/android/calendar/DayFragment.kt
@@ -151,7 +151,7 @@ class DayFragment : Fragment, CalendarController.EventHandler, ViewFactory {
val currentView: DayView? = mViewSwitcher?.getCurrentView() as? DayView
// How does goTo time compared to what's already displaying?
- val diff: Int = currentView?.compareToVisibleTimeRange(goToTime) as Int
+ val diff: Int = currentView?.compareToVisibleTimeRange(goToTime as Time) as Int
if (diff == 0) {
// In visible range. No need to switch view
currentView?.setSelected(goToTime, ignoreTime, animateToday)
@@ -166,7 +166,7 @@ class DayFragment : Fragment, CalendarController.EventHandler, ViewFactory {
}
val next: DayView? = mViewSwitcher?.getNextView() as? DayView
if (ignoreTime) {
- next?.setFirstVisibleHour(currentView?.getFirstVisibleHour())
+ next!!.firstVisibleHour = currentView.firstVisibleHour
}
next?.setSelected(goToTime, ignoreTime, animateToday)
next?.reloadEvents()
@@ -190,7 +190,7 @@ class DayFragment : Fragment, CalendarController.EventHandler, ViewFactory {
return -1
}
val view: DayView = mViewSwitcher?.getCurrentView() as DayView ?: return -1
- return view.getSelectedTimeInMillis()
+ return view.selectedTimeInMillis
}
override fun eventsChanged() {
diff --git a/src/com/android/calendar/DayView.kt b/src/com/android/calendar/DayView.kt
new file mode 100644
index 00000000..58126f20
--- /dev/null
+++ b/src/com/android/calendar/DayView.kt
@@ -0,0 +1,3990 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.calendar
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.app.Service
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Paint.Align
+import android.graphics.Paint.Style
+import android.graphics.Rect
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.os.Handler
+import android.provider.CalendarContract.Attendees
+import android.provider.CalendarContract.Calendars
+import android.text.Layout.Alignment
+import android.text.SpannableStringBuilder
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.text.format.Time
+import android.text.style.StyleSpan
+import android.util.Log
+import android.view.ContextMenu
+import android.view.ContextMenu.ContextMenuInfo
+import android.view.GestureDetector
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.view.animation.Animation
+import android.view.animation.Interpolator
+import android.view.animation.TranslateAnimation
+import android.widget.EdgeEffect
+import android.widget.OverScroller
+import android.widget.PopupWindow
+import android.widget.ViewSwitcher
+import com.android.calendar.CalendarController.EventType
+import com.android.calendar.CalendarController.ViewType
+import java.util.ArrayList
+import java.util.Arrays
+import java.util.Calendar
+import java.util.Formatter
+import java.util.Locale
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * View for multi-day view. So far only 1 and 7 day have been tested.
+ */
+class DayView(
+ context: Context?,
+ controller: CalendarController?,
+ viewSwitcher: ViewSwitcher?,
+ eventLoader: EventLoader?,
+ numDays: Int
+) : View(context), View.OnCreateContextMenuListener, ScaleGestureDetector.OnScaleGestureListener,
+ View.OnClickListener, View.OnLongClickListener {
+ private var mOnFlingCalled = false
+ private var mStartingScroll = false
+ protected var mPaused = true
+ private var mHandler: Handler? = null
+
+ /**
+ * ID of the last event which was displayed with the toast popup.
+ *
+ * This is used to prevent popping up multiple quick views for the same event, especially
+ * during calendar syncs. This becomes valid when an event is selected, either by default
+ * on starting calendar or by scrolling to an event. It becomes invalid when the user
+ * explicitly scrolls to an empty time slot, changes views, or deletes the event.
+ */
+ private var mLastPopupEventID: Long
+ protected var mContext: Context? = null
+ private val mContinueScroll: ContinueScroll = ContinueScroll()
+
+ // Make this visible within the package for more informative debugging
+ var mBaseDate: Time? = null
+ private var mCurrentTime: Time? = null
+ private val mUpdateCurrentTime: UpdateCurrentTime = UpdateCurrentTime()
+ private var mTodayJulianDay = 0
+ private val mBold: Typeface = Typeface.DEFAULT_BOLD
+ private var mFirstJulianDay = 0
+ private var mLoadedFirstJulianDay = -1
+ private var mLastJulianDay = 0
+ private var mMonthLength = 0
+ private var mFirstVisibleDate = 0
+ private var mFirstVisibleDayOfWeek = 0
+ private var mEarliestStartHour: IntArray? = null // indexed by the week day offset
+ private var mHasAllDayEvent: BooleanArray? = null // indexed by the week day offset
+ private var mEventCountTemplate: String? = null
+ private var mClickedEvent: Event? = null // The event the user clicked on
+ private var mSavedClickedEvent: Event? = null
+ private var mClickedYLocation = 0
+ private var mDownTouchTime: Long = 0
+ private var mEventsAlpha = 255
+ private var mEventsCrossFadeAnimation: ObjectAnimator? = null
+ private val mTZUpdater: Runnable = object : Runnable {
+ @Override
+ override fun run() {
+ val tz: String? = Utils.getTimeZone(mContext, this)
+ mBaseDate!!.timezone = tz
+ mBaseDate?.normalize(true)
+ mCurrentTime?.switchTimezone(tz)
+ invalidate()
+ }
+ }
+
+ // Sets the "clicked" color from the clicked event
+ private val mSetClick: Runnable = object : Runnable {
+ @Override
+ override fun run() {
+ mClickedEvent = mSavedClickedEvent
+ mSavedClickedEvent = null
+ this@DayView.invalidate()
+ }
+ }
+
+ // Clears the "clicked" color from the clicked event and launch the event
+ private val mClearClick: Runnable = object : Runnable {
+ @Override
+ override fun run() {
+ if (mClickedEvent != null) {
+ mController?.sendEventRelatedEvent(
+ this as Object?, EventType.VIEW_EVENT, mClickedEvent!!.id,
+ mClickedEvent!!.startMillis, mClickedEvent!!.endMillis,
+ this@DayView.getWidth() / 2, mClickedYLocation,
+ selectedTimeInMillis
+ )
+ }
+ mClickedEvent = null
+ this@DayView.invalidate()
+ }
+ }
+ private val mTodayAnimatorListener: TodayAnimatorListener = TodayAnimatorListener()
+
+ internal inner class TodayAnimatorListener : AnimatorListenerAdapter() {
+ @Volatile
+ private var mAnimator: Animator? = null
+
+ @Volatile
+ private var mFadingIn = false
+ @Override
+ override fun onAnimationEnd(animation: Animator) {
+ synchronized(this) {
+ if (mAnimator !== animation) {
+ animation.removeAllListeners()
+ animation.cancel()
+ return
+ }
+ if (mFadingIn) {
+ if (mTodayAnimator != null) {
+ mTodayAnimator?.removeAllListeners()
+ mTodayAnimator?.cancel()
+ }
+ mTodayAnimator = ObjectAnimator
+ .ofInt(this@DayView, "animateTodayAlpha", 255, 0)
+ mAnimator = mTodayAnimator
+ mFadingIn = false
+ mTodayAnimator?.addListener(this)
+ mTodayAnimator?.setDuration(600)
+ mTodayAnimator?.start()
+ } else {
+ mAnimateToday = false
+ mAnimateTodayAlpha = 0
+ mAnimator?.removeAllListeners()
+ mAnimator = null
+ mTodayAnimator = null
+ invalidate()
+ }
+ }
+ }
+
+ fun setAnimator(animation: Animator?) {
+ mAnimator = animation
+ }
+
+ fun setFadingIn(fadingIn: Boolean) {
+ mFadingIn = fadingIn
+ }
+ }
+
+ var mAnimatorListener: AnimatorListenerAdapter = object : AnimatorListenerAdapter() {
+ @Override
+ override fun onAnimationStart(animation: Animator?) {
+ mScrolling = true
+ }
+
+ @Override
+ override fun onAnimationCancel(animation: Animator?) {
+ mScrolling = false
+ }
+
+ @Override
+ override fun onAnimationEnd(animation: Animator?) {
+ mScrolling = false
+ resetSelectedHour()
+ invalidate()
+ }
+ }
+
+ /**
+ * This variable helps to avoid unnecessarily reloading events by keeping
+ * track of the start millis parameter used for the most recent loading
+ * of events. If the next reload matches this, then the events are not
+ * reloaded. To force a reload, set this to zero (this is set to zero
+ * in the method clearCachedEvents()).
+ */
+ private var mLastReloadMillis: Long = 0
+ private var mEvents: ArrayList<Event> = ArrayList<Event>()
+ private var mAllDayEvents: ArrayList<Event>? = ArrayList<Event>()
+ private var mLayouts: Array<StaticLayout?>? = null
+ private var mAllDayLayouts: Array<StaticLayout?>? = null
+ private var mSelectionDay = 0 // Julian day
+ private var mSelectionHour = 0
+ var mSelectionAllday = false
+
+ // Current selection info for accessibility
+ private var mSelectionDayForAccessibility = 0 // Julian day
+ private var mSelectionHourForAccessibility = 0
+ private var mSelectedEventForAccessibility: Event? = null
+
+ // Last selection info for accessibility
+ private var mLastSelectionDayForAccessibility = 0
+ private var mLastSelectionHourForAccessibility = 0
+ private var mLastSelectedEventForAccessibility: Event? = null
+
+ /** Width of a day or non-conflicting event */
+ private var mCellWidth = 0
+
+ // Pre-allocate these objects and re-use them
+ private val mRect: Rect = Rect()
+ private val mDestRect: Rect = Rect()
+ private val mSelectionRect: Rect = Rect()
+
+ // This encloses the more allDay events icon
+ private val mExpandAllDayRect: Rect = Rect()
+
+ // TODO Clean up paint usage
+ private val mPaint: Paint = Paint()
+ private val mEventTextPaint: Paint = Paint()
+ private val mSelectionPaint: Paint = Paint()
+ private var mLines: FloatArray = emptyArray<Float>().toFloatArray()
+ private var mFirstDayOfWeek = 0 // First day of the week
+ private var mPopup: PopupWindow? = null
+ private var mPopupView: View? = null
+ private val mDismissPopup: DismissPopup = DismissPopup()
+ private var mRemeasure = true
+ private val mEventLoader: EventLoader
+ protected val mEventGeometry: EventGeometry
+ private var mAnimationDistance = 0f
+ private var mViewStartX = 0
+ private var mViewStartY = 0
+ private var mMaxViewStartY = 0
+ private var mViewHeight = 0
+ private var mViewWidth = 0
+ private var mGridAreaHeight = -1
+ private var mScrollStartY = 0
+ private var mPreviousDirection = 0
+
+ /**
+ * Vertical distance or span between the two touch points at the start of a
+ * scaling gesture
+ */
+ private var mStartingSpanY = 0f
+
+ /** Height of 1 hour in pixels at the start of a scaling gesture */
+ private var mCellHeightBeforeScaleGesture = 0
+
+ /** The hour at the center two touch points */
+ private var mGestureCenterHour = 0f
+ private var mRecalCenterHour = false
+
+ /**
+ * Flag to decide whether to handle the up event. Cases where up events
+ * should be ignored are 1) right after a scale gesture and 2) finger was
+ * down before app launch
+ */
+ private var mHandleActionUp = true
+ private var mHoursTextHeight = 0
+
+ /**
+ * The height of the area used for allday events
+ */
+ private var mAlldayHeight = 0
+
+ /**
+ * The height of the allday event area used during animation
+ */
+ private var mAnimateDayHeight = 0
+
+ /**
+ * The height of an individual allday event during animation
+ */
+ private var mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+
+ /**
+ * Max of all day events in a given day in this view.
+ */
+ private var mMaxAlldayEvents = 0
+
+ /**
+ * A count of the number of allday events that were not drawn for each day
+ */
+ private var mSkippedAlldayEvents: IntArray? = null
+
+ /**
+ * The number of allDay events at which point we start hiding allDay events.
+ */
+ private var mMaxUnexpandedAlldayEventCount = 4
+ protected var mNumDays = 7
+ private var mNumHours = 10
+
+ /** Width of the time line (list of hours) to the left. */
+ private var mHoursWidth = 0
+ private var mDateStrWidth = 0
+
+ /** Top of the scrollable region i.e. below date labels and all day events */
+ private var mFirstCell = 0
+
+ /** First fully visible hour */
+ private var mFirstHour = -1
+
+ /** Distance between the mFirstCell and the top of first fully visible hour. */
+ private var mFirstHourOffset = 0
+ private var mHourStrs: Array<String>? = null
+ private var mDayStrs: Array<String?>? = null
+ private var mDayStrs2Letter: Array<String?>? = null
+ private var mIs24HourFormat = false
+ private val mSelectedEvents: ArrayList<Event> = ArrayList<Event>()
+ private var mComputeSelectedEvents = false
+ private var mUpdateToast = false
+ private var mSelectedEvent: Event? = null
+ private var mPrevSelectedEvent: Event? = null
+ private val mPrevBox: Rect = Rect()
+ protected val mResources: Resources
+ protected val mCurrentTimeLine: Drawable
+ protected val mCurrentTimeAnimateLine: Drawable
+ protected val mTodayHeaderDrawable: Drawable
+ protected val mExpandAlldayDrawable: Drawable
+ protected val mCollapseAlldayDrawable: Drawable
+ protected var mAcceptedOrTentativeEventBoxDrawable: Drawable
+ private var mAmString: String? = null
+ private var mPmString: String? = null
+ var mScaleGestureDetector: ScaleGestureDetector
+ private var mTouchMode = TOUCH_MODE_INITIAL_STATE
+ private var mSelectionMode = SELECTION_HIDDEN
+ private var mScrolling = false
+
+ // Pixels scrolled
+ private var mInitialScrollX = 0f
+ private var mInitialScrollY = 0f
+ private var mAnimateToday = false
+ private var mAnimateTodayAlpha = 0
+
+ // Animates the height of the allday region
+ var mAlldayAnimator: ObjectAnimator? = null
+
+ // Animates the height of events in the allday region
+ var mAlldayEventAnimator: ObjectAnimator? = null
+
+ // Animates the transparency of the more events text
+ var mMoreAlldayEventsAnimator: ObjectAnimator? = null
+
+ // Animates the current time marker when Today is pressed
+ var mTodayAnimator: ObjectAnimator? = null
+
+ // whether or not an event is stopping because it was cancelled
+ private var mCancellingAnimations = false
+
+ // tracks whether a touch originated in the allday area
+ private var mTouchStartedInAlldayArea = false
+ private val mController: CalendarController
+ private val mViewSwitcher: ViewSwitcher
+ private val mGestureDetector: GestureDetector
+ private val mScroller: OverScroller
+ private val mEdgeEffectTop: EdgeEffect
+ private val mEdgeEffectBottom: EdgeEffect
+ private var mCallEdgeEffectOnAbsorb = false
+ private val OVERFLING_DISTANCE: Int
+ private var mLastVelocity = 0f
+ private val mHScrollInterpolator: ScrollInterpolator
+ private var mAccessibilityMgr: AccessibilityManager? = null
+ private var mIsAccessibilityEnabled = false
+ private var mTouchExplorationEnabled = false
+ private val mNewEventHintString: String
+ @Override
+ protected override fun onAttachedToWindow() {
+ if (mHandler == null) {
+ mHandler = getHandler()
+ mHandler?.post(mUpdateCurrentTime)
+ }
+ }
+
+ private fun init(context: Context) {
+ setFocusable(true)
+
+ // Allow focus in touch mode so that we can do keyboard shortcuts
+ // even after we've entered touch mode.
+ setFocusableInTouchMode(true)
+ setClickable(true)
+ setOnCreateContextMenuListener(this)
+ mFirstDayOfWeek = Utils.getFirstDayOfWeek(context)
+ mCurrentTime = Time(Utils.getTimeZone(context, mTZUpdater))
+ val currentTime: Long = System.currentTimeMillis()
+ mCurrentTime?.set(currentTime)
+ mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime!!.gmtoff)
+ mWeek_saturdayColor = mResources.getColor(R.color.week_saturday)
+ mWeek_sundayColor = mResources.getColor(R.color.week_sunday)
+ mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color)
+ mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color)
+ mBgColor = mResources.getColor(R.color.calendar_hour_background)
+ mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label)
+ mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected)
+ mCalendarGridLineInnerHorizontalColor = mResources
+ .getColor(R.color.calendar_grid_line_inner_horizontal_color)
+ mCalendarGridLineInnerVerticalColor = mResources
+ .getColor(R.color.calendar_grid_line_inner_vertical_color)
+ mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label)
+ mEventTextColor = mResources.getColor(R.color.calendar_event_text_color)
+ mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color)
+ mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE)
+ mEventTextPaint.setTextAlign(Paint.Align.LEFT)
+ mEventTextPaint.setAntiAlias(true)
+ val gridLineColor: Int = mResources.getColor(R.color.calendar_grid_line_highlight_color)
+ var p: Paint = mSelectionPaint
+ p.setColor(gridLineColor)
+ p.setStyle(Style.FILL)
+ p.setAntiAlias(false)
+ p = mPaint
+ p.setAntiAlias(true)
+
+ // Allocate space for 2 weeks worth of weekday names so that we can
+ // easily start the week display at any week day.
+ mDayStrs = arrayOfNulls(14)
+
+ // Also create an array of 2-letter abbreviations.
+ mDayStrs2Letter = arrayOfNulls(14)
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ val index: Int = i - Calendar.SUNDAY
+ // e.g. Tue for Tuesday
+ mDayStrs!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM)
+ .toUpperCase()
+ mDayStrs!![index + 7] = mDayStrs!![index]
+ // e.g. Tu for Tuesday
+ mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT)
+ .toUpperCase()
+
+ // If we don't have 2-letter day strings, fall back to 1-letter.
+ if (mDayStrs2Letter!![index]!!.equals(mDayStrs!![index])) {
+ mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i,
+ DateUtils.LENGTH_SHORTEST)
+ }
+ mDayStrs2Letter!![index + 7] = mDayStrs2Letter!![index]
+ }
+
+ // Figure out how much space we need for the 3-letter abbrev names
+ // in the worst case.
+ p.setTextSize(DATE_HEADER_FONT_SIZE)
+ p.setTypeface(mBold)
+ val dateStrs = arrayOf<String?>(" 28", " 30")
+ mDateStrWidth = computeMaxStringWidth(0, dateStrs, p)
+ p.setTextSize(DAY_HEADER_FONT_SIZE)
+ mDateStrWidth += computeMaxStringWidth(0, mDayStrs as Array<String?>, p)
+ p.setTextSize(HOURS_TEXT_SIZE)
+ p.setTypeface(null)
+ handleOnResume()
+ mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase()
+ mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase()
+ val ampm = arrayOf(mAmString, mPmString)
+ p.setTextSize(AMPM_TEXT_SIZE)
+ mHoursWidth = Math.max(
+ HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) +
+ HOURS_RIGHT_MARGIN
+ )
+ mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth)
+ val inflater: LayoutInflater
+ inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ mPopupView = inflater.inflate(R.layout.bubble_event, null)
+ mPopupView?.setLayoutParams(
+ LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ mPopup = PopupWindow(context)
+ mPopup?.setContentView(mPopupView)
+ val dialogTheme: Resources.Theme = getResources().newTheme()
+ dialogTheme.applyStyle(android.R.style.Theme_Dialog, true)
+ val ta: TypedArray = dialogTheme.obtainStyledAttributes(
+ intArrayOf(
+ android.R.attr.windowBackground
+ )
+ )
+ mPopup?.setBackgroundDrawable(ta.getDrawable(0))
+ ta.recycle()
+
+ // Enable touching the popup window
+ mPopupView?.setOnClickListener(this)
+ // Catch long clicks for creating a new event
+ setOnLongClickListener(this)
+ mBaseDate = Time(Utils.getTimeZone(context, mTZUpdater))
+ val millis: Long = System.currentTimeMillis()
+ mBaseDate?.set(millis)
+ mEarliestStartHour = IntArray(mNumDays)
+ mHasAllDayEvent = BooleanArray(mNumDays)
+
+ // mLines is the array of points used with Canvas.drawLines() in
+ // drawGridBackground() and drawAllDayEvents(). Its size depends
+ // on the max number of lines that can ever be drawn by any single
+ // drawLines() call in either of those methods.
+ val maxGridLines = (24 + 1 + // max horizontal lines we might draw
+ (mNumDays + 1)) // max vertical lines we might draw
+ mLines = FloatArray(maxGridLines * 4)
+ }
+
+ /**
+ * This is called when the popup window is pressed.
+ */
+ override fun onClick(v: View) {
+ if (v === mPopupView) {
+ // Pretend it was a trackball click because that will always
+ // jump to the "View event" screen.
+ switchViews(true /* trackball */)
+ }
+ }
+
+ fun handleOnResume() {
+ initAccessibilityVariables()
+ if (Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) {
+ mFutureBgColor = 0
+ } else {
+ mFutureBgColor = mFutureBgColorRes
+ }
+ mIs24HourFormat = DateFormat.is24HourFormat(mContext)
+ mHourStrs = if (mIs24HourFormat) CalendarData.s24Hours else CalendarData.s12HoursNoAmPm
+ mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext)
+ mLastSelectionDayForAccessibility = 0
+ mLastSelectionHourForAccessibility = 0
+ mLastSelectedEventForAccessibility = null
+ mSelectionMode = SELECTION_HIDDEN
+ }
+
+ private fun initAccessibilityVariables() {
+ mAccessibilityMgr = mContext
+ ?.getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager
+ mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr!!.isEnabled()
+ mTouchExplorationEnabled = isTouchExplorationEnabled
+ } /* ignore isDst */ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ /**
+ * Returns the start of the selected time in milliseconds since the epoch.
+ *
+ * @return selected time in UTC milliseconds since the epoch.
+ */
+ val selectedTimeInMillis: Long
+ get() {
+ val time = Time(mBaseDate)
+ time.setJulianDay(mSelectionDay)
+ time.hour = mSelectionHour
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ return time.normalize(true /* ignore isDst */)
+ } /* ignore isDst */
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ val selectedTime: Time
+ get() {
+ val time = Time(mBaseDate)
+ time.setJulianDay(mSelectionDay)
+ time.hour = mSelectionHour
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */)
+ return time
+ } /* ignore isDst */
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ val selectedTimeForAccessibility: Time
+ get() {
+ val time = Time(mBaseDate)
+ time.setJulianDay(mSelectionDayForAccessibility)
+ time.hour = mSelectionHourForAccessibility
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */)
+ return time
+ }
+
+ /**
+ * Returns the start of the selected time in minutes since midnight,
+ * local time. The derived class must ensure that this is consistent
+ * with the return value from getSelectedTimeInMillis().
+ */
+ val selectedMinutesSinceMidnight: Int
+ get() = mSelectionHour * MINUTES_PER_HOUR
+ var firstVisibleHour: Int
+ get() = mFirstHour
+ set(firstHour) {
+ mFirstHour = firstHour
+ mFirstHourOffset = 0
+ }
+
+ fun setSelected(time: Time?, ignoreTime: Boolean, animateToday: Boolean) {
+ mBaseDate?.set(time)
+ setSelectedHour(mBaseDate!!.hour)
+ setSelectedEvent(null)
+ mPrevSelectedEvent = null
+ val millis: Long = mBaseDate!!.toMillis(false /* use isDst */)
+ setSelectedDay(Time.getJulianDay(millis, mBaseDate!!.gmtoff))
+ mSelectedEvents.clear()
+ mComputeSelectedEvents = true
+ var gotoY: Int = Integer.MIN_VALUE
+ if (!ignoreTime && mGridAreaHeight != -1) {
+ var lastHour = 0
+ if (mBaseDate!!.hour < mFirstHour) {
+ // Above visible region
+ gotoY = mBaseDate!!.hour * (mCellHeight + HOUR_GAP)
+ } else {
+ lastHour = ((mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) +
+ mFirstHour)
+ if (mBaseDate!!.hour >= lastHour) {
+ // Below visible region
+
+ // target hour + 1 (to give it room to see the event) -
+ // grid height (to get the y of the top of the visible
+ // region)
+ gotoY = ((mBaseDate!!.hour + 1 + mBaseDate!!.minute / 60.0f) *
+ (mCellHeight + HOUR_GAP) - mGridAreaHeight).toInt()
+ }
+ }
+ if (DEBUG) {
+ Log.e(
+ TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " +
+ (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight +
+ " ymax " + mMaxViewStartY
+ )
+ }
+ if (gotoY > mMaxViewStartY) {
+ gotoY = mMaxViewStartY
+ } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) {
+ gotoY = 0
+ }
+ }
+ recalc()
+ mRemeasure = true
+ invalidate()
+ var delayAnimateToday = false
+ if (gotoY != Integer.MIN_VALUE) {
+ val scrollAnim: ValueAnimator =
+ ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY)
+ scrollAnim.setDuration(GOTO_SCROLL_DURATION.toLong())
+ scrollAnim.setInterpolator(AccelerateDecelerateInterpolator())
+ scrollAnim.addListener(mAnimatorListener)
+ scrollAnim.start()
+ delayAnimateToday = true
+ }
+ if (animateToday) {
+ synchronized(mTodayAnimatorListener) {
+ if (mTodayAnimator != null) {
+ mTodayAnimator?.removeAllListeners()
+ mTodayAnimator?.cancel()
+ }
+ mTodayAnimator = ObjectAnimator.ofInt(
+ this, "animateTodayAlpha",
+ mAnimateTodayAlpha, 255
+ )
+ mAnimateToday = true
+ mTodayAnimatorListener.setFadingIn(true)
+ mTodayAnimatorListener.setAnimator(mTodayAnimator)
+ mTodayAnimator?.addListener(mTodayAnimatorListener)
+ mTodayAnimator?.setDuration(150)
+ if (delayAnimateToday) {
+ mTodayAnimator?.setStartDelay(GOTO_SCROLL_DURATION.toLong())
+ }
+ mTodayAnimator?.start()
+ }
+ }
+ sendAccessibilityEventAsNeeded(false)
+ }
+
+ // Called from animation framework via reflection. Do not remove
+ fun setViewStartY(viewStartY: Int) {
+ var viewStartY = viewStartY
+ if (viewStartY > mMaxViewStartY) {
+ viewStartY = mMaxViewStartY
+ }
+ mViewStartY = viewStartY
+ computeFirstHour()
+ invalidate()
+ }
+
+ fun setAnimateTodayAlpha(todayAlpha: Int) {
+ mAnimateTodayAlpha = todayAlpha
+ invalidate()
+ } /* ignore isDst */
+
+ fun getSelectedDay(): Time {
+ val time = Time(mBaseDate)
+ time.setJulianDay(mSelectionDay)
+ time.hour = mSelectionHour
+
+ // We ignore the "isDst" field because we want normalize() to figure
+ // out the correct DST value and not adjust the selected time based
+ // on the current setting of DST.
+ time.normalize(true /* ignore isDst */)
+ return time
+ }
+
+ fun updateTitle() {
+ val start = Time(mBaseDate)
+ start.normalize(true)
+ val end = Time(start)
+ end.monthDay += mNumDays - 1
+ // Move it forward one minute so the formatter doesn't lose a day
+ end.minute += 1
+ end.normalize(true)
+ var formatFlags: Long = DateUtils.FORMAT_SHOW_DATE.toLong() or
+ DateUtils.FORMAT_SHOW_YEAR.toLong()
+ if (mNumDays != 1) {
+ // Don't show day of the month if for multi-day view
+ formatFlags = formatFlags or DateUtils.FORMAT_NO_MONTH_DAY.toLong()
+
+ // Abbreviate the month if showing multiple months
+ if (start.month !== end.month) {
+ formatFlags = formatFlags or DateUtils.FORMAT_ABBREV_MONTH.toLong()
+ }
+ }
+ mController.sendEvent(
+ this as Object?, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT,
+ formatFlags, null, null
+ )
+ }
+
+ /**
+ * return a negative number if "time" is comes before the visible time
+ * range, a positive number if "time" is after the visible time range, and 0
+ * if it is in the visible time range.
+ */
+ fun compareToVisibleTimeRange(time: Time): Int {
+ val savedHour: Int = mBaseDate!!.hour
+ val savedMinute: Int = mBaseDate!!.minute
+ val savedSec: Int = mBaseDate!!.second
+ mBaseDate!!.hour = 0
+ mBaseDate!!.minute = 0
+ mBaseDate!!.second = 0
+ if (DEBUG) {
+ Log.d(TAG, "Begin " + mBaseDate.toString())
+ Log.d(TAG, "Diff " + time.toString())
+ }
+
+ // Compare beginning of range
+ var diff: Int = Time.compare(time, mBaseDate)
+ if (diff > 0) {
+ // Compare end of range
+ mBaseDate!!.monthDay += mNumDays
+ mBaseDate?.normalize(true)
+ diff = Time.compare(time, mBaseDate)
+ if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString())
+ mBaseDate!!.monthDay -= mNumDays
+ mBaseDate?.normalize(true)
+ if (diff < 0) {
+ // in visible time
+ diff = 0
+ } else if (diff == 0) {
+ // Midnight of following day
+ diff = 1
+ }
+ }
+ if (DEBUG) Log.d(TAG, "Diff: $diff")
+ mBaseDate!!.hour = savedHour
+ mBaseDate!!.minute = savedMinute
+ mBaseDate!!.second = savedSec
+ return diff
+ }
+
+ private fun recalc() {
+ // Set the base date to the beginning of the week if we are displaying
+ // 7 days at a time.
+ if (mNumDays == 7) {
+ adjustToBeginningOfWeek(mBaseDate)
+ }
+ val start: Long = mBaseDate!!.toMillis(false /* use isDst */)
+ mFirstJulianDay = Time.getJulianDay(start, mBaseDate!!.gmtoff)
+ mLastJulianDay = mFirstJulianDay + mNumDays - 1
+ mMonthLength = mBaseDate!!.getActualMaximum(Time.MONTH_DAY)
+ mFirstVisibleDate = mBaseDate!!.monthDay
+ mFirstVisibleDayOfWeek = mBaseDate!!.weekDay
+ }
+
+ private fun adjustToBeginningOfWeek(time: Time?) {
+ val dayOfWeek: Int = time!!.weekDay
+ var diff = dayOfWeek - mFirstDayOfWeek
+ if (diff != 0) {
+ if (diff < 0) {
+ diff += 7
+ }
+ time!!.monthDay -= diff
+ time?.normalize(true /* ignore isDst */)
+ }
+ }
+
+ @Override
+ protected override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) {
+ mViewWidth = width
+ mViewHeight = height
+ mEdgeEffectTop.setSize(mViewWidth, mViewHeight)
+ mEdgeEffectBottom.setSize(mViewWidth, mViewHeight)
+ val gridAreaWidth = width - mHoursWidth
+ mCellWidth = (gridAreaWidth - mNumDays * DAY_GAP) / mNumDays
+
+ // This would be about 1 day worth in a 7 day view
+ mHorizontalSnapBackThreshold = width / 7
+ val p = Paint()
+ p.setTextSize(HOURS_TEXT_SIZE)
+ mHoursTextHeight = Math.abs(p.ascent()).toInt()
+ remeasure(width, height)
+ }
+
+ /**
+ * Measures the space needed for various parts of the view after
+ * loading new events. This can change if there are all-day events.
+ */
+ private fun remeasure(width: Int, height: Int) {
+ // Shrink to fit available space but make sure we can display at least two events
+ MAX_UNEXPANDED_ALLDAY_HEIGHT = (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4).toInt()
+ MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6)
+ MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(
+ MAX_UNEXPANDED_ALLDAY_HEIGHT,
+ MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() * 2
+ )
+ mMaxUnexpandedAlldayEventCount =
+ (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+
+ // First, clear the array of earliest start times, and the array
+ // indicating presence of an all-day event.
+ for (day in 0 until mNumDays) {
+ mEarliestStartHour!![day] = 25 // some big number
+ mHasAllDayEvent!![day] = false
+ }
+ val maxAllDayEvents = mMaxAlldayEvents
+
+ // The min is where 24 hours cover the entire visible area
+ mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, MIN_EVENT_HEIGHT.toInt())
+ if (mCellHeight < mMinCellHeight) {
+ mCellHeight = mMinCellHeight
+ }
+
+ // Calculate mAllDayHeight
+ mFirstCell = DAY_HEADER_HEIGHT
+ var allDayHeight = 0
+ if (maxAllDayEvents > 0) {
+ val maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT
+ // If there is at most one all-day event per day, then use less
+ // space (but more than the space for a single event).
+ if (maxAllDayEvents == 1) {
+ allDayHeight = SINGLE_ALLDAY_HEIGHT
+ } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount) {
+ // Allow the all-day area to grow in height depending on the
+ // number of all-day events we need to show, up to a limit.
+ allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT
+ if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
+ allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT
+ }
+ } else {
+ // if we have more than the magic number, check if we're animating
+ // and if not adjust the sizes appropriately
+ if (mAnimateDayHeight != 0) {
+ // Don't shrink the space past the final allDay space. The animation
+ // continues to hide the last event so the more events text can
+ // fade in.
+ allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT)
+ } else {
+ // Try to fit all the events in
+ allDayHeight = (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+ // But clip the area depending on which mode we're in
+ if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
+ allDayHeight = (mMaxUnexpandedAlldayEventCount *
+ MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+ } else if (allDayHeight > maxAllAllDayHeight) {
+ allDayHeight = maxAllAllDayHeight
+ }
+ }
+ }
+ mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN
+ } else {
+ mSelectionAllday = false
+ }
+ mAlldayHeight = allDayHeight
+ mGridAreaHeight = height - mFirstCell
+
+ // Set up the expand icon position
+ val allDayIconWidth: Int = mExpandAlldayDrawable.getIntrinsicWidth()
+ mExpandAllDayRect.left = Math.max(
+ (mHoursWidth - allDayIconWidth) / 2,
+ EVENT_ALL_DAY_TEXT_LEFT_MARGIN
+ )
+ mExpandAllDayRect.right = Math.min(
+ mExpandAllDayRect.left + allDayIconWidth, mHoursWidth -
+ EVENT_ALL_DAY_TEXT_RIGHT_MARGIN
+ )
+ mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN
+ mExpandAllDayRect.top = (mExpandAllDayRect.bottom -
+ mExpandAlldayDrawable.getIntrinsicHeight())
+ mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP)
+ mEventGeometry.setHourHeight(mCellHeight.toFloat())
+ val minimumDurationMillis =
+ (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)).toLong()
+ Event.computePositions(mEvents, minimumDurationMillis)
+
+ // Compute the top of our reachable view
+ mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight
+ if (DEBUG) {
+ Log.e(TAG, "mViewStartY: $mViewStartY")
+ Log.e(TAG, "mMaxViewStartY: $mMaxViewStartY")
+ }
+ if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY
+ computeFirstHour()
+ }
+ if (mFirstHour == -1) {
+ initFirstHour()
+ mFirstHourOffset = 0
+ }
+
+ // When we change the base date, the number of all-day events may
+ // change and that changes the cell height. When we switch dates,
+ // we use the mFirstHourOffset from the previous view, but that may
+ // be too large for the new view if the cell height is smaller.
+ if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
+ mFirstHourOffset = mCellHeight + HOUR_GAP - 1
+ }
+ mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset
+ val eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP)
+ // When we get new events we don't want to dismiss the popup unless the event changes
+ if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent!!.id) {
+ mPopup?.dismiss()
+ }
+ mPopup?.setWidth(eventAreaWidth - 20)
+ mPopup?.setHeight(WindowManager.LayoutParams.WRAP_CONTENT)
+ }
+
+ /**
+ * Initialize the state for another view. The given view is one that has
+ * its own bitmap and will use an animation to replace the current view.
+ * The current view and new view are either both Week views or both Day
+ * views. They differ in their base date.
+ *
+ * @param view the view to initialize.
+ */
+ private fun initView(view: DayView) {
+ view.setSelectedHour(mSelectionHour)
+ view.mSelectedEvents.clear()
+ view.mComputeSelectedEvents = true
+ view.mFirstHour = mFirstHour
+ view.mFirstHourOffset = mFirstHourOffset
+ view.remeasure(getWidth(), getHeight())
+ view.initAllDayHeights()
+ view.setSelectedEvent(null)
+ view.mPrevSelectedEvent = null
+ view.mFirstDayOfWeek = mFirstDayOfWeek
+ if (view.mEvents.size > 0) {
+ view.mSelectionAllday = mSelectionAllday
+ } else {
+ view.mSelectionAllday = false
+ }
+
+ // Redraw the screen so that the selection box will be redrawn. We may
+ // have scrolled to a different part of the day in some other view
+ // so the selection box in this view may no longer be visible.
+ view.recalc()
+ }
+
+ /**
+ * Switch to another view based on what was selected (an event or a free
+ * slot) and how it was selected (by touch or by trackball).
+ *
+ * @param trackBallSelection true if the selection was made using the
+ * trackball.
+ */
+ private fun switchViews(trackBallSelection: Boolean) {
+ val selectedEvent: Event? = mSelectedEvent
+ mPopup?.dismiss()
+ mLastPopupEventID = INVALID_EVENT_ID
+ if (mNumDays > 1) {
+ // This is the Week view.
+ // With touch, we always switch to Day/Agenda View
+ // With track ball, if we selected a free slot, then create an event.
+ // If we selected a specific event, switch to EventInfo view.
+ if (trackBallSelection) {
+ if (selectedEvent != null) {
+ if (mIsAccessibilityEnabled) {
+ mAccessibilityMgr?.interrupt()
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
+ mScrolling = false
+ return super.onKeyUp(keyCode, event)
+ }
+
+ @Override
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ return super.onKeyDown(keyCode, event)
+ }
+
+ @Override
+ override fun onHoverEvent(event: MotionEvent?): Boolean {
+ return true
+ }
+
+ private val isTouchExplorationEnabled: Boolean
+ private get() = mIsAccessibilityEnabled && mAccessibilityMgr!!.isTouchExplorationEnabled()
+
+ private fun sendAccessibilityEventAsNeeded(speakEvents: Boolean) {
+ if (!mIsAccessibilityEnabled) {
+ return
+ }
+ val dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility
+ val hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility
+ if (dayChanged || hourChanged || mLastSelectedEventForAccessibility !==
+ mSelectedEventForAccessibility) {
+ mLastSelectionDayForAccessibility = mSelectionDayForAccessibility
+ mLastSelectionHourForAccessibility = mSelectionHourForAccessibility
+ mLastSelectedEventForAccessibility = mSelectedEventForAccessibility
+ val b = StringBuilder()
+
+ // Announce only the changes i.e. day or hour or both
+ if (dayChanged) {
+ b.append(selectedTimeForAccessibility.format("%A "))
+ }
+ if (hourChanged) {
+ b.append(selectedTimeForAccessibility.format(if (mIs24HourFormat) "%k" else "%l%p"))
+ }
+ if (dayChanged || hourChanged) {
+ b.append(PERIOD_SPACE)
+ }
+ if (speakEvents) {
+ if (mEventCountTemplate == null) {
+ mEventCountTemplate = mContext?.getString(R.string.template_announce_item_index)
+ }
+
+ // Read out the relevant event(s)
+ val numEvents: Int = mSelectedEvents.size
+ if (numEvents > 0) {
+ if (mSelectedEventForAccessibility == null) {
+ // Read out all the events
+ var i = 1
+ for (calEvent in mSelectedEvents) {
+ if (numEvents > 1) {
+ // Read out x of numEvents if there are more than one event
+ mStringBuilder.setLength(0)
+ b.append(mFormatter.format(mEventCountTemplate, i++, numEvents))
+ b.append(" ")
+ }
+ appendEventAccessibilityString(b, calEvent)
+ }
+ } else {
+ if (numEvents > 1) {
+ // Read out x of numEvents if there are more than one event
+ mStringBuilder.setLength(0)
+ b.append(
+ mFormatter.format(
+ mEventCountTemplate, mSelectedEvents
+ .indexOf(mSelectedEventForAccessibility) + 1, numEvents
+ )
+ )
+ b.append(" ")
+ }
+ appendEventAccessibilityString(b, mSelectedEventForAccessibility)
+ }
+ }
+ }
+ if (dayChanged || hourChanged || speakEvents) {
+ val event: AccessibilityEvent = AccessibilityEvent
+ .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ val msg: CharSequence = b.toString()
+ event.getText().add(msg)
+ event.setAddedCount(msg.length)
+ sendAccessibilityEventUnchecked(event)
+ }
+ }
+ }
+
+ /**
+ * @param b
+ * @param calEvent
+ */
+ private fun appendEventAccessibilityString(b: StringBuilder, calEvent: Event?) {
+ b.append(calEvent!!.titleAndLocation)
+ b.append(PERIOD_SPACE)
+ val `when`: String?
+ var flags: Int = DateUtils.FORMAT_SHOW_DATE
+ if (calEvent!!.allDay) {
+ flags = flags or (DateUtils.FORMAT_UTC or DateUtils.FORMAT_SHOW_WEEKDAY)
+ } else {
+ flags = flags or DateUtils.FORMAT_SHOW_TIME
+ if (DateFormat.is24HourFormat(mContext)) {
+ flags = flags or DateUtils.FORMAT_24HOUR
+ }
+ }
+ `when` = Utils.formatDateRange(mContext, calEvent!!.startMillis, calEvent!!.endMillis,
+ flags)
+ b.append(`when`)
+ b.append(PERIOD_SPACE)
+ }
+
+ private inner class GotoBroadcaster(start: Time, end: Time) : Animation.AnimationListener {
+ private val mCounter: Int
+ private val mStart: Time
+ private val mEnd: Time
+ @Override
+ override fun onAnimationEnd(animation: Animation?) {
+ var view = mViewSwitcher.getCurrentView() as DayView
+ view.mViewStartX = 0
+ view = mViewSwitcher.getNextView() as DayView
+ view.mViewStartX = 0
+ if (mCounter == sCounter) {
+ mController.sendEvent(
+ this as Object?, EventType.GO_TO, mStart, mEnd, null, -1,
+ ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null
+ )
+ }
+ }
+
+ @Override
+ override fun onAnimationRepeat(animation: Animation?) {
+ }
+
+ @Override
+ override fun onAnimationStart(animation: Animation?) {
+ }
+
+ init {
+ mCounter = ++sCounter
+ mStart = start
+ mEnd = end
+ }
+ }
+
+ private fun switchViews(forward: Boolean, xOffSet: Float, width: Float, velocity: Float): View {
+ mAnimationDistance = width - xOffSet
+ if (DEBUG) {
+ Log.d(TAG, "switchViews($forward) O:$xOffSet Dist:$mAnimationDistance")
+ }
+ var progress: Float = Math.abs(xOffSet) / width
+ if (progress > 1.0f) {
+ progress = 1.0f
+ }
+ val inFromXValue: Float
+ val inToXValue: Float
+ val outFromXValue: Float
+ val outToXValue: Float
+ if (forward) {
+ inFromXValue = 1.0f - progress
+ inToXValue = 0.0f
+ outFromXValue = -progress
+ outToXValue = -1.0f
+ } else {
+ inFromXValue = progress - 1.0f
+ inToXValue = 0.0f
+ outFromXValue = progress
+ outToXValue = 1.0f
+ }
+ val start = Time(mBaseDate!!.timezone)
+ start.set(mController.time as Long)
+ if (forward) {
+ start.monthDay += mNumDays
+ } else {
+ start.monthDay -= mNumDays
+ }
+ mController.time = start.normalize(true)
+ var newSelected: Time? = start
+ if (mNumDays == 7) {
+ newSelected = Time(start)
+ adjustToBeginningOfWeek(start)
+ }
+ val end = Time(start)
+ end.monthDay += mNumDays - 1
+
+ // We have to allocate these animation objects each time we switch views
+ // because that is the only way to set the animation parameters.
+ val inAnimation = TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, inFromXValue,
+ Animation.RELATIVE_TO_SELF, inToXValue,
+ Animation.ABSOLUTE, 0.0f,
+ Animation.ABSOLUTE, 0.0f
+ )
+ val outAnimation = TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, outFromXValue,
+ Animation.RELATIVE_TO_SELF, outToXValue,
+ Animation.ABSOLUTE, 0.0f,
+ Animation.ABSOLUTE, 0.0f
+ )
+ val duration = calculateDuration(width - Math.abs(xOffSet), width, velocity)
+ inAnimation.setDuration(duration)
+ inAnimation.setInterpolator(mHScrollInterpolator)
+ outAnimation.setInterpolator(mHScrollInterpolator)
+ outAnimation.setDuration(duration)
+ outAnimation.setAnimationListener(GotoBroadcaster(start, end))
+ mViewSwitcher.setInAnimation(inAnimation)
+ mViewSwitcher.setOutAnimation(outAnimation)
+ var view = mViewSwitcher.getCurrentView() as DayView
+ view.cleanup()
+ mViewSwitcher.showNext()
+ view = mViewSwitcher.getCurrentView() as DayView
+ view.setSelected(newSelected, true, false)
+ view.requestFocus()
+ view.reloadEvents()
+ view.updateTitle()
+ view.restartCurrentTimeUpdates()
+ return view
+ }
+
+ // This is called after scrolling stops to move the selected hour
+ // to the visible part of the screen.
+ private fun resetSelectedHour() {
+ if (mSelectionHour < mFirstHour + 1) {
+ setSelectedHour(mFirstHour + 1)
+ setSelectedEvent(null)
+ mSelectedEvents.clear()
+ mComputeSelectedEvents = true
+ } else if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ setSelectedHour(mFirstHour + mNumHours - 3)
+ setSelectedEvent(null)
+ mSelectedEvents.clear()
+ mComputeSelectedEvents = true
+ }
+ }
+
+ private fun initFirstHour() {
+ mFirstHour = mSelectionHour - mNumHours / 5
+ if (mFirstHour < 0) {
+ mFirstHour = 0
+ } else if (mFirstHour + mNumHours > 24) {
+ mFirstHour = 24 - mNumHours
+ }
+ }
+
+ /**
+ * Recomputes the first full hour that is visible on screen after the
+ * screen is scrolled.
+ */
+ private fun computeFirstHour() {
+ // Compute the first full hour that is visible on screen
+ mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP)
+ mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY
+ }
+
+ private fun adjustHourSelection() {
+ if (mSelectionHour < 0) {
+ setSelectedHour(0)
+ if (mMaxAlldayEvents > 0) {
+ mPrevSelectedEvent = null
+ mSelectionAllday = true
+ }
+ }
+ if (mSelectionHour > 23) {
+ setSelectedHour(23)
+ }
+
+ // If the selected hour is at least 2 time slots from the top and
+ // bottom of the screen, then don't scroll the view.
+ if (mSelectionHour < mFirstHour + 1) {
+ // If there are all-days events for the selected day but there
+ // are no more normal events earlier in the day, then jump to
+ // the all-day event area.
+ // Exception 1: allow the user to scroll to 8am with the trackball
+ // before jumping to the all-day event area.
+ // Exception 2: if 12am is on screen, then allow the user to select
+ // 12am before going up to the all-day event area.
+ val daynum = mSelectionDay - mFirstJulianDay
+ if (daynum < mEarliestStartHour!!.size && daynum >= 0 && mMaxAlldayEvents > 0 &&
+ mEarliestStartHour!![daynum] > mSelectionHour &&
+ mFirstHour > 0 && mFirstHour < 8) {
+ mPrevSelectedEvent = null
+ mSelectionAllday = true
+ setSelectedHour(mFirstHour + 1)
+ return
+ }
+ if (mFirstHour > 0) {
+ mFirstHour -= 1
+ mViewStartY -= mCellHeight + HOUR_GAP
+ if (mViewStartY < 0) {
+ mViewStartY = 0
+ }
+ return
+ }
+ }
+ if (mSelectionHour > mFirstHour + mNumHours - 3) {
+ if (mFirstHour < 24 - mNumHours) {
+ mFirstHour += 1
+ mViewStartY += mCellHeight + HOUR_GAP
+ if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY
+ }
+ return
+ } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
+ mViewStartY = mMaxViewStartY
+ }
+ }
+ }
+
+ fun clearCachedEvents() {
+ mLastReloadMillis = 0
+ }
+
+ private val mCancelCallback: Runnable = object : Runnable {
+ override fun run() {
+ clearCachedEvents()
+ }
+ }
+
+ /* package */
+ fun reloadEvents() {
+ // Protect against this being called before this view has been
+ // initialized.
+// if (mContext == null) {
+// return;
+// }
+
+ // Make sure our time zones are up to date
+ mTZUpdater.run()
+ setSelectedEvent(null)
+ mPrevSelectedEvent = null
+ mSelectedEvents.clear()
+
+ // The start date is the beginning of the week at 12am
+ val weekStart = Time(Utils.getTimeZone(mContext, mTZUpdater))
+ weekStart.set(mBaseDate)
+ weekStart.hour = 0
+ weekStart.minute = 0
+ weekStart.second = 0
+ val millis: Long = weekStart.normalize(true /* ignore isDst */)
+
+ // Avoid reloading events unnecessarily.
+ if (millis == mLastReloadMillis) {
+ return
+ }
+ mLastReloadMillis = millis
+
+ // load events in the background
+ // mContext.startProgressSpinner();
+ val events: ArrayList<Event> = ArrayList<Event>()
+ mEventLoader.loadEventsInBackground(mNumDays, events as ArrayList<Event?>, mFirstJulianDay,
+ object : Runnable {
+ override fun run() {
+ val fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay
+ mEvents = events
+ mLoadedFirstJulianDay = mFirstJulianDay
+ if (mAllDayEvents == null) {
+ mAllDayEvents = ArrayList<Event>()
+ } else {
+ mAllDayEvents?.clear()
+ }
+
+ // Create a shorter array for all day events
+ for (e in events) {
+ if (e.drawAsAllday()) {
+ mAllDayEvents?.add(e)
+ }
+ }
+
+ // New events, new layouts
+ if (mLayouts == null || mLayouts!!.size < events.size) {
+ mLayouts = arrayOfNulls<StaticLayout>(events.size)
+ } else {
+ Arrays.fill(mLayouts, null)
+ }
+ if (mAllDayLayouts == null || mAllDayLayouts!!.size < mAllDayEvents!!.size) {
+ mAllDayLayouts = arrayOfNulls<StaticLayout>(events.size)
+ } else {
+ Arrays.fill(mAllDayLayouts, null)
+ }
+ computeEventRelations()
+ mRemeasure = true
+ mComputeSelectedEvents = true
+ recalc()
+
+ // Start animation to cross fade the events
+ if (fadeinEvents) {
+ if (mEventsCrossFadeAnimation == null) {
+ mEventsCrossFadeAnimation =
+ ObjectAnimator.ofInt(this@DayView, "EventsAlpha", 0, 255)
+ mEventsCrossFadeAnimation?.setDuration(EVENTS_CROSS_FADE_DURATION.toLong())
+ }
+ mEventsCrossFadeAnimation?.start()
+ } else {
+ invalidate()
+ }
+ }
+ }, mCancelCallback)
+ }
+
+ var eventsAlpha: Int
+ get() = mEventsAlpha
+ set(alpha) {
+ mEventsAlpha = alpha
+ invalidate()
+ }
+
+ fun stopEventsAnimation() {
+ if (mEventsCrossFadeAnimation != null) {
+ mEventsCrossFadeAnimation?.cancel()
+ }
+ mEventsAlpha = 255
+ }
+
+ private fun computeEventRelations() {
+ // Compute the layout relation between each event before measuring cell
+ // width, as the cell width should be adjusted along with the relation.
+ //
+ // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm)
+ // We should mark them as "overwapped". Though they are not overwapped logically, but
+ // minimum cell height implicitly expands the cell height of A and it should look like
+ // (1:00pm - 1:15pm) after the cell height adjustment.
+
+ // Compute the space needed for the all-day events, if any.
+ // Make a pass over all the events, and keep track of the maximum
+ // number of all-day events in any one day. Also, keep track of
+ // the earliest event in each day.
+ var maxAllDayEvents = 0
+ val events: ArrayList<Event> = mEvents
+ val len: Int = events.size
+ // Num of all-day-events on each day.
+ val eventsCount = IntArray(mLastJulianDay - mFirstJulianDay + 1)
+ Arrays.fill(eventsCount, 0)
+ for (ii in 0 until len) {
+ val event: Event = events.get(ii)
+ if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) {
+ continue
+ }
+ if (event.drawAsAllday()) {
+ // Count all the events being drawn as allDay events
+ val firstDay: Int = Math.max(event.startDay, mFirstJulianDay)
+ val lastDay: Int = Math.min(event.endDay, mLastJulianDay)
+ for (day in firstDay..lastDay) {
+ val count = ++eventsCount[day - mFirstJulianDay]
+ if (maxAllDayEvents < count) {
+ maxAllDayEvents = count
+ }
+ }
+ var daynum: Int = event.startDay - mFirstJulianDay
+ var durationDays: Int = event.endDay - event.startDay + 1
+ if (daynum < 0) {
+ durationDays += daynum
+ daynum = 0
+ }
+ if (daynum + durationDays > mNumDays) {
+ durationDays = mNumDays - daynum
+ }
+ var day = daynum
+ while (durationDays > 0) {
+ mHasAllDayEvent!![day] = true
+ day++
+ durationDays--
+ }
+ } else {
+ var daynum: Int = event.startDay - mFirstJulianDay
+ var hour: Int = event.startTime / 60
+ if (daynum >= 0 && hour < mEarliestStartHour!![daynum]) {
+ mEarliestStartHour!![daynum] = hour
+ }
+
+ // Also check the end hour in case the event spans more than
+ // one day.
+ daynum = event.endDay - mFirstJulianDay
+ hour = event.endTime / 60
+ if (daynum < mNumDays && hour < mEarliestStartHour!![daynum]) {
+ mEarliestStartHour!![daynum] = hour
+ }
+ }
+ }
+ mMaxAlldayEvents = maxAllDayEvents
+ initAllDayHeights()
+ }
+
+ @Override
+ protected override fun onDraw(canvas: Canvas) {
+ if (mRemeasure) {
+ remeasure(getWidth(), getHeight())
+ mRemeasure = false
+ }
+ canvas.save()
+ val yTranslate = (-mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight).toFloat()
+ // offset canvas by the current drag and header position
+ canvas.translate(-mViewStartX.toFloat(), yTranslate)
+ // clip to everything below the allDay area
+ val dest: Rect = mDestRect
+ dest.top = (mFirstCell - yTranslate).toInt()
+ dest.bottom = (mViewHeight - yTranslate).toInt()
+ dest.left = 0
+ dest.right = mViewWidth
+ canvas.save()
+ canvas.clipRect(dest)
+ // Draw the movable part of the view
+ doDraw(canvas)
+ // restore to having no clip
+ canvas.restore()
+ if (mTouchMode and TOUCH_MODE_HSCROLL != 0) {
+ val xTranslate: Float
+ xTranslate = if (mViewStartX > 0) {
+ mViewWidth.toFloat()
+ } else {
+ -mViewWidth.toFloat()
+ }
+ // Move the canvas around to prep it for the next view
+ // specifically, shift it by a screen and undo the
+ // yTranslation which will be redone in the nextView's onDraw().
+ canvas.translate(xTranslate, -yTranslate)
+ val nextView = mViewSwitcher.getNextView() as DayView
+
+ // Prevent infinite recursive calls to onDraw().
+ nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE
+ nextView.onDraw(canvas)
+ // Move it back for this view
+ canvas.translate(-xTranslate, 0f)
+ } else {
+ // If we drew another view we already translated it back
+ // If we didn't draw another view we should be at the edge of the
+ // screen
+ canvas.translate(mViewStartX.toFloat(), -yTranslate)
+ }
+
+ // Draw the fixed areas (that don't scroll) directly to the canvas.
+ drawAfterScroll(canvas)
+ if (mComputeSelectedEvents && mUpdateToast) {
+ mUpdateToast = false
+ }
+ mComputeSelectedEvents = false
+
+ // Draw overscroll glow
+ if (!mEdgeEffectTop.isFinished()) {
+ if (DAY_HEADER_HEIGHT != 0) {
+ canvas.translate(0f, DAY_HEADER_HEIGHT.toFloat())
+ }
+ if (mEdgeEffectTop.draw(canvas)) {
+ invalidate()
+ }
+ if (DAY_HEADER_HEIGHT != 0) {
+ canvas.translate(0f, -DAY_HEADER_HEIGHT.toFloat())
+ }
+ }
+ if (!mEdgeEffectBottom.isFinished()) {
+ canvas.rotate(180f, mViewWidth.toFloat() / 2f, mViewHeight.toFloat() / 2f)
+ if (mEdgeEffectBottom.draw(canvas)) {
+ invalidate()
+ }
+ }
+ canvas.restore()
+ }
+
+ private fun drawAfterScroll(canvas: Canvas) {
+ val p: Paint = mPaint
+ val r: Rect = mRect
+ drawAllDayHighlights(r, canvas, p)
+ if (mMaxAlldayEvents != 0) {
+ drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p)
+ drawUpperLeftCorner(r, canvas, p)
+ }
+ drawScrollLine(r, canvas, p)
+ drawDayHeaderLoop(r, canvas, p)
+
+ // Draw the AM and PM indicators if we're in 12 hour mode
+ if (!mIs24HourFormat) {
+ drawAmPm(canvas, p)
+ }
+ }
+
+ // This isn't really the upper-left corner. It's the square area just
+ // below the upper-left corner, above the hours and to the left of the
+ // all-day area.
+ private fun drawUpperLeftCorner(r: Rect, canvas: Canvas, p: Paint) {
+ setupHourTextPaint(p)
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
+ // Draw the allDay expand/collapse icon
+ if (mUseExpandIcon) {
+ mExpandAlldayDrawable.setBounds(mExpandAllDayRect)
+ mExpandAlldayDrawable.draw(canvas)
+ } else {
+ mCollapseAlldayDrawable.setBounds(mExpandAllDayRect)
+ mCollapseAlldayDrawable.draw(canvas)
+ }
+ }
+ }
+
+ private fun drawScrollLine(r: Rect, canvas: Canvas, p: Paint) {
+ val right = computeDayLeftPosition(mNumDays)
+ val y = mFirstCell - 1
+ p.setAntiAlias(false)
+ p.setStyle(Style.FILL)
+ p.setColor(mCalendarGridLineInnerHorizontalColor)
+ p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
+ canvas.drawLine(GRID_LINE_LEFT_MARGIN, y.toFloat(), right.toFloat(), y.toFloat(), p)
+ p.setAntiAlias(true)
+ }
+
+ // Computes the x position for the left side of the given day (base 0)
+ private fun computeDayLeftPosition(day: Int): Int {
+ val effectiveWidth = mViewWidth - mHoursWidth
+ return day * effectiveWidth / mNumDays + mHoursWidth
+ }
+
+ private fun drawAllDayHighlights(r: Rect, canvas: Canvas, p: Paint) {
+ if (mFutureBgColor != 0) {
+ // First, color the labels area light gray
+ r.top = 0
+ r.bottom = DAY_HEADER_HEIGHT
+ r.left = 0
+ r.right = mViewWidth
+ p.setColor(mBgColor)
+ p.setStyle(Style.FILL)
+ canvas.drawRect(r, p)
+ // and the area that says All day
+ r.top = DAY_HEADER_HEIGHT
+ r.bottom = mFirstCell - 1
+ r.left = 0
+ r.right = mHoursWidth
+ canvas.drawRect(r, p)
+ var startIndex = -1
+ val todayIndex = mTodayJulianDay - mFirstJulianDay
+ if (todayIndex < 0) {
+ // Future
+ startIndex = 0
+ } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) {
+ // Multiday - tomorrow is visible.
+ startIndex = todayIndex + 1
+ }
+ if (startIndex >= 0) {
+ // Draw the future highlight
+ r.top = 0
+ r.bottom = mFirstCell - 1
+ r.left = computeDayLeftPosition(startIndex) + 1
+ r.right = computeDayLeftPosition(mNumDays)
+ p.setColor(mFutureBgColor)
+ p.setStyle(Style.FILL)
+ canvas.drawRect(r, p)
+ }
+ }
+ }
+
+ private fun drawDayHeaderLoop(r: Rect, canvas: Canvas, p: Paint) {
+ // Draw the horizontal day background banner
+ // p.setColor(mCalendarDateBannerBackground);
+ // r.top = 0;
+ // r.bottom = DAY_HEADER_HEIGHT;
+ // r.left = 0;
+ // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
+ // canvas.drawRect(r, p);
+ //
+ // Fill the extra space on the right side with the default background
+ // r.left = r.right;
+ // r.right = mViewWidth;
+ // p.setColor(mCalendarGridAreaBackground);
+ // canvas.drawRect(r, p);
+ if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) {
+ return
+ }
+ p.setTypeface(mBold)
+ p.setTextAlign(Paint.Align.RIGHT)
+ var cell = mFirstJulianDay
+ val dayNames: Array<String?>?
+ dayNames = if (mDateStrWidth < mCellWidth) {
+ mDayStrs
+ } else {
+ mDayStrs2Letter
+ }
+ p.setAntiAlias(true)
+ var day = 0
+ while (day < mNumDays) {
+ var dayOfWeek = day + mFirstVisibleDayOfWeek
+ if (dayOfWeek >= 14) {
+ dayOfWeek -= 14
+ }
+ var color = mCalendarDateBannerTextColor
+ if (mNumDays == 1) {
+ if (dayOfWeek == Time.SATURDAY) {
+ color = mWeek_saturdayColor
+ } else if (dayOfWeek == Time.SUNDAY) {
+ color = mWeek_sundayColor
+ }
+ } else {
+ val column = day % 7
+ if (Utils.isSaturday(column, mFirstDayOfWeek)) {
+ color = mWeek_saturdayColor
+ } else if (Utils.isSunday(column, mFirstDayOfWeek)) {
+ color = mWeek_sundayColor
+ }
+ }
+ p.setColor(color)
+ drawDayHeader(dayNames!![dayOfWeek], day, cell, canvas, p)
+ day++
+ cell++
+ }
+ p.setTypeface(null)
+ }
+
+ private fun drawAmPm(canvas: Canvas, p: Paint) {
+ p.setColor(mCalendarAmPmLabel)
+ p.setTextSize(AMPM_TEXT_SIZE)
+ p.setTypeface(mBold)
+ p.setAntiAlias(true)
+ p.setTextAlign(Paint.Align.RIGHT)
+ var text = mAmString
+ if (mFirstHour >= 12) {
+ text = mPmString
+ }
+ var y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP
+ canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
+ if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
+ // Also draw the "PM"
+ text = mPmString
+ y =
+ mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) +
+ 2 * mHoursTextHeight + HOUR_GAP
+ canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
+ }
+ }
+
+ private fun drawCurrentTimeLine(
+ r: Rect,
+ day: Int,
+ top: Int,
+ canvas: Canvas,
+ p: Paint
+ ) {
+ r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1
+ r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1
+ r.top = top - CURRENT_TIME_LINE_TOP_OFFSET
+ r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight()
+ mCurrentTimeLine.setBounds(r)
+ mCurrentTimeLine.draw(canvas)
+ if (mAnimateToday) {
+ mCurrentTimeAnimateLine.setBounds(r)
+ mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha)
+ mCurrentTimeAnimateLine.draw(canvas)
+ }
+ }
+
+ private fun doDraw(canvas: Canvas) {
+ val p: Paint = mPaint
+ val r: Rect = mRect
+ if (mFutureBgColor != 0) {
+ drawBgColors(r, canvas, p)
+ }
+ drawGridBackground(r, canvas, p)
+ drawHours(r, canvas, p)
+
+ // Draw each day
+ var cell = mFirstJulianDay
+ p.setAntiAlias(false)
+ val alpha: Int = p.getAlpha()
+ p.setAlpha(mEventsAlpha)
+ var day = 0
+ while (day < mNumDays) {
+
+ // TODO Wow, this needs cleanup. drawEvents loop through all the
+ // events on every call.
+ drawEvents(cell, day, HOUR_GAP, canvas, p)
+ // If this is today
+ if (cell == mTodayJulianDay) {
+ val lineY: Int =
+ mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
+ mCellHeight / 60 + 1
+
+ // And the current time shows up somewhere on the screen
+ if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
+ drawCurrentTimeLine(r, day, lineY, canvas, p)
+ }
+ }
+ day++
+ cell++
+ }
+ p.setAntiAlias(true)
+ p.setAlpha(alpha)
+ }
+
+ private fun drawHours(r: Rect, canvas: Canvas, p: Paint) {
+ setupHourTextPaint(p)
+ var y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN
+ for (i in 0..23) {
+ val time = mHourStrs!![i]
+ canvas.drawText(time, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
+ y += mCellHeight + HOUR_GAP
+ }
+ }
+
+ private fun setupHourTextPaint(p: Paint) {
+ p.setColor(mCalendarHourLabelColor)
+ p.setTextSize(HOURS_TEXT_SIZE)
+ p.setTypeface(Typeface.DEFAULT)
+ p.setTextAlign(Paint.Align.RIGHT)
+ p.setAntiAlias(true)
+ }
+
+ private fun drawDayHeader(dayStr: String?, day: Int, cell: Int, canvas: Canvas, p: Paint) {
+ var dateNum = mFirstVisibleDate + day
+ var x: Int
+ if (dateNum > mMonthLength) {
+ dateNum -= mMonthLength
+ }
+ p.setAntiAlias(true)
+ val todayIndex = mTodayJulianDay - mFirstJulianDay
+ // Draw day of the month
+ val dateNumStr: String = dateNum.toString()
+ if (mNumDays > 1) {
+ val y = (DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN).toFloat()
+
+ // Draw day of the month
+ x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN
+ p.setTextAlign(Align.RIGHT)
+ p.setTextSize(DATE_HEADER_FONT_SIZE)
+ p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT)
+ canvas.drawText(dateNumStr as String, x.toFloat(), y, p)
+
+ // Draw day of the week
+ x -= (p.measureText(" $dateNumStr")).toInt()
+ p.setTextSize(DAY_HEADER_FONT_SIZE)
+ p.setTypeface(Typeface.DEFAULT)
+ canvas.drawText(dayStr as String, x.toFloat(), y, p)
+ } else {
+ val y = (ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN).toFloat()
+ p.setTextAlign(Align.LEFT)
+
+ // Draw day of the week
+ x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN
+ p.setTextSize(DAY_HEADER_FONT_SIZE)
+ p.setTypeface(Typeface.DEFAULT)
+ canvas.drawText(dayStr as String, x.toFloat(), y, p)
+
+ // Draw day of the month
+ x += (p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN).toInt()
+ p.setTextSize(DATE_HEADER_FONT_SIZE)
+ p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT)
+ canvas.drawText(dateNumStr, x.toFloat(), y, p)
+ }
+ }
+
+ private fun drawGridBackground(r: Rect, canvas: Canvas, p: Paint) {
+ val savedStyle: Style = p.getStyle()
+ val stopX = computeDayLeftPosition(mNumDays).toFloat()
+ var y = 0f
+ val deltaY = (mCellHeight + HOUR_GAP).toFloat()
+ var linesIndex = 0
+ val startY = 0f
+ val stopY = (HOUR_GAP + 24 * (mCellHeight + HOUR_GAP)).toFloat()
+ var x = mHoursWidth.toFloat()
+
+ // Draw the inner horizontal grid lines
+ p.setColor(mCalendarGridLineInnerHorizontalColor)
+ p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
+ p.setAntiAlias(false)
+ y = 0f
+ linesIndex = 0
+ for (hour in 0..24) {
+ mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN
+ mLines[linesIndex++] = y
+ mLines[linesIndex++] = stopX
+ mLines[linesIndex++] = y
+ y += deltaY
+ }
+ if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) {
+ canvas.drawLines(mLines, 0, linesIndex, p)
+ linesIndex = 0
+ p.setColor(mCalendarGridLineInnerVerticalColor)
+ }
+
+ // Draw the inner vertical grid lines
+ for (day in 0..mNumDays) {
+ x = computeDayLeftPosition(day).toFloat()
+ mLines[linesIndex++] = x
+ mLines[linesIndex++] = startY
+ mLines[linesIndex++] = x
+ mLines[linesIndex++] = stopY
+ }
+ canvas.drawLines(mLines, 0, linesIndex, p)
+
+ // Restore the saved style.
+ p.setStyle(savedStyle)
+ p.setAntiAlias(true)
+ }
+
+ /**
+ * @param r
+ * @param canvas
+ * @param p
+ */
+ private fun drawBgColors(r: Rect, canvas: Canvas, p: Paint) {
+ val todayIndex = mTodayJulianDay - mFirstJulianDay
+ // Draw the hours background color
+ r.top = mDestRect.top
+ r.bottom = mDestRect.bottom
+ r.left = 0
+ r.right = mHoursWidth
+ p.setColor(mBgColor)
+ p.setStyle(Style.FILL)
+ p.setAntiAlias(false)
+ canvas.drawRect(r, p)
+
+ // Draw background for grid area
+ if (mNumDays == 1 && todayIndex == 0) {
+ // Draw a white background for the time later than current time
+ var lineY: Int =
+ mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
+ mCellHeight / 60 + 1
+ if (lineY < mViewStartY + mViewHeight) {
+ lineY = Math.max(lineY, mViewStartY)
+ r.left = mHoursWidth
+ r.right = mViewWidth
+ r.top = lineY
+ r.bottom = mViewStartY + mViewHeight
+ p.setColor(mFutureBgColor)
+ canvas.drawRect(r, p)
+ }
+ } else if (todayIndex >= 0 && todayIndex < mNumDays) {
+ // Draw today with a white background for the time later than current time
+ var lineY: Int =
+ mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
+ mCellHeight / 60 + 1
+ if (lineY < mViewStartY + mViewHeight) {
+ lineY = Math.max(lineY, mViewStartY)
+ r.left = computeDayLeftPosition(todayIndex) + 1
+ r.right = computeDayLeftPosition(todayIndex + 1)
+ r.top = lineY
+ r.bottom = mViewStartY + mViewHeight
+ p.setColor(mFutureBgColor)
+ canvas.drawRect(r, p)
+ }
+
+ // Paint Tomorrow and later days with future color
+ if (todayIndex + 1 < mNumDays) {
+ r.left = computeDayLeftPosition(todayIndex + 1) + 1
+ r.right = computeDayLeftPosition(mNumDays)
+ r.top = mDestRect.top
+ r.bottom = mDestRect.bottom
+ p.setColor(mFutureBgColor)
+ canvas.drawRect(r, p)
+ }
+ } else if (todayIndex < 0) {
+ // Future
+ r.left = computeDayLeftPosition(0) + 1
+ r.right = computeDayLeftPosition(mNumDays)
+ r.top = mDestRect.top
+ r.bottom = mDestRect.bottom
+ p.setColor(mFutureBgColor)
+ canvas.drawRect(r, p)
+ }
+ p.setAntiAlias(true)
+ }
+
+ private fun computeMaxStringWidth(currentMax: Int, strings: Array<String?>, p: Paint): Int {
+ var maxWidthF = 0.0f
+ val len = strings.size
+ for (i in 0 until len) {
+ val width: Float = p.measureText(strings[i])
+ maxWidthF = Math.max(width, maxWidthF)
+ }
+ var maxWidth = (maxWidthF + 0.5).toInt()
+ if (maxWidth < currentMax) {
+ maxWidth = currentMax
+ }
+ return maxWidth
+ }
+
+ private fun saveSelectionPosition(left: Float, top: Float, right: Float, bottom: Float) {
+ mPrevBox.left = left.toInt()
+ mPrevBox.right = right.toInt()
+ mPrevBox.top = top.toInt()
+ mPrevBox.bottom = bottom.toInt()
+ }
+
+ private fun setupTextRect(r: Rect) {
+ if (r.bottom <= r.top || r.right <= r.left) {
+ r.bottom = r.top
+ r.right = r.left
+ return
+ }
+ if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) {
+ r.top += EVENT_TEXT_TOP_MARGIN
+ r.bottom -= EVENT_TEXT_BOTTOM_MARGIN
+ }
+ if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) {
+ r.left += EVENT_TEXT_LEFT_MARGIN
+ r.right -= EVENT_TEXT_RIGHT_MARGIN
+ }
+ }
+
+ private fun setupAllDayTextRect(r: Rect) {
+ if (r.bottom <= r.top || r.right <= r.left) {
+ r.bottom = r.top
+ r.right = r.left
+ return
+ }
+ if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) {
+ r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN
+ r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN
+ }
+ if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) {
+ r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN
+ r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN
+ }
+ }
+
+ /**
+ * Return the layout for a numbered event. Create it if not already existing
+ */
+ private fun getEventLayout(
+ layouts: Array<StaticLayout?>?,
+ i: Int,
+ event: Event,
+ paint: Paint,
+ r: Rect
+ ): StaticLayout? {
+ if (i < 0 || i >= layouts!!.size) {
+ return null
+ }
+ var layout: StaticLayout? = layouts!![i]
+ // Check if we have already initialized the StaticLayout and that
+ // the width hasn't changed (due to vertical resizing which causes
+ // re-layout of events at min height)
+ if (layout == null || r.width() !== layout.getWidth()) {
+ val bob = SpannableStringBuilder()
+ if (event.title != null) {
+ // MAX - 1 since we add a space
+ bob.append(drawTextSanitizer(event.title.toString(),
+ MAX_EVENT_TEXT_LEN - 1))
+ bob.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length, 0)
+ bob.append(' ')
+ }
+ if (event.location != null) {
+ bob.append(
+ drawTextSanitizer(
+ event.location.toString(),
+ MAX_EVENT_TEXT_LEN - bob.length
+ )
+ )
+ }
+ when (event.selfAttendeeStatus) {
+ Attendees.ATTENDEE_STATUS_INVITED -> paint.setColor(event.color)
+ Attendees.ATTENDEE_STATUS_DECLINED -> {
+ paint.setColor(mEventTextColor)
+ paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA)
+ }
+ Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
+ Attendees.ATTENDEE_STATUS_TENTATIVE -> paint.setColor(
+ mEventTextColor
+ )
+ else -> paint.setColor(mEventTextColor)
+ }
+
+ // Leave a one pixel boundary on the left and right of the rectangle for the event
+ layout = StaticLayout(
+ bob, 0, bob.length, TextPaint(paint), r.width(),
+ Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()
+ )
+ layouts[i] = layout
+ }
+ layout.getPaint().setAlpha(mEventsAlpha)
+ return layout
+ }
+
+ private fun drawAllDayEvents(firstDay: Int, numDays: Int, canvas: Canvas, p: Paint) {
+ p.setTextSize(NORMAL_FONT_SIZE)
+ p.setTextAlign(Paint.Align.LEFT)
+ val eventTextPaint: Paint = mEventTextPaint
+ val startY = DAY_HEADER_HEIGHT.toFloat()
+ val stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN
+ var x = 0f
+ var linesIndex = 0
+
+ // Draw the inner vertical grid lines
+ p.setColor(mCalendarGridLineInnerVerticalColor)
+ x = mHoursWidth.toFloat()
+ p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
+ // Line bounding the top of the all day area
+ mLines!![linesIndex++] = GRID_LINE_LEFT_MARGIN
+ mLines!![linesIndex++] = startY
+ mLines!![linesIndex++] = computeDayLeftPosition(mNumDays).toFloat()
+ mLines!![linesIndex++] = startY
+ for (day in 0..mNumDays) {
+ x = computeDayLeftPosition(day).toFloat()
+ mLines!![linesIndex++] = x
+ mLines!![linesIndex++] = startY
+ mLines!![linesIndex++] = x
+ mLines!![linesIndex++] = stopY
+ }
+ p.setAntiAlias(false)
+ canvas.drawLines(mLines, 0, linesIndex, p)
+ p.setStyle(Style.FILL)
+ val y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN
+ val lastDay = firstDay + numDays - 1
+ val events: ArrayList<Event>? = mAllDayEvents
+ val numEvents: Int = events!!.size
+ // Whether or not we should draw the more events text
+ var hasMoreEvents = false
+ // size of the allDay area
+ val drawHeight = mAlldayHeight.toFloat()
+ // max number of events being drawn in one day of the allday area
+ var numRectangles = mMaxAlldayEvents.toFloat()
+ // Where to cut off drawn allday events
+ var allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN
+ // The number of events that weren't drawn in each day
+ mSkippedAlldayEvents = IntArray(numDays)
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount &&
+ !mShowAllAllDayEvents && mAnimateDayHeight == 0) {
+ // We draw one fewer event than will fit so that more events text
+ // can be drawn
+ numRectangles = (mMaxUnexpandedAlldayEventCount - 1).toFloat()
+ // We also clip the events above the more events text
+ allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+ hasMoreEvents = true
+ } else if (mAnimateDayHeight != 0) {
+ // clip at the end of the animating space
+ allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN
+ }
+ var alpha: Int = eventTextPaint.getAlpha()
+ eventTextPaint.setAlpha(mEventsAlpha)
+ for (i in 0 until numEvents) {
+ val event: Event = events!!.get(i)
+ var startDay: Int = event.startDay
+ var endDay: Int = event.endDay
+ if (startDay > lastDay || endDay < firstDay) {
+ continue
+ }
+ if (startDay < firstDay) {
+ startDay = firstDay
+ }
+ if (endDay > lastDay) {
+ endDay = lastDay
+ }
+ val startIndex = startDay - firstDay
+ val endIndex = endDay - firstDay
+ var height =
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount)
+ mAnimateDayEventHeight.toFloat() else drawHeight / numRectangles
+
+ // Prevent a single event from getting too big
+ if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
+ height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT.toFloat()
+ }
+
+ // Leave a one-pixel space between the vertical day lines and the
+ // event rectangle.
+ event.left = computeDayLeftPosition(startIndex).toFloat()
+ event.right = computeDayLeftPosition(endIndex + 1).toFloat() - DAY_GAP
+ event.top = y + height * event.getColumn()
+ event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
+ // check if we should skip this event. We skip if it starts
+ // after the clip bound or ends after the skip bound and we're
+ // not animating.
+ if (event.top >= allDayEventClip) {
+ incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex)
+ continue
+ } else if (event.bottom > allDayEventClip) {
+ if (hasMoreEvents) {
+ incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex)
+ continue
+ }
+ event.bottom = allDayEventClip.toFloat()
+ }
+ }
+ val r: Rect = drawEventRect(
+ event, canvas, p, eventTextPaint, event.top.toInt(),
+ event.bottom.toInt()
+ )
+ setupAllDayTextRect(r)
+ val layout: StaticLayout? = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r)
+ drawEventText(layout, r, canvas, r.top, r.bottom, true)
+
+ // Check if this all-day event intersects the selected day
+ if (mSelectionAllday && mComputeSelectedEvents) {
+ if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
+ mSelectedEvents.add(event)
+ }
+ }
+ }
+ eventTextPaint.setAlpha(alpha)
+ if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) {
+ // If the more allday text should be visible, draw it.
+ alpha = p.getAlpha()
+ p.setAlpha(mEventsAlpha)
+ p.setColor(mMoreAlldayEventsTextAlpha shl 24 and mMoreEventsTextColor)
+ for (i in mSkippedAlldayEvents!!.indices) {
+ if (mSkippedAlldayEvents!![i] > 0) {
+ drawMoreAlldayEvents(canvas, mSkippedAlldayEvents!![i], i, p)
+ }
+ }
+ p.setAlpha(alpha)
+ }
+ if (mSelectionAllday) {
+ // Compute the neighbors for the list of all-day events that
+ // intersect the selected day.
+ computeAllDayNeighbors()
+
+ // Set the selection position to zero so that when we move down
+ // to the normal event area, we will highlight the topmost event.
+ saveSelectionPosition(0f, 0f, 0f, 0f)
+ }
+ }
+
+ // Helper method for counting the number of allday events skipped on each day
+ private fun incrementSkipCount(counts: IntArray?, startIndex: Int, endIndex: Int) {
+ if (counts == null || startIndex < 0 || endIndex > counts.size) {
+ return
+ }
+ for (i in startIndex..endIndex) {
+ counts[i]++
+ }
+ }
+
+ // Draws the "box +n" text for hidden allday events
+ protected fun drawMoreAlldayEvents(canvas: Canvas, remainingEvents: Int, day: Int, p: Paint) {
+ var x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN
+ var y = (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - (.5f *
+ EVENT_SQUARE_WIDTH) + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN).toInt()
+ val r: Rect = mRect
+ r.top = y
+ r.left = x
+ r.bottom = y + EVENT_SQUARE_WIDTH
+ r.right = x + EVENT_SQUARE_WIDTH
+ p.setColor(mMoreEventsTextColor)
+ p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat())
+ p.setStyle(Style.STROKE)
+ p.setAntiAlias(false)
+ canvas.drawRect(r, p)
+ p.setAntiAlias(true)
+ p.setStyle(Style.FILL)
+ p.setTextSize(EVENT_TEXT_FONT_SIZE)
+ val text: String =
+ mResources.getQuantityString(R.plurals.month_more_events, remainingEvents)
+ y += EVENT_SQUARE_WIDTH
+ x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING
+ canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(), p)
+ }
+
+ private fun computeAllDayNeighbors() {
+ val len: Int = mSelectedEvents.size
+ if (len == 0 || mSelectedEvent != null) {
+ return
+ }
+
+ // First, clear all the links
+ for (ii in 0 until len) {
+ val ev: Event = mSelectedEvents.get(ii)
+ ev.nextUp = null
+ ev.nextDown = null
+ ev.nextLeft = null
+ ev.nextRight = null
+ }
+
+ // For each event in the selected event list "mSelectedEvents", find
+ // its neighbors in the up and down directions. This could be done
+ // more efficiently by sorting on the Event.getColumn() field, but
+ // the list is expected to be very small.
+
+ // Find the event in the same row as the previously selected all-day
+ // event, if any.
+ var startPosition = -1
+ if (mPrevSelectedEvent != null && mPrevSelectedEvent!!.drawAsAllday()) {
+ startPosition = mPrevSelectedEvent?.getColumn() as Int
+ }
+ var maxPosition = -1
+ var startEvent: Event? = null
+ var maxPositionEvent: Event? = null
+ for (ii in 0 until len) {
+ val ev: Event = mSelectedEvents.get(ii)
+ val position: Int = ev.getColumn()
+ if (position == startPosition) {
+ startEvent = ev
+ } else if (position > maxPosition) {
+ maxPositionEvent = ev
+ maxPosition = position
+ }
+ for (jj in 0 until len) {
+ if (jj == ii) {
+ continue
+ }
+ val neighbor: Event = mSelectedEvents.get(jj)
+ val neighborPosition: Int = neighbor.getColumn()
+ if (neighborPosition == position - 1) {
+ ev.nextUp = neighbor
+ } else if (neighborPosition == position + 1) {
+ ev.nextDown = neighbor
+ }
+ }
+ }
+ if (startEvent != null) {
+ setSelectedEvent(startEvent)
+ } else {
+ setSelectedEvent(maxPositionEvent)
+ }
+ }
+
+ private fun drawEvents(date: Int, dayIndex: Int, top: Int, canvas: Canvas, p: Paint) {
+ val eventTextPaint: Paint = mEventTextPaint
+ val left = computeDayLeftPosition(dayIndex) + 1
+ val cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1
+ val cellHeight = mCellHeight
+
+ // Use the selected hour as the selection region
+ val selectionArea: Rect = mSelectionRect
+ selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP)
+ selectionArea.bottom = selectionArea.top + cellHeight
+ selectionArea.left = left
+ selectionArea.right = selectionArea.left + cellWidth
+ val events: ArrayList<Event> = mEvents
+ val numEvents: Int = events.size
+ val geometry: EventGeometry = mEventGeometry
+ val viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight
+ val alpha: Int = eventTextPaint.getAlpha()
+ eventTextPaint.setAlpha(mEventsAlpha)
+ for (i in 0 until numEvents) {
+ val event: Event = events.get(i)
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
+ continue
+ }
+
+ // Don't draw it if it is not visible
+ if (event.bottom < mViewStartY || event.top > viewEndY) {
+ continue
+ }
+ if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents &&
+ geometry.eventIntersectsSelection(event, selectionArea)
+ ) {
+ mSelectedEvents.add(event)
+ }
+ val r: Rect = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY)
+ setupTextRect(r)
+
+ // Don't draw text if it is not visible
+ if (r.top > viewEndY || r.bottom < mViewStartY) {
+ continue
+ }
+ val layout: StaticLayout? = getEventLayout(mLayouts, i, event, eventTextPaint, r)
+ // TODO: not sure why we are 4 pixels off
+ drawEventText(
+ layout,
+ r,
+ canvas,
+ mViewStartY + 4,
+ mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight,
+ false
+ )
+ }
+ eventTextPaint.setAlpha(alpha)
+ }
+
+ private fun drawEventRect(
+ event: Event,
+ canvas: Canvas,
+ p: Paint,
+ eventTextPaint: Paint,
+ visibleTop: Int,
+ visibleBot: Int
+ ): Rect {
+ // Draw the Event Rect
+ val r: Rect = mRect
+ r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN, visibleTop)
+ r.bottom = Math.min(event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN, visibleBot)
+ r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN
+ r.right = event.right.toInt()
+ var color: Int = event.color
+ when (event.selfAttendeeStatus) {
+ Attendees.ATTENDEE_STATUS_INVITED -> if (event !== mClickedEvent) {
+ p.setStyle(Style.STROKE)
+ }
+ Attendees.ATTENDEE_STATUS_DECLINED -> {
+ if (event !== mClickedEvent) {
+ color = Utils.getDeclinedColorFromColor(color)
+ }
+ p.setStyle(Style.FILL_AND_STROKE)
+ }
+ Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
+ Attendees.ATTENDEE_STATUS_TENTATIVE -> p.setStyle(
+ Style.FILL_AND_STROKE
+ )
+ else -> p.setStyle(Style.FILL_AND_STROKE)
+ }
+ p.setAntiAlias(false)
+ val floorHalfStroke = Math.floor(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt()
+ val ceilHalfStroke = Math.ceil(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt()
+ r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop)
+ r.bottom = Math.min(
+ event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke,
+ visibleBot
+ )
+ r.left += floorHalfStroke
+ r.right -= ceilHalfStroke
+ p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat())
+ p.setColor(color)
+ val alpha: Int = p.getAlpha()
+ p.setAlpha(mEventsAlpha)
+ canvas.drawRect(r, p)
+ p.setAlpha(alpha)
+ p.setStyle(Style.FILL)
+
+ // Setup rect for drawEventText which follows
+ r.top = event.top.toInt() + EVENT_RECT_TOP_MARGIN
+ r.bottom = event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN
+ r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN
+ r.right = event.right.toInt() - EVENT_RECT_RIGHT_MARGIN
+ return r
+ }
+
+ private val drawTextSanitizerFilter: Pattern = Pattern.compile("[\t\n],")
+
+ // Sanitize a string before passing it to drawText or else we get little
+ // squares. For newlines and tabs before a comma, delete the character.
+ // Otherwise, just replace them with a space.
+ private fun drawTextSanitizer(string: String, maxEventTextLen: Int): String {
+ var string = string
+ val m: Matcher = drawTextSanitizerFilter.matcher(string)
+ string = m.replaceAll(",")
+ var len: Int = string.length
+ if (maxEventTextLen <= 0) {
+ string = ""
+ len = 0
+ } else if (len > maxEventTextLen) {
+ string = string.substring(0, maxEventTextLen)
+ len = maxEventTextLen
+ }
+ return string.replace('\n', ' ')
+ }
+
+ private fun drawEventText(
+ eventLayout: StaticLayout?,
+ rect: Rect,
+ canvas: Canvas,
+ top: Int,
+ bottom: Int,
+ center: Boolean
+ ) {
+ // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging
+ val width: Int = rect.right - rect.left
+ val height: Int = rect.bottom - rect.top
+
+ // If the rectangle is too small for text, then return
+ if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) {
+ return
+ }
+ var totalLineHeight = 0
+ val lineCount: Int = eventLayout.getLineCount()
+ for (i in 0 until lineCount) {
+ val lineBottom: Int = eventLayout.getLineBottom(i)
+ totalLineHeight = if (lineBottom <= height) {
+ lineBottom
+ } else {
+ break
+ }
+ }
+
+ // + 2 is small workaround when the font is slightly bigger than the rect. This will
+ // still allow the text to be shown without overflowing into the other all day rects.
+ if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight + 2 < top) {
+ return
+ }
+
+ // Use a StaticLayout to format the string.
+ canvas.save()
+ // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2));
+ val padding = if (center) (rect.bottom - rect.top - totalLineHeight) / 2 else 0
+ canvas.translate(rect.left.toFloat(), rect.top.toFloat() + padding)
+ rect.left = 0
+ rect.right = width
+ rect.top = 0
+ rect.bottom = totalLineHeight
+
+ // There's a bug somewhere. If this rect is outside of a previous
+ // cliprect, this becomes a no-op. What happens is that the text draw
+ // past the event rect. The current fix is to not draw the staticLayout
+ // at all if it is completely out of bound.
+ canvas.clipRect(rect)
+ eventLayout.draw(canvas)
+ canvas.restore()
+ }
+
+ // The following routines are called from the parent activity when certain
+ // touch events occur.
+ private fun doDown(ev: MotionEvent) {
+ mTouchMode = TOUCH_MODE_DOWN
+ mViewStartX = 0
+ mOnFlingCalled = false
+ mHandler?.removeCallbacks(mContinueScroll)
+ val x = ev.getX().toInt()
+ val y = ev.getY().toInt()
+
+ // Save selection information: we use setSelectionFromPosition to find the selected event
+ // in order to show the "clicked" color. But since it is also setting the selected info
+ // for new events, we need to restore the old info after calling the function.
+ val oldSelectedEvent: Event? = mSelectedEvent
+ val oldSelectionDay = mSelectionDay
+ val oldSelectionHour = mSelectionHour
+ if (setSelectionFromPosition(x, y, false)) {
+ // If a time was selected (a blue selection box is visible) and the click location
+ // is in the selected time, do not show a click on an event to prevent a situation
+ // of both a selection and an event are clicked when they overlap.
+ val pressedSelected = (mSelectionMode != SELECTION_HIDDEN &&
+ oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour)
+ if (!pressedSelected && mSelectedEvent != null) {
+ mSavedClickedEvent = mSelectedEvent
+ mDownTouchTime = System.currentTimeMillis()
+ postDelayed(mSetClick, mOnDownDelay.toLong())
+ } else {
+ eventClickCleanup()
+ }
+ }
+ mSelectedEvent = oldSelectedEvent
+ mSelectionDay = oldSelectionDay
+ mSelectionHour = oldSelectionHour
+ invalidate()
+ }
+
+ // Kicks off all the animations when the expand allday area is tapped
+ private fun doExpandAllDayClick() {
+ mShowAllAllDayEvents = !mShowAllAllDayEvents
+ ObjectAnimator.setFrameDelay(0)
+
+ // Determine the starting height
+ if (mAnimateDayHeight == 0) {
+ mAnimateDayHeight =
+ if (mShowAllAllDayEvents) mAlldayHeight - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+ else mAlldayHeight
+ }
+ // Cancel current animations
+ mCancellingAnimations = true
+ if (mAlldayAnimator != null) {
+ mAlldayAnimator?.cancel()
+ }
+ if (mAlldayEventAnimator != null) {
+ mAlldayEventAnimator?.cancel()
+ }
+ if (mMoreAlldayEventsAnimator != null) {
+ mMoreAlldayEventsAnimator?.cancel()
+ }
+ mCancellingAnimations = false
+ // get new animators
+ mAlldayAnimator = allDayAnimator
+ mAlldayEventAnimator = allDayEventAnimator
+ mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(
+ this,
+ "moreAllDayEventsTextAlpha",
+ if (mShowAllAllDayEvents) MORE_EVENTS_MAX_ALPHA else 0,
+ if (mShowAllAllDayEvents) 0 else MORE_EVENTS_MAX_ALPHA
+ )
+
+ // Set up delays and start the animators
+ mAlldayAnimator?.setStartDelay(if (mShowAllAllDayEvents) ANIMATION_SECONDARY_DURATION
+ else 0)
+ mAlldayAnimator?.start()
+ mMoreAlldayEventsAnimator?.setStartDelay(if (mShowAllAllDayEvents) 0
+ else ANIMATION_DURATION)
+ mMoreAlldayEventsAnimator?.setDuration(ANIMATION_SECONDARY_DURATION)
+ mMoreAlldayEventsAnimator?.start()
+ if (mAlldayEventAnimator != null) {
+ // This is the only animator that can return null, so check it
+ mAlldayEventAnimator
+ ?.setStartDelay(if (mShowAllAllDayEvents) ANIMATION_SECONDARY_DURATION else 0)
+ mAlldayEventAnimator?.start()
+ }
+ }
+
+ /**
+ * Figures out the initial heights for allDay events and space when
+ * a view is being set up.
+ */
+ fun initAllDayHeights() {
+ if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) {
+ return
+ }
+ if (mShowAllAllDayEvents) {
+ var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT
+ maxADHeight = Math.min(
+ maxADHeight,
+ (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+ )
+ mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents
+ } else {
+ mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+ }
+ } // First calculate the absolute max height
+ // Now expand to fit but not beyond the absolute max
+ // calculate the height of individual events in order to fit
+ // if there's nothing to animate just return
+
+ // Set up the animator with the calculated values
+ // Sets up an animator for changing the height of allday events
+ private val allDayEventAnimator: ObjectAnimator?
+ private get() {
+ // First calculate the absolute max height
+ var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT
+ // Now expand to fit but not beyond the absolute max
+ maxADHeight = Math.min(
+ maxADHeight,
+ (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+ )
+ // calculate the height of individual events in order to fit
+ val fitHeight = maxADHeight / mMaxAlldayEvents
+ val currentHeight = mAnimateDayEventHeight
+ val desiredHeight =
+ if (mShowAllAllDayEvents) fitHeight else MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+ // if there's nothing to animate just return
+ if (currentHeight == desiredHeight) {
+ return null
+ }
+
+ // Set up the animator with the calculated values
+ val animator: ObjectAnimator = ObjectAnimator.ofInt(
+ this, "animateDayEventHeight",
+ currentHeight, desiredHeight
+ )
+ animator.setDuration(ANIMATION_DURATION)
+ return animator
+ }
+
+ // Set up the animator with the calculated values
+ // Sets up an animator for changing the height of the allday area
+ private val allDayAnimator: ObjectAnimator
+ private get() {
+ // Calculate the absolute max height
+ var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT
+ // Find the desired height but don't exceed abs max
+ maxADHeight = Math.min(
+ maxADHeight,
+ (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
+ )
+ // calculate the current and desired heights
+ val currentHeight = if (mAnimateDayHeight != 0) mAnimateDayHeight else mAlldayHeight
+ val desiredHeight =
+ if (mShowAllAllDayEvents) maxADHeight else (MAX_UNEXPANDED_ALLDAY_HEIGHT -
+ MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1).toInt()
+
+ // Set up the animator with the calculated values
+ val animator: ObjectAnimator = ObjectAnimator.ofInt(
+ this, "animateDayHeight",
+ currentHeight, desiredHeight
+ )
+ animator.setDuration(ANIMATION_DURATION)
+ animator.addListener(object : AnimatorListenerAdapter() {
+ @Override
+ override fun onAnimationEnd(animation: Animator?) {
+ if (!mCancellingAnimations) {
+ // when finished, set this to 0 to signify not animating
+ mAnimateDayHeight = 0
+ mUseExpandIcon = !mShowAllAllDayEvents
+ }
+ mRemeasure = true
+ invalidate()
+ }
+ })
+ return animator
+ }
+
+ // setter for the 'box +n' alpha text used by the animator
+ fun setMoreAllDayEventsTextAlpha(alpha: Int) {
+ mMoreAlldayEventsTextAlpha = alpha
+ invalidate()
+ }
+
+ // setter for the height of the allday area used by the animator
+ fun setAnimateDayHeight(height: Int) {
+ mAnimateDayHeight = height
+ mRemeasure = true
+ invalidate()
+ }
+
+ // setter for the height of allday events used by the animator
+ fun setAnimateDayEventHeight(height: Int) {
+ mAnimateDayEventHeight = height
+ mRemeasure = true
+ invalidate()
+ }
+
+ private fun doSingleTapUp(ev: MotionEvent) {
+ if (!mHandleActionUp || mScrolling) {
+ return
+ }
+ val x = ev.getX().toInt()
+ val y = ev.getY().toInt()
+ val selectedDay = mSelectionDay
+ val selectedHour = mSelectionHour
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
+ // check if the tap was in the allday expansion area
+ val bottom = mFirstCell
+ if (x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight ||
+ !mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && y >= bottom -
+ MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT
+ ) {
+ doExpandAllDayClick()
+ return
+ }
+ }
+ val validPosition = setSelectionFromPosition(x, y, false)
+ if (!validPosition) {
+ if (y < DAY_HEADER_HEIGHT) {
+ val selectedTime = Time(mBaseDate)
+ selectedTime.setJulianDay(mSelectionDay)
+ selectedTime.hour = mSelectionHour
+ selectedTime.normalize(true /* ignore isDst */)
+ mController.sendEvent(
+ this as? Object, EventType.GO_TO, null, null, selectedTime, -1,
+ ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null
+ )
+ }
+ return
+ }
+ val hasSelection = mSelectionMode != SELECTION_HIDDEN
+ val pressedSelected = ((hasSelection || mTouchExplorationEnabled) &&
+ selectedDay == mSelectionDay && selectedHour == mSelectionHour)
+ if (mSelectedEvent != null) {
+ // If the tap is on an event, launch the "View event" view
+ if (mIsAccessibilityEnabled) {
+ mAccessibilityMgr?.interrupt()
+ }
+ mSelectionMode = SELECTION_HIDDEN
+ var yLocation = ((mSelectedEvent!!.top + mSelectedEvent!!.bottom) / 2) as Int
+ // Y location is affected by the position of the event in the scrolling
+ // view (mViewStartY) and the presence of all day events (mFirstCell)
+ if (!mSelectedEvent!!.allDay) {
+ yLocation += mFirstCell - mViewStartY
+ }
+ mClickedYLocation = yLocation
+ val clearDelay: Long = CLICK_DISPLAY_DURATION + mOnDownDelay -
+ (System.currentTimeMillis() - mDownTouchTime)
+ if (clearDelay > 0) {
+ this.postDelayed(mClearClick, clearDelay)
+ } else {
+ this.post(mClearClick)
+ }
+ }
+ invalidate()
+ }
+
+ private fun doLongPress(ev: MotionEvent) {
+ eventClickCleanup()
+ if (mScrolling) {
+ return
+ }
+
+ // Scale gesture in progress
+ if (mStartingSpanY != 0f) {
+ return
+ }
+ val x = ev.getX().toInt()
+ val y = ev.getY().toInt()
+ val validPosition = setSelectionFromPosition(x, y, false)
+ if (!validPosition) {
+ // return if the touch wasn't on an area of concern
+ return
+ }
+ invalidate()
+ performLongClick()
+ }
+
+ private fun doScroll(e1: MotionEvent, e2: MotionEvent, deltaX: Float, deltaY: Float) {
+ cancelAnimation()
+ if (mStartingScroll) {
+ mInitialScrollX = 0f
+ mInitialScrollY = 0f
+ mStartingScroll = false
+ }
+ mInitialScrollX += deltaX
+ mInitialScrollY += deltaY
+ val distanceX = mInitialScrollX.toInt()
+ val distanceY = mInitialScrollY.toInt()
+ val focusY = getAverageY(e2)
+ if (mRecalCenterHour) {
+ // Calculate the hour that correspond to the average of the Y touch points
+ mGestureCenterHour = ((mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) /
+ (mCellHeight + DAY_GAP))
+ mRecalCenterHour = false
+ }
+
+ // If we haven't figured out the predominant scroll direction yet,
+ // then do it now.
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ val absDistanceX: Int = Math.abs(distanceX)
+ val absDistanceY: Int = Math.abs(distanceY)
+ mScrollStartY = mViewStartY
+ mPreviousDirection = 0
+ if (absDistanceX > absDistanceY) {
+ val slopFactor = if (mScaleGestureDetector.isInProgress()) 20 else 2
+ if (absDistanceX > mScaledPagingTouchSlop * slopFactor) {
+ mTouchMode = TOUCH_MODE_HSCROLL
+ mViewStartX = distanceX
+ initNextView(-mViewStartX)
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_VSCROLL
+ }
+ } else if (mTouchMode and TOUCH_MODE_HSCROLL != 0) {
+ // We are already scrolling horizontally, so check if we
+ // changed the direction of scrolling so that the other week
+ // is now visible.
+ mViewStartX = distanceX
+ if (distanceX != 0) {
+ val direction = if (distanceX > 0) 1 else -1
+ if (direction != mPreviousDirection) {
+ // The user has switched the direction of scrolling
+ // so re-init the next view
+ initNextView(-mViewStartX)
+ mPreviousDirection = direction
+ }
+ }
+ }
+ if (mTouchMode and TOUCH_MODE_VSCROLL != 0) {
+ // Calculate the top of the visible region in the calendar grid.
+ // Increasing/decrease this will scroll the calendar grid up/down.
+ mViewStartY = ((mGestureCenterHour * (mCellHeight + DAY_GAP) -
+ focusY) + DAY_HEADER_HEIGHT + mAlldayHeight).toInt()
+
+ // If dragging while already at the end, do a glow
+ val pulledToY = (mScrollStartY + deltaY).toInt()
+ if (pulledToY < 0) {
+ mEdgeEffectTop.onPull(deltaY / mViewHeight)
+ if (!mEdgeEffectBottom.isFinished()) {
+ mEdgeEffectBottom.onRelease()
+ }
+ } else if (pulledToY > mMaxViewStartY) {
+ mEdgeEffectBottom.onPull(deltaY / mViewHeight)
+ if (!mEdgeEffectTop.isFinished()) {
+ mEdgeEffectTop.onRelease()
+ }
+ }
+ if (mViewStartY < 0) {
+ mViewStartY = 0
+ mRecalCenterHour = true
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY
+ mRecalCenterHour = true
+ }
+ if (mRecalCenterHour) {
+ // Calculate the hour that correspond to the average of the Y touch points
+ mGestureCenterHour = ((mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) /
+ (mCellHeight + DAY_GAP))
+ mRecalCenterHour = false
+ }
+ computeFirstHour()
+ }
+ mScrolling = true
+ mSelectionMode = SELECTION_HIDDEN
+ invalidate()
+ }
+
+ private fun getAverageY(me: MotionEvent): Float {
+ val count: Int = me.getPointerCount()
+ var focusY = 0f
+ for (i in 0 until count) {
+ focusY += me.getY(i)
+ }
+ focusY /= count.toFloat()
+ return focusY
+ }
+
+ private fun cancelAnimation() {
+ val `in`: Animation? = mViewSwitcher?.getInAnimation()
+ if (`in` != null) {
+ // cancel() doesn't terminate cleanly.
+ `in`?.scaleCurrentDuration(0f)
+ }
+ val out: Animation? = mViewSwitcher?.getOutAnimation()
+ if (out != null) {
+ // cancel() doesn't terminate cleanly.
+ out?.scaleCurrentDuration(0f)
+ }
+ }
+
+ private fun doFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float) {
+ cancelAnimation()
+ mSelectionMode = SELECTION_HIDDEN
+ eventClickCleanup()
+ mOnFlingCalled = true
+ if (mTouchMode and TOUCH_MODE_HSCROLL != 0) {
+ // Horizontal fling.
+ // initNextView(deltaX);
+ mTouchMode = TOUCH_MODE_INITIAL_STATE
+ if (DEBUG) Log.d(TAG, "doFling: velocityX $velocityX")
+ val deltaX = e2.getX().toInt() - e1.getX().toInt()
+ switchViews(deltaX < 0, mViewStartX.toFloat(), mViewWidth.toFloat(), velocityX)
+ mViewStartX = 0
+ return
+ }
+ if (mTouchMode and TOUCH_MODE_VSCROLL == 0) {
+ if (DEBUG) Log.d(TAG, "doFling: no fling")
+ return
+ }
+
+ // Vertical fling.
+ mTouchMode = TOUCH_MODE_INITIAL_STATE
+ mViewStartX = 0
+ if (DEBUG) {
+ Log.d(TAG, "doFling: mViewStartY$mViewStartY velocityY $velocityY")
+ }
+
+ // Continue scrolling vertically
+ mScrolling = true
+ mScroller.fling(
+ 0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */,
+ (-velocityY).toInt(), 0 /* minX */, 0 /* maxX */, 0 /* minY */,
+ mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE
+ )
+
+ // When flinging down, show a glow when it hits the end only if it
+ // wasn't started at the top
+ if (velocityY > 0 && mViewStartY != 0) {
+ mCallEdgeEffectOnAbsorb = true
+ } else if (velocityY < 0 && mViewStartY != mMaxViewStartY) {
+ mCallEdgeEffectOnAbsorb = true
+ }
+ mHandler?.post(mContinueScroll)
+ }
+
+ private fun initNextView(deltaX: Int): Boolean {
+ // Change the view to the previous day or week
+ val view = mViewSwitcher.getNextView() as DayView
+ val date: Time? = view.mBaseDate
+ date?.set(mBaseDate)
+ val switchForward: Boolean
+ if (deltaX > 0) {
+ date!!.monthDay -= mNumDays
+ view.setSelectedDay(mSelectionDay - mNumDays)
+ switchForward = false
+ } else {
+ date!!.monthDay += mNumDays
+ view.setSelectedDay(mSelectionDay + mNumDays)
+ switchForward = true
+ }
+ date?.normalize(true /* ignore isDst */)
+ initView(view)
+ view.layout(getLeft(), getTop(), getRight(), getBottom())
+ view.reloadEvents()
+ return switchForward
+ }
+
+ // ScaleGestureDetector.OnScaleGestureListener
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
+ mHandleActionUp = false
+ val gestureCenterInPixels: Float = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight
+ mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP)
+ mStartingSpanY = Math.max(MIN_Y_SPAN.toFloat(),
+ Math.abs(detector.getCurrentSpanY().toFloat()))
+ mCellHeightBeforeScaleGesture = mCellHeight
+ if (DEBUG_SCALING) {
+ val ViewStartHour = mViewStartY / (mCellHeight + DAY_GAP).toFloat()
+ Log.d(
+ TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour +
+ "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY +
+ "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()
+ )
+ }
+ return true
+ }
+
+ // ScaleGestureDetector.OnScaleGestureListener
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ val spanY: Float = Math.max(MIN_Y_SPAN.toFloat(),
+ Math.abs(detector.getCurrentSpanY().toFloat()))
+ mCellHeight = (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY).toInt()
+ if (mCellHeight < mMinCellHeight) {
+ // If mStartingSpanY is too small, even a small increase in the
+ // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT
+ mStartingSpanY = spanY
+ mCellHeight = mMinCellHeight
+ mCellHeightBeforeScaleGesture = mMinCellHeight
+ } else if (mCellHeight > MAX_CELL_HEIGHT) {
+ mStartingSpanY = spanY
+ mCellHeight = MAX_CELL_HEIGHT
+ mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT
+ }
+ val gestureCenterInPixels = detector.getFocusY().toInt() - DAY_HEADER_HEIGHT - mAlldayHeight
+ mViewStartY = (mGestureCenterHour * (mCellHeight + DAY_GAP)).toInt() - gestureCenterInPixels
+ mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight
+ if (DEBUG_SCALING) {
+ val ViewStartHour = mViewStartY / (mCellHeight + DAY_GAP).toFloat()
+ Log.d(
+ TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " +
+ ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" +
+ mCellHeight + " SpanY:" + detector.getCurrentSpanY()
+ )
+ }
+ if (mViewStartY < 0) {
+ mViewStartY = 0
+ mGestureCenterHour = ((mViewStartY + gestureCenterInPixels) /
+ (mCellHeight + DAY_GAP).toFloat())
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY
+ mGestureCenterHour = ((mViewStartY + gestureCenterInPixels) /
+ (mCellHeight + DAY_GAP).toFloat())
+ }
+ computeFirstHour()
+ mRemeasure = true
+ invalidate()
+ return true
+ }
+
+ // ScaleGestureDetector.OnScaleGestureListener
+ override fun onScaleEnd(detector: ScaleGestureDetector?) {
+ mScrollStartY = mViewStartY
+ mInitialScrollY = 0f
+ mInitialScrollX = 0f
+ mStartingSpanY = 0f
+ }
+
+ @Override
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
+ val action: Int = ev.getAction()
+ if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount())
+ if (ev.getActionMasked() === MotionEvent.ACTION_DOWN ||
+ ev.getActionMasked() === MotionEvent.ACTION_UP ||
+ ev.getActionMasked() === MotionEvent.ACTION_POINTER_UP ||
+ ev.getActionMasked() === MotionEvent.ACTION_POINTER_DOWN
+ ) {
+ mRecalCenterHour = true
+ }
+ if (mTouchMode and TOUCH_MODE_HSCROLL == 0) {
+ mScaleGestureDetector.onTouchEvent(ev)
+ }
+ return when (action) {
+ MotionEvent.ACTION_DOWN -> {
+ mStartingScroll = true
+ if (DEBUG) {
+ Log.e(
+ TAG,
+ "ACTION_DOWN ev.getDownTime = " + ev.getDownTime().toString() + " Cnt=" +
+ ev.getPointerCount()
+ )
+ }
+ val bottom =
+ mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN
+ mTouchStartedInAlldayArea = if (ev.getY() < bottom) {
+ true
+ } else {
+ false
+ }
+ mHandleActionUp = true
+ mGestureDetector.onTouchEvent(ev)
+ true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (DEBUG) Log.e(
+ TAG,
+ "ACTION_MOVE Cnt=" + ev.getPointerCount() + this@DayView
+ )
+ mGestureDetector.onTouchEvent(ev)
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ if (DEBUG) Log.e(
+ TAG,
+ "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp
+ )
+ mEdgeEffectTop.onRelease()
+ mEdgeEffectBottom.onRelease()
+ mStartingScroll = false
+ mGestureDetector.onTouchEvent(ev)
+ if (!mHandleActionUp) {
+ mHandleActionUp = true
+ mViewStartX = 0
+ invalidate()
+ return true
+ }
+ if (mOnFlingCalled) {
+ return true
+ }
+
+ // If we were scrolling, then reset the selected hour so that it
+ // is visible.
+ if (mScrolling) {
+ mScrolling = false
+ resetSelectedHour()
+ invalidate()
+ }
+ if (mTouchMode and TOUCH_MODE_HSCROLL != 0) {
+ mTouchMode = TOUCH_MODE_INITIAL_STATE
+ if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) {
+ // The user has gone beyond the threshold so switch views
+ if (DEBUG) Log.d(
+ TAG,
+ "- horizontal scroll: switch views"
+ )
+ switchViews(
+ mViewStartX > 0,
+ mViewStartX.toFloat(),
+ mViewWidth.toFloat(),
+ 0f
+ )
+ mViewStartX = 0
+ return true
+ } else {
+ // Not beyond the threshold so invalidate which will cause
+ // the view to snap back. Also call recalc() to ensure
+ // that we have the correct starting date and title.
+ if (DEBUG) Log.d(
+ TAG,
+ "- horizontal scroll: snap back"
+ )
+ recalc()
+ invalidate()
+ mViewStartX = 0
+ }
+ }
+ true
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ if (DEBUG) Log.e(
+ TAG,
+ "ACTION_CANCEL"
+ )
+ mGestureDetector.onTouchEvent(ev)
+ mScrolling = false
+ resetSelectedHour()
+ true
+ }
+ else -> {
+ if (DEBUG) Log.e(
+ TAG,
+ "Not MotionEvent " + ev.toString()
+ )
+ if (mGestureDetector.onTouchEvent(ev)) {
+ true
+ } else super.onTouchEvent(ev)
+ }
+ }
+ }
+
+ override fun onCreateContextMenu(menu: ContextMenu, view: View?, menuInfo: ContextMenuInfo?) {
+ var item: MenuItem
+
+ // If the trackball is held down, then the context menu pops up and
+ // we never get onKeyUp() for the long-press. So check for it here
+ // and change the selection to the long-press state.
+ if (mSelectionMode != SELECTION_LONGPRESS) {
+ invalidate()
+ }
+ val startMillis = selectedTimeInMillis
+ val flags: Int = (DateUtils.FORMAT_SHOW_TIME
+ or DateUtils.FORMAT_CAP_NOON_MIDNIGHT
+ or DateUtils.FORMAT_SHOW_WEEKDAY)
+ val title: String? = Utils.formatDateRange(mContext, startMillis, startMillis, flags)
+ menu.setHeaderTitle(title)
+ mPopup?.dismiss()
+ }
+
+ /**
+ * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
+ * If the touch position is not within the displayed grid, then this
+ * method returns false.
+ *
+ * @param x the x position of the touch
+ * @param y the y position of the touch
+ * @param keepOldSelection - do not change the selection info (used for invoking accessibility
+ * messages)
+ * @return true if the touch position is valid
+ */
+ private fun setSelectionFromPosition(x: Int, y: Int, keepOldSelection: Boolean): Boolean {
+ var x = x
+ var savedEvent: Event? = null
+ var savedDay = 0
+ var savedHour = 0
+ var savedAllDay = false
+ if (keepOldSelection) {
+ // Store selection info and restore it at the end. This way, we can invoke the
+ // right accessibility message without affecting the selection.
+ savedEvent = mSelectedEvent
+ savedDay = mSelectionDay
+ savedHour = mSelectionHour
+ savedAllDay = mSelectionAllday
+ }
+ if (x < mHoursWidth) {
+ x = mHoursWidth
+ }
+ var day = (x - mHoursWidth) / (mCellWidth + DAY_GAP)
+ if (day >= mNumDays) {
+ day = mNumDays - 1
+ }
+ day += mFirstJulianDay
+ setSelectedDay(day)
+ if (y < DAY_HEADER_HEIGHT) {
+ sendAccessibilityEventAsNeeded(false)
+ return false
+ }
+ setSelectedHour(mFirstHour) /* First fully visible hour */
+ mSelectionAllday = if (y < mFirstCell) {
+ true
+ } else {
+ // y is now offset from top of the scrollable region
+ val adjustedY = y - mFirstCell
+ if (adjustedY < mFirstHourOffset) {
+ setSelectedHour(mSelectionHour - 1) /* In the partially visible hour */
+ } else {
+ setSelectedHour(
+ mSelectionHour +
+ (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP)
+ )
+ }
+ false
+ }
+ findSelectedEvent(x, y)
+ sendAccessibilityEventAsNeeded(true)
+
+ // Restore old values
+ if (keepOldSelection) {
+ mSelectedEvent = savedEvent
+ mSelectionDay = savedDay
+ mSelectionHour = savedHour
+ mSelectionAllday = savedAllDay
+ }
+ return true
+ }
+
+ private fun findSelectedEvent(x: Int, y: Int) {
+ var y = y
+ val date = mSelectionDay
+ val cellWidth = mCellWidth
+ var events: ArrayList<Event>? = mEvents
+ var numEvents: Int = events!!.size
+ val left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay)
+ val top = 0
+ setSelectedEvent(null)
+ mSelectedEvents.clear()
+ if (mSelectionAllday) {
+ var yDistance: Float
+ var minYdistance = 10000.0f // any large number
+ var closestEvent: Event? = null
+ val drawHeight = mAlldayHeight.toFloat()
+ val yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN
+ var maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount
+ if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
+ // Leave a gap for the 'box +n' text
+ maxUnexpandedColumn--
+ }
+ events = mAllDayEvents
+ numEvents = events!!.size
+ for (i in 0 until numEvents) {
+ val event: Event? = events?.get(i)
+ if (!event!!.drawAsAllday() ||
+ !mShowAllAllDayEvents && event!!.getColumn() >= maxUnexpandedColumn
+ ) {
+ // Don't check non-allday events or events that aren't shown
+ continue
+ }
+ if (event!!.startDay <= mSelectionDay && event!!.endDay >= mSelectionDay) {
+ val numRectangles =
+ if (mShowAllAllDayEvents) mMaxAlldayEvents.toFloat()
+ else mMaxUnexpandedAlldayEventCount.toFloat()
+ var height = drawHeight / numRectangles
+ if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
+ height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT.toFloat()
+ }
+ val eventTop: Float = yOffset + height * event?.getColumn()
+ val eventBottom = eventTop + height
+ if (eventTop < y && eventBottom > y) {
+ // If the touch is inside the event rectangle, then
+ // add the event.
+ mSelectedEvents.add(event)
+ closestEvent = event
+ break
+ } else {
+ // Find the closest event
+ yDistance = if (eventTop >= y) {
+ eventTop - y
+ } else {
+ y - eventBottom
+ }
+ if (yDistance < minYdistance) {
+ minYdistance = yDistance
+ closestEvent = event
+ }
+ }
+ }
+ }
+ setSelectedEvent(closestEvent)
+ return
+ }
+
+ // Adjust y for the scrollable bitmap
+ y += mViewStartY - mFirstCell
+
+ // Use a region around (x,y) for the selection region
+ val region: Rect = mRect
+ region.left = x - 10
+ region.right = x + 10
+ region.top = y - 10
+ region.bottom = y + 10
+ val geometry: EventGeometry = mEventGeometry
+ for (i in 0 until numEvents) {
+ val event: Event? = events?.get(i)
+ // Compute the event rectangle.
+ if (!geometry.computeEventRect(date, left, top, cellWidth, event as Event)) {
+ continue
+ }
+
+ // If the event intersects the selection region, then add it to
+ // mSelectedEvents.
+ if (geometry.eventIntersectsSelection(event as Event, region)) {
+ mSelectedEvents.add(event as Event)
+ }
+ }
+
+ // If there are any events in the selected region, then assign the
+ // closest one to mSelectedEvent.
+ if (mSelectedEvents.size > 0) {
+ val len: Int = mSelectedEvents.size
+ var closestEvent: Event? = null
+ var minDist = (mViewWidth + mViewHeight).toFloat() // some large distance
+ for (index in 0 until len) {
+ val ev: Event? = mSelectedEvents?.get(index)
+ val dist: Float = geometry.pointToEvent(x.toFloat(), y.toFloat(), ev as Event)
+ if (dist < minDist) {
+ minDist = dist
+ closestEvent = ev
+ }
+ }
+ setSelectedEvent(closestEvent)
+
+ // Keep the selected hour and day consistent with the selected
+ // event. They could be different if we touched on an empty hour
+ // slot very close to an event in the previous hour slot. In
+ // that case we will select the nearby event.
+ val startDay: Int = mSelectedEvent!!.startDay
+ val endDay: Int = mSelectedEvent!!.endDay
+ if (mSelectionDay < startDay) {
+ setSelectedDay(startDay)
+ } else if (mSelectionDay > endDay) {
+ setSelectedDay(endDay)
+ }
+ val startHour: Int = mSelectedEvent!!.startTime / 60
+ val endHour: Int
+ endHour = if (mSelectedEvent!!.startTime < mSelectedEvent!!.endTime) {
+ (mSelectedEvent!!.endTime - 1) / 60
+ } else {
+ mSelectedEvent!!.endTime / 60
+ }
+ if (mSelectionHour < startHour && mSelectionDay == startDay) {
+ setSelectedHour(startHour)
+ } else if (mSelectionHour > endHour && mSelectionDay == endDay) {
+ setSelectedHour(endHour)
+ }
+ }
+ }
+
+ // Encapsulates the code to continue the scrolling after the
+ // finger is lifted. Instead of stopping the scroll immediately,
+ // the scroll continues to "free spin" and gradually slows down.
+ private inner class ContinueScroll : Runnable {
+ override fun run() {
+ mScrolling = mScrolling && mScroller.computeScrollOffset()
+ if (!mScrolling || mPaused) {
+ resetSelectedHour()
+ invalidate()
+ return
+ }
+ mViewStartY = mScroller.getCurrY()
+ if (mCallEdgeEffectOnAbsorb) {
+ if (mViewStartY < 0) {
+ mEdgeEffectTop.onAbsorb(mLastVelocity.toInt())
+ mCallEdgeEffectOnAbsorb = false
+ } else if (mViewStartY > mMaxViewStartY) {
+ mEdgeEffectBottom.onAbsorb(mLastVelocity.toInt())
+ mCallEdgeEffectOnAbsorb = false
+ }
+ mLastVelocity = mScroller.getCurrVelocity()
+ }
+ if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) {
+ // Allow overscroll/springback only on a fling,
+ // not a pull/fling from the end
+ if (mViewStartY < 0) {
+ mViewStartY = 0
+ } else if (mViewStartY > mMaxViewStartY) {
+ mViewStartY = mMaxViewStartY
+ }
+ }
+ computeFirstHour()
+ mHandler?.post(this)
+ invalidate()
+ }
+ }
+
+ /**
+ * Cleanup the pop-up and timers.
+ */
+ fun cleanup() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup?.dismiss()
+ }
+ mPaused = true
+ mLastPopupEventID = INVALID_EVENT_ID
+ if (mHandler != null) {
+ mHandler?.removeCallbacks(mDismissPopup)
+ mHandler?.removeCallbacks(mUpdateCurrentTime)
+ }
+ Utils.setSharedPreference(
+ mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT,
+ mCellHeight
+ )
+ // Clear all click animations
+ eventClickCleanup()
+ // Turn off redraw
+ mRemeasure = false
+ // Turn off scrolling to make sure the view is in the correct state if we fling back to it
+ mScrolling = false
+ }
+
+ private fun eventClickCleanup() {
+ this.removeCallbacks(mClearClick)
+ this.removeCallbacks(mSetClick)
+ mClickedEvent = null
+ mSavedClickedEvent = null
+ }
+
+ private fun setSelectedEvent(e: Event?) {
+ mSelectedEvent = e
+ mSelectedEventForAccessibility = e
+ }
+
+ private fun setSelectedHour(h: Int) {
+ mSelectionHour = h
+ mSelectionHourForAccessibility = h
+ }
+
+ private fun setSelectedDay(d: Int) {
+ mSelectionDay = d
+ mSelectionDayForAccessibility = d
+ }
+
+ /**
+ * Restart the update timer
+ */
+ fun restartCurrentTimeUpdates() {
+ mPaused = false
+ if (mHandler != null) {
+ mHandler?.removeCallbacks(mUpdateCurrentTime)
+ mHandler?.post(mUpdateCurrentTime)
+ }
+ }
+
+ @Override
+ protected override fun onDetachedFromWindow() {
+ cleanup()
+ super.onDetachedFromWindow()
+ }
+
+ internal inner class DismissPopup : Runnable {
+ override fun run() {
+ // Protect against null-pointer exceptions
+ if (mPopup != null) {
+ mPopup?.dismiss()
+ }
+ }
+ }
+
+ internal inner class UpdateCurrentTime : Runnable {
+ override fun run() {
+ val currentTime: Long = System.currentTimeMillis()
+ mCurrentTime?.set(currentTime)
+ // % causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.)
+ if (!mPaused) {
+ mHandler?.postDelayed(
+ mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY -
+ currentTime % UPDATE_CURRENT_TIME_DELAY
+ )
+ }
+ mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime!!.gmtoff)
+ invalidate()
+ }
+ }
+
+ internal inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() {
+ @Override
+ override fun onSingleTapUp(ev: MotionEvent): Boolean {
+ if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp")
+ doSingleTapUp(ev)
+ return true
+ }
+
+ @Override
+ override fun onLongPress(ev: MotionEvent) {
+ if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress")
+ doLongPress(ev)
+ }
+
+ @Override
+ override fun onScroll(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ var distanceY = distanceY
+ if (DEBUG) Log.e(TAG, "GestureDetector.onScroll")
+ eventClickCleanup()
+ if (mTouchStartedInAlldayArea) {
+ if (Math.abs(distanceX) < Math.abs(distanceY)) {
+ // Make sure that click feedback is gone when you scroll from the
+ // all day area
+ invalidate()
+ return false
+ }
+ // don't scroll vertically if this started in the allday area
+ distanceY = 0f
+ }
+ doScroll(e1, e2, distanceX, distanceY)
+ return true
+ }
+
+ @Override
+ override fun onFling(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ var velocityY = velocityY
+ if (DEBUG) Log.e(TAG, "GestureDetector.onFling")
+ if (mTouchStartedInAlldayArea) {
+ if (Math.abs(velocityX) < Math.abs(velocityY)) {
+ return false
+ }
+ // don't fling vertically if this started in the allday area
+ velocityY = 0f
+ }
+ doFling(e1, e2, velocityX, velocityY)
+ return true
+ }
+
+ @Override
+ override fun onDown(ev: MotionEvent): Boolean {
+ if (DEBUG) Log.e(TAG, "GestureDetector.onDown")
+ doDown(ev)
+ return true
+ }
+ }
+
+ @Override
+ override fun onLongClick(v: View?): Boolean {
+ return true
+ }
+
+ private inner class ScrollInterpolator : Interpolator {
+ override fun getInterpolation(t: Float): Float {
+ var t = t
+ t -= 1.0f
+ t = t * t * t * t * t + 1
+ if ((1 - t) * mAnimationDistance < 1) {
+ cancelAnimation()
+ }
+ return t
+ }
+ }
+
+ private fun calculateDuration(delta: Float, width: Float, velocity: Float): Long {
+ /*
+ * Here we compute a "distance" that will be used in the computation of
+ * the overall snap duration. This is a function of the actual distance
+ * that needs to be traveled; we keep this value close to half screen
+ * size in order to reduce the variance in snap duration as a function
+ * of the distance the page needs to travel.
+ */
+ var velocity = velocity
+ val halfScreenSize = width / 2
+ val distanceRatio = delta / width
+ val distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio)
+ val distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration
+ velocity = Math.abs(velocity)
+ velocity = Math.max(MINIMUM_SNAP_VELOCITY.toFloat(), velocity)
+
+ /*
+ * we want the page's snap velocity to approximately match the velocity
+ * at which the user flings, so we scale the duration by a value near to
+ * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to
+ * make it a little slower.
+ */
+ val duration: Long = 6L * Math.round(1000 * Math.abs(distance / velocity))
+ if (DEBUG) {
+ Log.e(
+ TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" +
+ distanceRatio + " distance:" + distance + " velocity:" + velocity +
+ " duration:" + duration + " distanceInfluenceForSnapDuration:" +
+ distanceInfluenceForSnapDuration
+ )
+ }
+ return duration
+ }
+
+ /*
+ * We want the duration of the page snap animation to be influenced by the
+ * distance that the screen has to travel, however, we don't want this
+ * duration to be effected in a purely linear fashion. Instead, we use this
+ * method to moderate the effect that the distance of travel has on the
+ * overall snap duration.
+ */
+ private fun distanceInfluenceForSnapDuration(f: Float): Float {
+ var f = f
+ f -= 0.5f // center the values about 0.
+ f *= (0.3f * Math.PI / 2.0f).toFloat()
+ return Math.sin(f.toDouble()).toFloat()
+ }
+
+ companion object {
+ private const val TAG = "DayView"
+ private const val DEBUG = false
+ private const val DEBUG_SCALING = false
+ private const val PERIOD_SPACE = ". "
+ private var mScale = 0f // Used for supporting different screen densities
+ private const val INVALID_EVENT_ID: Long = -1 // This is used for remembering a null event
+
+ // Duration of the allday expansion
+ private const val ANIMATION_DURATION: Long = 400
+
+ // duration of the more allday event text fade
+ private const val ANIMATION_SECONDARY_DURATION: Long = 200
+
+ // duration of the scroll to go to a specified time
+ private const val GOTO_SCROLL_DURATION = 200
+
+ // duration for events' cross-fade animation
+ private const val EVENTS_CROSS_FADE_DURATION = 400
+
+ // duration to show the event clicked
+ private const val CLICK_DISPLAY_DURATION = 50
+ private const val MENU_DAY = 3
+ private const val MENU_EVENT_VIEW = 5
+ private const val MENU_EVENT_CREATE = 6
+ private const val MENU_EVENT_EDIT = 7
+ private const val MENU_EVENT_DELETE = 8
+ private var DEFAULT_CELL_HEIGHT = 64
+ private var MAX_CELL_HEIGHT = 150
+ private var MIN_Y_SPAN = 100
+ private val CALENDARS_PROJECTION = arrayOf<String>(
+ Calendars._ID, // 0
+ Calendars.CALENDAR_ACCESS_LEVEL, // 1
+ Calendars.OWNER_ACCOUNT
+ )
+ private const val CALENDARS_INDEX_ACCESS_LEVEL = 1
+ private const val CALENDARS_INDEX_OWNER_ACCOUNT = 2
+ private val CALENDARS_WHERE: String = Calendars._ID.toString() + "=%d"
+ private const val FROM_NONE = 0
+ private const val FROM_ABOVE = 1
+ private const val FROM_BELOW = 2
+ private const val FROM_LEFT = 4
+ private const val FROM_RIGHT = 8
+ private const val ACCESS_LEVEL_NONE = 0
+ private const val ACCESS_LEVEL_DELETE = 1
+ private const val ACCESS_LEVEL_EDIT = 2
+ private var mHorizontalSnapBackThreshold = 128
+
+ // Update the current time line every five minutes if the window is left open that long
+ private const val UPDATE_CURRENT_TIME_DELAY = 300000
+ private var mOnDownDelay = 0
+ protected var mStringBuilder: StringBuilder = StringBuilder(50)
+
+ // TODO recreate formatter when locale changes
+ protected var mFormatter: Formatter = Formatter(mStringBuilder, Locale.getDefault())
+
+ // The number of milliseconds to show the popup window
+ private const val POPUP_DISMISS_DELAY = 3000
+ private var GRID_LINE_LEFT_MARGIN = 0f
+ private const val GRID_LINE_INNER_WIDTH = 1f
+ private const val DAY_GAP = 1
+ private const val HOUR_GAP = 1
+
+ // This is the standard height of an allday event with no restrictions
+ private var SINGLE_ALLDAY_HEIGHT = 34
+
+ /**
+ * This is the minimum desired height of a allday event.
+ * When unexpanded, allday events will use this height.
+ * When expanded allDay events will attempt to grow to fit all
+ * events at this height.
+ */
+ private var MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0f // in pixels
+
+ /**
+ * This is how big the unexpanded allday height is allowed to be.
+ * It will get adjusted based on screen size
+ */
+ private var MAX_UNEXPANDED_ALLDAY_HEIGHT = (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4).toInt()
+
+ /**
+ * This is the minimum size reserved for displaying regular events.
+ * The expanded allDay region can't expand into this.
+ */
+ private const val MIN_HOURS_HEIGHT = 180
+ private var ALLDAY_TOP_MARGIN = 1
+
+ // The largest a single allDay event will become.
+ private var MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34
+ private var HOURS_TOP_MARGIN = 2
+ private var HOURS_LEFT_MARGIN = 2
+ private var HOURS_RIGHT_MARGIN = 4
+ private var HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN
+ private var NEW_EVENT_MARGIN = 4
+ private var NEW_EVENT_WIDTH = 2
+ private var NEW_EVENT_MAX_LENGTH = 16
+ private var CURRENT_TIME_LINE_SIDE_BUFFER = 4
+ private var CURRENT_TIME_LINE_TOP_OFFSET = 2
+
+ /* package */
+ const val MINUTES_PER_HOUR = 60
+
+ /* package */
+ const val MINUTES_PER_DAY = MINUTES_PER_HOUR * 24
+
+ /* package */
+ const val MILLIS_PER_MINUTE = 60 * 1000
+
+ /* package */
+ const val MILLIS_PER_HOUR = 3600 * 1000
+
+ /* package */
+ const val MILLIS_PER_DAY = MILLIS_PER_HOUR * 24
+
+ // More events text will transition between invisible and this alpha
+ private const val MORE_EVENTS_MAX_ALPHA = 0x4C
+ private var DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0
+ private var DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5
+ private var DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6
+ private var DAY_HEADER_RIGHT_MARGIN = 4
+ private var DAY_HEADER_BOTTOM_MARGIN = 3
+ private var DAY_HEADER_FONT_SIZE = 14f
+ private var DATE_HEADER_FONT_SIZE = 32f
+ private var NORMAL_FONT_SIZE = 12f
+ private var EVENT_TEXT_FONT_SIZE = 12f
+ private var HOURS_TEXT_SIZE = 12f
+ private var AMPM_TEXT_SIZE = 9f
+ private var MIN_HOURS_WIDTH = 96
+ private var MIN_CELL_WIDTH_FOR_TEXT = 20
+ private const val MAX_EVENT_TEXT_LEN = 500
+
+ // smallest height to draw an event with
+ private var MIN_EVENT_HEIGHT = 24.0f // in pixels
+ private var CALENDAR_COLOR_SQUARE_SIZE = 10
+ private var EVENT_RECT_TOP_MARGIN = 1
+ private var EVENT_RECT_BOTTOM_MARGIN = 0
+ private var EVENT_RECT_LEFT_MARGIN = 1
+ private var EVENT_RECT_RIGHT_MARGIN = 0
+ private var EVENT_RECT_STROKE_WIDTH = 2
+ private var EVENT_TEXT_TOP_MARGIN = 2
+ private var EVENT_TEXT_BOTTOM_MARGIN = 2
+ private var EVENT_TEXT_LEFT_MARGIN = 6
+ private var EVENT_TEXT_RIGHT_MARGIN = 6
+ private var ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1
+ private var EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN
+ private var EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN
+ private var EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN
+ private var EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN
+
+ // margins and sizing for the expand allday icon
+ private var EXPAND_ALL_DAY_BOTTOM_MARGIN = 10
+
+ // sizing for "box +n" in allDay events
+ private var EVENT_SQUARE_WIDTH = 10
+ private var EVENT_LINE_PADDING = 4
+ private var NEW_EVENT_HINT_FONT_SIZE = 12
+ private var mEventTextColor = 0
+ private var mMoreEventsTextColor = 0
+ private var mWeek_saturdayColor = 0
+ private var mWeek_sundayColor = 0
+ private var mCalendarDateBannerTextColor = 0
+ private var mCalendarAmPmLabel = 0
+ private var mCalendarGridAreaSelected = 0
+ private var mCalendarGridLineInnerHorizontalColor = 0
+ private var mCalendarGridLineInnerVerticalColor = 0
+ private var mFutureBgColor = 0
+ private var mFutureBgColorRes = 0
+ private var mBgColor = 0
+ private var mNewEventHintColor = 0
+ private var mCalendarHourLabelColor = 0
+ private var mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA
+ private var mCellHeight = 0 // shared among all DayViews
+ private var mMinCellHeight = 32
+ private var mScaledPagingTouchSlop = 0
+
+ /**
+ * Whether to use the expand or collapse icon.
+ */
+ private var mUseExpandIcon = true
+
+ /**
+ * The height of the day names/numbers
+ */
+ private var DAY_HEADER_HEIGHT = 45
+
+ /**
+ * The height of the day names/numbers for multi-day views
+ */
+ private var MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT
+
+ /**
+ * The height of the day names/numbers when viewing a single day
+ */
+ private var ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT
+
+ /**
+ * Whether or not to expand the allDay area to fill the screen
+ */
+ private var mShowAllAllDayEvents = false
+ private var sCounter = 0
+
+ /**
+ * The initial state of the touch mode when we enter this view.
+ */
+ private const val TOUCH_MODE_INITIAL_STATE = 0
+
+ /**
+ * Indicates we just received the touch event and we are waiting to see if
+ * it is a tap or a scroll gesture.
+ */
+ private const val TOUCH_MODE_DOWN = 1
+
+ /**
+ * Indicates the touch gesture is a vertical scroll
+ */
+ private const val TOUCH_MODE_VSCROLL = 0x20
+
+ /**
+ * Indicates the touch gesture is a horizontal scroll
+ */
+ private const val TOUCH_MODE_HSCROLL = 0x40
+
+ /**
+ * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
+ */
+ private const val SELECTION_HIDDEN = 0
+ private const val SELECTION_PRESSED = 1 // D-pad down but not up yet
+ private const val SELECTION_SELECTED = 2
+ private const val SELECTION_LONGPRESS = 3
+
+ // The rest of this file was borrowed from Launcher2 - PagedView.java
+ private const val MINIMUM_SNAP_VELOCITY = 2200
+ }
+
+ init {
+ mContext = context
+ initAccessibilityVariables()
+ mResources = context!!.getResources()
+ mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint)
+ mNumDays = numDays
+ DATE_HEADER_FONT_SIZE =
+ mResources.getDimension(R.dimen.date_header_text_size).toInt().toFloat()
+ DAY_HEADER_FONT_SIZE =
+ mResources.getDimension(R.dimen.day_label_text_size).toInt().toFloat()
+ ONE_DAY_HEADER_HEIGHT = mResources.getDimension(R.dimen.one_day_header_height).toInt()
+ DAY_HEADER_BOTTOM_MARGIN = mResources.getDimension(R.dimen.day_header_bottom_margin).toInt()
+ EXPAND_ALL_DAY_BOTTOM_MARGIN =
+ mResources.getDimension(R.dimen.all_day_bottom_margin).toInt()
+ HOURS_TEXT_SIZE = mResources.getDimension(R.dimen.hours_text_size).toInt().toFloat()
+ AMPM_TEXT_SIZE = mResources.getDimension(R.dimen.ampm_text_size).toInt().toFloat()
+ MIN_HOURS_WIDTH = mResources.getDimension(R.dimen.min_hours_width).toInt()
+ HOURS_LEFT_MARGIN = mResources.getDimension(R.dimen.hours_left_margin).toInt()
+ HOURS_RIGHT_MARGIN = mResources.getDimension(R.dimen.hours_right_margin).toInt()
+ MULTI_DAY_HEADER_HEIGHT = mResources.getDimension(R.dimen.day_header_height).toInt()
+ val eventTextSizeId: Int
+ eventTextSizeId = if (mNumDays == 1) {
+ R.dimen.day_view_event_text_size
+ } else {
+ R.dimen.week_view_event_text_size
+ }
+ EVENT_TEXT_FONT_SIZE = mResources.getDimension(eventTextSizeId).toFloat()
+ NEW_EVENT_HINT_FONT_SIZE = mResources.getDimension(R.dimen.new_event_hint_text_size).toInt()
+ MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height)
+ MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT
+ EVENT_TEXT_TOP_MARGIN = mResources.getDimension(R.dimen.event_text_vertical_margin).toInt()
+ EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN
+ EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN
+ EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN
+ EVENT_TEXT_LEFT_MARGIN = mResources
+ .getDimension(R.dimen.event_text_horizontal_margin).toInt()
+ EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN
+ EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN
+ EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN
+ if (mScale == 0f) {
+ mScale = mResources.getDisplayMetrics().density
+ if (mScale != 1f) {
+ SINGLE_ALLDAY_HEIGHT *= mScale.toInt()
+ ALLDAY_TOP_MARGIN *= mScale.toInt()
+ MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale.toInt()
+ NORMAL_FONT_SIZE *= mScale
+ GRID_LINE_LEFT_MARGIN *= mScale
+ HOURS_TOP_MARGIN *= mScale.toInt()
+ MIN_CELL_WIDTH_FOR_TEXT *= mScale.toInt()
+ MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale.toInt()
+ mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
+ CURRENT_TIME_LINE_SIDE_BUFFER *= mScale.toInt()
+ CURRENT_TIME_LINE_TOP_OFFSET *= mScale.toInt()
+ MIN_Y_SPAN *= mScale.toInt()
+ MAX_CELL_HEIGHT *= mScale.toInt()
+ DEFAULT_CELL_HEIGHT *= mScale.toInt()
+ DAY_HEADER_HEIGHT *= mScale.toInt()
+ DAY_HEADER_RIGHT_MARGIN *= mScale.toInt()
+ DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale.toInt()
+ DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale.toInt()
+ DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale.toInt()
+ CALENDAR_COLOR_SQUARE_SIZE *= mScale.toInt()
+ EVENT_RECT_TOP_MARGIN *= mScale.toInt()
+ EVENT_RECT_BOTTOM_MARGIN *= mScale.toInt()
+ ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale.toInt()
+ EVENT_RECT_LEFT_MARGIN *= mScale.toInt()
+ EVENT_RECT_RIGHT_MARGIN *= mScale.toInt()
+ EVENT_RECT_STROKE_WIDTH *= mScale.toInt()
+ EVENT_SQUARE_WIDTH *= mScale.toInt()
+ EVENT_LINE_PADDING *= mScale.toInt()
+ NEW_EVENT_MARGIN *= mScale.toInt()
+ NEW_EVENT_WIDTH *= mScale.toInt()
+ NEW_EVENT_MAX_LENGTH *= mScale.toInt()
+ }
+ }
+ HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN
+ DAY_HEADER_HEIGHT = if (mNumDays == 1) ONE_DAY_HEADER_HEIGHT else MULTI_DAY_HEADER_HEIGHT
+ mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light)
+ mCurrentTimeAnimateLine = mResources
+ .getDrawable(R.drawable.timeline_indicator_activated_holo_light)
+ mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light)
+ mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light)
+ mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light)
+ mNewEventHintColor = mResources.getColor(R.color.new_event_hint_text_color)
+ mAcceptedOrTentativeEventBoxDrawable = mResources
+ .getDrawable(R.drawable.panel_month_event_holo_light)
+ mEventLoader = eventLoader as EventLoader
+ mEventGeometry = EventGeometry()
+ mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT)
+ mEventGeometry.setHourGap(HOUR_GAP.toFloat())
+ mEventGeometry.setCellMargin(DAY_GAP)
+ mLastPopupEventID = INVALID_EVENT_ID
+ mController = controller as CalendarController
+ mViewSwitcher = viewSwitcher as ViewSwitcher
+ mGestureDetector = GestureDetector(context, CalendarGestureListener())
+ mScaleGestureDetector = ScaleGestureDetector(getContext(), this)
+ if (mCellHeight == 0) {
+ mCellHeight = Utils.getSharedPreference(
+ mContext,
+ GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT
+ )
+ }
+ mScroller = OverScroller(context)
+ mHScrollInterpolator = ScrollInterpolator()
+ mEdgeEffectTop = EdgeEffect(context)
+ mEdgeEffectBottom = EdgeEffect(context)
+ val vc: ViewConfiguration = ViewConfiguration.get(context)
+ mScaledPagingTouchSlop = vc.getScaledPagingTouchSlop()
+ mOnDownDelay = ViewConfiguration.getTapTimeout()
+ OVERFLING_DISTANCE = vc.getScaledOverflingDistance()
+ init(context as Context)
+ }
+} \ No newline at end of file