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 17 package com.android.calendar.month; 18 19 import com.android.calendar.Event; 20 import com.android.calendar.R; 21 import com.android.calendar.Utils; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ObjectAnimator; 26 import android.app.Service; 27 import android.content.Context; 28 import android.content.res.Configuration; 29 import android.content.res.Resources; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.Paint; 33 import android.graphics.Paint.Align; 34 import android.graphics.Paint.Style; 35 import android.graphics.Typeface; 36 import android.graphics.drawable.Drawable; 37 import android.provider.CalendarContract.Attendees; 38 import android.text.TextPaint; 39 import android.text.TextUtils; 40 import android.text.format.DateFormat; 41 import android.text.format.DateUtils; 42 import android.text.format.Time; 43 import android.util.Log; 44 import android.view.MotionEvent; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.accessibility.AccessibilityManager; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Formatter; 51 import java.util.HashMap; 52 import java.util.Iterator; 53 import java.util.List; 54 import java.util.Locale; 55 56 public class MonthWeekEventsView extends SimpleWeekView { 57 58 private static final String TAG = "MonthView"; 59 60 private static final boolean DEBUG_LAYOUT = false; 61 62 public static final String VIEW_PARAMS_ORIENTATION = "orientation"; 63 public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today"; 64 65 /* NOTE: these are not constants, and may be multiplied by a scale factor */ 66 private static int TEXT_SIZE_MONTH_NUMBER = 32; 67 private static int TEXT_SIZE_EVENT = 12; 68 private static int TEXT_SIZE_EVENT_TITLE = 14; 69 private static int TEXT_SIZE_MORE_EVENTS = 12; 70 private static int TEXT_SIZE_MONTH_NAME = 14; 71 private static int TEXT_SIZE_WEEK_NUM = 12; 72 73 private static int DNA_MARGIN = 4; 74 private static int DNA_ALL_DAY_HEIGHT = 4; 75 private static int DNA_MIN_SEGMENT_HEIGHT = 4; 76 private static int DNA_WIDTH = 8; 77 private static int DNA_ALL_DAY_WIDTH = 32; 78 private static int DNA_SIDE_PADDING = 6; 79 private static int CONFLICT_COLOR = Color.BLACK; 80 private static int EVENT_TEXT_COLOR = Color.WHITE; 81 82 private static int DEFAULT_EDGE_SPACING = 0; 83 private static int SIDE_PADDING_MONTH_NUMBER = 4; 84 private static int TOP_PADDING_MONTH_NUMBER = 4; 85 private static int TOP_PADDING_WEEK_NUMBER = 4; 86 private static int SIDE_PADDING_WEEK_NUMBER = 20; 87 private static int DAY_SEPARATOR_OUTER_WIDTH = 0; 88 private static int DAY_SEPARATOR_INNER_WIDTH = 1; 89 private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53; 90 private static int DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT = 64; 91 private static int MIN_WEEK_WIDTH = 50; 92 93 private static int EVENT_X_OFFSET_LANDSCAPE = 38; 94 private static int EVENT_Y_OFFSET_LANDSCAPE = 8; 95 private static int EVENT_Y_OFFSET_PORTRAIT = 7; 96 private static int EVENT_SQUARE_WIDTH = 10; 97 private static int EVENT_SQUARE_BORDER = 2; 98 private static int EVENT_LINE_PADDING = 2; 99 private static int EVENT_RIGHT_PADDING = 4; 100 private static int EVENT_BOTTOM_PADDING = 3; 101 102 private static int TODAY_HIGHLIGHT_WIDTH = 2; 103 104 private static int SPACING_WEEK_NUMBER = 24; 105 private static boolean mInitialized = false; 106 private static boolean mShowDetailsInMonth; 107 108 protected Time mToday = new Time(); 109 protected boolean mHasToday = false; 110 protected int mTodayIndex = -1; 111 protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE; 112 protected List<ArrayList<Event>> mEvents = null; 113 protected ArrayList<Event> mUnsortedEvents = null; 114 HashMap<Integer, Utils.DNAStrand> mDna = null; 115 // This is for drawing the outlines around event chips and supports up to 10 116 // events being drawn on each day. The code will expand this if necessary. 117 protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7); 118 119 120 121 protected static StringBuilder mStringBuilder = new StringBuilder(50); 122 // TODO recreate formatter when locale changes 123 protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 124 125 protected Paint mMonthNamePaint; 126 protected TextPaint mEventPaint; 127 protected TextPaint mSolidBackgroundEventPaint; 128 protected TextPaint mFramedEventPaint; 129 protected TextPaint mDeclinedEventPaint; 130 protected TextPaint mEventExtrasPaint; 131 protected TextPaint mEventDeclinedExtrasPaint; 132 protected Paint mWeekNumPaint; 133 protected Paint mDNAAllDayPaint; 134 protected Paint mDNATimePaint; 135 protected Paint mEventSquarePaint; 136 137 138 protected Drawable mTodayDrawable; 139 140 protected int mMonthNumHeight; 141 protected int mMonthNumAscentHeight; 142 protected int mEventHeight; 143 protected int mEventAscentHeight; 144 protected int mExtrasHeight; 145 protected int mExtrasAscentHeight; 146 protected int mExtrasDescent; 147 protected int mWeekNumAscentHeight; 148 149 protected int mMonthBGColor; 150 protected int mMonthBGOtherColor; 151 protected int mMonthBGTodayColor; 152 protected int mMonthNumColor; 153 protected int mMonthNumOtherColor; 154 protected int mMonthNumTodayColor; 155 protected int mMonthNameColor; 156 protected int mMonthNameOtherColor; 157 protected int mMonthEventColor; 158 protected int mMonthDeclinedEventColor; 159 protected int mMonthDeclinedExtrasColor; 160 protected int mMonthEventExtraColor; 161 protected int mMonthEventOtherColor; 162 protected int mMonthEventExtraOtherColor; 163 protected int mMonthWeekNumColor; 164 protected int mMonthBusyBitsBgColor; 165 protected int mMonthBusyBitsBusyTimeColor; 166 protected int mMonthBusyBitsConflictTimeColor; 167 private int mClickedDayIndex = -1; 168 private int mClickedDayColor; 169 private static final int mClickedAlpha = 128; 170 171 protected int mEventChipOutlineColor = 0xFFFFFFFF; 172 protected int mDaySeparatorInnerColor; 173 protected int mTodayAnimateColor; 174 175 private boolean mAnimateToday; 176 private int mAnimateTodayAlpha = 0; 177 private ObjectAnimator mTodayAnimator = null; 178 179 private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener(); 180 181 class TodayAnimatorListener extends AnimatorListenerAdapter { 182 private volatile Animator mAnimator = null; 183 private volatile boolean mFadingIn = false; 184 185 @Override 186 public void onAnimationEnd(Animator animation) { 187 synchronized (this) { 188 if (mAnimator != animation) { 189 animation.removeAllListeners(); 190 animation.cancel(); 191 return; 192 } 193 if (mFadingIn) { 194 if (mTodayAnimator != null) { 195 mTodayAnimator.removeAllListeners(); 196 mTodayAnimator.cancel(); 197 } 198 mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this, 199 "animateTodayAlpha", 255, 0); 200 mAnimator = mTodayAnimator; 201 mFadingIn = false; 202 mTodayAnimator.addListener(this); 203 mTodayAnimator.setDuration(600); 204 mTodayAnimator.start(); 205 } else { 206 mAnimateToday = false; 207 mAnimateTodayAlpha = 0; 208 mAnimator.removeAllListeners(); 209 mAnimator = null; 210 mTodayAnimator = null; 211 invalidate(); 212 } 213 } 214 } 215 216 public void setAnimator(Animator animation) { 217 mAnimator = animation; 218 } 219 220 public void setFadingIn(boolean fadingIn) { 221 mFadingIn = fadingIn; 222 } 223 224 } 225 226 private int[] mDayXs; 227 228 /** 229 * This provides a reference to a float array which allows for easy size 230 * checking and reallocation. Used for drawing lines. 231 */ 232 private class FloatRef { 233 float[] array; 234 235 public FloatRef(int size) { 236 array = new float[size]; 237 } 238 239 public void ensureSize(int newSize) { 240 if (newSize >= array.length) { 241 // Add enough space for 7 more boxes to be drawn 242 array = Arrays.copyOf(array, newSize + 16 * 7); 243 } 244 } 245 } 246 247 /** 248 * Shows up as an error if we don't include this. 249 */ 250 public MonthWeekEventsView(Context context) { 251 super(context); 252 } 253 254 // Sets the list of events for this week. Takes a sorted list of arrays 255 // divided up by day for generating the large month version and the full 256 // arraylist sorted by start time to generate the dna version. 257 public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) { 258 setEvents(sortedEvents); 259 // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to 260 // generate dna bits before its width has been fixed. 261 createDna(unsortedEvents); 262 } 263 264 /** 265 * Sets up the dna bits for the view. This will return early if the view 266 * isn't in a state that will create a valid set of dna yet (such as the 267 * views width not being set correctly yet). 268 */ 269 public void createDna(ArrayList<Event> unsortedEvents) { 270 if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) { 271 // Stash the list of events for use when this view is ready, or 272 // just clear it if a null set has been passed to this view 273 mUnsortedEvents = unsortedEvents; 274 mDna = null; 275 return; 276 } else { 277 // clear the cached set of events since we're ready to build it now 278 mUnsortedEvents = null; 279 } 280 // Create the drawing coordinates for dna 281 if (!mShowDetailsInMonth) { 282 int numDays = mEvents.size(); 283 int effectiveWidth = mWidth - mPadding * 2; 284 if (mShowWeekNum) { 285 effectiveWidth -= SPACING_WEEK_NUMBER; 286 } 287 DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING; 288 mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH); 289 mDayXs = new int[numDays]; 290 for (int day = 0; day < numDays; day++) { 291 mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING; 292 293 } 294 295 int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1; 296 int bottom = mHeight - DNA_MARGIN; 297 mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom, 298 DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext()); 299 } 300 } 301 302 public void setEvents(List<ArrayList<Event>> sortedEvents) { 303 mEvents = sortedEvents; 304 if (sortedEvents == null) { 305 return; 306 } 307 if (sortedEvents.size() != mNumDays) { 308 if (Log.isLoggable(TAG, Log.ERROR)) { 309 Log.wtf(TAG, "Events size must be same as days displayed: size=" 310 + sortedEvents.size() + " days=" + mNumDays); 311 } 312 mEvents = null; 313 return; 314 } 315 } 316 317 protected void loadColors(Context context) { 318 Resources res = context.getResources(); 319 mMonthWeekNumColor = res.getColor(R.color.month_week_num_color); 320 mMonthNumColor = res.getColor(R.color.month_day_number); 321 mMonthNumOtherColor = res.getColor(R.color.month_day_number_other); 322 mMonthNumTodayColor = res.getColor(R.color.month_today_number); 323 mMonthNameColor = mMonthNumColor; 324 mMonthNameOtherColor = mMonthNumOtherColor; 325 mMonthEventColor = res.getColor(R.color.month_event_color); 326 mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color); 327 mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color); 328 mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color); 329 mMonthEventOtherColor = res.getColor(R.color.month_event_other_color); 330 mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color); 331 mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor); 332 mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor); 333 mMonthBGColor = res.getColor(R.color.month_bgcolor); 334 mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines); 335 mTodayAnimateColor = res.getColor(R.color.today_highlight_color); 336 mClickedDayColor = res.getColor(R.color.day_clicked_background_color); 337 mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light); 338 } 339 340 /** 341 * Sets up the text and style properties for painting. Override this if you 342 * want to use a different paint. 343 */ 344 @Override 345 protected void initView() { 346 super.initView(); 347 348 if (!mInitialized) { 349 Resources resources = getContext().getResources(); 350 mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month); 351 TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title); 352 TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number); 353 SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin); 354 CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color); 355 EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color); 356 if (mScale != 1) { 357 TOP_PADDING_MONTH_NUMBER *= mScale; 358 TOP_PADDING_WEEK_NUMBER *= mScale; 359 SIDE_PADDING_MONTH_NUMBER *= mScale; 360 SIDE_PADDING_WEEK_NUMBER *= mScale; 361 SPACING_WEEK_NUMBER *= mScale; 362 TEXT_SIZE_MONTH_NUMBER *= mScale; 363 TEXT_SIZE_EVENT *= mScale; 364 TEXT_SIZE_EVENT_TITLE *= mScale; 365 TEXT_SIZE_MORE_EVENTS *= mScale; 366 TEXT_SIZE_MONTH_NAME *= mScale; 367 TEXT_SIZE_WEEK_NUM *= mScale; 368 DAY_SEPARATOR_OUTER_WIDTH *= mScale; 369 DAY_SEPARATOR_INNER_WIDTH *= mScale; 370 DAY_SEPARATOR_VERTICAL_LENGTH *= mScale; 371 DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT *= mScale; 372 EVENT_X_OFFSET_LANDSCAPE *= mScale; 373 EVENT_Y_OFFSET_LANDSCAPE *= mScale; 374 EVENT_Y_OFFSET_PORTRAIT *= mScale; 375 EVENT_SQUARE_WIDTH *= mScale; 376 EVENT_SQUARE_BORDER *= mScale; 377 EVENT_LINE_PADDING *= mScale; 378 EVENT_BOTTOM_PADDING *= mScale; 379 EVENT_RIGHT_PADDING *= mScale; 380 DNA_MARGIN *= mScale; 381 DNA_WIDTH *= mScale; 382 DNA_ALL_DAY_HEIGHT *= mScale; 383 DNA_MIN_SEGMENT_HEIGHT *= mScale; 384 DNA_SIDE_PADDING *= mScale; 385 DEFAULT_EDGE_SPACING *= mScale; 386 DNA_ALL_DAY_WIDTH *= mScale; 387 TODAY_HIGHLIGHT_WIDTH *= mScale; 388 } 389 if (!mShowDetailsInMonth) { 390 TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN; 391 } 392 mInitialized = true; 393 } 394 mPadding = DEFAULT_EDGE_SPACING; 395 loadColors(getContext()); 396 // TODO modify paint properties depending on isMini 397 398 mMonthNumPaint = new Paint(); 399 mMonthNumPaint.setFakeBoldText(false); 400 mMonthNumPaint.setAntiAlias(true); 401 mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER); 402 mMonthNumPaint.setColor(mMonthNumColor); 403 mMonthNumPaint.setStyle(Style.FILL); 404 mMonthNumPaint.setTextAlign(Align.RIGHT); 405 mMonthNumPaint.setTypeface(Typeface.DEFAULT); 406 407 mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f); 408 mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f); 409 410 mEventPaint = new TextPaint(); 411 mEventPaint.setFakeBoldText(true); 412 mEventPaint.setAntiAlias(true); 413 mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE); 414 mEventPaint.setColor(mMonthEventColor); 415 416 mSolidBackgroundEventPaint = new TextPaint(mEventPaint); 417 mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR); 418 mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint); 419 420 mDeclinedEventPaint = new TextPaint(); 421 mDeclinedEventPaint.setFakeBoldText(true); 422 mDeclinedEventPaint.setAntiAlias(true); 423 mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE); 424 mDeclinedEventPaint.setColor(mMonthDeclinedEventColor); 425 426 mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f); 427 mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f); 428 429 mEventExtrasPaint = new TextPaint(); 430 mEventExtrasPaint.setFakeBoldText(false); 431 mEventExtrasPaint.setAntiAlias(true); 432 mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER); 433 mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT); 434 mEventExtrasPaint.setColor(mMonthEventExtraColor); 435 mEventExtrasPaint.setStyle(Style.FILL); 436 mEventExtrasPaint.setTextAlign(Align.LEFT); 437 mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f); 438 mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f); 439 mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f); 440 441 mEventDeclinedExtrasPaint = new TextPaint(); 442 mEventDeclinedExtrasPaint.setFakeBoldText(false); 443 mEventDeclinedExtrasPaint.setAntiAlias(true); 444 mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER); 445 mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT); 446 mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor); 447 mEventDeclinedExtrasPaint.setStyle(Style.FILL); 448 mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT); 449 450 mWeekNumPaint = new Paint(); 451 mWeekNumPaint.setFakeBoldText(false); 452 mWeekNumPaint.setAntiAlias(true); 453 mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM); 454 mWeekNumPaint.setColor(mWeekNumColor); 455 mWeekNumPaint.setStyle(Style.FILL); 456 mWeekNumPaint.setTextAlign(Align.RIGHT); 457 458 mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f); 459 460 mDNAAllDayPaint = new Paint(); 461 mDNATimePaint = new Paint(); 462 mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor); 463 mDNATimePaint.setStyle(Style.FILL_AND_STROKE); 464 mDNATimePaint.setStrokeWidth(DNA_WIDTH); 465 mDNATimePaint.setAntiAlias(false); 466 mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor); 467 mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE); 468 mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH); 469 mDNAAllDayPaint.setAntiAlias(false); 470 471 mEventSquarePaint = new Paint(); 472 mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER); 473 mEventSquarePaint.setAntiAlias(false); 474 475 if (DEBUG_LAYOUT) { 476 Log.d("EXTRA", "mScale=" + mScale); 477 Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent() 478 + " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight); 479 Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent() 480 + " descent=" + mEventPaint.descent() + " int height=" + mEventHeight 481 + " int ascent=" + mEventAscentHeight); 482 Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent() 483 + " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight); 484 Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent() 485 + " descent=" + mWeekNumPaint.descent()); 486 } 487 } 488 489 @Override 490 public void setWeekParams(HashMap<String, Integer> params, String tz) { 491 super.setWeekParams(params, tz); 492 493 if (params.containsKey(VIEW_PARAMS_ORIENTATION)) { 494 mOrientation = params.get(VIEW_PARAMS_ORIENTATION); 495 } 496 497 updateToday(tz); 498 mNumCells = mNumDays + 1; 499 500 if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) { 501 synchronized (mAnimatorListener) { 502 if (mTodayAnimator != null) { 503 mTodayAnimator.removeAllListeners(); 504 mTodayAnimator.cancel(); 505 } 506 mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", 507 Math.max(mAnimateTodayAlpha, 80), 255); 508 mTodayAnimator.setDuration(150); 509 mAnimatorListener.setAnimator(mTodayAnimator); 510 mAnimatorListener.setFadingIn(true); 511 mTodayAnimator.addListener(mAnimatorListener); 512 mAnimateToday = true; 513 mTodayAnimator.start(); 514 } 515 } 516 } 517 518 /** 519 * @param tz 520 */ 521 public boolean updateToday(String tz) { 522 mToday.timezone = tz; 523 mToday.setToNow(); 524 mToday.normalize(true); 525 int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff); 526 if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) { 527 mHasToday = true; 528 mTodayIndex = julianToday - mFirstJulianDay; 529 } else { 530 mHasToday = false; 531 mTodayIndex = -1; 532 } 533 return mHasToday; 534 } 535 536 public void setAnimateTodayAlpha(int alpha) { 537 mAnimateTodayAlpha = alpha; 538 invalidate(); 539 } 540 541 @Override 542 protected void onDraw(Canvas canvas) { 543 drawBackground(canvas); 544 drawWeekNums(canvas); 545 drawDaySeparators(canvas); 546 if (mHasToday && mAnimateToday) { 547 drawToday(canvas); 548 } 549 if (mShowDetailsInMonth) { 550 drawEvents(canvas); 551 } else { 552 if (mDna == null && mUnsortedEvents != null) { 553 createDna(mUnsortedEvents); 554 } 555 drawDNA(canvas); 556 } 557 drawClick(canvas); 558 } 559 560 protected void drawToday(Canvas canvas) { 561 r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2); 562 r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f); 563 p.setStyle(Style.STROKE); 564 p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH); 565 r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2); 566 r.right = computeDayLeftPosition(mTodayIndex + 1) 567 - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f); 568 p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24)); 569 canvas.drawRect(r, p); 570 p.setStyle(Style.FILL); 571 } 572 573 // TODO move into SimpleWeekView 574 // Computes the x position for the left side of the given day 575 private int computeDayLeftPosition(int day) { 576 int effectiveWidth = mWidth; 577 int x = 0; 578 int xOffset = 0; 579 if (mShowWeekNum) { 580 xOffset = SPACING_WEEK_NUMBER + mPadding; 581 effectiveWidth -= xOffset; 582 } 583 x = day * effectiveWidth / mNumDays + xOffset; 584 return x; 585 } 586 587 @Override 588 protected void drawDaySeparators(Canvas canvas) { 589 float lines[] = new float[8 * 4]; 590 int count = 6 * 4; 591 int wkNumOffset = 0; 592 int i = 0; 593 if (mShowWeekNum) { 594 // This adds the first line separating the week number 595 int xOffset = SPACING_WEEK_NUMBER + mPadding; 596 count += 4; 597 lines[i++] = xOffset; 598 lines[i++] = 0; 599 lines[i++] = xOffset; 600 lines[i++] = mHeight; 601 wkNumOffset++; 602 } 603 count += 4; 604 lines[i++] = 0; 605 lines[i++] = 0; 606 lines[i++] = mWidth; 607 lines[i++] = 0; 608 int y0 = 0; 609 int y1 = mHeight; 610 611 while (i < count) { 612 int x = computeDayLeftPosition(i / 4 - wkNumOffset); 613 lines[i++] = x; 614 lines[i++] = y0; 615 lines[i++] = x; 616 lines[i++] = y1; 617 } 618 p.setColor(mDaySeparatorInnerColor); 619 p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH); 620 canvas.drawLines(lines, 0, count, p); 621 } 622 623 @Override 624 protected void drawBackground(Canvas canvas) { 625 int i = 0; 626 int offset = 0; 627 r.top = DAY_SEPARATOR_INNER_WIDTH; 628 r.bottom = mHeight; 629 if (mShowWeekNum) { 630 i++; 631 offset++; 632 } 633 if (!mOddMonth[i]) { 634 while (++i < mOddMonth.length && !mOddMonth[i]) 635 ; 636 r.right = computeDayLeftPosition(i - offset); 637 r.left = 0; 638 p.setColor(mMonthBGOtherColor); 639 canvas.drawRect(r, p); 640 // compute left edge for i, set up r, draw 641 } else if (!mOddMonth[(i = mOddMonth.length - 1)]) { 642 while (--i >= offset && !mOddMonth[i]) 643 ; 644 i++; 645 // compute left edge for i, set up r, draw 646 r.right = mWidth; 647 r.left = computeDayLeftPosition(i - offset); 648 p.setColor(mMonthBGOtherColor); 649 canvas.drawRect(r, p); 650 } 651 if (mHasToday) { 652 p.setColor(mMonthBGTodayColor); 653 r.left = computeDayLeftPosition(mTodayIndex); 654 r.right = computeDayLeftPosition(mTodayIndex + 1); 655 canvas.drawRect(r, p); 656 } 657 } 658 659 // Draw the "clicked" color on the tapped day 660 private void drawClick(Canvas canvas) { 661 if (mClickedDayIndex != -1) { 662 int alpha = p.getAlpha(); 663 p.setColor(mClickedDayColor); 664 p.setAlpha(mClickedAlpha); 665 r.left = computeDayLeftPosition(mClickedDayIndex); 666 r.right = computeDayLeftPosition(mClickedDayIndex + 1); 667 r.top = DAY_SEPARATOR_INNER_WIDTH; 668 r.bottom = mHeight; 669 canvas.drawRect(r, p); 670 p.setAlpha(alpha); 671 } 672 } 673 674 @Override 675 protected void drawWeekNums(Canvas canvas) { 676 int y; 677 678 int i = 0; 679 int offset = -1; 680 int todayIndex = mTodayIndex; 681 int x = 0; 682 int numCount = mNumDays; 683 if (mShowWeekNum) { 684 x = SIDE_PADDING_WEEK_NUMBER + mPadding; 685 y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER; 686 canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint); 687 numCount++; 688 i++; 689 todayIndex++; 690 offset++; 691 692 } 693 694 y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER; 695 696 boolean isFocusMonth = mFocusDay[i]; 697 boolean isBold = false; 698 mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor); 699 for (; i < numCount; i++) { 700 if (mHasToday && todayIndex == i) { 701 mMonthNumPaint.setColor(mMonthNumTodayColor); 702 mMonthNumPaint.setFakeBoldText(isBold = true); 703 if (i + 1 < numCount) { 704 // Make sure the color will be set back on the next 705 // iteration 706 isFocusMonth = !mFocusDay[i + 1]; 707 } 708 } else if (mFocusDay[i] != isFocusMonth) { 709 isFocusMonth = mFocusDay[i]; 710 mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor); 711 } 712 x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER); 713 canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint); 714 if (isBold) { 715 mMonthNumPaint.setFakeBoldText(isBold = false); 716 } 717 } 718 } 719 720 protected void drawEvents(Canvas canvas) { 721 if (mEvents == null) { 722 return; 723 } 724 725 int day = -1; 726 for (ArrayList<Event> eventDay : mEvents) { 727 day++; 728 if (eventDay == null || eventDay.size() == 0) { 729 continue; 730 } 731 int ySquare; 732 int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1; 733 int rightEdge = computeDayLeftPosition(day + 1); 734 735 if (mOrientation == Configuration.ORIENTATION_PORTRAIT) { 736 ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER; 737 rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1; 738 } else { 739 ySquare = EVENT_Y_OFFSET_LANDSCAPE; 740 rightEdge -= EVENT_X_OFFSET_LANDSCAPE; 741 } 742 743 // Determine if everything will fit when time ranges are shown. 744 boolean showTimes = true; 745 Iterator<Event> iter = eventDay.iterator(); 746 int yTest = ySquare; 747 while (iter.hasNext()) { 748 Event event = iter.next(); 749 int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(), 750 showTimes, /*doDraw*/ false); 751 if (newY == yTest) { 752 showTimes = false; 753 break; 754 } 755 yTest = newY; 756 } 757 758 int eventCount = 0; 759 iter = eventDay.iterator(); 760 while (iter.hasNext()) { 761 Event event = iter.next(); 762 int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(), 763 showTimes, /*doDraw*/ true); 764 if (newY == ySquare) { 765 break; 766 } 767 eventCount++; 768 ySquare = newY; 769 } 770 771 int remaining = eventDay.size() - eventCount; 772 if (remaining > 0) { 773 drawMoreEvents(canvas, remaining, xSquare); 774 } 775 } 776 } 777 778 protected int addChipOutline(FloatRef lines, int count, int x, int y) { 779 lines.ensureSize(count + 16); 780 // top of box 781 lines.array[count++] = x; 782 lines.array[count++] = y; 783 lines.array[count++] = x + EVENT_SQUARE_WIDTH; 784 lines.array[count++] = y; 785 // right side of box 786 lines.array[count++] = x + EVENT_SQUARE_WIDTH; 787 lines.array[count++] = y; 788 lines.array[count++] = x + EVENT_SQUARE_WIDTH; 789 lines.array[count++] = y + EVENT_SQUARE_WIDTH; 790 // left side of box 791 lines.array[count++] = x; 792 lines.array[count++] = y; 793 lines.array[count++] = x; 794 lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1; 795 // bottom of box 796 lines.array[count++] = x; 797 lines.array[count++] = y + EVENT_SQUARE_WIDTH; 798 lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1; 799 lines.array[count++] = y + EVENT_SQUARE_WIDTH; 800 801 return count; 802 } 803 804 /** 805 * Attempts to draw the given event. Returns the y for the next event or the 806 * original y if the event will not fit. An event is considered to not fit 807 * if the event and its extras won't fit or if there are more events and the 808 * more events line would not fit after drawing this event. 809 * 810 * @param canvas the canvas to draw on 811 * @param event the event to draw 812 * @param x the top left corner for this event's color chip 813 * @param y the top left corner for this event's color chip 814 * @param rightEdge the rightmost point we're allowed to draw on (exclusive) 815 * @param moreEvents indicates whether additional events will follow this one 816 * @param showTimes if set, a second line with a time range will be displayed for non-all-day 817 * events 818 * @param doDraw if set, do the actual drawing; otherwise this just computes the height 819 * and returns 820 * @return the y for the next event or the original y if it won't fit 821 */ 822 protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge, 823 boolean moreEvents, boolean showTimes, boolean doDraw) { 824 /* 825 * Vertical layout: 826 * (top of box) 827 * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent 828 * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event 829 * c. [optional] Time range (mExtrasHeight) 830 * d. EVENT_LINE_PADDING 831 * 832 * Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need 833 * to leave room for something like "+2" at the bottom: 834 * 835 * e. "+ more" line (mExtrasHeight) 836 * 837 * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING) 838 * (bottom of box) 839 */ 840 final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1; // want a 1-pixel gap inside border 841 final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2; // adjust bounds for stroke width 842 boolean allDay = event.allDay; 843 int eventRequiredSpace = mEventHeight; 844 if (allDay) { 845 // Add a few pixels for the box we draw around all-day events. 846 eventRequiredSpace += BORDER_SPACE * 2; 847 } else if (showTimes) { 848 // Need room for the "1pm - 2pm" line. 849 eventRequiredSpace += mExtrasHeight; 850 } 851 int reservedSpace = EVENT_BOTTOM_PADDING; // leave a bit of room at the bottom 852 if (moreEvents) { 853 // More events follow. Leave a bit of space between events. 854 eventRequiredSpace += EVENT_LINE_PADDING; 855 856 // Make sure we have room for the "+ more" line. (The "+ more" line is expected 857 // to be <= the height of an event line, so we won't show "+1" when we could be 858 // showing the event.) 859 reservedSpace += mExtrasHeight; 860 } 861 862 if (y + eventRequiredSpace + reservedSpace > mHeight) { 863 // Not enough space, return original y 864 return y; 865 } else if (!doDraw) { 866 return y + eventRequiredSpace; 867 } 868 869 boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED; 870 int color = event.color; 871 if (isDeclined) { 872 color = Utils.getDeclinedColorFromColor(color); 873 } 874 875 int textX, textY, textRightEdge; 876 877 if (allDay) { 878 // We shift the render offset "inward", because drawRect with a stroke width greater 879 // than 1 draws outside the specified bounds. (We don't adjust the left edge, since 880 // we want to match the existing appearance of the "event square".) 881 r.left = x; 882 r.right = rightEdge - STROKE_WIDTH_ADJ; 883 r.top = y + STROKE_WIDTH_ADJ; 884 r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ; 885 textX = x + BORDER_SPACE; 886 textY = y + mEventAscentHeight + BORDER_SPACE; 887 textRightEdge = rightEdge - BORDER_SPACE; 888 } else { 889 r.left = x; 890 r.right = x + EVENT_SQUARE_WIDTH; 891 r.bottom = y + mEventAscentHeight; 892 r.top = r.bottom - EVENT_SQUARE_WIDTH; 893 textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING; 894 textY = y + mEventAscentHeight; 895 textRightEdge = rightEdge; 896 } 897 898 Style boxStyle = Style.STROKE; 899 boolean solidBackground = false; 900 if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) { 901 boxStyle = Style.FILL_AND_STROKE; 902 if (allDay) { 903 solidBackground = true; 904 } 905 } 906 mEventSquarePaint.setStyle(boxStyle); 907 mEventSquarePaint.setColor(color); 908 canvas.drawRect(r, mEventSquarePaint); 909 910 float avail = textRightEdge - textX; 911 CharSequence text = TextUtils.ellipsize( 912 event.title, mEventPaint, avail, TextUtils.TruncateAt.END); 913 Paint textPaint; 914 if (solidBackground) { 915 // Text color needs to contrast with solid background. 916 textPaint = mSolidBackgroundEventPaint; 917 } else if (isDeclined) { 918 // Use "declined event" color. 919 textPaint = mDeclinedEventPaint; 920 } else if (allDay) { 921 // Text inside frame is same color as frame. 922 mFramedEventPaint.setColor(color); 923 textPaint = mFramedEventPaint; 924 } else { 925 // Use generic event text color. 926 textPaint = mEventPaint; 927 } 928 canvas.drawText(text.toString(), textX, textY, textPaint); 929 y += mEventHeight; 930 if (allDay) { 931 y += BORDER_SPACE * 2; 932 } 933 934 if (showTimes && !allDay) { 935 // show start/end time, e.g. "1pm - 2pm" 936 textY = y + mExtrasAscentHeight; 937 mStringBuilder.setLength(0); 938 text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis, 939 event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL, 940 Utils.getTimeZone(getContext(), null)).toString(); 941 text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END); 942 canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint 943 : mEventExtrasPaint); 944 y += mExtrasHeight; 945 } 946 947 y += EVENT_LINE_PADDING; 948 949 return y; 950 } 951 952 protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) { 953 int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING); 954 String text = getContext().getResources().getQuantityString( 955 R.plurals.month_more_events, remainingEvents); 956 mEventExtrasPaint.setAntiAlias(true); 957 mEventExtrasPaint.setFakeBoldText(true); 958 canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint); 959 mEventExtrasPaint.setFakeBoldText(false); 960 } 961 962 /** 963 * Draws a line showing busy times in each day of week The method draws 964 * non-conflicting times in the event color and times with conflicting 965 * events in the dna conflict color defined in colors. 966 * 967 * @param canvas 968 */ 969 protected void drawDNA(Canvas canvas) { 970 // Draw event and conflict times 971 if (mDna != null) { 972 for (Utils.DNAStrand strand : mDna.values()) { 973 if (strand.color == CONFLICT_COLOR || strand.points == null 974 || strand.points.length == 0) { 975 continue; 976 } 977 mDNATimePaint.setColor(strand.color); 978 canvas.drawLines(strand.points, mDNATimePaint); 979 } 980 // Draw black last to make sure it's on top 981 Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR); 982 if (strand != null && strand.points != null && strand.points.length != 0) { 983 mDNATimePaint.setColor(strand.color); 984 canvas.drawLines(strand.points, mDNATimePaint); 985 } 986 if (mDayXs == null) { 987 return; 988 } 989 int numDays = mDayXs.length; 990 int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2; 991 if (strand != null && strand.allDays != null && strand.allDays.length == numDays) { 992 for (int i = 0; i < numDays; i++) { 993 // this adds at most 7 draws. We could sort it by color and 994 // build an array instead but this is easier. 995 if (strand.allDays[i] != 0) { 996 mDNAAllDayPaint.setColor(strand.allDays[i]); 997 canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset, 998 DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint); 999 } 1000 } 1001 } 1002 } 1003 } 1004 1005 @Override 1006 protected void updateSelectionPositions() { 1007 if (mHasSelectedDay) { 1008 int selectedPosition = mSelectedDay - mWeekStart; 1009 if (selectedPosition < 0) { 1010 selectedPosition += 7; 1011 } 1012 int effectiveWidth = mWidth - mPadding * 2; 1013 effectiveWidth -= SPACING_WEEK_NUMBER; 1014 mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding; 1015 mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding; 1016 mSelectedLeft += SPACING_WEEK_NUMBER; 1017 mSelectedRight += SPACING_WEEK_NUMBER; 1018 } 1019 } 1020 1021 public int getDayIndexFromLocation(float x) { 1022 int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding; 1023 if (x < dayStart || x > mWidth - mPadding) { 1024 return -1; 1025 } 1026 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 1027 return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding))); 1028 } 1029 1030 @Override 1031 public Time getDayFromLocation(float x) { 1032 int dayPosition = getDayIndexFromLocation(x); 1033 if (dayPosition == -1) { 1034 return null; 1035 } 1036 int day = mFirstJulianDay + dayPosition; 1037 1038 Time time = new Time(mTimeZone); 1039 if (mWeek == 0) { 1040 // This week is weird... 1041 if (day < Time.EPOCH_JULIAN_DAY) { 1042 day++; 1043 } else if (day == Time.EPOCH_JULIAN_DAY) { 1044 time.set(1, 0, 1970); 1045 time.normalize(true); 1046 return time; 1047 } 1048 } 1049 1050 time.setJulianDay(day); 1051 return time; 1052 } 1053 1054 @Override 1055 public boolean onHoverEvent(MotionEvent event) { 1056 Context context = getContext(); 1057 // only send accessibility events if accessibility and exploration are 1058 // on. 1059 AccessibilityManager am = (AccessibilityManager) context 1060 .getSystemService(Service.ACCESSIBILITY_SERVICE); 1061 if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { 1062 return super.onHoverEvent(event); 1063 } 1064 if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { 1065 Time hover = getDayFromLocation(event.getX()); 1066 if (hover != null 1067 && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) { 1068 Long millis = hover.toMillis(true); 1069 String date = Utils.formatDateRange(context, millis, millis, 1070 DateUtils.FORMAT_SHOW_DATE); 1071 AccessibilityEvent accessEvent = AccessibilityEvent 1072 .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); 1073 accessEvent.getText().add(date); 1074 if (mShowDetailsInMonth && mEvents != null) { 1075 int dayStart = SPACING_WEEK_NUMBER + mPadding; 1076 int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth 1077 - dayStart - mPadding)); 1078 ArrayList<Event> events = mEvents.get(dayPosition); 1079 List<CharSequence> text = accessEvent.getText(); 1080 for (Event e : events) { 1081 text.add(e.getTitleAndLocation() + ". "); 1082 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 1083 if (!e.allDay) { 1084 flags |= DateUtils.FORMAT_SHOW_TIME; 1085 if (DateFormat.is24HourFormat(context)) { 1086 flags |= DateUtils.FORMAT_24HOUR; 1087 } 1088 } else { 1089 flags |= DateUtils.FORMAT_UTC; 1090 } 1091 text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis, 1092 flags) + ". "); 1093 } 1094 } 1095 sendAccessibilityEventUnchecked(accessEvent); 1096 mLastHoverTime = hover; 1097 } 1098 } 1099 return true; 1100 } 1101 1102 public void setClickedDay(float xLocation) { 1103 mClickedDayIndex = getDayIndexFromLocation(xLocation); 1104 invalidate(); 1105 } 1106 public void clearClickedDay() { 1107 mClickedDayIndex = -1; 1108 invalidate(); 1109 } 1110 } 1111