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 
17 package com.android.calendar.month;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.os.SystemClock;
22 import android.text.format.Time;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.view.VelocityTracker;
26 import android.view.View;
27 import android.widget.ListView;
28 
29 import com.android.calendar.Utils;
30 
31 public 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 
MonthListView(Context context)65     public MonthListView(Context context) {
66         super(context);
67         init(context);
68     }
69 
MonthListView(Context context, AttributeSet attrs, int defStyle)70     public MonthListView(Context context, AttributeSet attrs, int defStyle) {
71         super(context, attrs, defStyle);
72         init(context);
73     }
74 
MonthListView(Context context, AttributeSet attrs)75     public MonthListView(Context context, AttributeSet attrs) {
76         super(context, attrs);
77         init(context);
78     }
79 
init(Context c)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
onTouchEvent(MotionEvent ev)95     public boolean onTouchEvent(MotionEvent ev) {
96         return processEvent(ev) || super.onTouchEvent(ev);
97     }
98 
99     @Override
onInterceptTouchEvent(MotionEvent ev)100     public boolean onInterceptTouchEvent(MotionEvent ev) {
101         return processEvent(ev) || super.onInterceptTouchEvent(ev);
102     }
103 
processEvent(MotionEvent ev)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
doFling(float velocityY)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
getUpperRightJulianDay()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