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