blob: 58126f207c31792a251744154709f81a481f0a30 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.calendar
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.app.Service
import android.content.Context
import android.content.res.Resources
import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.Align
import android.graphics.Paint.Style
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Handler
import android.provider.CalendarContract.Attendees
import android.provider.CalendarContract.Calendars
import android.text.Layout.Alignment
import android.text.SpannableStringBuilder
import android.text.StaticLayout
import android.text.TextPaint
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.text.format.Time
import android.text.style.StyleSpan
import android.util.Log
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.Interpolator
import android.view.animation.TranslateAnimation
import android.widget.EdgeEffect
import android.widget.OverScroller
import android.widget.PopupWindow
import android.widget.ViewSwitcher
import com.android.calendar.CalendarController.EventType
import com.android.calendar.CalendarController.ViewType
import java.util.ArrayList
import java.util.Arrays
import java.util.Calendar
import java.util.Formatter
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
/**
* View for multi-day view. So far only 1 and 7 day have been tested.
*/
class DayView(
context: Context?,
controller: CalendarController?,
viewSwitcher: ViewSwitcher?,
eventLoader: EventLoader?,
numDays: Int
) : View(context), View.OnCreateContextMenuListener, ScaleGestureDetector.OnScaleGestureListener,
View.OnClickListener, View.OnLongClickListener {
private var mOnFlingCalled = false
private var mStartingScroll = false
protected var mPaused = true
private var mHandler: Handler? = null
/**
* ID of the last event which was displayed with the toast popup.
*
* This is used to prevent popping up multiple quick views for the same event, especially
* during calendar syncs. This becomes valid when an event is selected, either by default
* on starting calendar or by scrolling to an event. It becomes invalid when the user
* explicitly scrolls to an empty time slot, changes views, or deletes the event.
*/
private var mLastPopupEventID: Long
protected var mContext: Context? = null
private val mContinueScroll: ContinueScroll = ContinueScroll()
// Make this visible within the package for more informative debugging
var mBaseDate: Time? = null
private var mCurrentTime: Time? = null
private val mUpdateCurrentTime: UpdateCurrentTime = UpdateCurrentTime()
private var mTodayJulianDay = 0
private val mBold: Typeface = Typeface.DEFAULT_BOLD
private var mFirstJulianDay = 0
private var mLoadedFirstJulianDay = -1
private var mLastJulianDay = 0
private var mMonthLength = 0
private var mFirstVisibleDate = 0
private var mFirstVisibleDayOfWeek = 0
private var mEarliestStartHour: IntArray? = null // indexed by the week day offset
private var mHasAllDayEvent: BooleanArray? = null // indexed by the week day offset
private var mEventCountTemplate: String? = null
private var mClickedEvent: Event? = null // The event the user clicked on
private var mSavedClickedEvent: Event? = null
private var mClickedYLocation = 0
private var mDownTouchTime: Long = 0
private var mEventsAlpha = 255
private var mEventsCrossFadeAnimation: ObjectAnimator? = null
private val mTZUpdater: Runnable = object : Runnable {
@Override
override fun run() {
val tz: String? = Utils.getTimeZone(mContext, this)
mBaseDate!!.timezone = tz
mBaseDate?.normalize(true)
mCurrentTime?.switchTimezone(tz)
invalidate()
}
}
// Sets the "clicked" color from the clicked event
private val mSetClick: Runnable = object : Runnable {
@Override
override fun run() {
mClickedEvent = mSavedClickedEvent
mSavedClickedEvent = null
this@DayView.invalidate()
}
}
// Clears the "clicked" color from the clicked event and launch the event
private val mClearClick: Runnable = object : Runnable {
@Override
override fun run() {
if (mClickedEvent != null) {
mController?.sendEventRelatedEvent(
this as Object?, EventType.VIEW_EVENT, mClickedEvent!!.id,
mClickedEvent!!.startMillis, mClickedEvent!!.endMillis,
this@DayView.getWidth() / 2, mClickedYLocation,
selectedTimeInMillis
)
}
mClickedEvent = null
this@DayView.invalidate()
}
}
private val mTodayAnimatorListener: TodayAnimatorListener = TodayAnimatorListener()
internal inner class TodayAnimatorListener : AnimatorListenerAdapter() {
@Volatile
private var mAnimator: Animator? = null
@Volatile
private var mFadingIn = false
@Override
override fun onAnimationEnd(animation: Animator) {
synchronized(this) {
if (mAnimator !== animation) {
animation.removeAllListeners()
animation.cancel()
return
}
if (mFadingIn) {
if (mTodayAnimator != null) {
mTodayAnimator?.removeAllListeners()
mTodayAnimator?.cancel()
}
mTodayAnimator = ObjectAnimator
.ofInt(this@DayView, "animateTodayAlpha", 255, 0)
mAnimator = mTodayAnimator
mFadingIn = false
mTodayAnimator?.addListener(this)
mTodayAnimator?.setDuration(600)
mTodayAnimator?.start()
} else {
mAnimateToday = false
mAnimateTodayAlpha = 0
mAnimator?.removeAllListeners()
mAnimator = null
mTodayAnimator = null
invalidate()
}
}
}
fun setAnimator(animation: Animator?) {
mAnimator = animation
}
fun setFadingIn(fadingIn: Boolean) {
mFadingIn = fadingIn
}
}
var mAnimatorListener: AnimatorListenerAdapter = object : AnimatorListenerAdapter() {
@Override
override fun onAnimationStart(animation: Animator?) {
mScrolling = true
}
@Override
override fun onAnimationCancel(animation: Animator?) {
mScrolling = false
}
@Override
override fun onAnimationEnd(animation: Animator?) {
mScrolling = false
resetSelectedHour()
invalidate()
}
}
/**
* This variable helps to avoid unnecessarily reloading events by keeping
* track of the start millis parameter used for the most recent loading
* of events. If the next reload matches this, then the events are not
* reloaded. To force a reload, set this to zero (this is set to zero
* in the method clearCachedEvents()).
*/
private var mLastReloadMillis: Long = 0
private var mEvents: ArrayList<Event> = ArrayList<Event>()
private var mAllDayEvents: ArrayList<Event>? = ArrayList<Event>()
private var mLayouts: Array<StaticLayout?>? = null
private var mAllDayLayouts: Array<StaticLayout?>? = null
private var mSelectionDay = 0 // Julian day
private var mSelectionHour = 0
var mSelectionAllday = false
// Current selection info for accessibility
private var mSelectionDayForAccessibility = 0 // Julian day
private var mSelectionHourForAccessibility = 0
private var mSelectedEventForAccessibility: Event? = null
// Last selection info for accessibility
private var mLastSelectionDayForAccessibility = 0
private var mLastSelectionHourForAccessibility = 0
private var mLastSelectedEventForAccessibility: Event? = null
/** Width of a day or non-conflicting event */
private var mCellWidth = 0
// Pre-allocate these objects and re-use them
private val mRect: Rect = Rect()
private val mDestRect: Rect = Rect()
private val mSelectionRect: Rect = Rect()
// This encloses the more allDay events icon
private val mExpandAllDayRect: Rect = Rect()
// TODO Clean up paint usage
private val mPaint: Paint = Paint()
private val mEventTextPaint: Paint = Paint()
private val mSelectionPaint: Paint = Paint()
private var mLines: FloatArray = emptyArray<Float>().toFloatArray()
private var mFirstDayOfWeek = 0 // First day of the week
private var mPopup: PopupWindow? = null
private var mPopupView: View? = null
private val mDismissPopup: DismissPopup = DismissPopup()
private var mRemeasure = true
private val mEventLoader: EventLoader
protected val mEventGeometry: EventGeometry
private var mAnimationDistance = 0f
private var mViewStartX = 0
private var mViewStartY = 0
private var mMaxViewStartY = 0
private var mViewHeight = 0
private var mViewWidth = 0
private var mGridAreaHeight = -1
private var mScrollStartY = 0
private var mPreviousDirection = 0
/**
* Vertical distance or span between the two touch points at the start of a
* scaling gesture
*/
private var mStartingSpanY = 0f
/** Height of 1 hour in pixels at the start of a scaling gesture */
private var mCellHeightBeforeScaleGesture = 0
/** The hour at the center two touch points */
private var mGestureCenterHour = 0f
private var mRecalCenterHour = false
/**
* Flag to decide whether to handle the up event. Cases where up events
* should be ignored are 1) right after a scale gesture and 2) finger was
* down before app launch
*/
private var mHandleActionUp = true
private var mHoursTextHeight = 0
/**
* The height of the area used for allday events
*/
private var mAlldayHeight = 0
/**
* The height of the allday event area used during animation
*/
private var mAnimateDayHeight = 0
/**
* The height of an individual allday event during animation
*/
private var mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
/**
* Max of all day events in a given day in this view.
*/
private var mMaxAlldayEvents = 0
/**
* A count of the number of allday events that were not drawn for each day
*/
private var mSkippedAlldayEvents: IntArray? = null
/**
* The number of allDay events at which point we start hiding allDay events.
*/
private var mMaxUnexpandedAlldayEventCount = 4
protected var mNumDays = 7
private var mNumHours = 10
/** Width of the time line (list of hours) to the left. */
private var mHoursWidth = 0
private var mDateStrWidth = 0
/** Top of the scrollable region i.e. below date labels and all day events */
private var mFirstCell = 0
/** First fully visible hour */
private var mFirstHour = -1
/** Distance between the mFirstCell and the top of first fully visible hour. */
private var mFirstHourOffset = 0
private var mHourStrs: Array<String>? = null
private var mDayStrs: Array<String?>? = null
private var mDayStrs2Letter: Array<String?>? = null
private var mIs24HourFormat = false
private val mSelectedEvents: ArrayList<Event> = ArrayList<Event>()
private var mComputeSelectedEvents = false
private var mUpdateToast = false
private var mSelectedEvent: Event? = null
private var mPrevSelectedEvent: Event? = null
private val mPrevBox: Rect = Rect()
protected val mResources: Resources
protected val mCurrentTimeLine: Drawable
protected val mCurrentTimeAnimateLine: Drawable
protected val mTodayHeaderDrawable: Drawable
protected val mExpandAlldayDrawable: Drawable
protected val mCollapseAlldayDrawable: Drawable
protected var mAcceptedOrTentativeEventBoxDrawable: Drawable
private var mAmString: String? = null
private var mPmString: String? = null
var mScaleGestureDetector: ScaleGestureDetector
private var mTouchMode = TOUCH_MODE_INITIAL_STATE
private var mSelectionMode = SELECTION_HIDDEN
private var mScrolling = false
// Pixels scrolled
private var mInitialScrollX = 0f
private var mInitialScrollY = 0f
private var mAnimateToday = false
private var mAnimateTodayAlpha = 0
// Animates the height of the allday region
var mAlldayAnimator: ObjectAnimator? = null
// Animates the height of events in the allday region
var mAlldayEventAnimator: ObjectAnimator? = null
// Animates the transparency of the more events text
var mMoreAlldayEventsAnimator: ObjectAnimator? = null
// Animates the current time marker when Today is pressed
var mTodayAnimator: ObjectAnimator? = null
// whether or not an event is stopping because it was cancelled
private var mCancellingAnimations = false
// tracks whether a touch originated in the allday area
private var mTouchStartedInAlldayArea = false
private val mController: CalendarController
private val mViewSwitcher: ViewSwitcher
private val mGestureDetector: GestureDetector
private val mScroller: OverScroller
private val mEdgeEffectTop: EdgeEffect
private val mEdgeEffectBottom: EdgeEffect
private var mCallEdgeEffectOnAbsorb = false
private val OVERFLING_DISTANCE: Int
private var mLastVelocity = 0f
private val mHScrollInterpolator: ScrollInterpolator
private var mAccessibilityMgr: AccessibilityManager? = null
private var mIsAccessibilityEnabled = false
private var mTouchExplorationEnabled = false
private val mNewEventHintString: String
@Override
protected override fun onAttachedToWindow() {
if (mHandler == null) {
mHandler = getHandler()
mHandler?.post(mUpdateCurrentTime)
}
}
private fun init(context: Context) {
setFocusable(true)
// Allow focus in touch mode so that we can do keyboard shortcuts
// even after we've entered touch mode.
setFocusableInTouchMode(true)
setClickable(true)
setOnCreateContextMenuListener(this)
mFirstDayOfWeek = Utils.getFirstDayOfWeek(context)
mCurrentTime = Time(Utils.getTimeZone(context, mTZUpdater))
val currentTime: Long = System.currentTimeMillis()
mCurrentTime?.set(currentTime)
mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime!!.gmtoff)
mWeek_saturdayColor = mResources.getColor(R.color.week_saturday)
mWeek_sundayColor = mResources.getColor(R.color.week_sunday)
mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color)
mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color)
mBgColor = mResources.getColor(R.color.calendar_hour_background)
mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label)
mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected)
mCalendarGridLineInnerHorizontalColor = mResources
.getColor(R.color.calendar_grid_line_inner_horizontal_color)
mCalendarGridLineInnerVerticalColor = mResources
.getColor(R.color.calendar_grid_line_inner_vertical_color)
mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label)
mEventTextColor = mResources.getColor(R.color.calendar_event_text_color)
mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color)
mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE)
mEventTextPaint.setTextAlign(Paint.Align.LEFT)
mEventTextPaint.setAntiAlias(true)
val gridLineColor: Int = mResources.getColor(R.color.calendar_grid_line_highlight_color)
var p: Paint = mSelectionPaint
p.setColor(gridLineColor)
p.setStyle(Style.FILL)
p.setAntiAlias(false)
p = mPaint
p.setAntiAlias(true)
// Allocate space for 2 weeks worth of weekday names so that we can
// easily start the week display at any week day.
mDayStrs = arrayOfNulls(14)
// Also create an array of 2-letter abbreviations.
mDayStrs2Letter = arrayOfNulls(14)
for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
val index: Int = i - Calendar.SUNDAY
// e.g. Tue for Tuesday
mDayStrs!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM)
.toUpperCase()
mDayStrs!![index + 7] = mDayStrs!![index]
// e.g. Tu for Tuesday
mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT)
.toUpperCase()
// If we don't have 2-letter day strings, fall back to 1-letter.
if (mDayStrs2Letter!![index]!!.equals(mDayStrs!![index])) {
mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i,
DateUtils.LENGTH_SHORTEST)
}
mDayStrs2Letter!![index + 7] = mDayStrs2Letter!![index]
}
// Figure out how much space we need for the 3-letter abbrev names
// in the worst case.
p.setTextSize(DATE_HEADER_FONT_SIZE)
p.setTypeface(mBold)
val dateStrs = arrayOf<String?>(" 28", " 30")
mDateStrWidth = computeMaxStringWidth(0, dateStrs, p)
p.setTextSize(DAY_HEADER_FONT_SIZE)
mDateStrWidth += computeMaxStringWidth(0, mDayStrs as Array<String?>, p)
p.setTextSize(HOURS_TEXT_SIZE)
p.setTypeface(null)
handleOnResume()
mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase()
mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase()
val ampm = arrayOf(mAmString, mPmString)
p.setTextSize(AMPM_TEXT_SIZE)
mHoursWidth = Math.max(
HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) +
HOURS_RIGHT_MARGIN
)
mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth)
val inflater: LayoutInflater
inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
mPopupView = inflater.inflate(R.layout.bubble_event, null)
mPopupView?.setLayoutParams(
LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
mPopup = PopupWindow(context)
mPopup?.setContentView(mPopupView)
val dialogTheme: Resources.Theme = getResources().newTheme()
dialogTheme.applyStyle(android.R.style.Theme_Dialog, true)
val ta: TypedArray = dialogTheme.obtainStyledAttributes(
intArrayOf(
android.R.attr.windowBackground
)
)
mPopup?.setBackgroundDrawable(ta.getDrawable(0))
ta.recycle()
// Enable touching the popup window
mPopupView?.setOnClickListener(this)
// Catch long clicks for creating a new event
setOnLongClickListener(this)
mBaseDate = Time(Utils.getTimeZone(context, mTZUpdater))
val millis: Long = System.currentTimeMillis()
mBaseDate?.set(millis)
mEarliestStartHour = IntArray(mNumDays)
mHasAllDayEvent = BooleanArray(mNumDays)
// mLines is the array of points used with Canvas.drawLines() in
// drawGridBackground() and drawAllDayEvents(). Its size depends
// on the max number of lines that can ever be drawn by any single
// drawLines() call in either of those methods.
val maxGridLines = (24 + 1 + // max horizontal lines we might draw
(mNumDays + 1)) // max vertical lines we might draw
mLines = FloatArray(maxGridLines * 4)
}
/**
* This is called when the popup window is pressed.
*/
override fun onClick(v: View) {
if (v === mPopupView) {
// Pretend it was a trackball click because that will always
// jump to the "View event" screen.
switchViews(true /* trackball */)
}
}
fun handleOnResume() {
initAccessibilityVariables()
if (Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) {
mFutureBgColor = 0
} else {
mFutureBgColor = mFutureBgColorRes
}
mIs24HourFormat = DateFormat.is24HourFormat(mContext)
mHourStrs = if (mIs24HourFormat) CalendarData.s24Hours else CalendarData.s12HoursNoAmPm
mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext)
mLastSelectionDayForAccessibility = 0
mLastSelectionHourForAccessibility = 0
mLastSelectedEventForAccessibility = null
mSelectionMode = SELECTION_HIDDEN
}
private fun initAccessibilityVariables() {
mAccessibilityMgr = mContext
?.getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager
mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr!!.isEnabled()
mTouchExplorationEnabled = isTouchExplorationEnabled
} /* ignore isDst */ // We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
/**
* Returns the start of the selected time in milliseconds since the epoch.
*
* @return selected time in UTC milliseconds since the epoch.
*/
val selectedTimeInMillis: Long
get() {
val time = Time(mBaseDate)
time.setJulianDay(mSelectionDay)
time.hour = mSelectionHour
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
return time.normalize(true /* ignore isDst */)
} /* ignore isDst */
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
val selectedTime: Time
get() {
val time = Time(mBaseDate)
time.setJulianDay(mSelectionDay)
time.hour = mSelectionHour
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
time.normalize(true /* ignore isDst */)
return time
} /* ignore isDst */
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
val selectedTimeForAccessibility: Time
get() {
val time = Time(mBaseDate)
time.setJulianDay(mSelectionDayForAccessibility)
time.hour = mSelectionHourForAccessibility
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
time.normalize(true /* ignore isDst */)
return time
}
/**
* Returns the start of the selected time in minutes since midnight,
* local time. The derived class must ensure that this is consistent
* with the return value from getSelectedTimeInMillis().
*/
val selectedMinutesSinceMidnight: Int
get() = mSelectionHour * MINUTES_PER_HOUR
var firstVisibleHour: Int
get() = mFirstHour
set(firstHour) {
mFirstHour = firstHour
mFirstHourOffset = 0
}
fun setSelected(time: Time?, ignoreTime: Boolean, animateToday: Boolean) {
mBaseDate?.set(time)
setSelectedHour(mBaseDate!!.hour)
setSelectedEvent(null)
mPrevSelectedEvent = null
val millis: Long = mBaseDate!!.toMillis(false /* use isDst */)
setSelectedDay(Time.getJulianDay(millis, mBaseDate!!.gmtoff))
mSelectedEvents.clear()
mComputeSelectedEvents = true
var gotoY: Int = Integer.MIN_VALUE
if (!ignoreTime && mGridAreaHeight != -1) {
var lastHour = 0
if (mBaseDate!!.hour < mFirstHour) {
// Above visible region
gotoY = mBaseDate!!.hour * (mCellHeight + HOUR_GAP)
} else {
lastHour = ((mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) +
mFirstHour)
if (mBaseDate!!.hour >= lastHour) {
// Below visible region
// target hour + 1 (to give it room to see the event) -
// grid height (to get the y of the top of the visible
// region)
gotoY = ((mBaseDate!!.hour + 1 + mBaseDate!!.minute / 60.0f) *
(mCellHeight + HOUR_GAP) - mGridAreaHeight).toInt()
}
}
if (DEBUG) {
Log.e(
TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " +
(mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight +
" ymax " + mMaxViewStartY
)
}
if (gotoY > mMaxViewStartY) {
gotoY = mMaxViewStartY
} else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) {
gotoY = 0
}
}
recalc()
mRemeasure = true
invalidate()
var delayAnimateToday = false
if (gotoY != Integer.MIN_VALUE) {
val scrollAnim: ValueAnimator =
ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY)
scrollAnim.setDuration(GOTO_SCROLL_DURATION.toLong())
scrollAnim.setInterpolator(AccelerateDecelerateInterpolator())
scrollAnim.addListener(mAnimatorListener)
scrollAnim.start()
delayAnimateToday = true
}
if (animateToday) {
synchronized(mTodayAnimatorListener) {
if (mTodayAnimator != null) {
mTodayAnimator?.removeAllListeners()
mTodayAnimator?.cancel()
}
mTodayAnimator = ObjectAnimator.ofInt(
this, "animateTodayAlpha",
mAnimateTodayAlpha, 255
)
mAnimateToday = true
mTodayAnimatorListener.setFadingIn(true)
mTodayAnimatorListener.setAnimator(mTodayAnimator)
mTodayAnimator?.addListener(mTodayAnimatorListener)
mTodayAnimator?.setDuration(150)
if (delayAnimateToday) {
mTodayAnimator?.setStartDelay(GOTO_SCROLL_DURATION.toLong())
}
mTodayAnimator?.start()
}
}
sendAccessibilityEventAsNeeded(false)
}
// Called from animation framework via reflection. Do not remove
fun setViewStartY(viewStartY: Int) {
var viewStartY = viewStartY
if (viewStartY > mMaxViewStartY) {
viewStartY = mMaxViewStartY
}
mViewStartY = viewStartY
computeFirstHour()
invalidate()
}
fun setAnimateTodayAlpha(todayAlpha: Int) {
mAnimateTodayAlpha = todayAlpha
invalidate()
} /* ignore isDst */
fun getSelectedDay(): Time {
val time = Time(mBaseDate)
time.setJulianDay(mSelectionDay)
time.hour = mSelectionHour
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
time.normalize(true /* ignore isDst */)
return time
}
fun updateTitle() {
val start = Time(mBaseDate)
start.normalize(true)
val end = Time(start)
end.monthDay += mNumDays - 1
// Move it forward one minute so the formatter doesn't lose a day
end.minute += 1
end.normalize(true)
var formatFlags: Long = DateUtils.FORMAT_SHOW_DATE.toLong() or
DateUtils.FORMAT_SHOW_YEAR.toLong()
if (mNumDays != 1) {
// Don't show day of the month if for multi-day view
formatFlags = formatFlags or DateUtils.FORMAT_NO_MONTH_DAY.toLong()
// Abbreviate the month if showing multiple months
if (start.month !== end.month) {
formatFlags = formatFlags or DateUtils.FORMAT_ABBREV_MONTH.toLong()
}
}
mController.sendEvent(
this as Object?, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT,
formatFlags, null, null
)
}
/**
* return a negative number if "time" is comes before the visible time
* range, a positive number if "time" is after the visible time range, and 0
* if it is in the visible time range.
*/
fun compareToVisibleTimeRange(time: Time): Int {
val savedHour: Int = mBaseDate!!.hour
val savedMinute: Int = mBaseDate!!.minute
val savedSec: Int = mBaseDate!!.second
mBaseDate!!.hour = 0
mBaseDate!!.minute = 0
mBaseDate!!.second = 0
if (DEBUG) {
Log.d(TAG, "Begin " + mBaseDate.toString())
Log.d(TAG, "Diff " + time.toString())
}
// Compare beginning of range
var diff: Int = Time.compare(time, mBaseDate)
if (diff > 0) {
// Compare end of range
mBaseDate!!.monthDay += mNumDays
mBaseDate?.normalize(true)
diff = Time.compare(time, mBaseDate)
if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString())
mBaseDate!!.monthDay -= mNumDays
mBaseDate?.normalize(true)
if (diff < 0) {
// in visible time
diff = 0
} else if (diff == 0) {
// Midnight of following day
diff = 1
}
}
if (DEBUG) Log.d(TAG, "Diff: $diff")
mBaseDate!!.hour = savedHour
mBaseDate!!.minute = savedMinute
mBaseDate!!.second = savedSec
return diff
}
private fun recalc() {
// Set the base date to the beginning of the week if we are displaying
// 7 days at a time.
if (mNumDays == 7) {
adjustToBeginningOfWeek(mBaseDate)
}
val start: Long = mBaseDate!!.toMillis(false /* use isDst */)
mFirstJulianDay = Time.getJulianDay(start, mBaseDate!!.gmtoff)
mLastJulianDay = mFirstJulianDay + mNumDays - 1
mMonthLength = mBaseDate!!.getActualMaximum(Time.MONTH_DAY)
mFirstVisibleDate = mBaseDate!!.monthDay
mFirstVisibleDayOfWeek = mBaseDate!!.weekDay
}
private fun adjustToBeginningOfWeek(time: Time?) {
val dayOfWeek: Int = time!!.weekDay
var diff = dayOfWeek - mFirstDayOfWeek
if (diff != 0) {
if (diff < 0) {
diff += 7
}
time!!.monthDay -= diff
time?.normalize(true /* ignore isDst */)
}
}
@Override
protected override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) {
mViewWidth = width
mViewHeight = height
mEdgeEffectTop.setSize(mViewWidth, mViewHeight)
mEdgeEffectBottom.setSize(mViewWidth, mViewHeight)
val gridAreaWidth = width - mHoursWidth
mCellWidth = (gridAreaWidth - mNumDays * DAY_GAP) / mNumDays
// This would be about 1 day worth in a 7 day view
mHorizontalSnapBackThreshold = width / 7
val p = Paint()
p.setTextSize(HOURS_TEXT_SIZE)
mHoursTextHeight = Math.abs(p.ascent()).toInt()
remeasure(width, height)
}
/**
* Measures the space needed for various parts of the view after
* loading new events. This can change if there are all-day events.
*/
private fun remeasure(width: Int, height: Int) {
// Shrink to fit available space but make sure we can display at least two events
MAX_UNEXPANDED_ALLDAY_HEIGHT = (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4).toInt()
MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6)
MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(
MAX_UNEXPANDED_ALLDAY_HEIGHT,
MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() * 2
)
mMaxUnexpandedAlldayEventCount =
(MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
// First, clear the array of earliest start times, and the array
// indicating presence of an all-day event.
for (day in 0 until mNumDays) {
mEarliestStartHour!![day] = 25 // some big number
mHasAllDayEvent!![day] = false
}
val maxAllDayEvents = mMaxAlldayEvents
// The min is where 24 hours cover the entire visible area
mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, MIN_EVENT_HEIGHT.toInt())
if (mCellHeight < mMinCellHeight) {
mCellHeight = mMinCellHeight
}
// Calculate mAllDayHeight
mFirstCell = DAY_HEADER_HEIGHT
var allDayHeight = 0
if (maxAllDayEvents > 0) {
val maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT
// If there is at most one all-day event per day, then use less
// space (but more than the space for a single event).
if (maxAllDayEvents == 1) {
allDayHeight = SINGLE_ALLDAY_HEIGHT
} else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount) {
// Allow the all-day area to grow in height depending on the
// number of all-day events we need to show, up to a limit.
allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT
if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT
}
} else {
// if we have more than the magic number, check if we're animating
// and if not adjust the sizes appropriately
if (mAnimateDayHeight != 0) {
// Don't shrink the space past the final allDay space. The animation
// continues to hide the last event so the more events text can
// fade in.
allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT)
} else {
// Try to fit all the events in
allDayHeight = (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
// But clip the area depending on which mode we're in
if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) {
allDayHeight = (mMaxUnexpandedAlldayEventCount *
MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt()
} else if (allDayHeight > maxAllAllDayHeight) {
allDayHeight = maxAllAllDayHeight
}
}
}
mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN
} else {
mSelectionAllday = false
}
mAlldayHeight = allDayHeight
mGridAreaHeight = height - mFirstCell
// Set up the expand icon position
val allDayIconWidth: Int = mExpandAlldayDrawable.getIntrinsicWidth()
mExpandAllDayRect.left = Math.max(
(mHoursWidth - allDayIconWidth) / 2,
EVENT_ALL_DAY_TEXT_LEFT_MARGIN
)
mExpandAllDayRect.right = Math.min(
mExpandAllDayRect.left + allDayIconWidth, mHoursWidth -
EVENT_ALL_DAY_TEXT_RIGHT_MARGIN
)
mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN
mExpandAllDayRect.top = (mExpandAllDayRect.bottom -
mExpandAlldayDrawable.getIntrinsicHeight())
mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP)
mEventGeometry.setHourHeight(mCellHeight.toFloat())
val minimumDurationMillis =
(MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)).toLong()
Event.computePositions(mEvents, minimumDurationMillis)
// Compute the top of our reachable view
mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight
if (DEBUG) {
Log.e(TAG, "mViewStartY: $mViewStartY")
Log.e(TAG, "mMaxViewStartY: $mMaxViewStartY")
}
if (mViewStartY > mMaxViewStartY) {
mViewStartY = mMaxViewStartY
computeFirstHour()
}
if (mFirstHour == -1) {
initFirstHour()
mFirstHourOffset = 0
}
// When we change the base date, the number of all-day events may
// change and that changes the cell height. When we switch dates,
// we use the mFirstHourOffset from the previous view, but that may
// be too large for the new view if the cell height is smaller.
if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
mFirstHourOffset = mCellHeight + HOUR_GAP - 1
}
mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset
val eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP)
// When we get new events we don't want to dismiss the popup unless the event changes
if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent!!.id) {
mPopup?.dismiss()
}
mPopup?.setWidth(eventAreaWidth - 20)
mPopup?.setHeight(WindowManager.LayoutParams.WRAP_CONTENT)
}
/**
* Initialize the state for another view. The given view is one that has
* its own bitmap and will use an animation to replace the current view.
* The current view and new view are either both Week views or both Day
* views. They differ in their base date.
*
* @param view the view to initialize.
*/
private fun initView(view: DayView) {
view.setSelectedHour(mSelectionHour)
view.mSelectedEvents.clear()
view.mComputeSelectedEvents = true
view.mFirstHour = mFirstHour
view.mFirstHourOffset = mFirstHourOffset
view.remeasure(getWidth(), getHeight())
view.initAllDayHeights()
view.setSelectedEvent(null)
view.mPrevSelectedEvent = null
view.mFirstDayOfWeek = mFirstDayOfWeek
if (view.mEvents.size > 0) {
view.mSelectionAllday = mSelectionAllday
} else {
view.mSelectionAllday = false
}
// Redraw the screen so that the selection box will be redrawn. We may
// have scrolled to a different part of the day in some other view
// so the selection box in this view may no longer be visible.
view.recalc()
}
/**
* Switch to another view based on what was selected (an event or a free
* slot) and how it was selected (by touch or by trackball).
*
* @param trackBallSelection true if the selection was made using the
* trackball.
*/
private fun switchViews(trackBallSelection: Boolean) {
val selectedEvent: Event? = mSelectedEvent
mPopup?.dismiss()
mLastPopupEventID = INVALID_EVENT_ID
if (mNumDays > 1) {
// This is the Week view.
// With touch, we always switch to Day/Agenda View
// With track ball, if we selected a free slot, then create an event.
// If we selected a specific event, switch to EventInfo view.
if (trackBallSelection) {
if (selectedEvent != null) {
if (mIsAccessibilityEnabled) {
mAccessibilityMgr?.interrupt()
}
}
}
}
}
@Override
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
mScrolling = false
return super.onKeyUp(keyCode, event)
}
@Override
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return super.onKeyDown(keyCode, event)
}
@Override
override fun onHoverEvent(event: MotionEvent?): Boolean {
return true
}
private val isTouchExplorationEnabled: Boolean
private get() = mIsAccessibilityEnabled && mAccessibilityMgr!!.isTouchExplorationEnabled()
private fun sendAccessibilityEventAsNeeded(speakEvents: Boolean) {
if (!mIsAccessibilityEnabled) {
return
}
val dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility
val hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility
if (dayChanged || hourChanged || mLastSelectedEventForAccessibility !==
mSelectedEventForAccessibility) {
mLastSelectionDayForAccessibility = mSelectionDayForAccessibility
mLastSelectionHourForAccessibility = mSelectionHourForAccessibility
mLastSelectedEventForAccessibility = mSelectedEventForAccessibility
val b = StringBuilder()
// Announce only the changes i.e. day or hour or both
if (dayChanged) {
b.append(selectedTimeForAccessibility.format("%A "))
}
if (hourChanged) {
b.append(selectedTimeForAccessibility.format(if (mIs24HourFormat) "%k" else "%l%p"))
}
if (dayChanged || hourChanged) {
b.append(PERIOD_SPACE)
}
if (speakEvents) {
if (mEventCountTemplate == null) {
mEventCountTemplate = mContext?.getString(R.string.template_announce_item_index)
}
// Read out the relevant event(s)
val numEvents: Int = mSelectedEvents.size
if (numEvents > 0) {
if (mSelectedEventForAccessibility == null) {
// Read out all the events
var i = 1
for (calEvent in mSelectedEvents) {
if (numEvents > 1) {
// Read out x of numEvents if there are more than one event
mStringBuilder.setLength(0)
b.append(mFormatter.format(mEventCountTemplate, i++, numEvents))
b.append(" ")
}
appendEventAccessibilityString(b, calEvent)
}
} else {
if (numEvents > 1) {
// Read out x of numEvents if there are more than one event
mStringBuilder.setLength(0)
b.append(
mFormatter.format(
mEventCountTemplate, mSelectedEvents
.indexOf(mSelectedEventForAccessibility) + 1, numEvents
)
)
b.append(" ")
}
appendEventAccessibilityString(b, mSelectedEventForAccessibility)
}
}
}
if (dayChanged || hourChanged || speakEvents) {
val event: AccessibilityEvent = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED)
val msg: CharSequence = b.toString()
event.getText().add(msg)
event.setAddedCount(msg.length)
sendAccessibilityEventUnchecked(event)
}
}
}
/**
* @param b
* @param calEvent
*/
private fun appendEventAccessibilityString(b: StringBuilder, calEvent: Event?) {
b.append(calEvent!!.titleAndLocation)
b.append(PERIOD_SPACE)
val `when`: String?
var flags: Int = DateUtils.FORMAT_SHOW_DATE
if (calEvent!!.allDay) {
flags = flags or (DateUtils.FORMAT_UTC or DateUtils.FORMAT_SHOW_WEEKDAY)
} else {
flags = flags or DateUtils.FORMAT_SHOW_TIME
if (DateFormat.is24HourFormat(mContext)) {
flags = flags or DateUtils.FORMAT_24HOUR
}
}
`when` = Utils.formatDateRange(mContext, calEvent!!.startMillis, calEvent!!.endMillis,
flags)
b.append(`when`)
b.append(PERIOD_SPACE)
}
private inner class GotoBroadcaster(start: Time, end: Time) : Animation.AnimationListener {
private val mCounter: Int
private val mStart: Time
private val mEnd: Time
@Override
override fun onAnimationEnd(animation: Animation?) {
var view = mViewSwitcher.getCurrentView() as DayView
view.mViewStartX = 0
view = mViewSwitcher.getNextView() as DayView
view.mViewStartX = 0
if (mCounter == sCounter) {
mController.sendEvent(
this as Object?, EventType.GO_TO, mStart, mEnd, null, -1,
ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null
)
}
}
@Override
override fun onAnimationRepeat(animation: Animation?) {
}
@Override
override fun onAnimationStart(animation: Animation?) {
}
init {
mCounter = ++sCounter
mStart = start
mEnd = end
}
}
private fun switchViews(forward: Boolean, xOffSet: Float, width: Float, velocity: Float): View {
mAnimationDistance = width - xOffSet
if (DEBUG) {
Log.d(TAG, "switchViews($forward) O:$xOffSet Dist:$mAnimationDistance")
}
var progress: Float = Math.abs(xOffSet) / width
if (progress > 1.0f) {
progress = 1.0f
}
val inFromXValue: Float
val inToXValue: Float
val outFromXValue: Float
val outToXValue: Float
if (forward) {
inFromXValue = 1.0f - progress
inToXValue = 0.0f
outFromXValue = -progress
outToXValue = -1.0f
} else {
inFromXValue = progress - 1.0f
inToXValue = 0.0f
outFromXValue = progress
outToXValue = 1.0f
}
val start = Time(mBaseDate!!.timezone)
start.set(mController.time as Long)
if (forward) {
start.monthDay += mNumDays
} else {
start.monthDay -= mNumDays
}
mController.time = start.normalize(true)
var newSelected: Time? = start
if (mNumDays == 7) {
newSelected = Time(start)
adjustToBeginningOfWeek(start)
}
val end = Time(start)
end.monthDay += mNumDays - 1
// We have to allocate these animation objects each time we switch views
// because that is the only way to set the animation parameters.
val inAnimation = TranslateAnimation(
Animation.RELATIVE_TO_SELF, inFromXValue,
Animation.RELATIVE_TO_SELF, inToXValue,
Animation.ABSOLUTE, 0.0f,
Animation.ABSOLUTE, 0.0f
)
val outAnimation = TranslateAnimation(
Animation.RELATIVE_TO_SELF, outFromXValue,
Animation.RELATIVE_TO_SELF, outToXValue,
Animation.ABSOLUTE, 0.0f,
Animation.ABSOLUTE, 0.0f
)
val duration = calculateDuration(width - Math.abs(xOffSet), width, velocity)
inAnimation.setDuration(duration)
inAnimation.setInterpolator(mHScrollInterpolator)
outAnimation.setInterpolator(mHScrollInterpolator)
outAnimation.setDuration(duration)
outAnimation.setAnimationListener(GotoBroadcaster(start, end))
mViewSwitcher.setInAnimation(inAnimation)
mViewSwitcher.setOutAnimation(outAnimation)
var view = mViewSwitcher.getCurrentView() as DayView
view.cleanup()
mViewSwitcher.showNext()
view = mViewSwitcher.getCurrentView() as DayView
view.setSelected(newSelected, true, false)
view.requestFocus()
view.reloadEvents()
view.updateTitle()
view.restartCurrentTimeUpdates()
return view
}
// This is called after scrolling stops to move the selected hour
// to the visible part of the screen.
private fun resetSelectedHour() {
if (mSelectionHour < mFirstHour + 1) {
setSelectedHour(mFirstHour + 1)
setSelectedEvent(null)
mSelectedEvents.clear()
mComputeSelectedEvents = true
} else if (mSelectionHour > mFirstHour + mNumHours - 3) {
setSelectedHour(mFirstHour + mNumHours - 3)
setSelectedEvent(null)
mSelectedEvents.clear()
mComputeSelectedEvents = true
}
}
private fun initFirstHour() {
mFirstHour = mSelectionHour - mNumHours / 5
if (mFirstHour < 0) {
mFirstHour = 0
} else if (mFirstHour + mNumHours > 24) {
mFirstHour = 24 - mNumHours
}
}
/**
* Recomputes the first full hour that is visible on screen after the
* screen is scrolled.
*/
private fun computeFirstHour() {
// Compute the first full hour that is visible on screen
mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP)
mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY
}
private fun adjustHourSelection() {
if (mSelectionHour < 0) {
setSelectedHour(0)
if (mMaxAlldayEvents > 0) {
mPrevSelectedEvent = null
mSelectionAllday = true
}
}
if (mSelectionHour > 23) {
setSelectedHour(23)
}
// If the selected hour is at least 2 time slots from the top and
// bottom of the screen, then don't scroll the view.
if (mSelectionHour < mFirstHour + 1) {
// If there are all-days events for the selected day but there
// are no more normal events earlier in the day, then jump to
// the all-day event area.
// Exception 1: allow the user to scroll to 8am with the trackball
// before jumping to the all-day event area.
// Exception 2: if 12am is on screen, then allow the user to select
// 12am before going up to the all-day event area.
val daynum = mSelectionDay - mFirstJulianDay
if (daynum < mEarliestStartHour!!.size && daynum >= 0 && mMaxAlldayEvents > 0 &&
mEarliestStartHour!![daynum] > mSelectionHour &&
mFirstHour > 0 && mFirstHour < 8) {
mPrevSelectedEvent = null
mSelectionAllday = true
setSelectedHour(mFirstHour + 1)
return
}
if (mFirstHour > 0) {
mFirstHour -= 1
mViewStartY -= mCellHeight + HOUR_GAP
if (mViewStartY < 0) {
mViewStartY = 0
}
return
}
}
if (mSelectionHour > mFirstHour + mNumHours - 3) {
if (mFirstHour < 24 - mNumHours) {
mFirstHour += 1
mViewStartY += mCellHeight + HOUR_GAP
if (mViewStartY > mMaxViewStartY) {
mViewStartY = mMaxViewStartY
}
return
} else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
mViewStartY = mMaxViewStartY
}
}
}
fun clearCachedEvents() {
mLastReloadMillis = 0
}
private val mCancelCallback: Runnable = object : Runnable {
override fun run() {
clearCachedEvents()
}
}
/* package */
fun reloadEvents() {
// Protect against this being called before this view has been
// initialized.
// if (mContext == null) {
// return;
// }
// Make sure our time zones are up to date
mTZUpdater.run()
setSelectedEvent(null)
mPrevSelectedEvent = null
mSelectedEvents.clear()
// The start date is the beginning of the week at 12am
val weekStart = Time(Utils.getTimeZone(mContext, mTZUpdater))
weekStart.set(mBaseDate)
weekStart.hour = 0
weekStart.minute = 0
weekStart.second = 0
val millis: Long = weekStart.normalize(true /* ignore isDst */)
// Avoid reloading events unnecessarily.
if (millis == mLastReloadMillis) {
return
}
mLastReloadMillis = millis
// load events in the background
// mContext.startProgressSpinner();
val events: ArrayList<Event> = ArrayList<Event>()
mEventLoader.loadEventsInBackground(mNumDays, events as ArrayList<Event?>, mFirstJulianDay,
object : Runnable {
override fun run() {
val fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay
mEvents = events
mLoadedFirstJulianDay = mFirstJulianDay
if (mAllDayEvents == null) {
mAllDayEvents = ArrayList<Event>()
} else {
mAllDayEvents?.clear()
}
// Create a shorter array for all day events
for (e in events) {
if (e.drawAsAllday()) {
mAllDayEvents?.add(e)
}
}
// New events, new layouts
if (mLayouts == null || mLayouts!!.size < events.size) {
mLayouts = arrayOfNulls<StaticLayout>(events.size)
} else {
Arrays.fill(mLayouts, null)
}
if (mAllDayLayouts == null || mAllDayLayouts!!.size < mAllDayEvents!!.size) {
mAllDayLayouts = arrayOfNulls<StaticLayout>(events.size)
} else {
Arrays.fill(mAllDayLayouts, null)
}
computeEventRelations()
mRemeasure = true
mComputeSelectedEvents = true
recalc()
// Start animation to cross fade the events
if (fadeinEvents) {
if (mEventsCrossFadeAnimation == null) {
mEventsCrossFadeAnimation =
ObjectAnimator.ofInt(this@DayView, "EventsAlpha", 0, 255)
mEventsCrossFadeAnimation?.setDuration(EVENTS_CROSS_FADE_DURATION.toLong())
}
mEventsCrossFadeAnimation?.start()
} else {
invalidate()
}
}
}, mCancelCallback)
}
var eventsAlpha: Int
get() = mEventsAlpha
set(alpha) {
mEventsAlpha = alpha
invalidate()
}
fun stopEventsAnimation() {
if (mEventsCrossFadeAnimation != null) {
mEventsCrossFadeAnimation?.cancel()
}
mEventsAlpha = 255
}
private fun computeEventRelations() {
// Compute the layout relation between each event before measuring cell
// width, as the cell width should be adjusted along with the relation.
//
// Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm)
// We should mark them as "overwapped". Though they are not overwapped logically, but
// minimum cell height implicitly expands the cell height of A and it should look like
// (1:00pm - 1:15pm) after the cell height adjustment.
// Compute the space needed for the all-day events, if any.
// Make a pass over all the events, and keep track of the maximum
// number of all-day events in any one day. Also, keep track of
// the earliest event in each day.
var maxAllDayEvents = 0
val events: ArrayList<Event> = mEvents
val len: Int = events.size
// Num of all-day-events on each day.
val eventsCount = IntArray(mLastJulianDay - mFirstJulianDay + 1)
Arrays.fill(eventsCount, 0)
for (ii in 0 until len) {
val event: Event = events.get(ii)
if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) {
continue
}
if (event.drawAsAllday()) {
// Count all the events being drawn as allDay events
val firstDay: Int = Math.max(event.startDay, mFirstJulianDay)
val lastDay: Int = Math.min(event.endDay, mLastJulianDay)
for (day in firstDay..lastDay) {
val count = ++eventsCount[day - mFirstJulianDay]
if (maxAllDayEvents < count) {
maxAllDayEvents = count
}
}
var daynum: Int = event.startDay - mFirstJulianDay
var durationDays: Int = event.endDay - event.startDay + 1
if (daynum < 0) {
durationDays += daynum
daynum = 0
}
if (daynum + durationDays > mNumDays) {
durationDays = mNumDays - daynum
}
var day = daynum
while (durationDays > 0) {
mHasAllDayEvent!![day] = true
day++
durationDays--
}
} else {
var daynum: Int = event.startDay - mFirstJulianDay
var hour: Int = event.startTime / 60
if (daynum >= 0 && hour < mEarliestStartHour!![daynum]) {
mEarliestStartHour!![daynum] = hour
}
// Also check the end hour in case the event spans more than
// one day.
daynum = event.endDay - mFirstJulianDay
hour = event.endTime / 60
if (daynum < mNumDays && hour < mEarliestStartHour!![daynum]) {
mEarliestStartHour!![daynum] = hour
}
}
}
mMaxAlldayEvents = maxAllDayEvents
initAllDayHeights()
}
@Override
protected override fun onDraw(canvas: Canvas) {
if (mRemeasure) {
remeasure(getWidth(), getHeight())
mRemeasure = false
}
canvas.save()
val yTranslate = (-mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight).toFloat()
// offset canvas by the current drag and header position
canvas.translate(-mViewStartX.toFloat(), yTranslate)
// clip to everything below the allDay area
val dest: Rect = mDestRect
dest.top = (mFirstCell - yTranslate).toInt()
dest.bottom = (mViewHeight - yTranslate).toInt()
dest.left = 0
dest.right = mViewWidth
canvas.save()
canvas.clipRect(dest)
// Draw the movable part of the view
doDraw(canvas)
// restore to having no clip
canvas.restore()
if (mTouchMode and TOUCH_MODE_HSCROLL != 0) {
val xTranslate: Float
xTranslate = if (mViewStartX > 0) {
mViewWidth.toFloat()
} else {
-mViewWidth.toFloat()
}
// Move the canvas around to prep it for the next view
// specifically, shift it by a screen and undo the
// yTranslation which will be redone in the nextView's onDraw().
canvas.translate(xTranslate, -yTranslate)
val nextView = mViewSwitcher.getNextView() as DayView
// Prevent infinite recursive calls to onDraw().
nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE
nextView.onDraw(canvas)
// Move it back for this view
canvas.translate(-xTranslate, 0f)
} else {
// If we drew another view we already translated it back
// If we didn't draw another view we should be at the edge of the
// screen
canvas.translate(mViewStartX.toFloat(), -yTranslate)
}
// Draw the fixed areas (that don't scroll) directly to the canvas.
drawAfterScroll(canvas)
if (mComputeSelectedEvents && mUpdateToast) {
mUpdateToast = false
}
mComputeSelectedEvents = false
// Draw overscroll glow
if (!mEdgeEffectTop.isFinished()) {
if (DAY_HEADER_HEIGHT != 0) {
canvas.translate(0f, DAY_HEADER_HEIGHT.toFloat())
}
if (mEdgeEffectTop.draw(canvas)) {
invalidate()
}
if (DAY_HEADER_HEIGHT != 0) {
canvas.translate(0f, -DAY_HEADER_HEIGHT.toFloat())
}
}
if (!mEdgeEffectBottom.isFinished()) {
canvas.rotate(180f, mViewWidth.toFloat() / 2f, mViewHeight.toFloat() / 2f)
if (mEdgeEffectBottom.draw(canvas)) {
invalidate()
}
}
canvas.restore()
}
private fun drawAfterScroll(canvas: Canvas) {
val p: Paint = mPaint
val r: Rect = mRect
drawAllDayHighlights(r, canvas, p)
if (mMaxAlldayEvents != 0) {
drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p)
drawUpperLeftCorner(r, canvas, p)
}
drawScrollLine(r, canvas, p)
drawDayHeaderLoop(r, canvas, p)
// Draw the AM and PM indicators if we're in 12 hour mode
if (!mIs24HourFormat) {
drawAmPm(canvas, p)
}
}
// This isn't really the upper-left corner. It's the square area just
// below the upper-left corner, above the hours and to the left of the
// all-day area.
private fun drawUpperLeftCorner(r: Rect, canvas: Canvas, p: Paint) {
setupHourTextPaint(p)
if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
// Draw the allDay expand/collapse icon
if (mUseExpandIcon) {
mExpandAlldayDrawable.setBounds(mExpandAllDayRect)
mExpandAlldayDrawable.draw(canvas)
} else {
mCollapseAlldayDrawable.setBounds(mExpandAllDayRect)
mCollapseAlldayDrawable.draw(canvas)
}
}
}
private fun drawScrollLine(r: Rect, canvas: Canvas, p: Paint) {
val right = computeDayLeftPosition(mNumDays)
val y = mFirstCell - 1
p.setAntiAlias(false)
p.setStyle(Style.FILL)
p.setColor(mCalendarGridLineInnerHorizontalColor)
p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
canvas.drawLine(GRID_LINE_LEFT_MARGIN, y.toFloat(), right.toFloat(), y.toFloat(), p)
p.setAntiAlias(true)
}
// Computes the x position for the left side of the given day (base 0)
private fun computeDayLeftPosition(day: Int): Int {
val effectiveWidth = mViewWidth - mHoursWidth
return day * effectiveWidth / mNumDays + mHoursWidth
}
private fun drawAllDayHighlights(r: Rect, canvas: Canvas, p: Paint) {
if (mFutureBgColor != 0) {
// First, color the labels area light gray
r.top = 0
r.bottom = DAY_HEADER_HEIGHT
r.left = 0
r.right = mViewWidth
p.setColor(mBgColor)
p.setStyle(Style.FILL)
canvas.drawRect(r, p)
// and the area that says All day
r.top = DAY_HEADER_HEIGHT
r.bottom = mFirstCell - 1
r.left = 0
r.right = mHoursWidth
canvas.drawRect(r, p)
var startIndex = -1
val todayIndex = mTodayJulianDay - mFirstJulianDay
if (todayIndex < 0) {
// Future
startIndex = 0
} else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) {
// Multiday - tomorrow is visible.
startIndex = todayIndex + 1
}
if (startIndex >= 0) {
// Draw the future highlight
r.top = 0
r.bottom = mFirstCell - 1
r.left = computeDayLeftPosition(startIndex) + 1
r.right = computeDayLeftPosition(mNumDays)
p.setColor(mFutureBgColor)
p.setStyle(Style.FILL)
canvas.drawRect(r, p)
}
}
}
private fun drawDayHeaderLoop(r: Rect, canvas: Canvas, p: Paint) {
// Draw the horizontal day background banner
// p.setColor(mCalendarDateBannerBackground);
// r.top = 0;
// r.bottom = DAY_HEADER_HEIGHT;
// r.left = 0;
// r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
// canvas.drawRect(r, p);
//
// Fill the extra space on the right side with the default background
// r.left = r.right;
// r.right = mViewWidth;
// p.setColor(mCalendarGridAreaBackground);
// canvas.drawRect(r, p);
if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) {
return
}
p.setTypeface(mBold)
p.setTextAlign(Paint.Align.RIGHT)
var cell = mFirstJulianDay
val dayNames: Array<String?>?
dayNames = if (mDateStrWidth < mCellWidth) {
mDayStrs
} else {
mDayStrs2Letter
}
p.setAntiAlias(true)
var day = 0
while (day < mNumDays) {
var dayOfWeek = day + mFirstVisibleDayOfWeek
if (dayOfWeek >= 14) {
dayOfWeek -= 14
}
var color = mCalendarDateBannerTextColor
if (mNumDays == 1) {
if (dayOfWeek == Time.SATURDAY) {
color = mWeek_saturdayColor
} else if (dayOfWeek == Time.SUNDAY) {
color = mWeek_sundayColor
}
} else {
val column = day % 7
if (Utils.isSaturday(column, mFirstDayOfWeek)) {
color = mWeek_saturdayColor
} else if (Utils.isSunday(column, mFirstDayOfWeek)) {
color = mWeek_sundayColor
}
}
p.setColor(color)
drawDayHeader(dayNames!![dayOfWeek], day, cell, canvas, p)
day++
cell++
}
p.setTypeface(null)
}
private fun drawAmPm(canvas: Canvas, p: Paint) {
p.setColor(mCalendarAmPmLabel)
p.setTextSize(AMPM_TEXT_SIZE)
p.setTypeface(mBold)
p.setAntiAlias(true)
p.setTextAlign(Paint.Align.RIGHT)
var text = mAmString
if (mFirstHour >= 12) {
text = mPmString
}
var y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP
canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
// Also draw the "PM"
text = mPmString
y =
mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) +
2 * mHoursTextHeight + HOUR_GAP
canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
}
}
private fun drawCurrentTimeLine(
r: Rect,
day: Int,
top: Int,
canvas: Canvas,
p: Paint
) {
r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1
r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1
r.top = top - CURRENT_TIME_LINE_TOP_OFFSET
r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight()
mCurrentTimeLine.setBounds(r)
mCurrentTimeLine.draw(canvas)
if (mAnimateToday) {
mCurrentTimeAnimateLine.setBounds(r)
mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha)
mCurrentTimeAnimateLine.draw(canvas)
}
}
private fun doDraw(canvas: Canvas) {
val p: Paint = mPaint
val r: Rect = mRect
if (mFutureBgColor != 0) {
drawBgColors(r, canvas, p)
}
drawGridBackground(r, canvas, p)
drawHours(r, canvas, p)
// Draw each day
var cell = mFirstJulianDay
p.setAntiAlias(false)
val alpha: Int = p.getAlpha()
p.setAlpha(mEventsAlpha)
var day = 0
while (day < mNumDays) {
// TODO Wow, this needs cleanup. drawEvents loop through all the
// events on every call.
drawEvents(cell, day, HOUR_GAP, canvas, p)
// If this is today
if (cell == mTodayJulianDay) {
val lineY: Int =
mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
mCellHeight / 60 + 1
// And the current time shows up somewhere on the screen
if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) {
drawCurrentTimeLine(r, day, lineY, canvas, p)
}
}
day++
cell++
}
p.setAntiAlias(true)
p.setAlpha(alpha)
}
private fun drawHours(r: Rect, canvas: Canvas, p: Paint) {
setupHourTextPaint(p)
var y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN
for (i in 0..23) {
val time = mHourStrs!![i]
canvas.drawText(time, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p)
y += mCellHeight + HOUR_GAP
}
}
private fun setupHourTextPaint(p: Paint) {
p.setColor(mCalendarHourLabelColor)
p.setTextSize(HOURS_TEXT_SIZE)
p.setTypeface(Typeface.DEFAULT)
p.setTextAlign(Paint.Align.RIGHT)
p.setAntiAlias(true)
}
private fun drawDayHeader(dayStr: String?, day: Int, cell: Int, canvas: Canvas, p: Paint) {
var dateNum = mFirstVisibleDate + day
var x: Int
if (dateNum > mMonthLength) {
dateNum -= mMonthLength
}
p.setAntiAlias(true)
val todayIndex = mTodayJulianDay - mFirstJulianDay
// Draw day of the month
val dateNumStr: String = dateNum.toString()
if (mNumDays > 1) {
val y = (DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN).toFloat()
// Draw day of the month
x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN
p.setTextAlign(Align.RIGHT)
p.setTextSize(DATE_HEADER_FONT_SIZE)
p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT)
canvas.drawText(dateNumStr as String, x.toFloat(), y, p)
// Draw day of the week
x -= (p.measureText(" $dateNumStr")).toInt()
p.setTextSize(DAY_HEADER_FONT_SIZE)
p.setTypeface(Typeface.DEFAULT)
canvas.drawText(dayStr as String, x.toFloat(), y, p)
} else {
val y = (ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN).toFloat()
p.setTextAlign(Align.LEFT)
// Draw day of the week
x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN
p.setTextSize(DAY_HEADER_FONT_SIZE)
p.setTypeface(Typeface.DEFAULT)
canvas.drawText(dayStr as String, x.toFloat(), y, p)
// Draw day of the month
x += (p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN).toInt()
p.setTextSize(DATE_HEADER_FONT_SIZE)
p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT)
canvas.drawText(dateNumStr, x.toFloat(), y, p)
}
}
private fun drawGridBackground(r: Rect, canvas: Canvas, p: Paint) {
val savedStyle: Style = p.getStyle()
val stopX = computeDayLeftPosition(mNumDays).toFloat()
var y = 0f
val deltaY = (mCellHeight + HOUR_GAP).toFloat()
var linesIndex = 0
val startY = 0f
val stopY = (HOUR_GAP + 24 * (mCellHeight + HOUR_GAP)).toFloat()
var x = mHoursWidth.toFloat()
// Draw the inner horizontal grid lines
p.setColor(mCalendarGridLineInnerHorizontalColor)
p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
p.setAntiAlias(false)
y = 0f
linesIndex = 0
for (hour in 0..24) {
mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN
mLines[linesIndex++] = y
mLines[linesIndex++] = stopX
mLines[linesIndex++] = y
y += deltaY
}
if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) {
canvas.drawLines(mLines, 0, linesIndex, p)
linesIndex = 0
p.setColor(mCalendarGridLineInnerVerticalColor)
}
// Draw the inner vertical grid lines
for (day in 0..mNumDays) {
x = computeDayLeftPosition(day).toFloat()
mLines[linesIndex++] = x
mLines[linesIndex++] = startY
mLines[linesIndex++] = x
mLines[linesIndex++] = stopY
}
canvas.drawLines(mLines, 0, linesIndex, p)
// Restore the saved style.
p.setStyle(savedStyle)
p.setAntiAlias(true)
}
/**
* @param r
* @param canvas
* @param p
*/
private fun drawBgColors(r: Rect, canvas: Canvas, p: Paint) {
val todayIndex = mTodayJulianDay - mFirstJulianDay
// Draw the hours background color
r.top = mDestRect.top
r.bottom = mDestRect.bottom
r.left = 0
r.right = mHoursWidth
p.setColor(mBgColor)
p.setStyle(Style.FILL)
p.setAntiAlias(false)
canvas.drawRect(r, p)
// Draw background for grid area
if (mNumDays == 1 && todayIndex == 0) {
// Draw a white background for the time later than current time
var lineY: Int =
mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
mCellHeight / 60 + 1
if (lineY < mViewStartY + mViewHeight) {
lineY = Math.max(lineY, mViewStartY)
r.left = mHoursWidth
r.right = mViewWidth
r.top = lineY
r.bottom = mViewStartY + mViewHeight
p.setColor(mFutureBgColor)
canvas.drawRect(r, p)
}
} else if (todayIndex >= 0 && todayIndex < mNumDays) {
// Draw today with a white background for the time later than current time
var lineY: Int =
mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute *
mCellHeight / 60 + 1
if (lineY < mViewStartY + mViewHeight) {
lineY = Math.max(lineY, mViewStartY)
r.left = computeDayLeftPosition(todayIndex) + 1
r.right = computeDayLeftPosition(todayIndex + 1)
r.top = lineY
r.bottom = mViewStartY + mViewHeight
p.setColor(mFutureBgColor)
canvas.drawRect(r, p)
}
// Paint Tomorrow and later days with future color
if (todayIndex + 1 < mNumDays) {
r.left = computeDayLeftPosition(todayIndex + 1) + 1
r.right = computeDayLeftPosition(mNumDays)
r.top = mDestRect.top
r.bottom = mDestRect.bottom
p.setColor(mFutureBgColor)
canvas.drawRect(r, p)
}
} else if (todayIndex < 0) {
// Future
r.left = computeDayLeftPosition(0) + 1
r.right = computeDayLeftPosition(mNumDays)
r.top = mDestRect.top
r.bottom = mDestRect.bottom
p.setColor(mFutureBgColor)
canvas.drawRect(r, p)
}
p.setAntiAlias(true)
}
private fun computeMaxStringWidth(currentMax: Int, strings: Array<String?>, p: Paint): Int {
var maxWidthF = 0.0f
val len = strings.size
for (i in 0 until len) {
val width: Float = p.measureText(strings[i])
maxWidthF = Math.max(width, maxWidthF)
}
var maxWidth = (maxWidthF + 0.5).toInt()
if (maxWidth < currentMax) {
maxWidth = currentMax
}
return maxWidth
}
private fun saveSelectionPosition(left: Float, top: Float, right: Float, bottom: Float) {
mPrevBox.left = left.toInt()
mPrevBox.right = right.toInt()
mPrevBox.top = top.toInt()
mPrevBox.bottom = bottom.toInt()
}
private fun setupTextRect(r: Rect) {
if (r.bottom <= r.top || r.right <= r.left) {
r.bottom = r.top
r.right = r.left
return
}
if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) {
r.top += EVENT_TEXT_TOP_MARGIN
r.bottom -= EVENT_TEXT_BOTTOM_MARGIN
}
if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) {
r.left += EVENT_TEXT_LEFT_MARGIN
r.right -= EVENT_TEXT_RIGHT_MARGIN
}
}
private fun setupAllDayTextRect(r: Rect) {
if (r.bottom <= r.top || r.right <= r.left) {
r.bottom = r.top
r.right = r.left
return
}
if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) {
r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN
r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN
}
if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) {
r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN
r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN
}
}
/**
* Return the layout for a numbered event. Create it if not already existing
*/
private fun getEventLayout(
layouts: Array<StaticLayout?>?,
i: Int,
event: Event,
paint: Paint,
r: Rect
): StaticLayout? {
if (i < 0 || i >= layouts!!.size) {
return null
}
var layout: StaticLayout? = layouts!![i]
// Check if we have already initialized the StaticLayout and that
// the width hasn't changed (due to vertical resizing which causes
// re-layout of events at min height)
if (layout == null || r.width() !== layout.getWidth()) {
val bob = SpannableStringBuilder()
if (event.title != null) {
// MAX - 1 since we add a space
bob.append(drawTextSanitizer(event.title.toString(),
MAX_EVENT_TEXT_LEN - 1))
bob.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length, 0)
bob.append(' ')
}
if (event.location != null) {
bob.append(
drawTextSanitizer(
event.location.toString(),
MAX_EVENT_TEXT_LEN - bob.length
)
)
}
when (event.selfAttendeeStatus) {
Attendees.ATTENDEE_STATUS_INVITED -> paint.setColor(event.color)
Attendees.ATTENDEE_STATUS_DECLINED -> {
paint.setColor(mEventTextColor)
paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA)
}
Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
Attendees.ATTENDEE_STATUS_TENTATIVE -> paint.setColor(
mEventTextColor
)
else -> paint.setColor(mEventTextColor)
}
// Leave a one pixel boundary on the left and right of the rectangle for the event
layout = StaticLayout(
bob, 0, bob.length, TextPaint(paint), r.width(),
Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()
)
layouts[i] = layout
}
layout.getPaint().setAlpha(mEventsAlpha)
return layout
}
private fun drawAllDayEvents(firstDay: Int, numDays: Int, canvas: Canvas, p: Paint) {
p.setTextSize(NORMAL_FONT_SIZE)
p.setTextAlign(Paint.Align.LEFT)
val eventTextPaint: Paint = mEventTextPaint
val startY = DAY_HEADER_HEIGHT.toFloat()
val stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN
var x = 0f
var linesIndex = 0
// Draw the inner vertical grid lines
p.setColor(mCalendarGridLineInnerVerticalColor)
x = mHoursWidth.toFloat()
p.setStrokeWidth(GRID_LINE_INNER_WIDTH)
// Line bounding the top of the all day area
mLines!![linesIndex++] = GRID_LINE_LEFT_MARGIN
mLines!![linesIndex++] = startY
mLines!![linesIndex++] = computeDayLeftPosition(mNumDays).toFloat()
mLines!![linesIndex++] = startY
for (day in 0..mNumDays) {
x = computeDayLeftPosition(day).toFloat()
mLines!![linesIndex++] = x
mLines!![linesIndex++] = startY
mLines!![linesIndex++] = x
mLines!![linesIndex++] = stopY
}
p.setAntiAlias(false)
canvas.drawLines(mLines, 0, linesIndex, p)
p.setStyle(Style.FILL)
val y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN
val lastDay = firstDay + numDays - 1
val events: ArrayList<Event>? = mAllDayEvents
val numEvents: Int = events!!.size
// Whether or not we should draw the more events text
var hasMoreEvents = false
// size of the allDay area
val drawHeight = mAlldayHeight.toFloat()
// max number of events being drawn in one day of the allday area
var numRectangles = mMaxAlldayEvents.toFloat()
// Where to cut off drawn allday events
var allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN
// The number of events that weren't drawn in each day
mSkippedAlldayEvents = IntArray(numDays)
if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount &&
!mShowAllAllDayEvents && mAnimateDayHeight == 0) {
// We draw one fewer event than will fit so that more events text
// can be drawn
numRectangles = (mMaxUnexpandedAlldayEventCount - 1).toFloat()
// We also clip the events above the more events text
allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt()
hasMoreEvents = true
} else if (mAnimateDayHeight != 0) {
// clip at the end of the animating space
allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN
}
var alpha: Int = eventTextPaint.getAlpha()
eventTextPaint.setAlpha(mEventsAlpha)
for (i in 0 until numEvents) {
val event: Event = events!!.get(i)
var startDay: Int = event.startDay
var endDay: Int = event.endDay
if (startDay > lastDay || endDay < firstDay) {
continue
}
if (startDay < firstDay) {
startDay = firstDay
}
if (endDay > lastDay) {
endDay = lastDay
}
val startIndex = startDay - firstDay
val endIndex = endDay - firstDay
var height =
if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount)
mAnimateDayEventHeight.toFloat() else drawHeight / numRectangles
// Prevent a single event from getting too big
if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) {
height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT.toFloat()
}
// Leave a one-pixel space between the vertical day lines and the
// event rectangle.
event.left = computeDayLeftPosition(startIndex).toFloat()
event.right = computeDayLeftPosition(endIndex + 1).toFloat() - DAY_GAP
event.top = y + height * event.getColumn()
event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN
if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) {
// check if we should skip this event. We skip if it starts
// after the clip bound or ends after the skip bound and we're
// not animating.
if (event.top >= allDayEventClip) {
incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex)
continue
} else if (event.bottom > allDayEventClip) {
if (hasMoreEvents) {
incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex)
continue
}
event.bottom = allDayEventClip.toFloat()
}
}
val r: Rect = drawEventRect(
event, canvas, p, eventTextPaint, event.top.toInt(),
event.bottom.toInt()
)
setupAllDayTextRect(r)
val layout: StaticLayout? = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r)
drawEventText(layout, r, canvas, r.top, r.bottom, true)
// Check if this all-day event intersects the selected day
if (mSelectionAllday && mComputeSelectedEvents) {
if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
mSelectedEvents.add(event)
}
}
}
eventTextPaint.setAlpha(alpha)
if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) {
// If the more allday text should be visible, draw it.
alpha = p.getAlpha()
p.setAlpha(mEventsAlpha)
p.setColor(mMoreAlldayEventsTextAlpha shl 24 and mMoreEventsTextColor)
for (i in mSkippedAlldayEvents!!.indices) {
if (mSkippedAlldayEvents!![i] > 0) {
drawMoreAlldayEvents(canvas, mSkippedAlldayEvents!![i], i, p)
}
}
p.setAlpha(alpha)
}
if (mSelectionAllday) {
// Compute the neighbors for the list of all-day events that
// intersect the selected day.
computeAllDayNeighbors()
// Set the selection position to zero so that when we move down
// to the normal event area, we will highlight the topmost event.
saveSelectionPosition(0f, 0f, 0f, 0f)
}
}
// Helper method for counting the number of allday events skipped on each day
private fun incrementSkipCount(counts: IntArray?, startIndex: Int, endIndex: Int) {
if (counts == null || startIndex < 0 || endIndex > counts.size) {
return
}
for (i in startIndex..endIndex) {
counts[i]++
}
}
// Draws the "box +n" text for hidden allday events
protected fun drawMoreAlldayEvents(canvas: Canvas, remainingEvents: Int, day: Int, p: Paint) {
var x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN
var y = (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - (.5f *
EVENT_SQUARE_WIDTH) + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN).toInt()
val r: Rect = mRect
r.top = y
r.left = x
r.bottom = y + EVENT_SQUARE_WIDTH
r.right = x + EVENT_SQUARE_WIDTH
p.setColor(mMoreEventsTextColor)
p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat())
p.setStyle(Style.STROKE)
p.setAntiAlias(false)
canvas.drawRect(r, p)
p.setAntiAlias(true)
p.setStyle(Style.FILL)
p.setTextSize(EVENT_TEXT_FONT_SIZE)
val text: String =
mResources.getQuantityString(R.plurals.month_more_events, remainingEvents)
y += EVENT_SQUARE_WIDTH
x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING
canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(), p)
}
private fun computeAllDayNeighbors() {
val len: Int = mSelectedEvents.size
if (len == 0 || mSelectedEvent != null) {
return
}
// First, clear all the links
for (ii in 0 until len) {
val ev: Event = mSelectedEvents.get(ii)
ev.nextUp = null
ev.nextDown = null
ev.nextLeft = null
ev.nextRight = null
}
// For each event in the selected event list "mSelectedEvents", find
// its neighbors in the up and down directions. This could be done
// more efficiently by sorting on the Event.getColumn() field, but
// the list is expected to be very small.
// Find the event in the same row as the previously selected all-day
// event, if any.
var startPosition = -1
if (mPrevSelectedEvent != null && mPrevSelectedEvent!!.drawAsAllday()) {
startPosition = mPrevSelectedEvent?.getColumn() as Int
}
var maxPosition = -1
var startEvent: Event? = null
var maxPositionEvent: Event? = null
for (ii in 0 until len) {
val ev: Event = mSelectedEvents.get(ii)
val position: Int = ev.getColumn()
if (position == startPosition) {
startEvent = ev
} else if (position > maxPosition) {
maxPositionEvent = ev
maxPosition = position
}
for (jj in 0 until len) {
if (jj == ii) {
continue
}
val neighbor: Event = mSelectedEvents.get(jj)
val neighborPosition: Int = neighbor.getColumn()
if (neighborPosition == position - 1) {
ev.nextUp = neighbor
} else if (neighborPosition == position + 1) {
ev.nextDown = neighbor
}
}
}
if (startEvent != null) {
setSelectedEvent(startEvent)
} else {
setSelectedEvent(maxPositionEvent)
}
}
private fun drawEvents(date: Int, dayIndex: Int, top: Int, canvas: Canvas, p: Paint) {
val eventTextPaint: Paint = mEventTextPaint
val left = computeDayLeftPosition(dayIndex) + 1
val cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1
val cellHeight = mCellHeight
// Use the selected hour as the selection region
val selectionArea: Rect = mSelectionRect
selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP)
selectionArea.bottom = selectionArea.top + cellHeight
selectionArea.left = left
selectionArea.right = selectionArea.left + cellWidth
val events: ArrayList<Event> = mEvents
val numEvents: Int = events.size
val geometry: EventGeometry = mEventGeometry
val viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight
val alpha: Int = eventTextPaint.getAlpha()
eventTextPaint.setAlpha(mEventsAlpha)
for (i in 0 until numEvents) {
val event: Event = events.get(i)
if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
continue
}
// Don't draw it if it is not visible
if (event.bottom < mViewStartY || event.top > viewEndY) {
continue
}
if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents &&
geometry.eventIntersectsSelection(event, selectionArea)
) {
mSelectedEvents.add(event)
}
val r: Rect = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY)
setupTextRect(r)
// Don't draw text if it is not visible
if (r.top > viewEndY || r.bottom < mViewStartY) {
continue
}
val layout: StaticLayout? = getEventLayout(mLayouts, i, event, eventTextPaint, r)
// TODO: not sure why we are 4 pixels off
drawEventText(
layout,
r,
canvas,
mViewStartY + 4,
mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight,
false
)
}
eventTextPaint.setAlpha(alpha)
}
private fun drawEventRect(
event: Event,
canvas: Canvas,
p: Paint,
eventTextPaint: Paint,
visibleTop: Int,
visibleBot: Int
): Rect {
// Draw the Event Rect
val r: Rect = mRect
r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN, visibleTop)
r.bottom = Math.min(event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN, visibleBot)
r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN
r.right = event.right.toInt()
var color: Int = event.color
when (event.selfAttendeeStatus) {
Attendees.ATTENDEE_STATUS_INVITED -> if (event !== mClickedEvent) {
p.setStyle(Style.STROKE)
}
Attendees.ATTENDEE_STATUS_DECLINED -> {
if (event !== mClickedEvent) {
color = Utils.getDeclinedColorFromColor(color)
}
p.setStyle(Style.FILL_AND_STROKE)
}
Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
Attendees.ATTENDEE_STATUS_TENTATIVE -> p.setStyle(
Style.FILL_AND_STROKE
)
else -> p.setStyle(Style.FILL_AND_STROKE)
}
p.setAntiAlias(false)
val floorHalfStroke = Math.floor(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt()
val ceilHalfStroke = Math.ceil(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt()
r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop)
r.bottom = Math.min(
event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke,
visibleBot
)
r.left += floorHalfStroke
r.right -= ceilHalfStroke
p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat())
p.setColor(color)
val alpha: Int = p.getAlpha()
p.setAlpha(mEventsAlpha)
canvas.drawRect(r, p)
p.setAlpha(alpha)
p.setStyle(Style.FILL)
// Setup rect for drawEventText which follows
r.top = event.top.toInt() + EVENT_RECT_TOP_MARGIN
r.bottom = event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN
r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN
r.right = event.right.toInt() - EVENT_RECT_RIGHT_MARGIN
return r
}
private val drawTextSanitizerFilter: Pattern = Pattern.compile("[\t\n],")
// Sanitize a string before passing it to drawText or else we get little
// squares. For newlines and tabs before a comma, delete the character.
// Otherwise, just replace them with a space.
private fun drawTextSanitizer(string: String, maxEventTextLen: Int): String {
var string = string
val m: Matcher = drawTextSanitizerFilter.matcher(string)
string = m.replaceAll(",")
var len: Int = string.length
if (maxEventTextLen <= 0) {
string = ""
len = 0
} else if (len > maxEventTextLen) {
string = string.substring(0, maxEventTextLen)
len = maxEventTextLen
}
return string.replace('\n', ' ')
}
private fun drawEventText(
eventLayout: StaticLayout?,
rect: Rect,
canvas: Canvas,
top: Int,
bottom: Int,
center: Boolean
) {
// drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging
val width: Int = rect.right - rect.left
val height: Int = rect.bottom - rect.top
// If the rectangle is too small for text, then return
if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) {
return
}
var totalLineHeight = 0
val lineCount: Int = eventLayout.getLineCount()
for (i in 0 until lineCount) {
val lineBottom: Int = eventLayout.getLineBottom(i)
totalLineHeight = if (lineBottom <= height) {
lineBottom
} else {
break
}
}
// + 2 is small workaround when the font is slightly bigger than the rect. This will
// still allow the text to be shown without overflowing into the other all day rects.
if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight + 2 < top) {
return
}
// Use a StaticLayout to format the string.
canvas.save()
// canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2));
val padding = if (center) (rect.bottom - rect.top - totalLineHeight) / 2 else 0
canvas.translate(rect.left.toFloat(), rect.top.toFloat() + padding)
rect.left =