diff options
Diffstat (limited to 'src/com/android/calendar/month/MonthWeekEventsView.kt')
-rw-r--r-- | src/com/android/calendar/month/MonthWeekEventsView.kt | 1061 |
1 files changed, 1061 insertions, 0 deletions
diff --git a/src/com/android/calendar/month/MonthWeekEventsView.kt b/src/com/android/calendar/month/MonthWeekEventsView.kt new file mode 100644 index 00000000..e4b15494 --- /dev/null +++ b/src/com/android/calendar/month/MonthWeekEventsView.kt @@ -0,0 +1,1061 @@ +/* + * 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 + } +}
\ No newline at end of file |