diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2021-07-08 12:17:54 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-07-08 12:17:54 +0000 |
commit | 9b3c5ce1f330b40bf072cf78069082ded3813cb3 (patch) | |
tree | 0980db5a0c3c85b7672caeb738a0d07460147694 | |
parent | aa98535cdb1e318f35aa3247a6819bc53a577367 (diff) | |
parent | 805e4e0e89fea65ba11abbeedced440ef1333ad7 (diff) | |
download | Calendar-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.bp | 1 | ||||
-rw-r--r-- | src/com/android/calendar/DayFragment.kt | 6 | ||||
-rw-r--r-- | src/com/android/calendar/DayView.kt | 3990 |
3 files changed, 3994 insertions, 3 deletions
@@ -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 |