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