/* * Copyright (C) 2010 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; /** * <p> * 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. * </p> */ public class SimpleDayPickerFragment extends ListFragment implements OnScrollListener { private static final String TAG = "MonthFragment"; private static final String KEY_CURRENT_TIME = "current_time"; // Affects when the month selection will change while scrolling up protected static final int SCROLL_HYST_WEEKS = 2; // How long the GoTo fling animation should last protected static final int GOTO_SCROLL_DURATION = 1000; // How long to wait after receiving an onScrollStateChanged notification // before acting on it protected static final int SCROLL_CHANGE_DELAY = 40; // The number of days to display in each week protected static final int DAYS_PER_WEEK = 7; // The size of the month name displayed above the week list protected static final int MINI_MONTH_NAME_TEXT_SIZE = 18; protected static int LIST_TOP_OFFSET = -1; // so that the top line will be under the separator protected int WEEK_MIN_VISIBLE_HEIGHT = 12; protected int BOTTOM_BUFFER = 20; protected int mSaturdayColor = 0; protected int mSundayColor = 0; protected int mDayNameColor = 0; // You can override these numbers to get a different appearance protected int mNumWeeks = 6; protected boolean mShowWeekNumber = false; protected int mDaysPerWeek = 7; // These affect the scroll speed and feel protected float mFriction = .05f; protected float mVelocityScale = 0.333f; protected Context mContext; protected Handler mHandler; protected float mMinimumFlingVelocity; // highlighted time protected Time mSelectedDay = new Time(); protected SimpleWeeksAdapter mAdapter; protected ListView mListView; protected ViewGroup mDayNamesHeader; protected String[] mDayLabels; // disposable variable used for time calculations protected Time mTempTime = new Time(); private static float mScale = 0; // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). protected int mFirstDayOfWeek; // The first day of the focus month protected Time mFirstDayOfMonth = new Time(); // The first day that is visible in the view protected Time mFirstVisibleDay = new Time(); // The name of the month to display protected TextView mMonthName; // The last name announced by accessibility protected CharSequence mPrevMonthName; // which month should be displayed/highlighted [0-11] protected int mCurrentMonthDisplayed; // used for tracking during a scroll protected long mPreviousScrollPosition; // used for tracking which direction the view is scrolling protected boolean mIsScrollingUp = false; // used for tracking what state listview is in protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; // used for tracking what state listview is in protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; // This causes an update of the view at midnight protected Runnable mTodayUpdater = new Runnable() { @Override public void run() { Time midnight = new Time(mFirstVisibleDay.timezone); midnight.setToNow(); long currentMillis = midnight.toMillis(true); midnight.hour = 0; midnight.minute = 0; midnight.second = 0; midnight.monthDay++; long millisToMidnight = 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 protected DataSetObserver mObserver = new DataSetObserver() { @Override public void onChanged() { Time day = mAdapter.getSelectedDay(); if (day.year != mSelectedDay.year || day.yearDay != mSelectedDay.yearDay) { goTo(day.toMillis(true), true, true, false); } } }; public SimpleDayPickerFragment(long initialTime) { goTo(initialTime, false, true, true); mHandler = new Handler(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity; String tz = Time.getCurrentTimezone(); ViewConfiguration viewConfig = ViewConfiguration.get(activity); mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity(); // 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; Resources res = 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 == 0) { mScale = activity.getResources().getDisplayMetrics().density; if (mScale != 1) { WEEK_MIN_VISIBLE_HEIGHT *= mScale; BOTTOM_BUFFER *= mScale; LIST_TOP_OFFSET *= mScale; } } setUpAdapter(); setListAdapter(mAdapter); } /** * Creates a new adapter if necessary and sets up its parameters. Override * this method to provide a custom adapter. */ protected void setUpAdapter() { HashMap<String, Integer> weekParams = new HashMap<String, Integer>(); weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks); weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 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 = new SimpleWeeksAdapter(getActivity(), weekParams); mAdapter.registerDataSetObserver(mObserver); } else { mAdapter.updateParams(weekParams); } // refresh the view with the new parameters mAdapter.notifyDataSetChanged(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_CURRENT_TIME)) { goTo(savedInstanceState.getLong(KEY_CURRENT_TIME), false, true, true); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setUpListView(); setUpHeader(); mMonthName = (TextView) getView().findViewById(R.id.month_name); SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0); if (child == null) { return; } int julianDay = 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 void setUpHeader() { mDayLabels = new String[7]; for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { 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 void 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(mFriction); mListView.setVelocityScale(mVelocityScale); } @Override public void onResume() { super.onResume(); setUpAdapter(); doResumeUpdates(); } @Override public void onPause() { super.onPause(); mHandler.removeCallbacks(mTodayUpdater); } @Override public void onSaveInstanceState(Bundle outState) { outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true)); } /** * Updates the user preference fields. Override this to use a different * preference space. */ protected void doResumeUpdates() { // Get default week start based on locale, subtracting one for use with android Time. Calendar cal = 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 void updateHeader() { TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label); if (mShowWeekNumber) { label.setVisibility(View.VISIBLE); } else { label.setVisibility(View.GONE); } int offset = mFirstDayOfWeek - 1; for (int i = 1; i < 8; i++) { label = (TextView) mDayNamesHeader.getChildAt(i); if (i < mDaysPerWeek + 1) { int 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 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.month_by_week, container, false); mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); return v; } /** * Returns the UTC millis since epoch representation of the currently * selected time. * * @return */ public long getSelectedTime() { return 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 */ public boolean goTo(long time, boolean animate, boolean setSelected, boolean forceScroll) { if (time == -1) { 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); long millis = mTempTime.normalize(true); // Get the week we're going to // TODO push Util function into Calendar public api. int position = Utils.getWeeksSinceEpochFromJulianDay( Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek); View child; int i = 0; int 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 int firstPosition; if (child != null) { firstPosition = mListView.getPositionForView(child); } else { firstPosition = 0; } int 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 public void onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { SimpleWeekView child = (SimpleWeekView)view.getChildAt(0); if (child == null) { return; } // Figure out where we are long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()); // If we have moved since our last call update the direction if (currScroll < mPreviousScrollPosition) { mIsScrollingUp = true; } else if (currScroll > mPreviousScrollPosition) { mIsScrollingUp = false; } else { return; } mPreviousScrollPosition = currScroll; mPreviousScrollState = mCurrentScrollState; updateMonthHighlight(mListView); } /** * Figures out if the month being shown has changed and updates the * highlight if needed * * @param view The ListView containing the weeks */ private void updateMonthHighlight(AbsListView view) { SimpleWeekView child = (SimpleWeekView) view.getChildAt(0); if (child == null) { return; } // Figure out where we are int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 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 = (SimpleWeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); if (child == null) { return; } // Find out which month we're moving into int month; if (mIsScrollingUp) { month = child.getFirstMonth(); } else { month = child.getLastMonth(); } // And how it relates to our current highlighted month int monthDiff; if (mCurrentMonthDisplayed == 11 && month == 0) { monthDiff = 1; } else if (mCurrentMonthDisplayed == 0 && month == 11) { monthDiff = -1; } else { monthDiff = month - mCurrentMonthDisplayed; } // Only switch months if we're scrolling away from the currently // selected month if (monthDiff != 0) { int julianDay = 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 void setMonthDisplayed(Time time, boolean updateHighlight) { CharSequence oldMonth = 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 public void onScrollStateChanged(AbsListView view, int scrollState) { // use a post to prevent re-entering onScrollStateChanged before it // exits mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); } protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); protected class ScrollStateRunnable implements Runnable { private int mNewState; /** * 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 */ public void doScrollStateChange(AbsListView view, int scrollState) { mHandler.removeCallbacks(this); mNewState = scrollState; mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); } public void 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; // Uncomment the below to add snap to week back // int i = 0; // View child = mView.getChildAt(i); // while (child != null && child.getBottom() <= 0) { // child = mView.getChildAt(++i); // } // if (child == null) { // // The view is no longer visible, just return // return; // } // int dist = child.getTop(); // if (dist < LIST_TOP_OFFSET) { // if (Log.isLoggable(TAG, Log.DEBUG)) { // Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp); // } // int firstPosition = mView.getFirstVisiblePosition(); // int lastPosition = mView.getLastVisiblePosition(); // boolean scroll = firstPosition != 0 && lastPosition != mView.getCount() - 1; // if (mIsScrollingUp && scroll) { // mView.smoothScrollBy(dist, 500); // } else if (!mIsScrollingUp && scroll) { // mView.smoothScrollBy(child.getHeight() + dist, 500); // } // } mAdapter.updateFocusMonth(mCurrentMonthDisplayed); } else { mPreviousScrollState = mNewState; } } } }