blob: e4b15494d7dca6082b058fc1dfb928b44f847fd7 [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.month
import com.android.calendar.Event
import com.android.calendar.R
import com.android.calendar.Utils
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.app.Service
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.Align
import android.graphics.Paint.Style
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.provider.CalendarContract.Attendees
import android.text.TextPaint
import android.text.TextUtils
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.text.format.Time
import android.util.Log
import android.view.MotionEvent
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import java.util.ArrayList
import java.util.Arrays
import java.util.Formatter
import java.util.HashMap
import java.util.Iterator
import java.util.List
import java.util.Locale
class MonthWeekEventsView
/**
* Shows up as an error if we don't include this.
*/
(context: Context) : SimpleWeekView(context) {
// Renamed to avoid override modifier and type mismatch error
protected val mTodayTime: Time = Time()
override protected var mHasToday = false
protected var mTodayIndex = -1
protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE
protected var mEvents: List<ArrayList<Event?>>? = null
protected var mUnsortedEvents: ArrayList<Event?>? = null
var mDna: HashMap<Int, Utils.DNAStrand>? = null
// This is for drawing the outlines around event chips and supports up to 10
// events being drawn on each day. The code will expand this if necessary.
protected var mEventOutlines: FloatRef = FloatRef(10 * 4 * 4 * 7)
protected var mMonthNamePaint: Paint? = null
protected var mEventPaint: TextPaint = TextPaint()
protected var mSolidBackgroundEventPaint: TextPaint? = null
protected var mFramedEventPaint: TextPaint? = null
protected var mDeclinedEventPaint: TextPaint? = null
protected var mEventExtrasPaint: TextPaint = TextPaint()
protected var mEventDeclinedExtrasPaint: TextPaint = TextPaint()
protected var mWeekNumPaint: Paint = Paint()
protected var mDNAAllDayPaint: Paint = Paint()
protected var mDNATimePaint: Paint = Paint()
protected var mEventSquarePaint: Paint = Paint()
protected var mTodayDrawable: Drawable? = null
protected var mMonthNumHeight = 0
protected var mMonthNumAscentHeight = 0
protected var mEventHeight = 0
protected var mEventAscentHeight = 0
protected var mExtrasHeight = 0
protected var mExtrasAscentHeight = 0
protected var mExtrasDescent = 0
protected var mWeekNumAscentHeight = 0
protected var mMonthBGColor = 0
protected var mMonthBGOtherColor = 0
protected var mMonthBGTodayColor = 0
protected var mMonthNumColor = 0
protected var mMonthNumOtherColor = 0
protected var mMonthNumTodayColor = 0
protected var mMonthNameColor = 0
protected var mMonthNameOtherColor = 0
protected var mMonthEventColor = 0
protected var mMonthDeclinedEventColor = 0
protected var mMonthDeclinedExtrasColor = 0
protected var mMonthEventExtraColor = 0
protected var mMonthEventOtherColor = 0
protected var mMonthEventExtraOtherColor = 0
protected var mMonthWeekNumColor = 0
protected var mMonthBusyBitsBgColor = 0
protected var mMonthBusyBitsBusyTimeColor = 0
protected var mMonthBusyBitsConflictTimeColor = 0
private var mClickedDayIndex = -1
private var mClickedDayColor = 0
protected var mEventChipOutlineColor = -0x1
protected var mDaySeparatorInnerColor = 0
protected var mTodayAnimateColor = 0
private var mAnimateToday = false
private var mAnimateTodayAlpha = 0
private var mTodayAnimator: ObjectAnimator? = null
private val mAnimatorListener: 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@MonthWeekEventsView,
"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
}
}
private var mDayXs: IntArray? = null
/**
* This provides a reference to a float array which allows for easy size
* checking and reallocation. Used for drawing lines.
*/
inner class FloatRef(size: Int) {
var array: FloatArray
fun ensureSize(newSize: Int) {
if (newSize >= array.size) {
// Add enough space for 7 more boxes to be drawn
array = Arrays.copyOf(array, newSize + 16 * 7)
}
}
init {
array = FloatArray(size)
}
}
// Sets the list of events for this week. Takes a sorted list of arrays
// divided up by day for generating the large month version and the full
// arraylist sorted by start time to generate the dna version.
fun setEvents(sortedEvents: List<ArrayList<Event?>>?, unsortedEvents: ArrayList<Event?>?) {
setEvents(sortedEvents)
// The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
// generate dna bits before its width has been fixed.
createDna(unsortedEvents)
}
/**
* Sets up the dna bits for the view. This will return early if the view
* isn't in a state that will create a valid set of dna yet (such as the
* views width not being set correctly yet).
*/
fun createDna(unsortedEvents: ArrayList<Event?>?) {
if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
// Stash the list of events for use when this view is ready, or
// just clear it if a null set has been passed to this view
mUnsortedEvents = unsortedEvents
mDna = null
return
} else {
// clear the cached set of events since we're ready to build it now
mUnsortedEvents = null
}
// Create the drawing coordinates for dna
if (!mShowDetailsInMonth) {
val numDays: Int = mEvents!!.size
var effectiveWidth: Int = mWidth - mPadding * 2
if (mShowWeekNum) {
effectiveWidth -= SPACING_WEEK_NUMBER
}
DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING
mDNAAllDayPaint?.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat())
mDayXs = IntArray(numDays)
for (day in 0 until numDays) {
mDayXs!![day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING
}
val top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1
val bottom: Int = mHeight - DNA_MARGIN
mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext())
}
}
fun setEvents(sortedEvents: List<ArrayList<Event?>>?) {
mEvents = sortedEvents
if (sortedEvents == null) {
return
}
if (sortedEvents.size !== mNumDays) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.wtf(TAG, ("Events size must be same as days displayed: size="
+ sortedEvents.size) + " days=" + mNumDays)
}
mEvents = null
return
}
}
protected fun loadColors(context: Context) {
val res: Resources = context.getResources()
mMonthWeekNumColor = res.getColor(R.color.month_week_num_color)
mMonthNumColor = res.getColor(R.color.month_day_number)
mMonthNumOtherColor = res.getColor(R.color.month_day_number_other)
mMonthNumTodayColor = res.getColor(R.color.month_today_number)
mMonthNameColor = mMonthNumColor
mMonthNameOtherColor = mMonthNumOtherColor
mMonthEventColor = res.getColor(R.color.month_event_color)
mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color)
mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color)
mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color)
mMonthEventOtherColor = res.getColor(R.color.month_event_other_color)
mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color)
mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor)
mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor)
mMonthBGColor = res.getColor(R.color.month_bgcolor)
mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines)
mTodayAnimateColor = res.getColor(R.color.today_highlight_color)
mClickedDayColor = res.getColor(R.color.day_clicked_background_color)
mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light)
}
/**
* Sets up the text and style properties for painting. Override this if you
* want to use a different paint.
*/
@Override
protected override fun initView() {
super.initView()
if (!mInitialized) {
val resources: Resources = getContext().getResources()
mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month)
TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title)
TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number)
SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin)
CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color)
EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color)
if (mScale != 1f) {
TOP_PADDING_MONTH_NUMBER *= mScale.toInt()
TOP_PADDING_WEEK_NUMBER *= mScale.toInt()
SIDE_PADDING_MONTH_NUMBER *= mScale.toInt()
SIDE_PADDING_WEEK_NUMBER *= mScale.toInt()
SPACING_WEEK_NUMBER *= mScale.toInt()
TEXT_SIZE_MONTH_NUMBER *= mScale.toInt()
TEXT_SIZE_EVENT *= mScale.toInt()
TEXT_SIZE_EVENT_TITLE *= mScale.toInt()
TEXT_SIZE_MORE_EVENTS *= mScale.toInt()
TEXT_SIZE_MONTH_NAME *= mScale.toInt()
TEXT_SIZE_WEEK_NUM *= mScale.toInt()
DAY_SEPARATOR_OUTER_WIDTH *= mScale.toInt()
DAY_SEPARATOR_INNER_WIDTH *= mScale.toInt()
DAY_SEPARATOR_VERTICAL_LENGTH *= mScale.toInt()
DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT *= mScale.toInt()
EVENT_X_OFFSET_LANDSCAPE *= mScale.toInt()
EVENT_Y_OFFSET_LANDSCAPE *= mScale.toInt()
EVENT_Y_OFFSET_PORTRAIT *= mScale.toInt()
EVENT_SQUARE_WIDTH *= mScale.toInt()
EVENT_SQUARE_BORDER *= mScale.toInt()
EVENT_LINE_PADDING *= mScale.toInt()
EVENT_BOTTOM_PADDING *= mScale.toInt()
EVENT_RIGHT_PADDING *= mScale.toInt()
DNA_MARGIN *= mScale.toInt()
DNA_WIDTH *= mScale.toInt()
DNA_ALL_DAY_HEIGHT *= mScale.toInt()
DNA_MIN_SEGMENT_HEIGHT *= mScale.toInt()
DNA_SIDE_PADDING *= mScale.toInt()
DEFAULT_EDGE_SPACING *= mScale.toInt()
DNA_ALL_DAY_WIDTH *= mScale.toInt()
TODAY_HIGHLIGHT_WIDTH *= mScale.toInt()
}
if (!mShowDetailsInMonth) {
TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN
}
mInitialized = true
}
mPadding = DEFAULT_EDGE_SPACING
loadColors(getContext())
// TODO modify paint properties depending on isMini
mMonthNumPaint = Paint()
mMonthNumPaint?.setFakeBoldText(false)
mMonthNumPaint?.setAntiAlias(true)
mMonthNumPaint?.setTextSize(TEXT_SIZE_MONTH_NUMBER.toFloat())
mMonthNumPaint?.setColor(mMonthNumColor)
mMonthNumPaint?.setStyle(Style.FILL)
mMonthNumPaint?.setTextAlign(Align.RIGHT)
mMonthNumPaint?.setTypeface(Typeface.DEFAULT)
mMonthNumAscentHeight = (-mMonthNumPaint!!.ascent() + 0.5f).toInt()
mMonthNumHeight = (mMonthNumPaint!!.descent() - mMonthNumPaint!!.ascent() + 0.5f).toInt()
mEventPaint = TextPaint()
mEventPaint?.setFakeBoldText(true)
mEventPaint?.setAntiAlias(true)
mEventPaint?.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat())
mEventPaint?.setColor(mMonthEventColor)
mSolidBackgroundEventPaint = TextPaint(mEventPaint)
mSolidBackgroundEventPaint?.setColor(EVENT_TEXT_COLOR)
mFramedEventPaint = TextPaint(mSolidBackgroundEventPaint)
mDeclinedEventPaint = TextPaint()
mDeclinedEventPaint?.setFakeBoldText(true)
mDeclinedEventPaint?.setAntiAlias(true)
mDeclinedEventPaint?.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat())
mDeclinedEventPaint?.setColor(mMonthDeclinedEventColor)
mEventAscentHeight = (-mEventPaint.ascent() + 0.5f).toInt()
mEventHeight = (mEventPaint.descent() - mEventPaint.ascent() + 0.5f).toInt()
mEventExtrasPaint = TextPaint()
mEventExtrasPaint?.setFakeBoldText(false)
mEventExtrasPaint?.setAntiAlias(true)
mEventExtrasPaint?.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
mEventExtrasPaint?.setTextSize(TEXT_SIZE_EVENT.toFloat())
mEventExtrasPaint?.setColor(mMonthEventExtraColor)
mEventExtrasPaint?.setStyle(Style.FILL)
mEventExtrasPaint?.setTextAlign(Align.LEFT)
mExtrasHeight = (mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f).toInt()
mExtrasAscentHeight = (-mEventExtrasPaint.ascent() + 0.5f).toInt()
mExtrasDescent = (mEventExtrasPaint.descent() + 0.5f).toInt()
mEventDeclinedExtrasPaint = TextPaint()
mEventDeclinedExtrasPaint.setFakeBoldText(false)
mEventDeclinedExtrasPaint.setAntiAlias(true)
mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT.toFloat())
mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor)
mEventDeclinedExtrasPaint.setStyle(Style.FILL)
mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT)
mWeekNumPaint = Paint()
mWeekNumPaint.setFakeBoldText(false)
mWeekNumPaint.setAntiAlias(true)
mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM.toFloat())
mWeekNumPaint.setColor(mWeekNumColor)
mWeekNumPaint.setStyle(Style.FILL)
mWeekNumPaint.setTextAlign(Align.RIGHT)
mWeekNumAscentHeight = (-mWeekNumPaint.ascent() + 0.5f).toInt()
mDNAAllDayPaint = Paint()
mDNATimePaint = Paint()
mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor)
mDNATimePaint.setStyle(Style.FILL_AND_STROKE)
mDNATimePaint.setStrokeWidth(DNA_WIDTH.toFloat())
mDNATimePaint.setAntiAlias(false)
mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor)
mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE)
mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat())
mDNAAllDayPaint.setAntiAlias(false)
mEventSquarePaint = Paint()
mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat())
mEventSquarePaint.setAntiAlias(false)
if (DEBUG_LAYOUT) {
Log.d("EXTRA", "mScale=$mScale")
Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint?.ascent()
?.toString() + " descent=" + mMonthNumPaint?.descent()?.toString() +
" int height=" + mMonthNumHeight)
Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint?.ascent()
?.toString() + " descent=" + mEventPaint.descent().toString() +
" int height=" + mEventHeight
.toString() + " int ascent=" + mEventAscentHeight)
Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
.toString() + " descent=" + mEventExtrasPaint.descent().toString() +
" int height=" + mExtrasHeight)
Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
.toString() + " descent=" + mWeekNumPaint.descent())
}
}
@Override
override fun setWeekParams(params: HashMap<String?, Int?>, tz: String) {
super.setWeekParams(params, tz)
if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
mOrientation = params.get(VIEW_PARAMS_ORIENTATION) ?:
Configuration.ORIENTATION_LANDSCAPE
}
updateToday(tz)
mNumCells = mNumDays + 1
if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
synchronized(mAnimatorListener) {
if (mTodayAnimator != null) {
mTodayAnimator?.removeAllListeners()
mTodayAnimator?.cancel()
}
mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
Math.max(mAnimateTodayAlpha, 80), 255)
mTodayAnimator?.setDuration(150)
mAnimatorListener.setAnimator(mTodayAnimator)
mAnimatorListener.setFadingIn(true)
mTodayAnimator?.addListener(mAnimatorListener)
mAnimateToday = true
mTodayAnimator?.start()
}
}
}
/**
* @param tz
*/
fun updateToday(tz: String): Boolean {
mTodayTime.timezone = tz
mTodayTime.setToNow()
mTodayTime.normalize(true)
val julianToday: Int = Time.getJulianDay(mTodayTime.toMillis(false), mTodayTime.gmtoff)
if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
mHasToday = true
mTodayIndex = julianToday - mFirstJulianDay
} else {
mHasToday = false
mTodayIndex = -1
}
return mHasToday
}
fun setAnimateTodayAlpha(alpha: Int) {
mAnimateTodayAlpha = alpha
invalidate()
}
@Override
protected override fun onDraw(canvas: Canvas) {
drawBackground(canvas)
drawWeekNums(canvas)
drawDaySeparators(canvas)
if (mHasToday && mAnimateToday) {
drawToday(canvas)
}
if (mShowDetailsInMonth) {
drawEvents(canvas)
} else {
if (mDna == null && mUnsortedEvents != null) {
createDna(mUnsortedEvents)
}
drawDNA(canvas)
}
drawClick(canvas)
}
protected fun drawToday(canvas: Canvas) {
r.top = DAY_SEPARATOR_INNER_WIDTH + TODAY_HIGHLIGHT_WIDTH / 2
r.bottom = mHeight - Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt()
p.setStyle(Style.STROKE)
p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH.toFloat())
r.left = computeDayLeftPosition(mTodayIndex) + TODAY_HIGHLIGHT_WIDTH / 2
r.right = (computeDayLeftPosition(mTodayIndex + 1)
- Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt())
p.setColor(mTodayAnimateColor or (mAnimateTodayAlpha shl 24))
canvas.drawRect(r, p)
p.setStyle(Style.FILL)
}
// TODO move into SimpleWeekView
// Computes the x position for the left side of the given day
private fun computeDayLeftPosition(day: Int): Int {
var effectiveWidth: Int = mWidth
var x = 0
var xOffset = 0
if (mShowWeekNum) {
xOffset = SPACING_WEEK_NUMBER + mPadding
effectiveWidth -= xOffset
}
x = day * effectiveWidth / mNumDays + xOffset
return x
}
@Override
protected override fun drawDaySeparators(canvas: Canvas) {
val lines = FloatArray(8 * 4)
var count = 6 * 4
var wkNumOffset = 0
var i = 0
if (mShowWeekNum) {
// This adds the first line separating the week number
val xOffset: Int = SPACING_WEEK_NUMBER + mPadding
count += 4
lines[i++] = xOffset.toFloat()
lines[i++] = 0f
lines[i++] = xOffset.toFloat()
lines[i++] = mHeight.toFloat()
wkNumOffset++
}
count += 4
lines[i++] = 0f
lines[i++] = 0f
lines[i++] = mWidth.toFloat()
lines[i++] = 0f
val y0 = 0
val y1: Int = mHeight
while (i < count) {
val x = computeDayLeftPosition(i / 4 - wkNumOffset)
lines[i++] = x.toFloat()
lines[i++] = y0.toFloat()
lines[i++] = x.toFloat()
lines[i++] = y1.toFloat()
}
p.setColor(mDaySeparatorInnerColor)
p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH.toFloat())
canvas.drawLines(lines, 0, count, p)
}
@Override
protected override fun drawBackground(canvas: Canvas) {
var i = 0
var offset = 0
r.top = DAY_SEPARATOR_INNER_WIDTH
r.bottom = mHeight
if (mShowWeekNum) {
i++
offset++
}
if (!mOddMonth!!.get(i)) {
while (++i < mOddMonth!!.size && !mOddMonth!!.get(i));
r.right = computeDayLeftPosition(i - offset)
r.left = 0
p.setColor(mMonthBGOtherColor)
canvas.drawRect(r, p)
// compute left edge for i, set up r, draw
} else if (!mOddMonth!!.get(mOddMonth!!.size - 1.also { i = it })) {
while (--i >= offset && !mOddMonth!!.get(i));
i++
// compute left edge for i, set up r, draw
r.right = mWidth
r.left = computeDayLeftPosition(i - offset)
p.setColor(mMonthBGOtherColor)
canvas.drawRect(r, p)
}
if (mHasToday) {
p.setColor(mMonthBGTodayColor)
r.left = computeDayLeftPosition(mTodayIndex)
r.right = computeDayLeftPosition(mTodayIndex + 1)
canvas.drawRect(r, p)
}
}
// Draw the "clicked" color on the tapped day
private fun drawClick(canvas: Canvas) {
if (mClickedDayIndex != -1) {
val alpha: Int = p.getAlpha()
p.setColor(mClickedDayColor)
p.setAlpha(mClickedAlpha)
r.left = computeDayLeftPosition(mClickedDayIndex)
r.right = computeDayLeftPosition(mClickedDayIndex + 1)
r.top = DAY_SEPARATOR_INNER_WIDTH
r.bottom = mHeight
canvas.drawRect(r, p)
p.setAlpha(alpha)
}
}
@Override
protected override fun drawWeekNums(canvas: Canvas) {
var y: Int
var i = 0
var offset = -1
var todayIndex = mTodayIndex
var x = 0
var numCount: Int = mNumDays
if (mShowWeekNum) {
x = SIDE_PADDING_WEEK_NUMBER + mPadding
y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER
canvas.drawText(mDayNumbers!!.get(0) as String, x.toFloat(), y.toFloat(), mWeekNumPaint)
numCount++
i++
todayIndex++
offset++
}
y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER
var isFocusMonth: Boolean = mFocusDay!!.get(i)
var isBold = false
mMonthNumPaint?.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor)
while (i < numCount) {
if (mHasToday && todayIndex == i) {
mMonthNumPaint?.setColor(mMonthNumTodayColor)
mMonthNumPaint?.setFakeBoldText(true.also { isBold = it })
if (i + 1 < numCount) {
// Make sure the color will be set back on the next
// iteration
isFocusMonth = !mFocusDay!!.get(i + 1)
}
} else if (mFocusDay?.get(i) !== isFocusMonth) {
isFocusMonth = mFocusDay!!.get(i)
mMonthNumPaint?.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor)
}
x = computeDayLeftPosition(i - offset) - SIDE_PADDING_MONTH_NUMBER
canvas.drawText(mDayNumbers!!.get(i) as String, x.toFloat(), y.toFloat(),
mMonthNumPaint as Paint)
if (isBold) {
mMonthNumPaint?.setFakeBoldText(false.also { isBold = it })
}
i++
}
}
protected fun drawEvents(canvas: Canvas) {
if (mEvents == null) {
return
}
var day = -1
for (eventDay in mEvents!!) {
day++
if (eventDay == null || eventDay.size === 0) {
continue
}
var ySquare: Int
val xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1
var rightEdge = computeDayLeftPosition(day + 1)
if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER
rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1
} else {
ySquare = EVENT_Y_OFFSET_LANDSCAPE
rightEdge -= EVENT_X_OFFSET_LANDSCAPE
}
// Determine if everything will fit when time ranges are shown.
var showTimes = true
var iter: Iterator<Event> = eventDay.iterator() as Iterator<Event>
var yTest = ySquare
while (iter.hasNext()) {
val event: Event = iter.next()
val newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
showTimes, /*doDraw*/false)
if (newY == yTest) {
showTimes = false
break
}
yTest = newY
}
var eventCount = 0
iter = eventDay.iterator() as Iterator<Event>
while (iter.hasNext()) {
val event: Event = iter.next()
val newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
showTimes, /*doDraw*/true)
if (newY == ySquare) {
break
}
eventCount++
ySquare = newY
}
val remaining: Int = eventDay.size- eventCount
if (remaining > 0) {
drawMoreEvents(canvas, remaining, xSquare)
}
}
}
protected fun addChipOutline(lines: FloatRef, count: Int, x: Int, y: Int): Int {
var count = count
lines.ensureSize(count + 16)
// top of box
lines.array[count++] = x.toFloat()
lines.array[count++] = y.toFloat()
lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
lines.array[count++] = y.toFloat()
// right side of box
lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
lines.array[count++] = y.toFloat()
lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat()
lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
// left side of box
lines.array[count++] = x.toFloat()
lines.array[count++] = y.toFloat()
lines.array[count++] = x.toFloat()
lines.array[count++] = (y + EVENT_SQUARE_WIDTH + 1).toFloat()
// bottom of box
lines.array[count++] = x.toFloat()
lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
lines.array[count++] = (x + EVENT_SQUARE_WIDTH + 1).toFloat()
lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat()
return count
}
/**
* Attempts to draw the given event. Returns the y for the next event or the
* original y if the event will not fit. An event is considered to not fit
* if the event and its extras won't fit or if there are more events and the
* more events line would not fit after drawing this event.
*
* @param canvas the canvas to draw on
* @param event the event to draw
* @param x the top left corner for this event's color chip
* @param y the top left corner for this event's color chip
* @param rightEdge the rightmost point we're allowed to draw on (exclusive)
* @param moreEvents indicates whether additional events will follow this one
* @param showTimes if set, a second line with a time range will be displayed for non-all-day
* events
* @param doDraw if set, do the actual drawing; otherwise this just computes the height
* and returns
* @return the y for the next event or the original y if it won't fit
*/
protected fun drawEvent(canvas: Canvas, event: Event, x: Int, y: Int, rightEdge: Int,
moreEvents: Boolean, showTimes: Boolean, doDraw: Boolean): Int {
/*
* Vertical layout:
* (top of box)
* a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
* b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
* c. [optional] Time range (mExtrasHeight)
* d. EVENT_LINE_PADDING
*
* Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need
* to leave room for something like "+2" at the bottom:
*
* e. "+ more" line (mExtrasHeight)
*
* f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
* (bottom of box)
*/
var y = y
val BORDER_SPACE = EVENT_SQUARE_BORDER + 1 // want a 1-pixel gap inside border
val STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2 // adjust bounds for stroke width
val allDay: Boolean = event.allDay
var eventRequiredSpace = mEventHeight
if (allDay) {
// Add a few pixels for the box we draw around all-day events.
eventRequiredSpace += BORDER_SPACE * 2
} else if (showTimes) {
// Need room for the "1pm - 2pm" line.
eventRequiredSpace += mExtrasHeight
}
var reservedSpace = EVENT_BOTTOM_PADDING // leave a bit of room at the bottom
if (moreEvents) {
// More events follow. Leave a bit of space between events.
eventRequiredSpace += EVENT_LINE_PADDING
// Make sure we have room for the "+ more" line. (The "+ more" line is expected
// to be <= the height of an event line, so we won't show "+1" when we could be
// showing the event.)
reservedSpace += mExtrasHeight
}
if (y + eventRequiredSpace + reservedSpace > mHeight) {
// Not enough space, return original y
return y
} else if (!doDraw) {
return y + eventRequiredSpace
}
val isDeclined = event.selfAttendeeStatus === Attendees.ATTENDEE_STATUS_DECLINED
var color: Int = event.color
if (isDeclined) {
color = Utils.getDeclinedColorFromColor(color)
}
val textX: Int
var textY: Int
val textRightEdge: Int
if (allDay) {
// We shift the render offset "inward", because drawRect with a stroke width greater
// than 1 draws outside the specified bounds. (We don't adjust the left edge, since
// we want to match the existing appearance of the "event square".)
r.left = x
r.right = rightEdge - STROKE_WIDTH_ADJ
r.top = y + STROKE_WIDTH_ADJ
r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ
textX = x + BORDER_SPACE
textY = y + mEventAscentHeight + BORDER_SPACE
textRightEdge = rightEdge - BORDER_SPACE
} else {
r.left = x
r.right = x + EVENT_SQUARE_WIDTH
r.bottom = y + mEventAscentHeight
r.top = r.bottom - EVENT_SQUARE_WIDTH
textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING
textY = y + mEventAscentHeight
textRightEdge = rightEdge
}
var boxStyle: Style = Style.STROKE
var solidBackground = false
if (event.selfAttendeeStatus !== Attendees.ATTENDEE_STATUS_INVITED) {
boxStyle = Style.FILL_AND_STROKE
if (allDay) {
solidBackground = true
}
}
mEventSquarePaint.setStyle(boxStyle)
mEventSquarePaint.setColor(color)
canvas.drawRect(r, mEventSquarePaint)
val avail = (textRightEdge - textX).toFloat()
var text: CharSequence = TextUtils.ellipsize(
event.title, mEventPaint, avail, TextUtils.TruncateAt.END)
val textPaint: TextPaint?
textPaint = if (solidBackground) {
// Text color needs to contrast with solid background.
mSolidBackgroundEventPaint
} else if (isDeclined) {
// Use "declined event" color.
mDeclinedEventPaint
} else if (allDay) {
// Text inside frame is same color as frame.
mFramedEventPaint?.setColor(color)
mFramedEventPaint
} else {
// Use generic event text color.
mEventPaint
}
canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(), textPaint as Paint)
y += mEventHeight
if (allDay) {
y += BORDER_SPACE * 2
}
if (showTimes && !allDay) {
// show start/end time, e.g. "1pm - 2pm"
textY = y + mExtrasAscentHeight
mStringBuilder.setLength(0)
text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
event.endMillis, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
Utils.getTimeZone(getContext(), null)).toString()
text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END)
canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(),
if (isDeclined) mEventDeclinedExtrasPaint else mEventExtrasPaint)
y += mExtrasHeight
}
y += EVENT_LINE_PADDING
return y
}
protected fun drawMoreEvents(canvas: Canvas, remainingEvents: Int, x: Int) {
val y: Int = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING)
val text: String = getContext().getResources().getQuantityString(
R.plurals.month_more_events, remainingEvents)
mEventExtrasPaint.setAntiAlias(true)
mEventExtrasPaint.setFakeBoldText(true)
canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(),
mEventExtrasPaint as Paint)
mEventExtrasPaint!!.setFakeBoldText(false)
}
/**
* Draws a line showing busy times in each day of week The method draws
* non-conflicting times in the event color and times with conflicting
* events in the dna conflict color defined in colors.
*
* @param canvas
*/
protected fun drawDNA(canvas: Canvas) {
// Draw event and conflict times
if (mDna != null) {
for (strand in mDna!!.values) {
if (strand.color === CONFLICT_COLOR || strand.points == null ||
(strand.points as FloatArray).size === 0) {
continue
}
mDNATimePaint!!.setColor(strand.color)
canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint)
}
// Draw black last to make sure it's on top
val strand: Utils.DNAStrand? = mDna?.get(CONFLICT_COLOR)
if (strand != null && strand!!.points != null && strand!!.points?.size !== 0) {
mDNATimePaint!!.setColor(strand.color)
canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint)
}
if (mDayXs == null) {
return
}
val numDays = mDayXs!!.size
val xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2
if (strand != null && strand!!.allDays != null && strand!!.allDays?.size === numDays) {
for (i in 0 until numDays) {
// this adds at most 7 draws. We could sort it by color and
// build an array instead but this is easier.
if (strand!!.allDays?.get(i) !== 0) {
mDNAAllDayPaint!!.setColor(strand!!.allDays!!.get(i))
canvas.drawLine(mDayXs!![i].toFloat() + xOffset.toFloat(),
DNA_MARGIN.toFloat(), mDayXs!![i].toFloat() + xOffset.toFloat(),
DNA_MARGIN.toFloat() + DNA_ALL_DAY_HEIGHT.toFloat(),
mDNAAllDayPaint as Paint)
}
}
}
}
}
@Override
protected override fun updateSelectionPositions() {
if (mHasSelectedDay) {
var selectedPosition: Int = mSelectedDay - mWeekStart
if (selectedPosition < 0) {
selectedPosition += 7
}
var effectiveWidth: Int = mWidth - mPadding * 2
effectiveWidth -= SPACING_WEEK_NUMBER
mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding
mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding
mSelectedLeft += SPACING_WEEK_NUMBER
mSelectedRight += SPACING_WEEK_NUMBER
}
}
fun getDayIndexFromLocation(x: Float): Int {
val dayStart: Int = if (mShowWeekNum) SPACING_WEEK_NUMBER + mPadding else mPadding
return if (x < dayStart || x > mWidth - mPadding) {
-1
} else (((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt())
// Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
}
@Override
override fun getDayFromLocation(x: Float): Time? {
val dayPosition = getDayIndexFromLocation(x)
if (dayPosition == -1) {
return null
}
var day: Int = mFirstJulianDay + dayPosition
val time = Time(mTimeZone)
if (mWeek === 0) {
// This week is weird...
if (day < Time.EPOCH_JULIAN_DAY) {
day++
} else if (day == Time.EPOCH_JULIAN_DAY) {
time.set(1, 0, 1970)
time.normalize(true)
return time
}
}
time.setJulianDay(day)
return time
}
@Override
override fun onHoverEvent(event: MotionEvent): Boolean {
val context: Context = getContext()
// only send accessibility events if accessibility and exploration are
// on.
val am: AccessibilityManager = context
.getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager
if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
return super.onHoverEvent(event)
}
if (event.getAction() !== MotionEvent.ACTION_HOVER_EXIT) {
val hover: Time? = getDayFromLocation(event.getX())
if (hover != null
&& (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) !== 0)) {
val millis: Long = hover.toMillis(true)
val date: String = Utils!!.formatDateRange(context, millis, millis,
DateUtils.FORMAT_SHOW_DATE) as String
val accessEvent: AccessibilityEvent = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
accessEvent.getText().add(date)
if (mShowDetailsInMonth && mEvents != null) {
val dayStart: Int = SPACING_WEEK_NUMBER + mPadding
val dayPosition = ((event.getX() - dayStart) * mNumDays / (mWidth
- dayStart - mPadding)).toInt()
val events: ArrayList<Event?> = mEvents!![dayPosition]
val text: List<CharSequence> = accessEvent.getText() as List<CharSequence>
for (e in events) {
text.add(e!!.titleAndLocation.toString() + ". ")
var flags: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
if (!e!!.allDay) {
flags = flags or DateUtils.FORMAT_SHOW_TIME
if (DateFormat.is24HourFormat(context)) {
flags = flags or DateUtils.FORMAT_24HOUR
}
} else {
flags = flags or DateUtils.FORMAT_UTC
}
text.add(Utils.formatDateRange(context, e!!.startMillis, e!!.endMillis,
flags).toString() + ". ")
}
}
sendAccessibilityEventUnchecked(accessEvent)
mLastHoverTime = hover
}
}
return true
}
fun setClickedDay(xLocation: Float) {
mClickedDayIndex = getDayIndexFromLocation(xLocation)
invalidate()
}
fun clearClickedDay() {
mClickedDayIndex = -1
invalidate()
}
companion object {
private const val TAG = "MonthView"
private const val DEBUG_LAYOUT = false
const val VIEW_PARAMS_ORIENTATION = "orientation"
const val VIEW_PARAMS_ANIMATE_TODAY = "animate_today"
/* NOTE: these are not constants, and may be multiplied by a scale factor */
private var TEXT_SIZE_MONTH_NUMBER = 32
private var TEXT_SIZE_EVENT = 12
private var TEXT_SIZE_EVENT_TITLE = 14
private var TEXT_SIZE_MORE_EVENTS = 12
private var TEXT_SIZE_MONTH_NAME = 14
private var TEXT_SIZE_WEEK_NUM = 12
private var DNA_MARGIN = 4
private var DNA_ALL_DAY_HEIGHT = 4
private var DNA_MIN_SEGMENT_HEIGHT = 4
private var DNA_WIDTH = 8
private var DNA_ALL_DAY_WIDTH = 32
private var DNA_SIDE_PADDING = 6
private var CONFLICT_COLOR: Int = Color.BLACK
private var EVENT_TEXT_COLOR: Int = Color.WHITE
private var DEFAULT_EDGE_SPACING = 0
private var SIDE_PADDING_MONTH_NUMBER = 4
private var TOP_PADDING_MONTH_NUMBER = 4
private var TOP_PADDING_WEEK_NUMBER = 4
private var SIDE_PADDING_WEEK_NUMBER = 20
private var DAY_SEPARATOR_OUTER_WIDTH = 0
private var DAY_SEPARATOR_INNER_WIDTH = 1
private var DAY_SEPARATOR_VERTICAL_LENGTH = 53
private var DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT = 64
private const val MIN_WEEK_WIDTH = 50
private var EVENT_X_OFFSET_LANDSCAPE = 38
private var EVENT_Y_OFFSET_LANDSCAPE = 8
private var EVENT_Y_OFFSET_PORTRAIT = 7
private var EVENT_SQUARE_WIDTH = 10
private var EVENT_SQUARE_BORDER = 2
private var EVENT_LINE_PADDING = 2
private var EVENT_RIGHT_PADDING = 4
private var EVENT_BOTTOM_PADDING = 3
private var TODAY_HIGHLIGHT_WIDTH = 2
private var SPACING_WEEK_NUMBER = 24
private var mInitialized = false
private var mShowDetailsInMonth = false
protected var mStringBuilder: StringBuilder = StringBuilder(50)
// TODO recreate formatter when locale changes
protected var mFormatter: Formatter = Formatter(mStringBuilder, Locale.getDefault())
private const val mClickedAlpha = 128
}
}