summaryrefslogtreecommitdiff
path: root/src/com/android/calendar/month/SimpleDayPickerFragment.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calendar/month/SimpleDayPickerFragment.kt')
-rw-r--r--src/com/android/calendar/month/SimpleDayPickerFragment.kt616
1 files changed, 616 insertions, 0 deletions
diff --git a/src/com/android/calendar/month/SimpleDayPickerFragment.kt b/src/com/android/calendar/month/SimpleDayPickerFragment.kt
new file mode 100644
index 00000000..01fcbac6
--- /dev/null
+++ b/src/com/android/calendar/month/SimpleDayPickerFragment.kt
@@ -0,0 +1,616 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.calendar.month
+
+import com.android.calendar.R
+import com.android.calendar.Utils
+import android.app.Activity
+import android.app.ListFragment
+import android.content.Context
+import android.content.res.Resources
+import android.database.DataSetObserver
+import android.os.Bundle
+import android.os.Handler
+import android.text.TextUtils
+import android.text.format.DateUtils
+import android.text.format.Time
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.AbsListView
+import android.widget.AbsListView.OnScrollListener
+import android.widget.ListView
+import android.widget.TextView
+import java.util.Calendar
+import java.util.HashMap
+import java.util.Locale
+
+/**
+ *
+ *
+ * This displays a titled list of weeks with selectable days. It can be
+ * configured to display the week number, start the week on a given day, show a
+ * reduced number of days, or display an arbitrary number of weeks at a time. By
+ * overriding methods and changing variables this fragment can be customized to
+ * easily display a month selection component in a given style.
+ *
+ */
+open class SimpleDayPickerFragment(initialTime: Long) : ListFragment(), OnScrollListener {
+ protected var WEEK_MIN_VISIBLE_HEIGHT = 12
+ protected var BOTTOM_BUFFER = 20
+ protected var mSaturdayColor = 0
+ protected var mSundayColor = 0
+ protected var mDayNameColor = 0
+
+ // You can override these numbers to get a different appearance
+ @JvmField protected var mNumWeeks = 6
+ @JvmField protected var mShowWeekNumber = false
+ @JvmField protected var mDaysPerWeek = 7
+
+ // These affect the scroll speed and feel
+ protected var mFriction = 1.0f
+ @JvmField protected var mContext: Context? = null
+ @JvmField protected var mHandler: Handler = Handler()
+ protected var mMinimumFlingVelocity = 0f
+
+ // highlighted time
+ @JvmField protected var mSelectedDay: Time = Time()
+ @JvmField protected var mAdapter: SimpleWeeksAdapter? = null
+ @JvmField protected var mListView: ListView? = null
+ @JvmField protected var mDayNamesHeader: ViewGroup? = null
+ @JvmField protected var mDayLabels: Array<String?> = arrayOfNulls(7)
+
+ // disposable variable used for time calculations
+ @JvmField protected var mTempTime: Time = Time()
+
+ // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0).
+ @JvmField protected var mFirstDayOfWeek = 0
+
+ // The first day of the focus month
+ @JvmField protected var mFirstDayOfMonth: Time = Time()
+
+ // The first day that is visible in the view
+ @JvmField protected var mFirstVisibleDay: Time = Time()
+
+ // The name of the month to display
+ protected var mMonthName: TextView? = null
+
+ // The last name announced by accessibility
+ protected var mPrevMonthName: CharSequence? = null
+
+ // which month should be displayed/highlighted [0-11]
+ protected var mCurrentMonthDisplayed = 0
+
+ // used for tracking during a scroll
+ protected var mPreviousScrollPosition: Long = 0
+
+ // used for tracking which direction the view is scrolling
+ protected var mIsScrollingUp = false
+
+ // used for tracking what state listview is in
+ protected var mPreviousScrollState: Int = OnScrollListener.SCROLL_STATE_IDLE
+
+ // used for tracking what state listview is in
+ protected var mCurrentScrollState: Int = OnScrollListener.SCROLL_STATE_IDLE
+
+ // This causes an update of the view at midnight
+ @JvmField protected var mTodayUpdater: Runnable = object : Runnable {
+ @Override
+ override fun run() {
+ val midnight = Time(mFirstVisibleDay.timezone)
+ midnight.setToNow()
+ val currentMillis: Long = midnight.toMillis(true)
+ midnight.hour = 0
+ midnight.minute = 0
+ midnight.second = 0
+ midnight.monthDay++
+ val millisToMidnight: Long = midnight.normalize(true) - currentMillis
+ mHandler?.postDelayed(this, millisToMidnight)
+ if (mAdapter != null) {
+ mAdapter?.notifyDataSetChanged()
+ }
+ }
+ }
+
+ // This allows us to update our position when a day is tapped
+ @JvmField protected var mObserver: DataSetObserver = object : DataSetObserver() {
+ @Override
+ override fun onChanged() {
+ val day: Time? = mAdapter!!.getSelectedDay()
+ if (day!!.year !== mSelectedDay!!.year || day!!.yearDay !== mSelectedDay.yearDay) {
+ goTo(day!!.toMillis(true), true, true, false)
+ }
+ }
+ }
+
+ @Override
+ override fun onAttach(activity: Activity) {
+ super.onAttach(activity)
+ mContext = activity
+ val tz: String = Time.getCurrentTimezone()
+ val viewConfig: ViewConfiguration = ViewConfiguration.get(activity)
+ mMinimumFlingVelocity = (viewConfig.getScaledMinimumFlingVelocity()).toFloat()
+
+ // Ensure we're in the correct time zone
+ mSelectedDay.switchTimezone(tz)
+ mSelectedDay.normalize(true)
+ mFirstDayOfMonth.timezone = tz
+ mFirstDayOfMonth.normalize(true)
+ mFirstVisibleDay.timezone = tz
+ mFirstVisibleDay.normalize(true)
+ mTempTime.timezone = tz
+ val res: Resources = activity.getResources()
+ mSaturdayColor = res.getColor(R.color.month_saturday)
+ mSundayColor = res.getColor(R.color.month_sunday)
+ mDayNameColor = res.getColor(R.color.month_day_names_color)
+
+ // Adjust sizes for screen density
+ if (mScale == 0f) {
+ mScale = activity.getResources().getDisplayMetrics().density
+ if (mScale != 1f) {
+ WEEK_MIN_VISIBLE_HEIGHT *= mScale.toInt()
+ BOTTOM_BUFFER *= mScale.toInt()
+ LIST_TOP_OFFSET *= mScale.toInt()
+ }
+ }
+ setUpAdapter()
+ setListAdapter(mAdapter)
+ }
+
+ /**
+ * Creates a new adapter if necessary and sets up its parameters. Override
+ * this method to provide a custom adapter.
+ */
+ protected open fun setUpAdapter() {
+ val weekParams = HashMap<String?, Int?>()
+ weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks)
+ weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, if (mShowWeekNumber) 1 else 0)
+ weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek)
+ weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY,
+ Time.getJulianDay(mSelectedDay.toMillis(false), mSelectedDay.gmtoff))
+ if (mAdapter == null) {
+ mAdapter = SimpleWeeksAdapter(getActivity(), weekParams)
+ mAdapter?.registerDataSetObserver(mObserver)
+ } else {
+ mAdapter?.updateParams(weekParams)
+ }
+ // refresh the view with the new parameters
+ mAdapter?.notifyDataSetChanged()
+ }
+
+ @Override
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+
+ @Override
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ setUpListView()
+ setUpHeader()
+ mMonthName = getView()?.findViewById(R.id.month_name) as? TextView
+ val child = mListView?.getChildAt(0) as? SimpleWeekView
+ if (child == null) {
+ return
+ }
+ val julianDay: Int = child.getFirstJulianDay()
+ mFirstVisibleDay.setJulianDay(julianDay)
+ // set the title to the month of the second week
+ mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK)
+ setMonthDisplayed(mTempTime, true)
+ }
+
+ /**
+ * Sets up the strings to be used by the header. Override this method to use
+ * different strings or modify the view params.
+ */
+ protected open fun setUpHeader() {
+ mDayLabels = arrayOfNulls(7)
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i,
+ DateUtils.LENGTH_SHORTEST).toUpperCase()
+ }
+ }
+
+ /**
+ * Sets all the required fields for the list view. Override this method to
+ * set a different list view behavior.
+ */
+ protected fun setUpListView() {
+ // Configure the listview
+ mListView = getListView()
+ // Transparent background on scroll
+ mListView?.setCacheColorHint(0)
+ // No dividers
+ mListView?.setDivider(null)
+ // Items are clickable
+ mListView?.setItemsCanFocus(true)
+ // The thumb gets in the way, so disable it
+ mListView?.setFastScrollEnabled(false)
+ mListView?.setVerticalScrollBarEnabled(false)
+ mListView?.setOnScrollListener(this)
+ mListView?.setFadingEdgeLength(0)
+ // Make the scrolling behavior nicer
+ mListView?.setFriction(ViewConfiguration.getScrollFriction() * mFriction)
+ }
+
+ @Override
+ override fun onResume() {
+ super.onResume()
+ setUpAdapter()
+ doResumeUpdates()
+ }
+
+ @Override
+ override fun onPause() {
+ super.onPause()
+ mHandler.removeCallbacks(mTodayUpdater)
+ }
+
+ @Override
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true))
+ }
+
+ /**
+ * Updates the user preference fields. Override this to use a different
+ * preference space.
+ */
+ protected open fun doResumeUpdates() {
+ // Get default week start based on locale, subtracting one for use with android Time.
+ val cal: Calendar = Calendar.getInstance(Locale.getDefault())
+ mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1
+ mShowWeekNumber = false
+ updateHeader()
+ goTo(mSelectedDay.toMillis(true), false, false, false)
+ mAdapter?.setSelectedDay(mSelectedDay)
+ mTodayUpdater.run()
+ }
+
+ /**
+ * Fixes the day names header to provide correct spacing and updates the
+ * label text. Override this to set up a custom header.
+ */
+ protected fun updateHeader() {
+ var label: TextView = mDayNamesHeader!!.findViewById(R.id.wk_label) as TextView
+ if (mShowWeekNumber) {
+ label.setVisibility(View.VISIBLE)
+ } else {
+ label.setVisibility(View.GONE)
+ }
+ val offset = mFirstDayOfWeek - 1
+ for (i in 1..7) {
+ label = mDayNamesHeader!!.getChildAt(i) as TextView
+ if (i < mDaysPerWeek + 1) {
+ val position = (offset + i) % 7
+ label.setText(mDayLabels[position])
+ label.setVisibility(View.VISIBLE)
+ if (position == Time.SATURDAY) {
+ label.setTextColor(mSaturdayColor)
+ } else if (position == Time.SUNDAY) {
+ label.setTextColor(mSundayColor)
+ } else {
+ label.setTextColor(mDayNameColor)
+ }
+ } else {
+ label.setVisibility(View.GONE)
+ }
+ }
+ mDayNamesHeader?.invalidate()
+ }
+
+ @Override
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val v: View = inflater.inflate(R.layout.month_by_week,
+ container, false)
+ mDayNamesHeader = v.findViewById(R.id.day_names) as ViewGroup
+ return v
+ }
+
+ /**
+ * Returns the UTC millis since epoch representation of the currently
+ * selected time.
+ *
+ * @return
+ */
+ val selectedTime: Long
+ get() = mSelectedDay.toMillis(true)
+
+ /**
+ * This moves to the specified time in the view. If the time is not already
+ * in range it will move the list so that the first of the month containing
+ * the time is at the top of the view. If the new time is already in view
+ * the list will not be scrolled unless forceScroll is true. This time may
+ * optionally be highlighted as selected as well.
+ *
+ * @param time The time to move to
+ * @param animate Whether to scroll to the given time or just redraw at the
+ * new location
+ * @param setSelected Whether to set the given time as selected
+ * @param forceScroll Whether to recenter even if the time is already
+ * visible
+ * @return Whether or not the view animated to the new location
+ */
+ fun goTo(time: Long, animate: Boolean, setSelected: Boolean, forceScroll: Boolean): Boolean {
+ if (time == -1L) {
+ Log.e(TAG, "time is invalid")
+ return false
+ }
+
+ // Set the selected day
+ if (setSelected) {
+ mSelectedDay.set(time)
+ mSelectedDay.normalize(true)
+ }
+
+ // If this view isn't returned yet we won't be able to load the lists
+ // current position, so return after setting the selected day.
+ if (!isResumed()) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "We're not visible yet")
+ }
+ return false
+ }
+ mTempTime.set(time)
+ var millis: Long = mTempTime.normalize(true)
+ // Get the week we're going to
+ // TODO push Util function into Calendar public api.
+ var position: Int = Utils.getWeeksSinceEpochFromJulianDay(
+ Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek)
+ var child: View?
+ var i = 0
+ var top = 0
+ // Find a child that's completely in the view
+ do {
+ child = mListView?.getChildAt(i++)
+ if (child == null) {
+ break
+ }
+ top = child.getTop()
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "child at " + (i - 1) + " has top " + top)
+ }
+ } while (top < 0)
+
+ // Compute the first and last position visible
+ val firstPosition: Int
+ firstPosition = if (child != null) {
+ mListView!!.getPositionForView(child)
+ } else {
+ 0
+ }
+ var lastPosition = firstPosition + mNumWeeks - 1
+ if (top > BOTTOM_BUFFER) {
+ lastPosition--
+ }
+ if (setSelected) {
+ mAdapter?.setSelectedDay(mSelectedDay)
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "GoTo position $position")
+ }
+ // Check if the selected day is now outside of our visible range
+ // and if so scroll to the month that contains it
+ if (position < firstPosition || position > lastPosition || forceScroll) {
+ mFirstDayOfMonth.set(mTempTime)
+ mFirstDayOfMonth.monthDay = 1
+ millis = mFirstDayOfMonth.normalize(true)
+ setMonthDisplayed(mFirstDayOfMonth, true)
+ position = Utils.getWeeksSinceEpochFromJulianDay(
+ Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek)
+ mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING
+ if (animate) {
+ mListView?.smoothScrollToPositionFromTop(
+ position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION)
+ return true
+ } else {
+ mListView?.setSelectionFromTop(position, LIST_TOP_OFFSET)
+ // Perform any after scroll operations that are needed
+ onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE)
+ }
+ } else if (setSelected) {
+ // Otherwise just set the selection
+ setMonthDisplayed(mSelectedDay, true)
+ }
+ return false
+ }
+
+ /**
+ * Updates the title and selected month if the view has moved to a new
+ * month.
+ */
+ @Override
+ override fun onScroll(
+ view: AbsListView,
+ firstVisibleItem: Int,
+ visibleItemCount: Int,
+ totalItemCount: Int
+ ) {
+ val child = view.getChildAt(0) as? SimpleWeekView
+ if (child == null) {
+ return
+ }
+
+ // Figure out where we are
+ val currScroll: Long = (view.getFirstVisiblePosition() * child.getHeight() -
+ child.getBottom()).toLong()
+ mFirstVisibleDay.setJulianDay(child.getFirstJulianDay())
+
+ // If we have moved since our last call update the direction
+ mIsScrollingUp = if (currScroll < mPreviousScrollPosition) {
+ true
+ } else if (currScroll > mPreviousScrollPosition) {
+ false
+ } else {
+ return
+ }
+ mPreviousScrollPosition = currScroll
+ mPreviousScrollState = mCurrentScrollState
+ updateMonthHighlight(mListView as? AbsListView)
+ }
+
+ /**
+ * Figures out if the month being shown has changed and updates the
+ * highlight if needed
+ *
+ * @param view The ListView containing the weeks
+ */
+ private fun updateMonthHighlight(view: AbsListView?) {
+ var child = view?.getChildAt(0) as? SimpleWeekView
+ if (child == null) {
+ return
+ }
+
+ // Figure out where we are
+ val offset = if (child?.getBottom() < WEEK_MIN_VISIBLE_HEIGHT) 1 else 0
+ // Use some hysteresis for checking which month to highlight. This
+ // causes the month to transition when two full weeks of a month are
+ // visible.
+ child = view?.getChildAt(SCROLL_HYST_WEEKS + offset) as? SimpleWeekView
+ if (child == null) {
+ return
+ }
+
+ // Find out which month we're moving into
+ val month: Int
+ month = if (mIsScrollingUp) {
+ child?.getFirstMonth()
+ } else {
+ child?.getLastMonth()
+ }
+
+ // And how it relates to our current highlighted month
+ val monthDiff: Int
+ monthDiff = if (mCurrentMonthDisplayed == 11 && month == 0) {
+ 1
+ } else if (mCurrentMonthDisplayed == 0 && month == 11) {
+ -1
+ } else {
+ month - mCurrentMonthDisplayed
+ }
+
+ // Only switch months if we're scrolling away from the currently
+ // selected month
+ if (monthDiff != 0) {
+ var julianDay: Int = child.getFirstJulianDay()
+ if (mIsScrollingUp) {
+ // Takes the start of the week
+ } else {
+ // Takes the start of the following week
+ julianDay += DAYS_PER_WEEK
+ }
+ mTempTime.setJulianDay(julianDay)
+ setMonthDisplayed(mTempTime, false)
+ }
+ }
+
+ /**
+ * Sets the month displayed at the top of this view based on time. Override
+ * to add custom events when the title is changed.
+ *
+ * @param time A day in the new focus month.
+ * @param updateHighlight TODO(epastern):
+ */
+ protected open fun setMonthDisplayed(time: Time, updateHighlight: Boolean) {
+ val oldMonth: CharSequence = mMonthName!!.getText()
+ mMonthName?.setText(Utils.formatMonthYear(mContext, time))
+ mMonthName?.invalidate()
+ if (!TextUtils.equals(oldMonth, mMonthName?.getText())) {
+ mMonthName?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ mCurrentMonthDisplayed = time.month
+ if (updateHighlight) {
+ mAdapter?.updateFocusMonth(mCurrentMonthDisplayed)
+ }
+ }
+
+ @Override
+ override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
+ // use a post to prevent re-entering onScrollStateChanged before it
+ // exits
+ mScrollStateChangedRunnable.doScrollStateChange(view, scrollState)
+ }
+
+ @JvmField protected var mScrollStateChangedRunnable: ScrollStateRunnable = ScrollStateRunnable()
+
+ protected inner class ScrollStateRunnable : Runnable {
+ private var mNewState = 0
+
+ /**
+ * Sets up the runnable with a short delay in case the scroll state
+ * immediately changes again.
+ *
+ * @param view The list view that changed state
+ * @param scrollState The new state it changed to
+ */
+ fun doScrollStateChange(view: AbsListView?, scrollState: Int) {
+ mHandler.removeCallbacks(this)
+ mNewState = scrollState
+ mHandler.postDelayed(this, SCROLL_CHANGE_DELAY.toLong())
+ }
+
+ override fun run() {
+ mCurrentScrollState = mNewState
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG,
+ "new scroll state: $mNewState old state: $mPreviousScrollState")
+ }
+ // Fix the position after a scroll or a fling ends
+ if (mNewState == OnScrollListener.SCROLL_STATE_IDLE &&
+ mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+ mPreviousScrollState = mNewState
+ mAdapter?.updateFocusMonth(mCurrentMonthDisplayed)
+ } else {
+ mPreviousScrollState = mNewState
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "MonthFragment"
+ private const val KEY_CURRENT_TIME = "current_time"
+
+ // Affects when the month selection will change while scrolling up
+ protected const val SCROLL_HYST_WEEKS = 2
+
+ // How long the GoTo fling animation should last
+ @JvmStatic protected val GOTO_SCROLL_DURATION = 500
+
+ // How long to wait after receiving an onScrollStateChanged notification
+ // before acting on it
+ protected const val SCROLL_CHANGE_DELAY = 40
+
+ // The number of days to display in each week
+ const val DAYS_PER_WEEK = 7
+
+ // The size of the month name displayed above the week list
+ protected const val MINI_MONTH_NAME_TEXT_SIZE = 18
+ var LIST_TOP_OFFSET = -1 // so that the top line will be under the separator
+ private var mScale = 0f
+ }
+
+ init {
+ goTo(initialTime, false, true, true)
+ mHandler = Handler()
+ }
+} \ No newline at end of file