MonthByWeekAdapter.java revision fac4eef32ec496aeb866cc82648d369e1c443b57
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 v.setOnLongClickListener(this); 212 } 213 } else { 214 drawingParams = (HashMap<String, Integer>) v.getTag(); 215 } 216 } else { 217 v = new MonthWeekEventsView(mContext); 218 v.setOnLongClickListener(this); 219 } 220 if (drawingParams == null) { 221 drawingParams = new HashMap<String, Integer>(); 222 } 223 drawingParams.clear(); 224 225 v.setLayoutParams(params); 226 v.setClickable(true); 227 v.setOnTouchListener(this); 228 229 int selectedDay = -1; 230 if (mSelectedWeek == position) { 231 selectedDay = mSelectedDay.weekDay; 232 } 233 234 drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT, 235 (parent.getHeight() + parent.getTop()) / mNumWeeks); 236 drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay); 237 drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0); 238 drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek); 239 drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek); 240 drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position); 241 drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth); 242 drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation); 243 244 if (isAnimatingToday) { 245 drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1); 246 mAnimateToday = false; 247 } 248 249 v.setWeekParams(drawingParams, mSelectedDay.timezone); 250 sendEventsToView(v); 251 return v; 252 } 253 254 private void sendEventsToView(MonthWeekEventsView v) { 255 if (mEventDayList.size() == 0) { 256 if (Log.isLoggable(TAG, Log.DEBUG)) { 257 Log.d(TAG, "No events loaded, did not pass any events to view."); 258 } 259 v.setEvents(null, null); 260 return; 261 } 262 int viewJulianDay = v.getFirstJulianDay(); 263 int start = viewJulianDay - mFirstJulianDay; 264 int end = start + v.mNumDays; 265 if (start < 0 || end > mEventDayList.size()) { 266 if (Log.isLoggable(TAG, Log.DEBUG)) { 267 Log.d(TAG, "Week is outside range of loaded events. viewStart: " + viewJulianDay 268 + " eventsStart: " + mFirstJulianDay); 269 } 270 v.setEvents(null, null); 271 return; 272 } 273 v.setEvents(mEventDayList.subList(start, end), mEvents); 274 } 275 276 @Override 277 protected void refresh() { 278 mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); 279 mShowWeekNumber = Utils.getShowWeekNumber(mContext); 280 mHomeTimeZone = Utils.getTimeZone(mContext, null); 281 mOrientation = mContext.getResources().getConfiguration().orientation; 282 updateTimeZones(); 283 notifyDataSetChanged(); 284 } 285 286 @Override 287 protected void onDayTapped(Time day) { 288 setDayParameters(day); 289 if (mShowAgendaWithMonth || mIsMiniMonth) { 290 // If agenda view is visible with month view , refresh the views 291 // with the selected day's info 292 mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, 293 ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); 294 } else { 295 // Else , switch to the detailed view 296 mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, 297 ViewType.DETAIL, 298 CalendarController.EXTRA_GOTO_DATE 299 | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); 300 } 301 } 302 303 private void setDayParameters(Time day) { 304 day.timezone = mHomeTimeZone; 305 Time currTime = new Time(mHomeTimeZone); 306 currTime.set(mController.getTime()); 307 day.hour = currTime.hour; 308 day.minute = currTime.minute; 309 day.allDay = false; 310 day.normalize(true); 311 } 312 313 @Override 314 public boolean onTouch(View v, MotionEvent event) { 315 if (!(v instanceof MonthWeekEventsView)) { 316 return super.onTouch(v, event); 317 } 318 319 int action = event.getAction(); 320 321 // Event was tapped - switch to the detailed view making sure the click animation 322 // is done first. 323 if (mGestureDetector.onTouchEvent(event)) { 324 mSingleTapUpView = (MonthWeekEventsView) v; 325 long delay = System.currentTimeMillis() - mClickTime; 326 // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms 327 mListView.postDelayed(mDoSingleTapUp, 328 delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay); 329 return true; 330 } else { 331 // Animate a click - on down: show the selected day in the "clicked" color. 332 // On Up/scroll/move/cancel: hide the "clicked" color. 333 switch (action) { 334 case MotionEvent.ACTION_DOWN: 335 mClickedView = (MonthWeekEventsView)v; 336 mClickedXLocation = event.getX(); 337 mClickTime = System.currentTimeMillis(); 338 mListView.postDelayed(mDoClick, mOnDownDelay); 339 break; 340 case MotionEvent.ACTION_UP: 341 case MotionEvent.ACTION_SCROLL: 342 case MotionEvent.ACTION_CANCEL: 343 clearClickedView((MonthWeekEventsView)v); 344 break; 345 case MotionEvent.ACTION_MOVE: 346 // No need to cancel on vertical movement, ACTION_SCROLL will do that. 347 if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { 348 clearClickedView((MonthWeekEventsView)v); 349 } 350 break; 351 default: 352 break; 353 } 354 } 355 // Do not tell the frameworks we consumed the touch action so that fling actions can be 356 // processed by the fragment. 357 return false; 358 } 359 360 /** 361 * This is here so we can identify events and process them 362 */ 363 protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 364 @Override 365 public boolean onSingleTapUp(MotionEvent e) { 366 return true; 367 } 368 } 369 370 // Clear the visual cues of the click animation and related running code. 371 private void clearClickedView(MonthWeekEventsView v) { 372 mListView.removeCallbacks(mDoClick); 373 synchronized(v) { 374 v.clearClickedDay(); 375 } 376 mClickedView = null; 377 } 378 379 // Perform the tap animation in a runnable to allow a delay before showing the tap color. 380 // This is done to prevent a click animation when a fling is done. 381 private final Runnable mDoClick = new Runnable() { 382 @Override 383 public void run() { 384 if (mClickedView != null) { 385 synchronized(mClickedView) { 386 mClickedView.setClickedDay(mClickedXLocation); 387 } 388 mClickedView = null; 389 // This is a workaround , sometimes the top item on the listview doesn't refresh on 390 // invalidate, so this forces a re-draw. 391 mListView.invalidate(); 392 } 393 } 394 }; 395 396 // Performs the single tap operation: go to the tapped day. 397 // This is done in a runnable to allow the click animation to finish before switching views 398 private final Runnable mDoSingleTapUp = new Runnable() { 399 @Override 400 public void run() { 401 if (mSingleTapUpView != null) { 402 Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation); 403 if (Log.isLoggable(TAG, Log.DEBUG)) { 404 Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString()); 405 } 406 if (day != null) { 407 onDayTapped(day); 408 } 409 clearClickedView(mSingleTapUpView); 410 mSingleTapUpView = null; 411 } 412 } 413 }; 414 415 @Override 416 public boolean onLongClick(View v) { 417 MonthWeekEventsView weekView = (MonthWeekEventsView) v; 418 Time day = weekView.getDayFromLocation(mClickedXLocation); 419 if (day == null) { 420 // The day that was clicked is malformed, so just die here. 421 // Return true so nothing else tries to use the malformed day. 422 return true; 423 } 424 Message message = new Message(); 425 message.obj = day; 426 mEventDialogHandler.sendMessage(message); 427 return true; 428 } 429} 430