MonthByWeekFragment.java revision 124b831510317b41acf3e391b25882a785272654
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
17package com.android.calendar.month;
18
19import com.android.calendar.CalendarController;
20import com.android.calendar.CalendarController.EventInfo;
21import com.android.calendar.CalendarController.EventType;
22import com.android.calendar.R;
23import com.android.calendar.Utils;
24import com.android.calendar.month.MonthByWeekAdapter.WeekParameters;
25
26import android.app.Activity;
27import android.app.ListFragment;
28import android.app.LoaderManager;
29import android.content.Context;
30import android.content.CursorLoader;
31import android.content.Loader;
32import android.database.Cursor;
33import android.net.Uri;
34import android.os.Bundle;
35import android.provider.Calendar.Calendars;
36import android.provider.Calendar.Instances;
37import android.text.format.Time;
38import android.util.Log;
39import android.view.GestureDetector;
40import android.view.GestureDetector.SimpleOnGestureListener;
41import android.view.Gravity;
42import android.view.LayoutInflater;
43import android.view.MotionEvent;
44import android.view.View;
45import android.view.View.OnTouchListener;
46import android.view.ViewConfiguration;
47import android.view.ViewGroup;
48import android.view.ViewGroup.LayoutParams;
49import android.widget.AbsListView;
50import android.widget.AbsListView.OnScrollListener;
51import android.widget.FrameLayout;
52import android.widget.LinearLayout;
53import android.widget.ListView;
54import android.widget.TextView;
55
56public class MonthByWeekFragment extends ListFragment implements CalendarController.EventHandler,
57        LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener, OnTouchListener{
58    private static final String TAG = "MonthFragment";
59
60    // Selection and selection args for adding event queries
61    private static final String WHERE_CALENDARS_SELECTED = Calendars.SELECTED + "=?";
62    private static final String[] WHERE_CALENDARS_SELECTED_ARGS = {"1"};
63
64    // How many weeks of hysteresis to use between scrolling up and scrolling
65    // down
66    private static final int SCROLL_HYST_WEEKS = 2;
67    // How far down from top to align the first week
68    private static final int LIST_TOP_OFFSET = 2;
69    // How long to spend on goTo scrolls
70    private static final int GOTO_SCROLL_DURATION = 1000;
71    private static final int DAYS_PER_WEEK = 7;
72    // The number of pixels that must be showing for a week to be counted as
73    // visible
74    private static int WEEK_MIN_VISIBLE_HEIGHT = 12;
75
76    private static final int MINI_MONTH_NAME_TEXT_SIZE = 18;
77    private static int MINI_MONTH_WIDTH = 254;
78    private static int MINI_MONTH_HEIGHT = 212;
79
80    protected int BOTTOM_BUFFER = 20;
81
82    private static float mScale = 0;
83    private float mFriction = .05f;
84    private float mVelocityScale = 0.333f;
85
86    private Context mContext;
87    private CursorLoader mLoader;
88    private Uri mEventUri;
89
90    protected final boolean mIsMiniMonth;
91    protected MonthByWeekAdapter mAdapter;
92    protected ListView mListView;
93    protected ViewGroup mDayNamesHeader;
94    protected String[] mDayLabels;
95    private int mFirstDayOfWeek;
96    private Time mSelectedDay = new Time();
97    private Time mFirstDayOfMonth = new Time();
98    private Time mFirstVisibleDay = new Time();
99    private Time mTempTime = new Time();
100    private WeekParameters mWeekParams = new WeekParameters();
101
102    private TextView mMonthName;
103    private int mCurrentMonthDisplayed;
104    private long mPreviousScrollPosition;
105    private boolean mIsScrollingUp = false;
106    private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
107    private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
108    private float mMinimumFlingVelocity;
109    private float mMinimumTwoMonthFlingVelocity;
110
111    private GestureDetector mGestureDetector;
112
113    private class MonthGestureListener extends SimpleOnGestureListener {
114        @Override
115        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
116                float velocityY) {
117            // TODO decide how to handle flings
118//            float absX = Math.abs(velocityX);
119//            float absY = Math.abs(velocityY);
120//            Log.d(TAG, "velX: " + velocityX + " velY: " + velocityY);
121//            if (absX > absY && absX > mMinimumFlingVelocity) {
122//                mTempTime.set(mFirstDayOfMonth);
123//                if(velocityX > 0) {
124//                    mTempTime.month++;
125//                } else {
126//                    mTempTime.month--;
127//                }
128//                mTempTime.normalize(true);
129//                goTo(mTempTime, true, false, true);
130//
131//            } else if (absY > absX && absY > mMinimumFlingVelocity) {
132//                mTempTime.set(mFirstDayOfMonth);
133//                int diff = 1;
134//                if (absY > mMinimumTwoMonthFlingVelocity) {
135//                    diff = 2;
136//                }
137//                if(velocityY < 0) {
138//                    mTempTime.month += diff;
139//                } else {
140//                    mTempTime.month -= diff;
141//                }
142//                mTempTime.normalize(true);
143//
144//                goTo(mTempTime, true, false, true);
145//            }
146            return false;
147        }
148    }
149
150    private Runnable mTZUpdater = null;
151
152    public MonthByWeekFragment() {
153        this(true);
154    }
155
156    public MonthByWeekFragment(boolean isMiniMonth) {
157        mIsMiniMonth = isMiniMonth;
158        mSelectedDay.setToNow();
159    }
160
161    @Override
162    public void onAttach(Activity activity) {
163        super.onAttach(activity);
164        mContext = activity;
165        String tz = Utils.getTimeZone(activity, mTZUpdater);
166        ViewConfiguration viewConfig = ViewConfiguration.get(activity);
167        mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity();
168        mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2;
169        mGestureDetector = new GestureDetector(activity, new MonthGestureListener());
170
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        if (mScale == 0) {
180            mScale = activity.getResources().getDisplayMetrics().density;
181            if (mScale != 1) {
182                MINI_MONTH_WIDTH *= mScale;
183                MINI_MONTH_HEIGHT *= mScale;
184                WEEK_MIN_VISIBLE_HEIGHT *= mScale;
185                BOTTOM_BUFFER *= mScale;
186            }
187        }
188
189        mAdapter = new MonthByWeekAdapter(getActivity(), mWeekParams);
190        setListAdapter(mAdapter);
191    }
192
193    @Override
194    public void onActivityCreated(Bundle savedInstanceState) {
195        super.onActivityCreated(savedInstanceState);
196        mListView = getListView();
197        mListView.setCacheColorHint(0);
198        mListView.setDivider(null);
199        mListView.setItemsCanFocus(true);
200        mListView.setFastScrollEnabled(false);
201        mListView.setOnScrollListener(this);
202        mListView.setFriction(mFriction);
203        mListView.setVelocityScale(mVelocityScale);
204        mListView.setOnTouchListener(this);
205
206
207        mDayLabels = getActivity().getResources().getStringArray(
208                R.array.day_of_week_smallest_labels);
209
210        if (mIsMiniMonth) {
211            FrameLayout.LayoutParams listParams = new FrameLayout.LayoutParams(
212                    MINI_MONTH_WIDTH, MINI_MONTH_HEIGHT);
213            listParams.gravity = Gravity.CENTER_HORIZONTAL;
214            mListView.setLayoutParams(listParams);
215            LinearLayout.LayoutParams headerParams = new LinearLayout.LayoutParams(
216                    MINI_MONTH_WIDTH, LayoutParams.WRAP_CONTENT);
217            headerParams.gravity = Gravity.CENTER_HORIZONTAL;
218            mDayNamesHeader.setLayoutParams(headerParams);
219        }
220
221        mMonthName = (TextView) getView().findViewById(R.id.month_name);
222        MonthWeekView child = (MonthWeekView) mListView.getChildAt(0);
223        if (child == null) {
224            return;
225        }
226        int julianDay = child.getFirstJulianDay();
227        mFirstVisibleDay.setJulianDay(julianDay);
228        mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK);
229        setMonthDisplayed(mTempTime);
230    }
231
232    @Override
233    public void onResume() {
234        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
235
236        WeekParameters params = mWeekParams;
237        params.firstJulianDay = Time.getJulianDay(System.currentTimeMillis(), 0);
238        params.weekHeight = 50; // This is a dummy value for now
239        params.focusMonth = mCurrentMonthDisplayed;
240        mAdapter.updateParams(params);
241        updateHeader();
242        goTo(mSelectedDay, false);
243        mAdapter.setSelectedDay(mSelectedDay);
244        super.onResume();
245    }
246
247    private void updateHeader() {
248        boolean showWeekNumber = Utils.getShowWeekNumber(mContext);
249        TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label);
250        if (showWeekNumber) {
251            label.setVisibility(View.VISIBLE);
252        } else {
253            label.setVisibility(View.GONE);
254        }
255        int offset = mFirstDayOfWeek - 1;
256        for (int i = 1; i < 8; i++) {
257            label = (TextView) mDayNamesHeader.getChildAt(i);
258            label.setText(mDayLabels[(offset + i) % 7]);
259        }
260        mDayNamesHeader.invalidate();
261    }
262
263    // TODO
264    @Override
265    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
266        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
267//        ContentUris.appendId(builder, begin);
268//        ContentUris.appendId(builder, end);
269//
270//        mLoader = new CursorLoader(getActivity(), Calendars.CONTENT_URI, PROJECTION, WHERE_CALENDARS_SELECTED, mArgs, SORT_ORDER);)
271        return null;
272    }
273
274    @Override
275    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
276        // TODO Auto-generated method stub
277
278    }
279
280    @Override
281    public View onCreateView(LayoutInflater inflater, ViewGroup container,
282            Bundle savedInstanceState) {
283        View v = inflater.inflate(R.layout.month_by_week,
284                container, false);
285        mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names);
286        return v;
287    }
288
289    @Override
290    public void eventsChanged() {
291        // TODO Auto-generated method stub
292
293    }
294
295    @Override
296    public boolean getAllDay() {
297        return false;
298    }
299
300    @Override
301    public long getSelectedTime() {
302        // TODO Auto-generated method stub
303        return 0;
304    }
305
306    @Override
307    public long getSupportedEventTypes() {
308        return EventType.GO_TO;
309    }
310
311    @Override
312    public void goTo(Time time, boolean animate) {
313        goTo(time, animate, true, false);
314    }
315
316    public void goTo(Time time, boolean animate, boolean setSelected, boolean forceScroll) {
317
318        if (time == null){
319            Log.e(TAG, "time is null");
320            return;
321        }
322
323        mTempTime.set(time);
324        long millis = mTempTime.normalize(true);
325        if (setSelected) {
326            mSelectedDay.set(time);
327            mSelectedDay.normalize(true);
328        }
329
330        if (!isResumed()) {
331            if (Log.isLoggable(TAG, Log.DEBUG)) {
332                Log.d(TAG, "We're not visible yet");
333            }
334            return;
335        }
336
337        // Get the week we're going to
338        int position = Utils.getWeeksSinceEpochFromJulianDay(
339                Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek);
340
341        View child;
342        int i = 0;
343        int top = 0;
344        // Find a child that's completely in the view
345        do {
346            child = mListView.getChildAt(i++);
347            if (child == null) {
348                break;
349            }
350            top = child.getTop();
351            if (Log.isLoggable(TAG, Log.DEBUG)) {
352                Log.d(TAG, "child at " + (i-1) + " has top " + top);
353            }
354        } while (top < 0);
355
356        // Compute the first and last position visible
357        int firstPosition;
358        if (child != null) {
359            firstPosition = mListView.getPositionForView(child);
360        } else {
361            firstPosition = 0;
362        }
363        int lastPosition = firstPosition + mWeekParams.numWeeks - 1;
364        if (top > BOTTOM_BUFFER) {
365            lastPosition--;
366        }
367
368        if (setSelected) {
369            mAdapter.setSelectedDay(mSelectedDay);
370        }
371
372        // Check if the selected day is now outside of our visible range
373        // and if so scroll to the month that contains it
374        if (position < firstPosition || position > lastPosition || forceScroll) {
375            mFirstDayOfMonth.set(mTempTime);
376            mFirstDayOfMonth.monthDay = 1;
377            millis = mFirstDayOfMonth.normalize(true);
378            setMonthDisplayed(mFirstDayOfMonth);
379            position = Utils.getWeeksSinceEpochFromJulianDay(
380                    Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek);
381
382            mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
383            if (animate) {
384                mListView.smoothScrollToPositionFromTop(
385                        position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION);
386            } else {
387                mListView.setSelectionFromTop(position, LIST_TOP_OFFSET);
388            }
389        } else if (setSelected) {
390            // Otherwise just set the selection
391            setMonthDisplayed(mSelectedDay);
392        }
393    }
394
395    @Override
396    public void goToToday() {
397        // TODO Auto-generated method stub
398
399    }
400
401    @Override
402    public void handleEvent(EventInfo event) {
403        if (event.eventType == EventType.GO_TO) {
404            goTo(event.selectedTime, true);
405        }
406    }
407
408    @Override
409    public void onScroll(
410            AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
411        MonthWeekView child = (MonthWeekView)view.getChildAt(0);
412        if (child == null) {
413            return;
414        }
415
416        // Figure out where we are
417        int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0;
418        long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
419        mFirstVisibleDay.setJulianDay(child.getFirstJulianDay());
420
421        // If we have moved since our last call update the direction
422        if (currScroll < mPreviousScrollPosition) {
423            mIsScrollingUp = true;
424        } else if (currScroll > mPreviousScrollPosition) {
425            mIsScrollingUp = false;
426        } else {
427            return;
428        }
429
430        // Use some hysteresis for checking which month to highlight. This
431        // causes the month to transition when two full weeks of a month are
432        // visible when scrolling up, and when the first day in a month reaches
433        // the top of the screen when scrolling down.
434        if (mIsScrollingUp) {
435            child = (MonthWeekView)view.getChildAt(SCROLL_HYST_WEEKS + offset);
436        } else if (offset != 0) {
437            child = (MonthWeekView)view.getChildAt(offset);
438        }
439
440        // Find out which month we're moving into
441        int month;
442        if (mIsScrollingUp) {
443            month = child.getFirstMonth();
444        } else {
445            month = child.getLastMonth();
446        }
447
448        // And how it relates to our current highlighted month
449        int monthDiff;
450        if (mCurrentMonthDisplayed == 11 && month == 0) {
451            monthDiff = 1;
452        } else if (mCurrentMonthDisplayed == 0 && month == 11) {
453            monthDiff = -1;
454        } else {
455            monthDiff = month - mCurrentMonthDisplayed;
456        }
457
458        // Only switch months if we're scrolling away from the currently
459        // selected month
460        if ((!mIsScrollingUp && monthDiff > 0)
461                || (mIsScrollingUp && monthDiff < 0)) {
462                int julianDay = child.getFirstJulianDay();
463                if (mIsScrollingUp) {
464                    julianDay -= DAYS_PER_WEEK;
465                } else {
466                    julianDay += DAYS_PER_WEEK;
467                }
468                mTempTime.setJulianDay(julianDay);
469                setMonthDisplayed(mTempTime);
470        }
471        mPreviousScrollPosition = currScroll;
472        mPreviousScrollState = mCurrentScrollState;
473    }
474
475    // Updates the month shown and highlighted
476    private void setMonthDisplayed(Time time) {
477        mMonthName.setText(time.format("%B %Y"));
478        mMonthName.invalidate();
479        mCurrentMonthDisplayed = time.month;
480        mAdapter.updateFocusMonth(mCurrentMonthDisplayed);
481    }
482
483    @Override
484    public void onScrollStateChanged(AbsListView view, int scrollState) {
485        mCurrentScrollState = scrollState;
486        if (Log.isLoggable(TAG, Log.DEBUG)) {
487            Log.d(TAG, "new scroll state: " + scrollState + " old state: " + mPreviousScrollState);
488        }
489        // For now we fix our position after a scroll or a fling ends
490        if (scrollState == OnScrollListener.SCROLL_STATE_IDLE
491                && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE
492                /*&& mPreviousScrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL*/) {
493            View child = view.getChildAt(0);
494            int dist = child.getBottom() - LIST_TOP_OFFSET;
495            if (dist > LIST_TOP_OFFSET) {
496                mPreviousScrollState = scrollState;
497                if (Log.isLoggable(TAG, Log.DEBUG)) {
498                    Log.d(TAG, "scrolling by " + dist + " up? " + mIsScrollingUp);
499                }
500                if (mIsScrollingUp) {
501                    view.smoothScrollBy(dist - child.getHeight(), 500);
502                } else {
503                    view.smoothScrollBy(dist, 500);
504                }
505            }
506        }
507    }
508
509    @Override
510    public boolean onTouch(View v, MotionEvent event) {
511        return mGestureDetector.onTouchEvent(event);
512        // TODO post a cleanup to push us back onto the grid if something went
513        // wrong in a scroll such as the user stopping the view but not
514        // scrolling
515    }
516
517}
518