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.R; 20 import com.android.calendar.Utils; 21 22 import android.app.Service; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Align; 28 import android.graphics.Paint.Style; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.text.format.DateUtils; 32 import android.text.format.Time; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.accessibility.AccessibilityEvent; 36 import android.view.accessibility.AccessibilityManager; 37 38 import java.security.InvalidParameterException; 39 import java.util.HashMap; 40 41 /** 42 * <p> 43 * This is a dynamic view for drawing a single week. It can be configured to 44 * display the week number, start the week on a given day, or show a reduced 45 * number of days. It is intended for use as a single view within a ListView. 46 * See {@link SimpleWeeksAdapter} for usage. 47 * </p> 48 */ 49 public class SimpleWeekView extends View { 50 private static final String TAG = "MonthView"; 51 52 /** 53 * These params can be passed into the view to control how it appears. 54 * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default 55 * values are unlikely to fit most layouts correctly. 56 */ 57 /** 58 * This sets the height of this week in pixels 59 */ 60 public static final String VIEW_PARAMS_HEIGHT = "height"; 61 /** 62 * This specifies the position (or weeks since the epoch) of this week, 63 * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay} 64 */ 65 public static final String VIEW_PARAMS_WEEK = "week"; 66 /** 67 * This sets one of the days in this view as selected {@link Time#SUNDAY} 68 * through {@link Time#SATURDAY}. 69 */ 70 public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; 71 /** 72 * Which day the week should start on. {@link Time#SUNDAY} through 73 * {@link Time#SATURDAY}. 74 */ 75 public static final String VIEW_PARAMS_WEEK_START = "week_start"; 76 /** 77 * How many days to display at a time. Days will be displayed starting with 78 * {@link #mWeekStart}. 79 */ 80 public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; 81 /** 82 * Which month is currently in focus, as defined by {@link Time#month} 83 * [0-11]. 84 */ 85 public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; 86 /** 87 * If this month should display week numbers. false if 0, true otherwise. 88 */ 89 public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; 90 91 protected static int DEFAULT_HEIGHT = 32; 92 protected static int MIN_HEIGHT = 10; 93 protected static final int DEFAULT_SELECTED_DAY = -1; 94 protected static final int DEFAULT_WEEK_START = Time.SUNDAY; 95 protected static final int DEFAULT_NUM_DAYS = 7; 96 protected static final int DEFAULT_SHOW_WK_NUM = 0; 97 protected static final int DEFAULT_FOCUS_MONTH = -1; 98 99 protected static int DAY_SEPARATOR_WIDTH = 1; 100 101 protected static int MINI_DAY_NUMBER_TEXT_SIZE = 14; 102 protected static int MINI_WK_NUMBER_TEXT_SIZE = 12; 103 protected static int MINI_TODAY_NUMBER_TEXT_SIZE = 18; 104 protected static int MINI_TODAY_OUTLINE_WIDTH = 2; 105 protected static int WEEK_NUM_MARGIN_BOTTOM = 4; 106 107 // used for scaling to the device density 108 protected static float mScale = 0; 109 110 // affects the padding on the sides of this view 111 protected int mPadding = 0; 112 113 protected Rect r = new Rect(); 114 protected Paint p = new Paint(); 115 protected Paint mMonthNumPaint; 116 protected Drawable mSelectedDayLine; 117 118 // Cache the number strings so we don't have to recompute them each time 119 protected String[] mDayNumbers; 120 // Quick lookup for checking which days are in the focus month 121 protected boolean[] mFocusDay; 122 // Quick lookup for checking which days are in an odd month (to set a different background) 123 protected boolean[] mOddMonth; 124 // The Julian day of the first day displayed by this item 125 protected int mFirstJulianDay = -1; 126 // The month of the first day in this week 127 protected int mFirstMonth = -1; 128 // The month of the last day in this week 129 protected int mLastMonth = -1; 130 // The position of this week, equivalent to weeks since the week of Jan 1st, 131 // 1970 132 protected int mWeek = -1; 133 // Quick reference to the width of this view, matches parent 134 protected int mWidth; 135 // The height this view should draw at in pixels, set by height param 136 protected int mHeight = DEFAULT_HEIGHT; 137 // Whether the week number should be shown 138 protected boolean mShowWeekNum = false; 139 // If this view contains the selected day 140 protected boolean mHasSelectedDay = false; 141 // If this view contains the today 142 protected boolean mHasToday = false; 143 // Which day is selected [0-6] or -1 if no day is selected 144 protected int mSelectedDay = DEFAULT_SELECTED_DAY; 145 // Which day is today [0-6] or -1 if no day is today 146 protected int mToday = DEFAULT_SELECTED_DAY; 147 // Which day of the week to start on [0-6] 148 protected int mWeekStart = DEFAULT_WEEK_START; 149 // How many days to display 150 protected int mNumDays = DEFAULT_NUM_DAYS; 151 // The number of days + a spot for week number if it is displayed 152 protected int mNumCells = mNumDays; 153 // The left edge of the selected day 154 protected int mSelectedLeft = -1; 155 // The right edge of the selected day 156 protected int mSelectedRight = -1; 157 // The timezone to display times/dates in (used for determining when Today 158 // is) 159 protected String mTimeZone = Time.getCurrentTimezone(); 160 161 protected int mBGColor; 162 protected int mSelectedWeekBGColor; 163 protected int mFocusMonthColor; 164 protected int mOtherMonthColor; 165 protected int mDaySeparatorColor; 166 protected int mTodayOutlineColor; 167 protected int mWeekNumColor; 168 SimpleWeekView(Context context)169 public SimpleWeekView(Context context) { 170 super(context); 171 172 Resources res = context.getResources(); 173 174 mBGColor = res.getColor(R.color.month_bgcolor); 175 mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor); 176 mFocusMonthColor = res.getColor(R.color.month_mini_day_number); 177 mOtherMonthColor = res.getColor(R.color.month_other_month_day_number); 178 mDaySeparatorColor = res.getColor(R.color.month_grid_lines); 179 mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color); 180 mWeekNumColor = res.getColor(R.color.month_week_num_color); 181 mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light); 182 183 if (mScale == 0) { 184 mScale = context.getResources().getDisplayMetrics().density; 185 if (mScale != 1) { 186 DEFAULT_HEIGHT *= mScale; 187 MIN_HEIGHT *= mScale; 188 MINI_DAY_NUMBER_TEXT_SIZE *= mScale; 189 MINI_TODAY_NUMBER_TEXT_SIZE *= mScale; 190 MINI_TODAY_OUTLINE_WIDTH *= mScale; 191 WEEK_NUM_MARGIN_BOTTOM *= mScale; 192 DAY_SEPARATOR_WIDTH *= mScale; 193 MINI_WK_NUMBER_TEXT_SIZE *= mScale; 194 } 195 } 196 197 // Sets up any standard paints that will be used 198 initView(); 199 } 200 201 /** 202 * Sets all the parameters for displaying this week. The only required 203 * parameter is the week number. Other parameters have a default value and 204 * will only update if a new value is included, except for focus month, 205 * which will always default to no focus month if no value is passed in. See 206 * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. 207 * 208 * @param params A map of the new parameters, see 209 * {@link #VIEW_PARAMS_HEIGHT} 210 * @param tz The time zone this view should reference times in 211 */ setWeekParams(HashMap<String, Integer> params, String tz)212 public void setWeekParams(HashMap<String, Integer> params, String tz) { 213 if (!params.containsKey(VIEW_PARAMS_WEEK)) { 214 throw new InvalidParameterException("You must specify the week number for this view"); 215 } 216 setTag(params); 217 mTimeZone = tz; 218 // We keep the current value for any params not present 219 if (params.containsKey(VIEW_PARAMS_HEIGHT)) { 220 mHeight = params.get(VIEW_PARAMS_HEIGHT); 221 if (mHeight < MIN_HEIGHT) { 222 mHeight = MIN_HEIGHT; 223 } 224 } 225 if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { 226 mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); 227 } 228 mHasSelectedDay = mSelectedDay != -1; 229 if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) { 230 mNumDays = params.get(VIEW_PARAMS_NUM_DAYS); 231 } 232 if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) { 233 if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) { 234 mShowWeekNum = true; 235 } else { 236 mShowWeekNum = false; 237 } 238 } 239 mNumCells = mShowWeekNum ? mNumDays + 1 : mNumDays; 240 241 // Allocate space for caching the day numbers and focus values 242 mDayNumbers = new String[mNumCells]; 243 mFocusDay = new boolean[mNumCells]; 244 mOddMonth = new boolean[mNumCells]; 245 mWeek = params.get(VIEW_PARAMS_WEEK); 246 int julianMonday = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek); 247 Time time = new Time(tz); 248 time.setJulianDay(julianMonday); 249 250 // If we're showing the week number calculate it based on Monday 251 int i = 0; 252 if (mShowWeekNum) { 253 mDayNumbers[0] = Integer.toString(time.getWeekNumber()); 254 i++; 255 } 256 257 if (params.containsKey(VIEW_PARAMS_WEEK_START)) { 258 mWeekStart = params.get(VIEW_PARAMS_WEEK_START); 259 } 260 261 // Now adjust our starting day based on the start day of the week 262 // If the week is set to start on a Saturday the first week will be 263 // Dec 27th 1969 -Jan 2nd, 1970 264 if (time.weekDay != mWeekStart) { 265 int diff = time.weekDay - mWeekStart; 266 if (diff < 0) { 267 diff += 7; 268 } 269 time.monthDay -= diff; 270 time.normalize(true); 271 } 272 273 mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff); 274 mFirstMonth = time.month; 275 276 // Figure out what day today is 277 Time today = new Time(tz); 278 today.setToNow(); 279 mHasToday = false; 280 mToday = -1; 281 282 int focusMonth = params.containsKey(VIEW_PARAMS_FOCUS_MONTH) ? params.get( 283 VIEW_PARAMS_FOCUS_MONTH) 284 : DEFAULT_FOCUS_MONTH; 285 286 for (; i < mNumCells; i++) { 287 if (time.monthDay == 1) { 288 mFirstMonth = time.month; 289 } 290 mOddMonth [i] = (time.month %2) == 1; 291 if (time.month == focusMonth) { 292 mFocusDay[i] = true; 293 } else { 294 mFocusDay[i] = false; 295 } 296 if (time.year == today.year && time.yearDay == today.yearDay) { 297 mHasToday = true; 298 mToday = i; 299 } 300 mDayNumbers[i] = Integer.toString(time.monthDay++); 301 time.normalize(true); 302 } 303 // We do one extra add at the end of the loop, if that pushed us to a 304 // new month undo it 305 if (time.monthDay == 1) { 306 time.monthDay--; 307 time.normalize(true); 308 } 309 mLastMonth = time.month; 310 311 updateSelectionPositions(); 312 } 313 314 /** 315 * Sets up the text and style properties for painting. Override this if you 316 * want to use a different paint. 317 */ initView()318 protected void initView() { 319 p.setFakeBoldText(false); 320 p.setAntiAlias(true); 321 p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); 322 p.setStyle(Style.FILL); 323 324 mMonthNumPaint = new Paint(); 325 mMonthNumPaint.setFakeBoldText(true); 326 mMonthNumPaint.setAntiAlias(true); 327 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); 328 mMonthNumPaint.setColor(mFocusMonthColor); 329 mMonthNumPaint.setStyle(Style.FILL); 330 mMonthNumPaint.setTextAlign(Align.CENTER); 331 } 332 333 /** 334 * Returns the month of the first day in this week 335 * 336 * @return The month the first day of this view is in 337 */ getFirstMonth()338 public int getFirstMonth() { 339 return mFirstMonth; 340 } 341 342 /** 343 * Returns the month of the last day in this week 344 * 345 * @return The month the last day of this view is in 346 */ getLastMonth()347 public int getLastMonth() { 348 return mLastMonth; 349 } 350 351 /** 352 * Returns the julian day of the first day in this view. 353 * 354 * @return The julian day of the first day in the view. 355 */ getFirstJulianDay()356 public int getFirstJulianDay() { 357 return mFirstJulianDay; 358 } 359 360 /** 361 * Calculates the day that the given x position is in, accounting for week 362 * number. Returns a Time referencing that day or null if 363 * 364 * @param x The x position of the touch event 365 * @return A time object for the tapped day or null if the position wasn't 366 * in a day 367 */ getDayFromLocation(float x)368 public Time getDayFromLocation(float x) { 369 int dayStart = mShowWeekNum ? (mWidth - mPadding * 2) / mNumCells + mPadding : mPadding; 370 if (x < dayStart || x > mWidth - mPadding) { 371 return null; 372 } 373 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 374 int dayPosition = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 375 int day = mFirstJulianDay + dayPosition; 376 377 Time time = new Time(mTimeZone); 378 if (mWeek == 0) { 379 // This week is weird... 380 if (day < Time.EPOCH_JULIAN_DAY) { 381 day++; 382 } else if (day == Time.EPOCH_JULIAN_DAY) { 383 time.set(1, 0, 1970); 384 time.normalize(true); 385 return time; 386 } 387 } 388 389 time.setJulianDay(day); 390 return time; 391 } 392 393 @Override onDraw(Canvas canvas)394 protected void onDraw(Canvas canvas) { 395 drawBackground(canvas); 396 drawWeekNums(canvas); 397 drawDaySeparators(canvas); 398 } 399 400 /** 401 * This draws the selection highlight if a day is selected in this week. 402 * Override this method if you wish to have a different background drawn. 403 * 404 * @param canvas The canvas to draw on 405 */ drawBackground(Canvas canvas)406 protected void drawBackground(Canvas canvas) { 407 if (mHasSelectedDay) { 408 p.setColor(mSelectedWeekBGColor); 409 p.setStyle(Style.FILL); 410 } else { 411 return; 412 } 413 r.top = 1; 414 r.bottom = mHeight - 1; 415 r.left = mPadding; 416 r.right = mSelectedLeft; 417 canvas.drawRect(r, p); 418 r.left = mSelectedRight; 419 r.right = mWidth - mPadding; 420 canvas.drawRect(r, p); 421 } 422 423 /** 424 * Draws the week and month day numbers for this week. Override this method 425 * if you need different placement. 426 * 427 * @param canvas The canvas to draw on 428 */ drawWeekNums(Canvas canvas)429 protected void drawWeekNums(Canvas canvas) { 430 int y = ((mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH; 431 int nDays = mNumCells; 432 433 int i = 0; 434 int divisor = 2 * nDays; 435 if (mShowWeekNum) { 436 p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE); 437 p.setStyle(Style.FILL); 438 p.setTextAlign(Align.CENTER); 439 p.setAntiAlias(true); 440 p.setColor(mWeekNumColor); 441 int x = (mWidth - mPadding * 2) / divisor + mPadding; 442 canvas.drawText(mDayNumbers[0], x, y, p); 443 i++; 444 } 445 446 boolean isFocusMonth = mFocusDay[i]; 447 mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor); 448 mMonthNumPaint.setFakeBoldText(false); 449 for (; i < nDays; i++) { 450 if (mFocusDay[i] != isFocusMonth) { 451 isFocusMonth = mFocusDay[i]; 452 mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor); 453 } 454 if (mHasToday && mToday == i) { 455 mMonthNumPaint.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE); 456 mMonthNumPaint.setFakeBoldText(true); 457 } 458 int x = (2 * i + 1) * (mWidth - mPadding * 2) / (divisor) + mPadding; 459 canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint); 460 if (mHasToday && mToday == i) { 461 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); 462 mMonthNumPaint.setFakeBoldText(false); 463 } 464 } 465 } 466 467 /** 468 * Draws a horizontal line for separating the weeks. Override this method if 469 * you want custom separators. 470 * 471 * @param canvas The canvas to draw on 472 */ drawDaySeparators(Canvas canvas)473 protected void drawDaySeparators(Canvas canvas) { 474 if (mHasSelectedDay) { 475 r.top = 1; 476 r.bottom = mHeight - 1; 477 r.left = mSelectedLeft + 1; 478 r.right = mSelectedRight - 1; 479 p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH); 480 p.setStyle(Style.STROKE); 481 p.setColor(mTodayOutlineColor); 482 canvas.drawRect(r, p); 483 } 484 if (mShowWeekNum) { 485 p.setColor(mDaySeparatorColor); 486 p.setStrokeWidth(DAY_SEPARATOR_WIDTH); 487 488 int x = (mWidth - mPadding * 2) / mNumCells + mPadding; 489 canvas.drawLine(x, 0, x, mHeight, p); 490 } 491 } 492 493 @Override onSizeChanged(int w, int h, int oldw, int oldh)494 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 495 mWidth = w; 496 updateSelectionPositions(); 497 } 498 499 /** 500 * This calculates the positions for the selected day lines. 501 */ updateSelectionPositions()502 protected void updateSelectionPositions() { 503 if (mHasSelectedDay) { 504 int selectedPosition = mSelectedDay - mWeekStart; 505 if (selectedPosition < 0) { 506 selectedPosition += 7; 507 } 508 if (mShowWeekNum) { 509 selectedPosition++; 510 } 511 mSelectedLeft = selectedPosition * (mWidth - mPadding * 2) / mNumCells 512 + mPadding; 513 mSelectedRight = (selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells 514 + mPadding; 515 } 516 } 517 518 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)519 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 520 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); 521 } 522 523 @Override onHoverEvent(MotionEvent event)524 public boolean onHoverEvent(MotionEvent event) { 525 Context context = getContext(); 526 // only send accessibility events if accessibility and exploration are 527 // on. 528 AccessibilityManager am = (AccessibilityManager) context 529 .getSystemService(Service.ACCESSIBILITY_SERVICE); 530 if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { 531 return super.onHoverEvent(event); 532 } 533 if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { 534 Time hover = getDayFromLocation(event.getX()); 535 if (hover != null 536 && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) { 537 Long millis = hover.toMillis(true); 538 String date = Utils.formatDateRange(context, millis, millis, 539 DateUtils.FORMAT_SHOW_DATE); 540 AccessibilityEvent accessEvent = 541 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); 542 accessEvent.getText().add(date); 543 sendAccessibilityEventUnchecked(accessEvent); 544 mLastHoverTime = hover; 545 } 546 } 547 return true; 548 } 549 550 Time mLastHoverTime = null; 551 }