| /* |
| * 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 = 500; |
| // 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 |
| public 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; |
| public 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 = 1.0f; |
| |
| 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(ViewConfiguration.getScrollFriction() * mFriction); |
| } |
| |
| @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; |
| } |
| } |
| } |
| } |