/* * 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 = ArrayList() private var mAllDayEvents: ArrayList? = ArrayList() private var mLayouts: Array? = null private var mAllDayLayouts: Array? = 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().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? = null private var mDayStrs: Array? = null private var mDayStrs2Letter: Array? = null private var mIs24HourFormat = false private val mSelectedEvents: ArrayList = ArrayList() 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(" 28", " 30") mDateStrWidth = computeMaxStringWidth(0, dateStrs, p) p.setTextSize(DAY_HEADER_FONT_SIZE) mDateStrWidth += computeMaxStringWidth(0, mDayStrs as Array, 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 = ArrayList() mEventLoader.loadEventsInBackground(mNumDays, events as ArrayList, mFirstJulianDay, object : Runnable { override fun run() { val fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay mEvents = events mLoadedFirstJulianDay = mFirstJulianDay if (mAllDayEvents == null) { mAllDayEvents = ArrayList() } 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(events.size) } else { Arrays.fill(mLayouts, null) } if (mAllDayLayouts == null || mAllDayLayouts!!.size < mAllDayEvents!!.size) { mAllDayLayouts = arrayOfNulls(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 = 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? 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, 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?, 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? = 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 = 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? = 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( 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) } }