| /* |
| * 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.R |
| import com.android.calendar.Utils |
| import android.app.Service |
| import android.content.Context |
| import android.content.res.Resources |
| 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.drawable.Drawable |
| import android.text.format.DateUtils |
| import android.text.format.Time |
| import android.view.MotionEvent |
| import android.view.View |
| import android.view.accessibility.AccessibilityEvent |
| import android.view.accessibility.AccessibilityManager |
| import java.security.InvalidParameterException |
| import java.util.HashMap |
| |
| /** |
| * |
| * |
| * This is a dynamic view for drawing a single week. It can be configured to |
| * display the week number, start the week on a given day, or show a reduced |
| * number of days. It is intended for use as a single view within a ListView. |
| * See [SimpleWeeksAdapter] for usage. |
| * |
| */ |
| open class SimpleWeekView(context: Context) : View(context) { |
| // affects the padding on the sides of this view |
| @JvmField protected var mPadding = 0 |
| @JvmField protected var r: Rect = Rect() |
| @JvmField protected var p: Paint = Paint() |
| @JvmField protected var mMonthNumPaint: Paint = Paint() |
| @JvmField protected var mSelectedDayLine: Drawable |
| |
| // Cache the number strings so we don't have to recompute them each time |
| @JvmField protected var mDayNumbers: Array<String?>? = null |
| |
| // How many days to display |
| @JvmField protected var mNumDays = DEFAULT_NUM_DAYS |
| |
| // The number of days + a spot for week number if it is displayed |
| @JvmField protected var mNumCells = mNumDays |
| |
| // Quick lookup for checking which days are in the focus month |
| @JvmField protected var mFocusDay: BooleanArray = BooleanArray(mNumCells) |
| |
| // Quick lookup for checking which days are in an odd month (to set a different background) |
| @JvmField protected var mOddMonth: BooleanArray = BooleanArray(mNumCells) |
| |
| // The Julian day of the first day displayed by this item |
| @JvmField protected var mFirstJulianDay = -1 |
| |
| // The month of the first day in this week |
| @JvmField protected var firstMonth = -1 |
| |
| // The month of the last day in this week |
| @JvmField protected var lastMonth = -1 |
| |
| // The position of this week, equivalent to weeks since the week of Jan 1st, |
| // 1970 |
| @JvmField var mWeek = -1 |
| |
| // Quick reference to the width of this view, matches parent |
| @JvmField protected var mWidth = 0 |
| |
| // The height this view should draw at in pixels, set by height param |
| @JvmField protected var mHeight = DEFAULT_HEIGHT |
| |
| // Whether the week number should be shown |
| @JvmField protected var mShowWeekNum = false |
| |
| // If this view contains the selected day |
| @JvmField protected var mHasSelectedDay = false |
| |
| // If this view contains the today |
| open protected var mHasToday = false |
| |
| // Which day is selected [0-6] or -1 if no day is selected |
| @JvmField protected var mSelectedDay = DEFAULT_SELECTED_DAY |
| |
| // Which day is today [0-6] or -1 if no day is today |
| @JvmField protected var mToday: Int = DEFAULT_SELECTED_DAY |
| |
| // Which day of the week to start on [0-6] |
| @JvmField protected var mWeekStart = DEFAULT_WEEK_START |
| |
| // The left edge of the selected day |
| @JvmField protected var mSelectedLeft = -1 |
| |
| // The right edge of the selected day |
| @JvmField protected var mSelectedRight = -1 |
| |
| // The timezone to display times/dates in (used for determining when Today |
| // is) |
| @JvmField protected var mTimeZone: String = Time.getCurrentTimezone() |
| @JvmField protected var mBGColor: Int |
| @JvmField protected var mSelectedWeekBGColor: Int |
| @JvmField protected var mFocusMonthColor: Int |
| @JvmField protected var mOtherMonthColor: Int |
| @JvmField protected var mDaySeparatorColor: Int |
| @JvmField protected var mTodayOutlineColor: Int |
| @JvmField protected var mWeekNumColor: Int |
| |
| /** |
| * Sets all the parameters for displaying this week. The only required |
| * parameter is the week number. Other parameters have a default value and |
| * will only update if a new value is included, except for focus month, |
| * which will always default to no focus month if no value is passed in. See |
| * [.VIEW_PARAMS_HEIGHT] for more info on parameters. |
| * |
| * @param params A map of the new parameters, see |
| * [.VIEW_PARAMS_HEIGHT] |
| * @param tz The time zone this view should reference times in |
| */ |
| open fun setWeekParams(params: HashMap<String?, Int?>, tz: String) { |
| if (!params.containsKey(VIEW_PARAMS_WEEK)) { |
| throw InvalidParameterException("You must specify the week number for this view") |
| } |
| setTag(params) |
| mTimeZone = tz |
| // We keep the current value for any params not present |
| if (params.containsKey(VIEW_PARAMS_HEIGHT)) { |
| mHeight = (params.get(VIEW_PARAMS_HEIGHT))!!.toInt() |
| if (mHeight < MIN_HEIGHT) { |
| mHeight = MIN_HEIGHT |
| } |
| } |
| if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { |
| mSelectedDay = (params.get(VIEW_PARAMS_SELECTED_DAY))!!.toInt() |
| } |
| mHasSelectedDay = mSelectedDay != -1 |
| if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) { |
| mNumDays = (params.get(VIEW_PARAMS_NUM_DAYS))!!.toInt() |
| } |
| if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) { |
| mShowWeekNum = |
| if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) { |
| true |
| } else { |
| false |
| } |
| } |
| mNumCells = if (mShowWeekNum) mNumDays + 1 else mNumDays |
| |
| // Allocate space for caching the day numbers and focus values |
| mDayNumbers = arrayOfNulls(mNumCells) |
| mFocusDay = BooleanArray(mNumCells) |
| mOddMonth = BooleanArray(mNumCells) |
| mWeek = (params.get(VIEW_PARAMS_WEEK))!!.toInt() |
| val julianMonday: Int = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek) |
| val time = Time(tz) |
| time.setJulianDay(julianMonday) |
| |
| // If we're showing the week number calculate it based on Monday |
| var i = 0 |
| if (mShowWeekNum) { |
| mDayNumbers!![0] = Integer.toString(time.getWeekNumber()) |
| i++ |
| } |
| if (params.containsKey(VIEW_PARAMS_WEEK_START)) { |
| mWeekStart = (params.get(VIEW_PARAMS_WEEK_START))!!.toInt() |
| } |
| |
| // Now adjust our starting day based on the start day of the week |
| // If the week is set to start on a Saturday the first week will be |
| // Dec 27th 1969 -Jan 2nd, 1970 |
| if (time.weekDay !== mWeekStart) { |
| var diff: Int = time.weekDay - mWeekStart |
| if (diff < 0) { |
| diff += 7 |
| } |
| time.monthDay -= diff |
| time.normalize(true) |
| } |
| mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff) |
| firstMonth = time.month |
| |
| // Figure out what day today is |
| val today = Time(tz) |
| today.setToNow() |
| mHasToday = false |
| mToday = -1 |
| val focusMonth = if (params.containsKey(VIEW_PARAMS_FOCUS_MONTH)) params.get( |
| VIEW_PARAMS_FOCUS_MONTH |
| ) else DEFAULT_FOCUS_MONTH |
| while (i < mNumCells) { |
| if (time.monthDay === 1) { |
| firstMonth = time.month |
| } |
| mOddMonth!![i] = time.month % 2 === 1 |
| if (time.month === focusMonth) { |
| mFocusDay!![i] = true |
| } else { |
| mFocusDay!![i] = false |
| } |
| if (time.year === today.year && time.yearDay === today.yearDay) { |
| mHasToday = true |
| mToday = i |
| } |
| mDayNumbers!![i] = Integer.toString(time.monthDay++) |
| time.normalize(true) |
| i++ |
| } |
| // We do one extra add at the end of the loop, if that pushed us to a |
| // new month undo it |
| if (time.monthDay === 1) { |
| time.monthDay-- |
| time.normalize(true) |
| } |
| lastMonth = time.month |
| updateSelectionPositions() |
| } |
| |
| /** |
| * Sets up the text and style properties for painting. Override this if you |
| * want to use a different paint. |
| */ |
| protected open fun initView() { |
| p.setFakeBoldText(false) |
| p.setAntiAlias(true) |
| p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) |
| p.setStyle(Style.FILL) |
| mMonthNumPaint = Paint() |
| mMonthNumPaint?.setFakeBoldText(true) |
| mMonthNumPaint?.setAntiAlias(true) |
| mMonthNumPaint?.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) |
| mMonthNumPaint?.setColor(mFocusMonthColor) |
| mMonthNumPaint?.setStyle(Style.FILL) |
| mMonthNumPaint?.setTextAlign(Align.CENTER) |
| } |
| |
| /** |
| * Returns the month of the first day in this week |
| * |
| * @return The month the first day of this view is in |
| */ |
| fun getFirstMonth(): Int { |
| return firstMonth |
| } |
| |
| /** |
| * Returns the month of the last day in this week |
| * |
| * @return The month the last day of this view is in |
| */ |
| fun getLastMonth(): Int { |
| return lastMonth |
| } |
| |
| /** |
| * Returns the julian day of the first day in this view. |
| * |
| * @return The julian day of the first day in the view. |
| */ |
| fun getFirstJulianDay(): Int { |
| return mFirstJulianDay |
| } |
| |
| /** |
| * Calculates the day that the given x position is in, accounting for week |
| * number. Returns a Time referencing that day or null if |
| * |
| * @param x The x position of the touch event |
| * @return A time object for the tapped day or null if the position wasn't |
| * in a day |
| */ |
| open fun getDayFromLocation(x: Float): Time? { |
| val dayStart = |
| if (mShowWeekNum) (mWidth - mPadding * 2) / mNumCells + mPadding else mPadding |
| if (x < dayStart || x > mWidth - mPadding) { |
| return null |
| } |
| // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels |
| val dayPosition = ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt() |
| var day = 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 |
| protected override fun onDraw(canvas: Canvas) { |
| drawBackground(canvas) |
| drawWeekNums(canvas) |
| drawDaySeparators(canvas) |
| } |
| |
| /** |
| * This draws the selection highlight if a day is selected in this week. |
| * Override this method if you wish to have a different background drawn. |
| * |
| * @param canvas The canvas to draw on |
| */ |
| protected open fun drawBackground(canvas: Canvas) { |
| if (mHasSelectedDay) { |
| p.setColor(mSelectedWeekBGColor) |
| p.setStyle(Style.FILL) |
| } else { |
| return |
| } |
| r.top = 1 |
| r.bottom = mHeight - 1 |
| r.left = mPadding |
| r.right = mSelectedLeft |
| canvas.drawRect(r, p) |
| r.left = mSelectedRight |
| r.right = mWidth - mPadding |
| canvas.drawRect(r, p) |
| } |
| |
| /** |
| * Draws the week and month day numbers for this week. Override this method |
| * if you need different placement. |
| * |
| * @param canvas The canvas to draw on |
| */ |
| protected open fun drawWeekNums(canvas: Canvas) { |
| val y = (mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH |
| val nDays = mNumCells |
| var i = 0 |
| val divisor = 2 * nDays |
| if (mShowWeekNum) { |
| p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE.toFloat()) |
| p.setStyle(Style.FILL) |
| p.setTextAlign(Align.CENTER) |
| p.setAntiAlias(true) |
| p.setColor(mWeekNumColor) |
| val x = (mWidth - mPadding * 2) / divisor + mPadding |
| canvas.drawText(mDayNumbers!![0] as String, x.toFloat(), y.toFloat(), p) |
| i++ |
| } |
| var isFocusMonth = mFocusDay!![i] |
| mMonthNumPaint?.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) |
| mMonthNumPaint?.setFakeBoldText(false) |
| while (i < nDays) { |
| if (mFocusDay!![i] != isFocusMonth) { |
| isFocusMonth = mFocusDay!![i] |
| mMonthNumPaint?.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) |
| } |
| if (mHasToday && mToday == i) { |
| mMonthNumPaint?.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE.toFloat()) |
| mMonthNumPaint?.setFakeBoldText(true) |
| } |
| val x = (2 * i + 1) * (mWidth - mPadding * 2) / divisor + mPadding |
| canvas.drawText(mDayNumbers!![i] as String, x.toFloat(), y.toFloat(), |
| mMonthNumPaint as Paint) |
| if (mHasToday && mToday == i) { |
| mMonthNumPaint?.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) |
| mMonthNumPaint?.setFakeBoldText(false) |
| } |
| i++ |
| } |
| } |
| |
| /** |
| * Draws a horizontal line for separating the weeks. Override this method if |
| * you want custom separators. |
| * |
| * @param canvas The canvas to draw on |
| */ |
| protected open fun drawDaySeparators(canvas: Canvas) { |
| if (mHasSelectedDay) { |
| r.top = 1 |
| r.bottom = mHeight - 1 |
| r.left = mSelectedLeft + 1 |
| r.right = mSelectedRight - 1 |
| p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH.toFloat()) |
| p.setStyle(Style.STROKE) |
| p.setColor(mTodayOutlineColor) |
| canvas.drawRect(r, p) |
| } |
| if (mShowWeekNum) { |
| p.setColor(mDaySeparatorColor) |
| p.setStrokeWidth(DAY_SEPARATOR_WIDTH.toFloat()) |
| val x = (mWidth - mPadding * 2) / mNumCells + mPadding |
| canvas.drawLine(x.toFloat(), 0f, x.toFloat(), mHeight.toFloat(), p) |
| } |
| } |
| |
| @Override |
| protected override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { |
| mWidth = w |
| updateSelectionPositions() |
| } |
| |
| /** |
| * This calculates the positions for the selected day lines. |
| */ |
| protected open fun updateSelectionPositions() { |
| if (mHasSelectedDay) { |
| var selectedPosition = mSelectedDay - mWeekStart |
| if (selectedPosition < 0) { |
| selectedPosition += 7 |
| } |
| if (mShowWeekNum) { |
| selectedPosition++ |
| } |
| mSelectedLeft = (selectedPosition * (mWidth - mPadding * 2) / mNumCells + |
| mPadding) |
| mSelectedRight = ((selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells + |
| mPadding) |
| } |
| } |
| |
| @Override |
| protected override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
| setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight) |
| } |
| |
| @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 |
| ) |
| val accessEvent: AccessibilityEvent = |
| AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) |
| accessEvent.getText().add(date) |
| sendAccessibilityEventUnchecked(accessEvent) |
| mLastHoverTime = hover |
| } |
| } |
| return true |
| } |
| |
| @JvmField var mLastHoverTime: Time? = null |
| |
| companion object { |
| private const val TAG = "MonthView" |
| /** |
| * These params can be passed into the view to control how it appears. |
| * [.VIEW_PARAMS_WEEK] is the only required field, though the default |
| * values are unlikely to fit most layouts correctly. |
| */ |
| /** |
| * This sets the height of this week in pixels |
| */ |
| const val VIEW_PARAMS_HEIGHT = "height" |
| |
| /** |
| * This specifies the position (or weeks since the epoch) of this week, |
| * calculated using [Utils.getWeeksSinceEpochFromJulianDay] |
| */ |
| const val VIEW_PARAMS_WEEK = "week" |
| |
| /** |
| * This sets one of the days in this view as selected [Time.SUNDAY] |
| * through [Time.SATURDAY]. |
| */ |
| const val VIEW_PARAMS_SELECTED_DAY = "selected_day" |
| |
| /** |
| * Which day the week should start on. [Time.SUNDAY] through |
| * [Time.SATURDAY]. |
| */ |
| const val VIEW_PARAMS_WEEK_START = "week_start" |
| |
| /** |
| * How many days to display at a time. Days will be displayed starting with |
| * [.mWeekStart]. |
| */ |
| const val VIEW_PARAMS_NUM_DAYS = "num_days" |
| |
| /** |
| * Which month is currently in focus, as defined by [Time.month] |
| * [0-11]. |
| */ |
| const val VIEW_PARAMS_FOCUS_MONTH = "focus_month" |
| |
| /** |
| * If this month should display week numbers. false if 0, true otherwise. |
| */ |
| const val VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num" |
| protected var DEFAULT_HEIGHT = 32 |
| protected var MIN_HEIGHT = 10 |
| protected const val DEFAULT_SELECTED_DAY = -1 |
| protected val DEFAULT_WEEK_START: Int = Time.SUNDAY |
| protected const val DEFAULT_NUM_DAYS = 7 |
| protected const val DEFAULT_SHOW_WK_NUM = 0 |
| protected const val DEFAULT_FOCUS_MONTH = -1 |
| protected var DAY_SEPARATOR_WIDTH = 1 |
| protected var MINI_DAY_NUMBER_TEXT_SIZE = 14 |
| protected var MINI_WK_NUMBER_TEXT_SIZE = 12 |
| protected var MINI_TODAY_NUMBER_TEXT_SIZE = 18 |
| protected var MINI_TODAY_OUTLINE_WIDTH = 2 |
| protected var WEEK_NUM_MARGIN_BOTTOM = 4 |
| |
| // used for scaling to the device density |
| @JvmStatic protected var mScale = 0f |
| } |
| |
| init { |
| val res: Resources = context.getResources() |
| mBGColor = res.getColor(R.color.month_bgcolor) |
| mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor) |
| mFocusMonthColor = res.getColor(R.color.month_mini_day_number) |
| mOtherMonthColor = res.getColor(R.color.month_other_month_day_number) |
| mDaySeparatorColor = res.getColor(R.color.month_grid_lines) |
| mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color) |
| mWeekNumColor = res.getColor(R.color.month_week_num_color) |
| mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light) |
| if (mScale == 0f) { |
| mScale = context.getResources().getDisplayMetrics().density |
| if (mScale != 1f) { |
| DEFAULT_HEIGHT *= mScale.toInt() |
| MIN_HEIGHT *= mScale.toInt() |
| MINI_DAY_NUMBER_TEXT_SIZE *= mScale.toInt() |
| MINI_TODAY_NUMBER_TEXT_SIZE *= mScale.toInt() |
| MINI_TODAY_OUTLINE_WIDTH *= mScale.toInt() |
| WEEK_NUM_MARGIN_BOTTOM *= mScale.toInt() |
| DAY_SEPARATOR_WIDTH *= mScale.toInt() |
| MINI_WK_NUMBER_TEXT_SIZE *= mScale.toInt() |
| } |
| } |
| |
| // Sets up any standard paints that will be used |
| initView() |
| } |
| } |