1/*
2 * Copyright (C) 2012 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 android.content.Context;
20import android.graphics.Rect;
21import android.os.SystemClock;
22import android.text.format.Time;
23import android.util.AttributeSet;
24import android.view.MotionEvent;
25import android.view.VelocityTracker;
26import android.view.View;
27import android.widget.ListView;
28
29import com.android.calendar.Utils;
30
31public class MonthListView extends ListView {
32
33    private static final String TAG = "MonthListView";
34    VelocityTracker mTracker;
35    private static float mScale = 0;
36
37    // These define the behavior of the fling. Below MIN_VELOCITY_FOR_FLING, do the system fling
38    // behavior. Between MIN_VELOCITY_FOR_FLING and MULTIPLE_MONTH_VELOCITY_THRESHOLD, do one month
39    // fling. Above MULTIPLE_MONTH_VELOCITY_THRESHOLD, do multiple month flings according to the
40    // fling strength. When doing multiple month fling, the velocity is reduced by this threshold
41    // to prevent moving from one month fling to 4 months and above flings.
42    private static int MIN_VELOCITY_FOR_FLING = 1500;
43    private static int MULTIPLE_MONTH_VELOCITY_THRESHOLD = 2000;
44    private static int FLING_VELOCITY_DIVIDER = 500;
45    private static int FLING_TIME = 1000;
46
47    // disposable variable used for time calculations
48    protected Time mTempTime;
49    private long mDownActionTime;
50    private final Rect mFirstViewRect = new Rect();
51
52    Context mListContext;
53
54    // Updates the time zone when it changes
55    private final Runnable mTimezoneUpdater = new Runnable() {
56        @Override
57        public void run() {
58            if (mTempTime != null && mListContext != null) {
59                mTempTime.timezone =
60                        Utils.getTimeZone(mListContext, mTimezoneUpdater);
61            }
62        }
63    };
64
65    public MonthListView(Context context) {
66        super(context);
67        init(context);
68    }
69
70    public MonthListView(Context context, AttributeSet attrs, int defStyle) {
71        super(context, attrs, defStyle);
72        init(context);
73    }
74
75    public MonthListView(Context context, AttributeSet attrs) {
76        super(context, attrs);
77        init(context);
78    }
79
80    private void init(Context c) {
81        mListContext = c;
82        mTracker  = VelocityTracker.obtain();
83        mTempTime = new Time(Utils.getTimeZone(c,mTimezoneUpdater));
84        if (mScale == 0) {
85            mScale = c.getResources().getDisplayMetrics().density;
86            if (mScale != 1) {
87                MIN_VELOCITY_FOR_FLING *= mScale;
88                MULTIPLE_MONTH_VELOCITY_THRESHOLD *= mScale;
89                FLING_VELOCITY_DIVIDER *= mScale;
90            }
91        }
92    }
93
94    @Override
95    public boolean onTouchEvent(MotionEvent ev) {
96        return processEvent(ev) || super.onTouchEvent(ev);
97    }
98
99    @Override
100    public boolean onInterceptTouchEvent(MotionEvent ev) {
101        return processEvent(ev) || super.onInterceptTouchEvent(ev);
102    }
103
104    private boolean processEvent (MotionEvent ev) {
105        switch (ev.getAction() & MotionEvent.ACTION_MASK) {
106            // Since doFling sends a cancel, make sure not to process it.
107            case MotionEvent.ACTION_CANCEL:
108                return false;
109            // Start tracking movement velocity
110            case MotionEvent.ACTION_DOWN:
111                mTracker.clear();
112                mDownActionTime = SystemClock.uptimeMillis();
113                break;
114            // Accumulate velocity and do a custom fling when above threshold
115            case MotionEvent.ACTION_UP:
116                mTracker.addMovement(ev);
117                mTracker.computeCurrentVelocity(1000);    // in pixels per second
118                float vel =  mTracker.getYVelocity ();
119                if (Math.abs(vel) > MIN_VELOCITY_FOR_FLING) {
120                    doFling(vel);
121                    return true;
122                }
123                break;
124            default:
125                 mTracker.addMovement(ev);
126                 break;
127        }
128        return false;
129    }
130
131    // Do a "snap to start of month" fling
132    private void doFling(float velocityY) {
133
134        // Stop the list-view movement and take over
135        MotionEvent cancelEvent = MotionEvent.obtain(mDownActionTime,  SystemClock.uptimeMillis(),
136                MotionEvent.ACTION_CANCEL, 0, 0, 0);
137        onTouchEvent(cancelEvent);
138
139        // Below the threshold, fling one month. Above the threshold , fling
140        // according to the speed of the fling.
141        int monthsToJump;
142        if (Math.abs(velocityY) < MULTIPLE_MONTH_VELOCITY_THRESHOLD) {
143            if (velocityY < 0) {
144                monthsToJump = 1;
145            } else {
146                // value here is zero and not -1 since by the time the fling is
147                // detected the list moved back one month.
148                monthsToJump = 0;
149            }
150        } else {
151            if (velocityY < 0) {
152                monthsToJump = 1 - (int) ((velocityY + MULTIPLE_MONTH_VELOCITY_THRESHOLD)
153                        / FLING_VELOCITY_DIVIDER);
154            } else {
155                monthsToJump = -(int) ((velocityY - MULTIPLE_MONTH_VELOCITY_THRESHOLD)
156                        / FLING_VELOCITY_DIVIDER);
157            }
158        }
159
160        // Get the day at the top right corner
161        int day = getUpperRightJulianDay();
162        // Get the day of the first day of the next/previous month
163        // (according to scroll direction)
164        mTempTime.setJulianDay(day);
165        mTempTime.monthDay = 1;
166        mTempTime.month += monthsToJump;
167        long timeInMillis = mTempTime.normalize(false);
168        // Since each view is 7 days, round the target day up to make sure the
169        // scroll will be  at least one view.
170        int scrollToDay = Time.getJulianDay(timeInMillis, mTempTime.gmtoff)
171                + ((monthsToJump > 0) ? 6 : 0);
172
173        // Since all views have the same height, scroll by pixels instead of
174        // "to position".
175        // Compensate for the top view offset from the top.
176        View firstView = getChildAt(0);
177        int firstViewHeight = firstView.getHeight();
178        // Get visible part length
179        firstView.getLocalVisibleRect(mFirstViewRect);
180        int topViewVisiblePart = mFirstViewRect.bottom - mFirstViewRect.top;
181        int viewsToFling = (scrollToDay - day) / 7 - ((monthsToJump <= 0) ? 1 : 0);
182        int offset = (viewsToFling > 0) ? -(firstViewHeight - topViewVisiblePart
183                + SimpleDayPickerFragment.LIST_TOP_OFFSET) : (topViewVisiblePart
184                - SimpleDayPickerFragment.LIST_TOP_OFFSET);
185        // Fling
186        smoothScrollBy(viewsToFling * firstViewHeight + offset, FLING_TIME);
187    }
188
189    // Returns the julian day of the day in the upper right corner
190    private int getUpperRightJulianDay() {
191        SimpleWeekView child = (SimpleWeekView) getChildAt(0);
192        if (child == null) {
193            return -1;
194        }
195        return child.getFirstJulianDay() + SimpleDayPickerFragment.DAYS_PER_WEEK - 1;
196    }
197}
198