• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calendar.month;
18 
19 import com.android.calendar.R;
20 import com.android.calendar.Utils;
21 
22 import android.app.Activity;
23 import android.app.ListFragment;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.database.DataSetObserver;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.text.TextUtils;
30 import android.text.format.DateUtils;
31 import android.text.format.Time;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewConfiguration;
36 import android.view.ViewGroup;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.widget.AbsListView;
39 import android.widget.AbsListView.OnScrollListener;
40 import android.widget.ListView;
41 import android.widget.TextView;
42 
43 import java.util.Calendar;
44 import java.util.HashMap;
45 import java.util.Locale;
46 
47 /**
48  * <p>
49  * This displays a titled list of weeks with selectable days. It can be
50  * configured to display the week number, start the week on a given day, show a
51  * reduced number of days, or display an arbitrary number of weeks at a time. By
52  * overriding methods and changing variables this fragment can be customized to
53  * easily display a month selection component in a given style.
54  * </p>
55  */
56 public class SimpleDayPickerFragment extends ListFragment implements OnScrollListener {
57 
58     private static final String TAG = "MonthFragment";
59     private static final String KEY_CURRENT_TIME = "current_time";
60 
61     // Affects when the month selection will change while scrolling up
62     protected static final int SCROLL_HYST_WEEKS = 2;
63     // How long the GoTo fling animation should last
64     protected static final int GOTO_SCROLL_DURATION = 1000;
65     // How long to wait after receiving an onScrollStateChanged notification
66     // before acting on it
67     protected static final int SCROLL_CHANGE_DELAY = 40;
68     // The number of days to display in each week
69     protected static final int DAYS_PER_WEEK = 7;
70     // The size of the month name displayed above the week list
71     protected static final int MINI_MONTH_NAME_TEXT_SIZE = 18;
72     protected static int LIST_TOP_OFFSET = 0;
73     protected int WEEK_MIN_VISIBLE_HEIGHT = 12;
74     protected int BOTTOM_BUFFER = 20;
75     protected int mSaturdayColor = 0;
76     protected int mSundayColor = 0;
77     protected int mDayNameColor = 0;
78 
79     // You can override these numbers to get a different appearance
80     protected int mNumWeeks = 6;
81     protected boolean mShowWeekNumber = false;
82     protected int mDaysPerWeek = 7;
83 
84     // These affect the scroll speed and feel
85     protected float mFriction = .05f;
86     protected float mVelocityScale = 0.333f;
87 
88     protected Context mContext;
89     protected Handler mHandler;
90 
91     protected float mMinimumFlingVelocity;
92 
93     // highlighted time
94     protected Time mSelectedDay = new Time();
95     protected SimpleWeeksAdapter mAdapter;
96     protected ListView mListView;
97     protected ViewGroup mDayNamesHeader;
98     protected String[] mDayLabels;
99 
100     // disposable variable used for time calculations
101     protected Time mTempTime = new Time();
102 
103     private static float mScale = 0;
104     // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0).
105     protected int mFirstDayOfWeek;
106     // The first day of the focus month
107     protected Time mFirstDayOfMonth = new Time();
108     // The first day that is visible in the view
109     protected Time mFirstVisibleDay = new Time();
110     // The name of the month to display
111     protected TextView mMonthName;
112     // The last name announced by accessibility
113     protected CharSequence mPrevMonthName;
114     // which month should be displayed/highlighted [0-11]
115     protected int mCurrentMonthDisplayed;
116     // used for tracking during a scroll
117     protected long mPreviousScrollPosition;
118     // used for tracking which direction the view is scrolling
119     protected boolean mIsScrollingUp = false;
120     // used for tracking what state listview is in
121     protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
122     // used for tracking what state listview is in
123     protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
124 
125     // This causes an update of the view at midnight
126     protected Runnable mTodayUpdater = new Runnable() {
127         @Override
128         public void run() {
129             Time midnight = new Time(mFirstVisibleDay.timezone);
130             midnight.setToNow();
131             long currentMillis = midnight.toMillis(true);
132 
133             midnight.hour = 0;
134             midnight.minute = 0;
135             midnight.second = 0;
136             midnight.monthDay++;
137             long millisToMidnight = midnight.normalize(true) - currentMillis;
138             mHandler.postDelayed(this, millisToMidnight);
139 
140             if (mAdapter != null) {
141                 mAdapter.notifyDataSetChanged();
142             }
143         }
144     };
145 
146     // This allows us to update our position when a day is tapped
147     protected DataSetObserver mObserver = new DataSetObserver() {
148         @Override
149         public void onChanged() {
150             Time day = mAdapter.getSelectedDay();
151             if (day.year != mSelectedDay.year || day.yearDay != mSelectedDay.yearDay) {
152                 goTo(day.toMillis(true), true, true, false);
153             }
154         }
155     };
156 
SimpleDayPickerFragment(long initialTime)157     public SimpleDayPickerFragment(long initialTime) {
158         goTo(initialTime, false, true, true);
159         mHandler = new Handler();
160     }
161 
162     @Override
onAttach(Activity activity)163     public void onAttach(Activity activity) {
164         super.onAttach(activity);
165         mContext = activity;
166         String tz = Time.getCurrentTimezone();
167         ViewConfiguration viewConfig = ViewConfiguration.get(activity);
168         mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity();
169 
170         // Ensure we're in the correct time zone
171         mSelectedDay.switchTimezone(tz);
172         mSelectedDay.normalize(true);
173         mFirstDayOfMonth.timezone = tz;
174         mFirstDayOfMonth.normalize(true);
175         mFirstVisibleDay.timezone = tz;
176         mFirstVisibleDay.normalize(true);
177         mTempTime.timezone = tz;
178 
179         Resources res = activity.getResources();
180         mSaturdayColor = res.getColor(R.color.month_saturday);
181         mSundayColor = res.getColor(R.color.month_sunday);
182         mDayNameColor = res.getColor(R.color.month_day_names_color);
183 
184         // Adjust sizes for screen density
185         if (mScale == 0) {
186             mScale = activity.getResources().getDisplayMetrics().density;
187             if (mScale != 1) {
188                 WEEK_MIN_VISIBLE_HEIGHT *= mScale;
189                 BOTTOM_BUFFER *= mScale;
190                 LIST_TOP_OFFSET *= mScale;
191             }
192         }
193         setUpAdapter();
194         setListAdapter(mAdapter);
195     }
196 
197     /**
198      * Creates a new adapter if necessary and sets up its parameters. Override
199      * this method to provide a custom adapter.
200      */
setUpAdapter()201     protected void setUpAdapter() {
202         HashMap<String, Integer> weekParams = new HashMap<String, Integer>();
203         weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks);
204         weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0);
205         weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek);
206         weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY,
207                 Time.getJulianDay(mSelectedDay.toMillis(false), mSelectedDay.gmtoff));
208         if (mAdapter == null) {
209             mAdapter = new SimpleWeeksAdapter(getActivity(), weekParams);
210             mAdapter.registerDataSetObserver(mObserver);
211         } else {
212             mAdapter.updateParams(weekParams);
213         }
214         // refresh the view with the new parameters
215         mAdapter.notifyDataSetChanged();
216     }
217 
218     @Override
onCreate(Bundle savedInstanceState)219     public void onCreate(Bundle savedInstanceState) {
220         super.onCreate(savedInstanceState);
221         if (savedInstanceState != null && savedInstanceState.containsKey(KEY_CURRENT_TIME)) {
222             goTo(savedInstanceState.getLong(KEY_CURRENT_TIME), false, true, true);
223         }
224     }
225 
226     @Override
onActivityCreated(Bundle savedInstanceState)227     public void onActivityCreated(Bundle savedInstanceState) {
228         super.onActivityCreated(savedInstanceState);
229 
230         setUpListView();
231         setUpHeader();
232 
233         mMonthName = (TextView) getView().findViewById(R.id.month_name);
234         SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0);
235         if (child == null) {
236             return;
237         }
238         int julianDay = child.getFirstJulianDay();
239         mFirstVisibleDay.setJulianDay(julianDay);
240         // set the title to the month of the second week
241         mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK);
242         setMonthDisplayed(mTempTime, true);
243     }
244 
245     /**
246      * Sets up the strings to be used by the header. Override this method to use
247      * different strings or modify the view params.
248      */
setUpHeader()249     protected void setUpHeader() {
250         mDayLabels = new String[7];
251         for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
252             mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i,
253                     DateUtils.LENGTH_SHORTEST).toUpperCase();
254         }
255     }
256 
257     /**
258      * Sets all the required fields for the list view. Override this method to
259      * set a different list view behavior.
260      */
setUpListView()261     protected void setUpListView() {
262         // Configure the listview
263         mListView = getListView();
264         // Transparent background on scroll
265         mListView.setCacheColorHint(0);
266         // No dividers
267         mListView.setDivider(null);
268         // Items are clickable
269         mListView.setItemsCanFocus(true);
270         // The thumb gets in the way, so disable it
271         mListView.setFastScrollEnabled(false);
272         mListView.setVerticalScrollBarEnabled(false);
273         mListView.setOnScrollListener(this);
274         mListView.setFadingEdgeLength(0);
275         // Make the scrolling behavior nicer
276         mListView.setFriction(mFriction);
277         mListView.setVelocityScale(mVelocityScale);
278     }
279 
280     @Override
onResume()281     public void onResume() {
282         super.onResume();
283         doResumeUpdates();
284         setUpAdapter();
285     }
286 
287     @Override
onPause()288     public void onPause() {
289         super.onPause();
290         mHandler.removeCallbacks(mTodayUpdater);
291     }
292 
293     @Override
onSaveInstanceState(Bundle outState)294     public void onSaveInstanceState(Bundle outState) {
295         outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true));
296     }
297 
298     /**
299      * Updates the user preference fields. Override this to use a different
300      * preference space.
301      */
doResumeUpdates()302     protected void doResumeUpdates() {
303         // Get default week start based on locale, subtracting one for use with android Time.
304         Calendar cal = Calendar.getInstance(Locale.getDefault());
305         mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1;
306 
307         mShowWeekNumber = false;
308 
309         updateHeader();
310         goTo(mSelectedDay.toMillis(true), false, false, false);
311         mAdapter.setSelectedDay(mSelectedDay);
312         mTodayUpdater.run();
313     }
314 
315     /**
316      * Fixes the day names header to provide correct spacing and updates the
317      * label text. Override this to set up a custom header.
318      */
updateHeader()319     protected void updateHeader() {
320         TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label);
321         if (mShowWeekNumber) {
322             label.setVisibility(View.VISIBLE);
323         } else {
324             label.setVisibility(View.GONE);
325         }
326         int offset = mFirstDayOfWeek - 1;
327         for (int i = 1; i < 8; i++) {
328             label = (TextView) mDayNamesHeader.getChildAt(i);
329             if (i < mDaysPerWeek + 1) {
330                 int position = (offset + i) % 7;
331                 label.setText(mDayLabels[position]);
332                 label.setVisibility(View.VISIBLE);
333                 if (position == Time.SATURDAY) {
334                     label.setTextColor(mSaturdayColor);
335                 } else if (position == Time.SUNDAY) {
336                     label.setTextColor(mSundayColor);
337                 } else {
338                     label.setTextColor(mDayNameColor);
339                 }
340             } else {
341                 label.setVisibility(View.GONE);
342             }
343         }
344         mDayNamesHeader.invalidate();
345     }
346 
347     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)348     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
349         View v = inflater.inflate(R.layout.month_by_week,
350                 container, false);
351         mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names);
352         return v;
353     }
354 
355     /**
356      * Returns the UTC millis since epoch representation of the currently
357      * selected time.
358      *
359      * @return
360      */
getSelectedTime()361     public long getSelectedTime() {
362         return mSelectedDay.toMillis(true);
363     }
364 
365     /**
366      * This moves to the specified time in the view. If the time is not already
367      * in range it will move the list so that the first of the month containing
368      * the time is at the top of the view. If the new time is already in view
369      * the list will not be scrolled unless forceScroll is true. This time may
370      * optionally be highlighted as selected as well.
371      *
372      * @param time The time to move to
373      * @param animate Whether to scroll to the given time or just redraw at the
374      *            new location
375      * @param setSelected Whether to set the given time as selected
376      * @param forceScroll Whether to recenter even if the time is already
377      *            visible
378      * @return Whether or not the view animated to the new location
379      */
goTo(long time, boolean animate, boolean setSelected, boolean forceScroll)380     public boolean goTo(long time, boolean animate, boolean setSelected, boolean forceScroll) {
381         if (time == -1) {
382             Log.e(TAG, "time is invalid");
383             return false;
384         }
385 
386         // Set the selected day
387         if (setSelected) {
388             mSelectedDay.set(time);
389             mSelectedDay.normalize(true);
390         }
391 
392         // If this view isn't returned yet we won't be able to load the lists
393         // current position, so return after setting the selected day.
394         if (!isResumed()) {
395             if (Log.isLoggable(TAG, Log.DEBUG)) {
396                 Log.d(TAG, "We're not visible yet");
397             }
398             return false;
399         }
400 
401         mTempTime.set(time);
402         long millis = mTempTime.normalize(true);
403         // Get the week we're going to
404         // TODO push Util function into Calendar public api.
405         int position = Utils.getWeeksSinceEpochFromJulianDay(
406                 Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek);
407 
408         View child;
409         int i = 0;
410         int top = 0;
411         // Find a child that's completely in the view
412         do {
413             child = mListView.getChildAt(i++);
414             if (child == null) {
415                 break;
416             }
417             top = child.getTop();
418             if (Log.isLoggable(TAG, Log.DEBUG)) {
419                 Log.d(TAG, "child at " + (i-1) + " has top " + top);
420             }
421         } while (top < 0);
422 
423         // Compute the first and last position visible
424         int firstPosition;
425         if (child != null) {
426             firstPosition = mListView.getPositionForView(child);
427         } else {
428             firstPosition = 0;
429         }
430         int lastPosition = firstPosition + mNumWeeks - 1;
431         if (top > BOTTOM_BUFFER) {
432             lastPosition--;
433         }
434 
435         if (setSelected) {
436             mAdapter.setSelectedDay(mSelectedDay);
437         }
438 
439         if (Log.isLoggable(TAG, Log.DEBUG)) {
440             Log.d(TAG, "GoTo position " + position);
441         }
442         // Check if the selected day is now outside of our visible range
443         // and if so scroll to the month that contains it
444         if (position < firstPosition || position > lastPosition || forceScroll) {
445             mFirstDayOfMonth.set(mTempTime);
446             mFirstDayOfMonth.monthDay = 1;
447             millis = mFirstDayOfMonth.normalize(true);
448             setMonthDisplayed(mFirstDayOfMonth, true);
449             position = Utils.getWeeksSinceEpochFromJulianDay(
450                     Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek);
451 
452             mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
453             if (animate) {
454                 mListView.smoothScrollToPositionFromTop(
455                         position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
456                 return true;
457             } else {
458                 mListView.setSelectionFromTop(position, LIST_TOP_OFFSET);
459                 // Perform any after scroll operations that are needed
460                 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
461             }
462         } else if (setSelected) {
463             // Otherwise just set the selection
464             setMonthDisplayed(mSelectedDay, true);
465         }
466         return false;
467     }
468 
469      /**
470      * Updates the title and selected month if the view has moved to a new
471      * month.
472      */
473     @Override
onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)474     public void onScroll(
475             AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
476         SimpleWeekView child = (SimpleWeekView)view.getChildAt(0);
477         if (child == null) {
478             return;
479         }
480 
481         // Figure out where we are
482         long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
483         mFirstVisibleDay.setJulianDay(child.getFirstJulianDay());
484 
485         // If we have moved since our last call update the direction
486         if (currScroll < mPreviousScrollPosition) {
487             mIsScrollingUp = true;
488         } else if (currScroll > mPreviousScrollPosition) {
489             mIsScrollingUp = false;
490         } else {
491             return;
492         }
493 
494         mPreviousScrollPosition = currScroll;
495         mPreviousScrollState = mCurrentScrollState;
496 
497         updateMonthHighlight(mListView);
498     }
499 
500     /**
501      * Figures out if the month being shown has changed and updates the
502      * highlight if needed
503      *
504      * @param view The ListView containing the weeks
505      */
updateMonthHighlight(AbsListView view)506     private void updateMonthHighlight(AbsListView view) {
507         SimpleWeekView child = (SimpleWeekView) view.getChildAt(0);
508         if (child == null) {
509             return;
510         }
511 
512         // Figure out where we are
513         int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0;
514         // Use some hysteresis for checking which month to highlight. This
515         // causes the month to transition when two full weeks of a month are
516         // visible.
517         child = (SimpleWeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
518 
519         if (child == null) {
520             return;
521         }
522 
523         // Find out which month we're moving into
524         int month;
525         if (mIsScrollingUp) {
526             month = child.getFirstMonth();
527         } else {
528             month = child.getLastMonth();
529         }
530 
531         // And how it relates to our current highlighted month
532         int monthDiff;
533         if (mCurrentMonthDisplayed == 11 && month == 0) {
534             monthDiff = 1;
535         } else if (mCurrentMonthDisplayed == 0 && month == 11) {
536             monthDiff = -1;
537         } else {
538             monthDiff = month - mCurrentMonthDisplayed;
539         }
540 
541         // Only switch months if we're scrolling away from the currently
542         // selected month
543         if (monthDiff != 0) {
544             int julianDay = child.getFirstJulianDay();
545             if (mIsScrollingUp) {
546                 // Takes the start of the week
547             } else {
548                 // Takes the start of the following week
549                 julianDay += DAYS_PER_WEEK;
550             }
551             mTempTime.setJulianDay(julianDay);
552             setMonthDisplayed(mTempTime, false);
553         }
554     }
555 
556     /**
557      * Sets the month displayed at the top of this view based on time. Override
558      * to add custom events when the title is changed.
559      *
560      * @param time A day in the new focus month.
561      * @param updateHighlight TODO(epastern):
562      */
563     protected void setMonthDisplayed(Time time, boolean updateHighlight) {
564         CharSequence oldMonth = mMonthName.getText();
565         mMonthName.setText(Utils.formatMonthYear(mContext, time));
566         mMonthName.invalidate();
567         if (!TextUtils.equals(oldMonth, mMonthName.getText())) {
568             mMonthName.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
569         }
570         mCurrentMonthDisplayed = time.month;
571         if (updateHighlight) {
572             mAdapter.updateFocusMonth(mCurrentMonthDisplayed);
573         }
574     }
575 
576     @Override
577     public void onScrollStateChanged(AbsListView view, int scrollState) {
578         // use a post to prevent re-entering onScrollStateChanged before it
579         // exits
580         mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
581     }
582 
583     protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
584 
585     protected class ScrollStateRunnable implements Runnable {
586         private int mNewState;
587 
588         /**
589          * Sets up the runnable with a short delay in case the scroll state
590          * immediately changes again.
591          *
592          * @param view The list view that changed state
593          * @param scrollState The new state it changed to
594          */
595         public void doScrollStateChange(AbsListView view, int scrollState) {
596             mHandler.removeCallbacks(this);
597             mNewState = scrollState;
598             mHandler.postDelayed(this, SCROLL_CHANGE_DELAY);
599         }
600 
601         public void run() {
602             mCurrentScrollState = mNewState;
603             if (Log.isLoggable(TAG, Log.DEBUG)) {
604                 Log.d(TAG,
605                         "new scroll state: " + mNewState + " old state: " + mPreviousScrollState);
606             }
607             // Fix the position after a scroll or a fling ends
608             if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
609                     && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
610                 mPreviousScrollState = mNewState;
611                 // Uncomment the below to add snap to week back
612 //                int i = 0;
613 //                View child = mView.getChildAt(i);
614 //                while (child != null && child.getBottom() <= 0) {
615 //                    child = mView.getChildAt(++i);
616 //                }
617 //                if (child == null) {
618 //                    // The view is no longer visible, just return
619 //                    return;
620 //                }
621 //                int dist = child.getTop();
622 //                if (dist < LIST_TOP_OFFSET) {
623 //                    if (Log.isLoggable(TAG, Log.DEBUG)) {
624 //                        Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp);
625 //                    }
626 //                    int firstPosition = mView.getFirstVisiblePosition();
627 //                    int lastPosition = mView.getLastVisiblePosition();
628 //                    boolean scroll = firstPosition != 0 && lastPosition != mView.getCount() - 1;
629 //                    if (mIsScrollingUp && scroll) {
630 //                        mView.smoothScrollBy(dist, 500);
631 //                    } else if (!mIsScrollingUp && scroll) {
632 //                        mView.smoothScrollBy(child.getHeight() + dist, 500);
633 //                    }
634 //                }
635                 mAdapter.updateFocusMonth(mCurrentMonthDisplayed);
636             } else {
637                 mPreviousScrollState = mNewState;
638             }
639         }
640     }
641 }
642