MonthByWeekAdapter.java revision 09fbd8e9ef61f667c0f20d36fbf40e5a4479c8d9
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 android.content.Context; 20import android.content.res.Configuration; 21import android.os.Handler; 22import android.os.Message; 23import android.text.format.Time; 24import android.util.Log; 25import android.view.GestureDetector; 26import android.view.MotionEvent; 27import android.view.View; 28import android.view.ViewConfiguration; 29import android.view.ViewGroup; 30import android.widget.AbsListView.LayoutParams; 31 32import com.android.calendar.CalendarController; 33import com.android.calendar.CalendarController.EventType; 34import com.android.calendar.CalendarController.ViewType; 35import com.android.calendar.Event; 36import com.android.calendar.R; 37import com.android.calendar.Utils; 38 39import java.util.ArrayList; 40import java.util.HashMap; 41 42public class MonthByWeekAdapter extends SimpleWeeksAdapter implements View.OnLongClickListener { 43 private static final String TAG = "MonthByWeekAdapter"; 44 45 public static final String WEEK_PARAMS_IS_MINI = "mini_month"; 46 protected static int DEFAULT_QUERY_DAYS = 7 * 8; // 8 weeks 47 private static final long ANIMATE_TODAY_TIMEOUT = 1000; 48 49 protected CalendarController mController; 50 protected String mHomeTimeZone; 51 protected Time mTempTime; 52 protected Time mToday; 53 protected int mFirstJulianDay; 54 protected int mQueryDays; 55 protected boolean mIsMiniMonth = true; 56 protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE; 57 private final boolean mShowAgendaWithMonth; 58 59 protected ArrayList<ArrayList<Event>> mEventDayList = new ArrayList<ArrayList<Event>>(); 60 protected ArrayList<Event> mEvents = null; 61 62 private boolean mAnimateToday = false; 63 private long mAnimateTime = 0; 64 65 private Handler mEventDialogHandler; 66 67 MonthWeekEventsView mClickedView; 68 MonthWeekEventsView mSingleTapUpView; 69 70 float mClickedXLocation; // Used to find which day was clicked 71 long mClickTime; // Used to calculate minimum click animation time 72 // Used to insure minimal time for seeing the click animation before switching views 73 private static final int mOnTapDelay = 100; 74 // Minimal time for a down touch action before stating the click animation, this insures that 75 // there is no click animation on flings 76 private static int mOnDownDelay; 77 private static int mTotalClickDelay; 78 // Minimal distance to move the finger in order to cancel the click animation 79 private static float mMovedPixelToCancel; 80 81 public MonthByWeekAdapter(Context context, HashMap<String, Integer> params, Handler handler) { 82 super(context, params); 83 mEventDialogHandler = handler; 84 if (params.containsKey(WEEK_PARAMS_IS_MINI)) { 85 mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0; 86 } 87 mShowAgendaWithMonth = Utils.getConfigBool(context, R.bool.show_agenda_with_month); 88 ViewConfiguration vc = ViewConfiguration.get(context); 89 mOnDownDelay = ViewConfiguration.getTapTimeout(); 90 mMovedPixelToCancel = vc.getScaledTouchSlop(); 91 mTotalClickDelay = mOnDownDelay + mOnTapDelay; 92 } 93 94 public void animateToday() { 95 mAnimateToday = true; 96 mAnimateTime = System.currentTimeMillis(); 97 } 98 99 @Override 100 protected void init() { 101 super.init(); 102 mController = CalendarController.getInstance(mContext); 103 mHomeTimeZone = Utils.getTimeZone(mContext, null); 104 mSelectedDay.switchTimezone(mHomeTimeZone); 105 mToday = new Time(mHomeTimeZone); 106 mToday.setToNow(); 107 mTempTime = new Time(mHomeTimeZone); 108 } 109 110 private void updateTimeZones() { 111 mSelectedDay.timezone = mHomeTimeZone; 112 mSelectedDay.normalize(true); 113 mToday.timezone = mHomeTimeZone; 114 mToday.setToNow(); 115 mTempTime.switchTimezone(mHomeTimeZone); 116 } 117 118 @Override 119 public void setSelectedDay(Time selectedTime) { 120 mSelectedDay.set(selectedTime); 121 long millis = mSelectedDay.normalize(true); 122 mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( 123 Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek); 124 notifyDataSetChanged(); 125 } 126 127 public void setEvents(int firstJulianDay, int numDays, ArrayList<Event> events) { 128 if (mIsMiniMonth) { 129 if (Log.isLoggable(TAG, Log.ERROR)) { 130 Log.e(TAG, "Attempted to set events for mini view. Events only supported in full" 131 + " view."); 132 } 133 return; 134 } 135 mEvents = events; 136 mFirstJulianDay = firstJulianDay; 137 mQueryDays = numDays; 138 // Create a new list, this is necessary since the weeks are referencing 139 // pieces of the old list 140 ArrayList<ArrayList<Event>> eventDayList = new ArrayList<ArrayList<Event>>(); 141 for (int i = 0; i < numDays; i++) { 142 eventDayList.add(new ArrayList<Event>()); 143 } 144 145 if (events == null || events.size() == 0) { 146 if(Log.isLoggable(TAG, Log.DEBUG)) { 147 Log.d(TAG, "No events. Returning early--go schedule something fun."); 148 } 149 mEventDayList = eventDayList; 150 refresh(); 151 return; 152 } 153 154 // Compute the new set of days with events 155 for (Event event : events) { 156 int startDay = event.startDay - mFirstJulianDay; 157 int endDay = event.endDay - mFirstJulianDay + 1; 158 if (startDay < numDays || endDay >= 0) { 159 if (startDay < 0) { 160 startDay = 0; 161 } 162 if (startDay > numDays) { 163 continue; 164 } 165 if (endDay < 0) { 166 continue; 167 } 168 if (endDay > numDays) { 169 endDay = numDays; 170 } 171 for (int j = startDay; j < endDay; j++) { 172 eventDayList.get(j).add(event); 173 } 174 } 175 } 176 if(Log.isLoggable(TAG, Log.DEBUG)) { 177 Log.d(TAG, "Processed " + events.size() + " events."); 178 } 179 mEventDayList = eventDayList; 180 refresh(); 181 } 182 183 @SuppressWarnings("unchecked") 184 @Override 185 public View getView(int position, View convertView, ViewGroup parent) { 186 if (mIsMiniMonth) { 187 return super.getView(position, convertView, parent); 188 } 189 MonthWeekEventsView v; 190 LayoutParams params = new LayoutParams( 191 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 192 HashMap<String, Integer> drawingParams = null; 193 boolean isAnimatingToday = false; 194 if (convertView != null) { 195 v = (MonthWeekEventsView) convertView; 196 // Checking updateToday uses the current params instead of the new 197 // params, so this is assuming the view is relatively stable 198 if (mAnimateToday && v.updateToday(mSelectedDay.timezone)) { 199 long currentTime = System.currentTimeMillis(); 200 // If it's been too long since we tried to start the animation 201 // don't show it. This can happen if the user stops a scroll 202 // before reaching today. 203 if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) { 204 mAnimateToday = false; 205 mAnimateTime = 0; 206 } else { 207 isAnimatingToday = true; 208 // There is a bug that causes invalidates to not work some 209 // of the time unless we recreate the view. 210 v = new MonthWeekEventsView(mContext); 211 } 212 } else { 213 drawingParams = (HashMap<String, Integer>) v.getTag(); 214 } 215 } else { 216 v = new MonthWeekEventsView(mContext); 217 v.setOnLongClickListener(this); 218 } 219 if (drawingParams == null) { 220 drawingParams = new HashMap<String, Integer>(); 221 } 222 drawingParams.clear(); 223 224 v.setLayoutParams(params); 225 v.setClickable(true); 226 v.setOnTouchListener(this); 227 228 int selectedDay = -1; 229 if (mSelectedWeek == position) { 230 selectedDay = mSelectedDay.weekDay; 231 } 232 233 drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT, 234 (parent.getHeight() + parent.getTop()) / mNumWeeks); 235 drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay); 236 drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0); 237 drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek); 238 drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek); 239 drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position); 240 drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth); 241 drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation); 242 243 if (isAnimatingToday) { 244 drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1); 245 mAnimateToday = false; 246 } 247 248 v.setWeekParams(drawingParams, mSelectedDay.timezone); 249 sendEventsToView(v); 250 return v; 251 } 252 253 private void sendEventsToView(MonthWeekEventsView v) { 254 if (mEventDayList.size() == 0) { 255 if (Log.isLoggable(TAG, Log.DEBUG)) { 256 Log.d(TAG, "No events loaded, did not pass any events to view."); 257 } 258 v.setEvents(null, null); 259 return; 260 } 261 int viewJulianDay = v.getFirstJulianDay(); 262 int start = viewJulianDay - mFirstJulianDay; 263 int end = start + v.mNumDays; 264 if (start < 0 || end > mEventDayList.size()) { 265 if (Log.isLoggable(TAG, Log.DEBUG)) { 266 Log.d(TAG, "Week is outside range of loaded events. viewStart: " + viewJulianDay 267 + " eventsStart: " + mFirstJulianDay); 268 } 269 v.setEvents(null, null); 270 return; 271 } 272 v.setEvents(mEventDayList.subList(start, end), mEvents); 273 } 274 275 @Override 276 protected void refresh() { 277 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 278 mShowWeekNumber = Utils.getShowWeekNumber(mContext); 279 mHomeTimeZone = Utils.getTimeZone(mContext, null); 280 mOrientation = mContext.getResources().getConfiguration().orientation; 281 updateTimeZones(); 282 notifyDataSetChanged(); 283 } 284 285 @Override 286 protected void onDayTapped(Time day) { 287 setDayParameters(day); 288 if (mShowAgendaWithMonth || mIsMiniMonth) { 289 // If agenda view is visible with month view , refresh the views 290 // with the selected day's info 291 mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, 292 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 293 } else { 294 // Else , switch to the detailed view 295 mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, 296 ViewType.DETAIL, 297 CalendarController.EXTRA_GOTO_DATE 298 | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); 299 } 300 } 301 302 private void setDayParameters(Time day) { 303 day.timezone = mHomeTimeZone; 304 Time currTime = new Time(mHomeTimeZone); 305 currTime.set(mController.getTime()); 306 day.hour = currTime.hour; 307 day.minute = currTime.minute; 308 day.allDay = false; 309 day.normalize(true); 310 } 311 312 @Override 313 public boolean onTouch(View v, MotionEvent event) { 314 if (!(v instanceof MonthWeekEventsView)) { 315 return super.onTouch(v, event); 316 } 317 318 int action = event.getAction(); 319 320 // Event was tapped - switch to the detailed view making sure the click animation 321 // is done first. 322 if (mGestureDetector.onTouchEvent(event)) { 323 mSingleTapUpView = (MonthWeekEventsView) v; 324 long delay = System.currentTimeMillis() - mClickTime; 325 // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms 326 mListView.postDelayed(mDoSingleTapUp, 327 delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay); 328 return true; 329 } else { 330 // Animate a click - on down: show the selected day in the "clicked" color. 331 // On Up/scroll/move/cancel: hide the "clicked" color. 332 switch (action) { 333 case MotionEvent.ACTION_DOWN: 334 mClickedView = (MonthWeekEventsView)v; 335 mClickedXLocation = event.getX(); 336 mClickTime = System.currentTimeMillis(); 337 mListView.postDelayed(mDoClick, mOnDownDelay); 338 break; 339 case MotionEvent.ACTION_UP: 340 case MotionEvent.ACTION_SCROLL: 341 case MotionEvent.ACTION_CANCEL: 342 clearClickedView((MonthWeekEventsView)v); 343 break; 344 case MotionEvent.ACTION_MOVE: 345 // No need to cancel on vertical movement, ACTION_SCROLL will do that. 346 if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { 347 clearClickedView((MonthWeekEventsView)v); 348 } 349 break; 350 default: 351 break; 352 } 353 } 354 // Do not tell the frameworks we consumed the touch action so that fling actions can be 355 // processed by the fragment. 356 return false; 357 } 358 359 /** 360 * This is here so we can identify events and process them 361 */ 362 protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 363 @Override 364 public boolean onSingleTapUp(MotionEvent e) { 365 return true; 366 } 367 } 368 369 // Clear the visual cues of the click animation and related running code. 370 private void clearClickedView(MonthWeekEventsView v) { 371 mListView.removeCallbacks(mDoClick); 372 synchronized(v) { 373 v.clearClickedDay(); 374 } 375 mClickedView = null; 376 } 377 378 // Perform the tap animation in a runnable to allow a delay before showing the tap color. 379 // This is done to prevent a click animation when a fling is done. 380 private final Runnable mDoClick = new Runnable() { 381 @Override 382 public void run() { 383 if (mClickedView != null) { 384 synchronized(mClickedView) { 385 mClickedView.setClickedDay(mClickedXLocation); 386 } 387 mClickedView = null; 388 // This is a workaround , sometimes the top item on the listview doesn't refresh on 389 // invalidate, so this forces a re-draw. 390 mListView.invalidate(); 391 } 392 } 393 }; 394 395 // Performs the single tap operation: go to the tapped day. 396 // This is done in a runnable to allow the click animation to finish before switching views 397 private final Runnable mDoSingleTapUp = new Runnable() { 398 @Override 399 public void run() { 400 if (mSingleTapUpView != null) { 401 Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation); 402 if (Log.isLoggable(TAG, Log.DEBUG)) { 403 Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString()); 404 } 405 if (day != null) { 406 onDayTapped(day); 407 } 408 clearClickedView(mSingleTapUpView); 409 mSingleTapUpView = null; 410 } 411 } 412 }; 413 414 @Override 415 public boolean onLongClick(View v) { 416 MonthWeekEventsView weekView = (MonthWeekEventsView) v; 417 Time day = weekView.getDayFromLocation(mClickedXLocation); 418 Message message = new Message(); 419 message.obj = day; 420 mEventDialogHandler.sendMessage(message); 421 return true; 422 } 423} 424