| /* |
| * 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() |
| } |
| } |