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 onAnimationEnd(Animator animation)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 setAnimator(Animator animation)216 public void setAnimator(Animator animation) { 217 mAnimator = animation; 218 } 219 setFadingIn(boolean fadingIn)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 FloatRef(int size)235 public FloatRef(int size) { 236 array = new float[size]; 237 } 238 ensureSize(int newSize)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 */ MonthWeekEventsView(Context context)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. setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents)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 */ createDna(ArrayList<Event> unsortedEvents)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 setEvents(List<ArrayList<Event>> sortedEvents)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 loadColors(Context context)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 initView()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 setWeekParams(HashMap<String, Integer> params, String tz)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 */ updateToday(String tz)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 setAnimateTodayAlpha(int alpha)536 public void setAnimateTodayAlpha(int alpha) { 537 mAnimateTodayAlpha = alpha; 538 invalidate(); 539 } 540 541 @Override onDraw(Canvas canvas)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 drawToday(Canvas canvas)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 computeDayLeftPosition(int 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 drawDaySeparators(Canvas canvas)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 drawBackground(Canvas canvas)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 drawClick(Canvas canvas)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 drawWeekNums(Canvas canvas)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 drawEvents(Canvas canvas)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 addChipOutline(FloatRef lines, int count, int x, int y)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 */ drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge, boolean moreEvents, boolean showTimes, boolean doDraw)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 drawMoreEvents(Canvas canvas, int remainingEvents, int x)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 */ drawDNA(Canvas canvas)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 updateSelectionPositions()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 getDayIndexFromLocation(float x)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 getDayFromLocation(float x)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 onHoverEvent(MotionEvent event)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 setClickedDay(float xLocation)1102 public void setClickedDay(float xLocation) { 1103 mClickedDayIndex = getDayIndexFromLocation(xLocation); 1104 invalidate(); 1105 } clearClickedDay()1106 public void clearClickedDay() { 1107 mClickedDayIndex = -1; 1108 invalidate(); 1109 } 1110 } 1111