blob: e1c78c67393633eb960142d6fc447a7a963c0017 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.calendar.month;
import com.android.calendar.Event;
import com.android.calendar.R;
import com.android.calendar.Utils;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.app.Service;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.provider.CalendarContract.Attendees;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
public class MonthWeekEventsView extends SimpleWeekView {
private static final String TAG = "MonthView";
private static final boolean DEBUG_LAYOUT = false;
public static final String VIEW_PARAMS_ORIENTATION = "orientation";
public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today";
/* NOTE: these are not constants, and may be multiplied by a scale factor */
private static int TEXT_SIZE_MONTH_NUMBER = 32;
private static int TEXT_SIZE_EVENT = 12;
private static int TEXT_SIZE_EVENT_TITLE = 14;
private static int TEXT_SIZE_MORE_EVENTS = 12;
private static int TEXT_SIZE_MONTH_NAME = 14;
private static int TEXT_SIZE_WEEK_NUM = 12;
private static int DNA_MARGIN = 4;
private static int DNA_ALL_DAY_HEIGHT = 4;
private static int DNA_MIN_SEGMENT_HEIGHT = 4;
private static int DNA_WIDTH = 8;
private static int DNA_ALL_DAY_WIDTH = 32;
private static int DNA_SIDE_PADDING = 6;
private static int CONFLICT_COLOR = Color.BLACK;
private static int EVENT_TEXT_COLOR = Color.WHITE;
private static int DEFAULT_EDGE_SPACING = 0;
private static int SIDE_PADDING_MONTH_NUMBER = 4;
private static int TOP_PADDING_MONTH_NUMBER = 4;
private static int TOP_PADDING_WEEK_NUMBER = 4;
private static int SIDE_PADDING_WEEK_NUMBER = 20;
private static int DAY_SEPARATOR_OUTER_WIDTH = 0;
private static int DAY_SEPARATOR_INNER_WIDTH = 1;
private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53;
private static int DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT = 64;
private static int MIN_WEEK_WIDTH = 50;
private static int EVENT_X_OFFSET_LANDSCAPE = 38;
private static int EVENT_Y_OFFSET_LANDSCAPE = 8;
private static int EVENT_Y_OFFSET_PORTRAIT = 7;
private static int EVENT_SQUARE_WIDTH = 10;
private static int EVENT_SQUARE_BORDER = 2;
private static int EVENT_LINE_PADDING = 2;
private static int EVENT_RIGHT_PADDING = 4;
private static int EVENT_BOTTOM_PADDING = 3;
private static int TODAY_HIGHLIGHT_WIDTH = 2;
private static int SPACING_WEEK_NUMBER = 24;
private static boolean mInitialized = false;
private static boolean mShowDetailsInMonth;
protected Time mToday = new Time();
protected boolean mHasToday = false;
protected int mTodayIndex = -1;
protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
protected List<ArrayList<Event>> mEvents = null;
protected ArrayList<Event> mUnsortedEvents = null;
HashMap<Integer, Utils.DNAStrand> mDna = null;
// This is for drawing the outlines around event chips and supports up to 10
// events being drawn on each day. The code will expand this if necessary.
protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7);
protected static StringBuilder mStringBuilder = new StringBuilder(50);
// TODO recreate formatter when locale changes
protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
protected Paint mMonthNamePaint;
protected TextPaint mEventPaint;
protected TextPaint mSolidBackgroundEventPaint;
protected TextPaint mFramedEventPaint;
protected TextPaint mDeclinedEventPaint;
protected TextPaint mEventExtrasPaint;
protected TextPaint mEventDeclinedExtrasPaint;
protected Paint mWeekNumPaint;
protected Paint mDNAAllDayPaint;
protected Paint mDNATimePaint;
protected Paint mEventSquarePaint;
protected Drawable mTodayDrawable;
protected int mMonthNumHeight;
protected int mMonthNumAscentHeight;
protected int mEventHeight;
protected int mEventAscentHeight;
protected int mExtrasHeight;
protected int mExtrasAscentHeight;
protected int mExtrasDescent;
protected int mWeekNumAscentHeight;
protected int mMonthBGColor;
protected int mMonthBGOtherColor;
protected int mMonthBGTodayColor;
protected int mMonthNumColor;
protected int mMonthNumOtherColor;
protected int mMonthNumTodayColor;
protected int mMonthNameColor;
protected int mMonthNameOtherColor;
protected int mMonthEventColor;
protected int mMonthDeclinedEventColor;
protected int mMonthDeclinedExtrasColor;
protected int mMonthEventExtraColor;
protected int mMonthEventOtherColor;
protected int mMonthEventExtraOtherColor;
protected int mMonthWeekNumColor;
protected int mMonthBusyBitsBgColor;
protected int mMonthBusyBitsBusyTimeColor;
protected int mMonthBusyBitsConflictTimeColor;
private int mClickedDayIndex = -1;
private int mClickedDayColor;
private static final int mClickedAlpha = 128;
protected int mEventChipOutlineColor = 0xFFFFFFFF;
protected int mDaySeparatorInnerColor;
protected int mTodayAnimateColor;
private boolean mAnimateToday;
private int mAnimateTodayAlpha = 0;
private ObjectAnimator mTodayAnimator = null;
private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener();
class TodayAnimatorListener extends AnimatorListenerAdapter {
private volatile Animator mAnimator = null;
private volatile boolean mFadingIn = false;
@Override
public void onAnimationEnd(Animator animation) {
synchronized (this) {
if (mAnimator != animation) {
animation.removeAllListeners();
animation.cancel();
return;
}
if (mFadingIn) {
if (mTodayAnimator != null) {
mTodayAnimator.removeAllListeners();
mTodayAnimator.cancel();
}
mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this,
"animateTodayAlpha", 255, 0);
mAnimator = mTodayAnimator;
mFadingIn = false;
mTodayAnimator.addListener(this);
mTodayAnimator.setDuration(600);
mTodayAnimator.start();
} else {
mAnimateToday = false;
mAnimateTodayAlpha = 0;
mAnimator.removeAllListeners();
mAnimator = null;
mTodayAnimator = null;
invalidate();
}
}
}
public void setAnimator(Animator animation) {
mAnimator = animation;
}
public void setFadingIn(boolean fadingIn) {
mFadingIn = fadingIn;
}
}
private int[] mDayXs;
/**
* This provides a reference to a float array which allows for easy size
* checking and reallocation. Used for drawing lines.
*/
private class FloatRef {
float[] array;
public FloatRef(int size) {
array = new float[size];
}
public void ensureSize(int newSize) {
if (newSize >= array.length) {
// Add enough space for 7 more boxes to be drawn
array = Arrays.copyOf(array, newSize + 16 * 7);
}
}
}
/**
* Shows up as an error if we don't include this.
*/
public MonthWeekEventsView(Context context) {
super(context);
}
// Sets the list of events for this week. Takes a sorted list of arrays
// divided up by day for generating the large month version and the full
// arraylist sorted by start time to generate the dna version.
public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) {
setEvents(sortedEvents);
// The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
// generate dna bits before its width has been fixed.
createDna(unsortedEvents);
}
/**
* Sets up the dna bits for the view. This will return early if the view
* isn't in a state that will create a valid set of dna yet (such as the
* views width not being set correctly yet).
*/
public void createDna(ArrayList<Event> unsortedEvents) {
if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
// Stash the list of events for use when this view is ready, or
// just clear it if a null set has been passed to this view
mUnsortedEvents = unsortedEvents;
mDna = null;
return;
} else {
// clear the cached set of events since we're ready to build it now
mUnsortedEvents = null;
}
// Create the drawing coordinates for dna
if (!mShowDetailsInMonth) {
int numDays = mEvents.size();
int effectiveWidth = mWidth - mPadding * 2;
if (mShowWeekNum) {
effectiveWidth -= SPACING_WEEK_NUMBER;
}
DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING;
mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
mDayXs = new int[numDays];
for (int day = 0; day < numDays; day++) {
mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING;
}
int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1;
int bottom = mHeight - DNA_MARGIN;
mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext());
}
}
public void setEvents(List<ArrayList<Event>> sortedEvents) {
mEvents = sortedEvents;
if (sortedEvents == null) {
return;
}
if (sortedEvents.size() != mNumDays) {
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.wtf(TAG, "Events size must be same as days displayed: size="
+ sortedEvents.size() + " days=" + mNumDays);
}
mEvents = null;
return;
}
}
protected void loadColors(Context context) {
Resources res = context.getResources();
mMonthWeekNumColor = res.getColor(R.color.month_week_num_color);
mMonthNumColor = res.getColor(R.color.month_day_number);
mMonthNumOtherColor = res.getColor(R.color.month_day_number_other);
mMonthNumTodayColor = res.getColor(R.color.month_today_number);
mMonthNameColor = mMonthNumColor;
mMonthNameOtherColor = mMonthNumOtherColor;
mMonthEventColor = res.getColor(R.color.month_event_color);
mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color);
mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color);
mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color);
mMonthEventOtherColor = res.getColor(R.color.month_event_other_color);
mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color);
mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor);
mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor);
mMonthBGColor = res.getColor(R.color.month_bgcolor);
mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines);
mTodayAnimateColor = res.getColor(R.color.today_highlight_color);
mClickedDayColor = res.getColor(R.color.day_clicked_background_color);
mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light);
}
/**
* Sets up the text and style properties for painting. Override this if you
* want to use a different paint.
*/
@Override
protected void initView() {
super.initView();
if (!mInitialized) {
Resources resources = getContext().getResources();
mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month);
TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title);
TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number);
SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin);
CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color);
EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color);
if (mScale != 1) {
TOP_PADDING_MONTH_NUMBER *= mScale;
TOP_PADDING_WEEK_NUMBER *= mScale;
SIDE_PADDING_MONTH_NUMBER *= mScale;
SIDE_PADDING_WEEK_NUMBER *= mScale;
SPACING_WEEK_NUMBER *= mScale;
TEXT_SIZE_MONTH_NUMBER *= mScale;
TEXT_SIZE_EVENT *= mScale;
TEXT_SIZE_EVENT_TITLE *= mScale;
TEXT_SIZE_MORE_EVENTS *= mScale;
TEXT_SIZE_MONTH_NAME *= mScale;
TEXT_SIZE_WEEK_NUM *= mScale;
DAY_SEPARATOR_OUTER_WIDTH *= mScale;
DAY_SEPARATOR_INNER_WIDTH *= mScale;
DAY_SEPARATOR_VERTICAL_LENGTH *= mScale;
DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT *= mScale;
EVENT_X_OFFSET_LANDSCAPE *= mScale;
EVENT_Y_OFFSET_LANDSCAPE *= mScale;
EVENT_Y_OFFSET_PORTRAIT *= mScale;
EVENT_SQUARE_WIDTH *= mScale;
EVENT_SQUARE_BORDER *= mScale;
EVENT_LINE_PADDING *= mScale;
EVENT_BOTTOM_PADDING *= mScale;
EVENT_RIGHT_PADDING *= mScale;
DNA_MARGIN *= mScale;
DNA_WIDTH *= mScale;
DNA_ALL_DAY_HEIGHT *= mScale;
DNA_MIN_SEGMENT_HEIGHT *= mScale;
DNA_SIDE_PADDING *= mScale;
DEFAULT_EDGE_SPACING *= mScale;
DNA_ALL_DAY_WIDTH *= mScale;
TODAY_HIGHLIGHT_WIDTH *= mScale;
}
if (!mShowDetailsInMonth) {
TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN;
}
mInitialized = true;
}
mPadding = DEFAULT_EDGE_SPACING;
loadColors(getContext());
// TODO modify paint properties depending on isMini
mMonthNumPaint = new Paint();
mMonthNumPaint.setFakeBoldText(false);
mMonthNumPaint.setAntiAlias(true);
mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER);
mMonthNumPaint.setColor(mMonthNumColor);
mMonthNumPaint.setStyle(Style.FILL);
mMonthNumPaint.setTextAlign(Align.RIGHT);
mMonthNumPaint.setTypeface(Typeface.DEFAULT);
mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f);
mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f);
mEventPaint = new TextPaint();
mEventPaint.setFakeBoldText(true);
mEventPaint.setAntiAlias(true);
mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
mEventPaint.setColor(mMonthEventColor);
mSolidBackgroundEventPaint = new TextPaint(mEventPaint);
mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR);
mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint);
mDeclinedEventPaint = new TextPaint();
mDeclinedEventPaint.setFakeBoldText(true);
mDeclinedEventPaint.setAntiAlias(true);
mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
mDeclinedEventPaint.setColor(mMonthDeclinedEventColor);
mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f);
mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f);
mEventExtrasPaint = new TextPaint();
mEventExtrasPaint.setFakeBoldText(false);
mEventExtrasPaint.setAntiAlias(true);
mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
mEventExtrasPaint.setColor(mMonthEventExtraColor);
mEventExtrasPaint.setStyle(Style.FILL);
mEventExtrasPaint.setTextAlign(Align.LEFT);
mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f);
mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f);
mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f);
mEventDeclinedExtrasPaint = new TextPaint();
mEventDeclinedExtrasPaint.setFakeBoldText(false);
mEventDeclinedExtrasPaint.setAntiAlias(true);
mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor);
mEventDeclinedExtrasPaint.setStyle(Style.FILL);
mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT);
mWeekNumPaint = new Paint();
mWeekNumPaint.setFakeBoldText(false);
mWeekNumPaint.setAntiAlias(true);
mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM);
mWeekNumPaint.setColor(mWeekNumColor);
mWeekNumPaint.setStyle(Style.FILL);
mWeekNumPaint.setTextAlign(Align.RIGHT);
mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f);
mDNAAllDayPaint = new Paint();
mDNATimePaint = new Paint();
mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor);
mDNATimePaint.setStyle(Style.FILL_AND_STROKE);
mDNATimePaint.setStrokeWidth(DNA_WIDTH);
mDNATimePaint.setAntiAlias(false);
mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor);
mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE);
mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
mDNAAllDayPaint.setAntiAlias(false);
mEventSquarePaint = new Paint();
mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER);
mEventSquarePaint.setAntiAlias(false);
if (DEBUG_LAYOUT) {
Log.d("EXTRA", "mScale=" + mScale);
Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent()
+ " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight);
Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent()
+ " descent=" + mEventPaint.descent() + " int height=" + mEventHeight
+ " int ascent=" + mEventAscentHeight);
Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
+ " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight);
Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
+ " descent=" + mWeekNumPaint.descent());
}
}
@Override
public void setWeekParams(HashMap<String, Integer> params, String tz) {
super.setWeekParams(params, tz);
if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
mOrientation = params.get(VIEW_PARAMS_ORIENTATION);
}
updateToday(tz);
mNumCells = mNumDays + 1;
if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
synchronized (mAnimatorListener) {
if (mTodayAnimator != null) {
mTodayAnimator.removeAllListeners();
mTodayAnimator.cancel();
}
mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
Math.max(mAnimateTodayAlpha, 80), 255);
mTodayAnimator.setDuration(150);
mAnimatorListener.setAnimator(mTodayAnimator);
mAnimatorListener.setFadingIn(true);
mTodayAnimator.addListener(mAnimatorListener);
mAnimateToday = true;
mTodayAnimator.start();
}
}
}
/**
* @param tz
*/
public boolean updateToday(String tz) {
mToday.timezone = tz;
mToday.setToNow();
mToday.normalize(true);
int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff);
if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
mHasToday = true;
mTodayIndex = julianToday - mFirstJulianDay;
} else {
mHasToday = false;
mTodayIndex = -1;
}
return mHasToday;
}
public void setAnimateTodayAlpha(int alpha) {
mAnimateTodayAlpha = alpha;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
drawBackground(canvas);
drawWeekNums(canvas);
drawDaySeparators(canvas);
if (mHasToday && mAnimateToday) {
drawToday(canvas);
}
if (mShowDetailsInMonth) {
drawEvents(canvas);
} else {
if (mDna == null && mUnsortedEvents != null) {
createDna(mUnsortedEvents);
}
drawDNA(canvas);
}
drawClick(canvas);
}
protected void drawToday(Canvas canvas) {
r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2);
r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
p.setStyle(Style.STROKE);
p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH);
r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2);
r.right = computeDayLeftPosition(mTodayIndex + 1)
- (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24));
canvas.drawRect(r, p);
p.setStyle(Style.FILL);
}
// TODO move into SimpleWeekView
// Computes the x position for the left side of the given day
private int computeDayLeftPosition(int day) {
int effectiveWidth = mWidth;
int x = 0;
int xOffset = 0;
if (mShowWeekNum) {
xOffset = SPACING_WEEK_NUMBER + mPadding;
effectiveWidth -= xOffset;
}
x = day * effectiveWidth / mNumDays + xOffset;
return x;
}
@Override
protected void drawDaySeparators(Canvas canvas) {
float lines[] = new float[8 * 4];
int count = 6 * 4;
int wkNumOffset = 0;
int i = 0;
if (mShowWeekNum) {
// This adds the first line separating the week number
int xOffset = SPACING_WEEK_NUMBER + mPadding;
count += 4;
lines[i++] = xOffset;
lines[i++] = 0;
lines[i++] = xOffset;
lines[i++] = mHeight;
wkNumOffset++;
}
count += 4;
lines[i++] = 0;
lines[i++] = 0;
lines[i++] = mWidth;
lines[i++] = 0;
int y0 = 0;
int y1 = mHeight;
while (i < count) {
int x = computeDayLeftPosition(i / 4 - wkNumOffset);
lines[i++] = x;
lines[i++] = y0;
lines[i++] = x;
lines[i++] = y1;
}
p.setColor(mDaySeparatorInnerColor);
p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
canvas.drawLines(lines, 0, count, p);
}
@Override
protected void drawBackground(Canvas canvas) {
int i = 0;
int offset = 0;
r.top = DAY_SEPARATOR_INNER_WIDTH;
r.bottom = mHeight;
if (mShowWeekNum) {
i++;
offset++;
}
if (!mOddMonth[i]) {
while (++i < mOddMonth.length && !mOddMonth[i])
;
r.right = computeDayLeftPosition(i - offset);
r.left = 0;
p.setColor(mMonthBGOtherColor);
canvas.drawRect(r, p);
// compute left edge for i, set up r, draw
} else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
while (--i >= offset && !mOddMonth[i])
;
i++;
// compute left edge for i, set up r, draw
r.right = mWidth;
r.left = computeDayLeftPosition(i - offset);
p.setColor(mMonthBGOtherColor);
canvas.drawRect(r, p);
}
if (mHasToday) {
p.setColor(mMonthBGTodayColor);
r.left = computeDayLeftPosition(mTodayIndex);
r.right = computeDayLeftPosition(mTodayIndex + 1);
canvas.drawRect(r, p);
}
}
// Draw the "clicked" color on the tapped day
private void drawClick(Canvas canvas) {
if (mClickedDayIndex != -1) {
int alpha = p.getAlpha();
p.setColor(mClickedDayColor);
p.setAlpha(mClickedAlpha);
r.left = computeDayLeftPosition(mClickedDayIndex);
r.right = computeDayLeftPosition(mClickedDayIndex + 1);
r.top = DAY_SEPARATOR_INNER_WIDTH;
r.bottom = mHeight;
canvas.drawRect(r, p);
p.setAlpha(alpha);
}
}
@Override
protected void drawWeekNums(Canvas canvas) {
int y;
int i = 0;
int offset = -1;
int todayIndex = mTodayIndex;
int x = 0;
int numCount = mNumDays;
if (mShowWeekNum) {
x = SIDE_PADDING_WEEK_NUMBER + mPadding;
y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER;
canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint);
numCount++;
i++;
todayIndex++;
offset++;
}
y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER;
boolean isFocusMonth = mFocusDay[i];
boolean isBold = false;
mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
for (; i < numCount; i++) {
if (mHasToday && todayIndex == i) {
mMonthNumPaint.setColor(mMonthNumTodayColor);
mMonthNumPaint.setFakeBoldText(isBold = true);
if (i + 1 < numCount) {
// Make sure the color will be set back on the next
// iteration
isFocusMonth = !mFocusDay[i + 1];
}
} else if (mFocusDay[i] != isFocusMonth) {
isFocusMonth = mFocusDay[i];
mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
}
x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
if (isBold) {
mMonthNumPaint.setFakeBoldText(isBold = false);
}
}
}
protected void drawEvents(Canvas canvas) {
if (mEvents == null) {
return;
}
int day = -1;
for (ArrayList<Event> eventDay : mEvents) {
day++;
if (eventDay == null || eventDay.size() == 0) {
continue;
}
int ySquare;
int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1;
int rightEdge = computeDayLeftPosition(day + 1);
if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER;
rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1;
} else {
ySquare = EVENT_Y_OFFSET_LANDSCAPE;
rightEdge -= EVENT_X_OFFSET_LANDSCAPE;
}
// Determine if everything will fit when time ranges are shown.
boolean showTimes = true;
Iterator<Event> iter = eventDay.iterator();
int yTest = ySquare;
while (iter.hasNext()) {
Event event = iter.next();
int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
showTimes, /*doDraw*/ false);
if (newY == yTest) {
showTimes = false;
break;
}
yTest = newY;
}
int eventCount = 0;
iter = eventDay.iterator();
while (iter.hasNext()) {
Event event = iter.next();
int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
showTimes, /*doDraw*/ true);
if (newY == ySquare) {
break;
}
eventCount++;
ySquare = newY;
}
int remaining = eventDay.size() - eventCount;
if (remaining > 0) {
drawMoreEvents(canvas, remaining, xSquare);
}
}
}
protected int addChipOutline(FloatRef lines, int count, int x, int y) {
lines.ensureSize(count + 16);
// top of box
lines.array[count++] = x;
lines.array[count++] = y;
lines.array[count++] = x + EVENT_SQUARE_WIDTH;
lines.array[count++] = y;
// right side of box
lines.array[count++] = x + EVENT_SQUARE_WIDTH;
lines.array[count++] = y;
lines.array[count++] = x + EVENT_SQUARE_WIDTH;
lines.array[count++] = y + EVENT_SQUARE_WIDTH;
// left side of box
lines.array[count++] = x;
lines.array[count++] = y;
lines.array[count++] = x;
lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1;
// bottom of box
lines.array[count++] = x;
lines.array[count++] = y + EVENT_SQUARE_WIDTH;
lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1;
lines.array[count++] = y + EVENT_SQUARE_WIDTH;
return count;
}
/**
* Attempts to draw the given event. Returns the y for the next event or the
* original y if the event will not fit. An event is considered to not fit
* if the event and its extras won't fit or if there are more events and the
* more events line would not fit after drawing this event.
*
* @param canvas the canvas to draw on
* @param event the event to draw
* @param x the top left corner for this event's color chip
* @param y the top left corner for this event's color chip
* @param rightEdge the rightmost point we're allowed to draw on (exclusive)
* @param moreEvents indicates whether additional events will follow this one
* @param showTimes if set, a second line with a time range will be displayed for non-all-day
* events
* @param doDraw if set, do the actual drawing; otherwise this just computes the height
* and returns
* @return the y for the next event or the original y if it won't fit
*/
protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge,
boolean moreEvents, boolean showTimes, boolean doDraw) {
/*
* Vertical layout:
* (top of box)
* a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
* b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
* c. [optional] Time range (mExtrasHeight)
* d. EVENT_LINE_PADDING
*
* Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need
* to leave room for something like "+2" at the bottom:
*
* e. "+ more" line (mExtrasHeight)
*
* f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
* (bottom of box)
*/
final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1; // want a 1-pixel gap inside border
final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2; // adjust bounds for stroke width
boolean allDay = event.allDay;
int eventRequiredSpace = mEventHeight;
if (allDay) {
// Add a few pixels for the box we draw around all-day events.
eventRequiredSpace += BORDER_SPACE * 2;
} else if (showTimes) {
// Need room for the "1pm - 2pm" line.
eventRequiredSpace += mExtrasHeight;
}
int reservedSpace = EVENT_BOTTOM_PADDING; // leave a bit of room at the bottom
if (moreEvents) {
// More events follow. Leave a bit of space between events.
eventRequiredSpace += EVENT_LINE_PADDING;
// Make sure we have room for the "+ more" line. (The "+ more" line is expected
// to be <= the height of an event line, so we won't show "+1" when we could be
// showing the event.)
reservedSpace += mExtrasHeight;
}
if (y + eventRequiredSpace + reservedSpace > mHeight) {
// Not enough space, return original y
return y;
} else if (!doDraw) {
return y + eventRequiredSpace;
}
boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED;
int color = event.color;
if (isDeclined) {
color = Utils.getDeclinedColorFromColor(color);
}
int textX, textY, textRightEdge;
if (allDay) {
// We shift the render offset "inward", because drawRect with a stroke width greater
// than 1 draws outside the specified bounds. (We don't adjust the left edge, since
// we want to match the existing appearance of the "event square".)
r.left = x;
r.right = rightEdge - STROKE_WIDTH_ADJ;
r.top = y + STROKE_WIDTH_ADJ;
r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ;
textX = x + BORDER_SPACE;
textY = y + mEventAscentHeight + BORDER_SPACE;
textRightEdge = rightEdge - BORDER_SPACE;
} else {
r.left = x;
r.right = x + EVENT_SQUARE_WIDTH;
r.bottom = y + mEventAscentHeight;
r.top = r.bottom - EVENT_SQUARE_WIDTH;
textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING;
textY = y + mEventAscentHeight;
textRightEdge = rightEdge;
}
Style boxStyle = Style.STROKE;
boolean solidBackground = false;
if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) {
boxStyle = Style.FILL_AND_STROKE;
if (allDay) {
solidBackground = true;
}
}
mEventSquarePaint.setStyle(boxStyle);
mEventSquarePaint.setColor(color);
canvas.drawRect(r, mEventSquarePaint);
float avail = textRightEdge - textX;
CharSequence text = TextUtils.ellipsize(
event.title, mEventPaint, avail, TextUtils.TruncateAt.END);
Paint textPaint;
if (solidBackground) {
// Text color needs to contrast with solid background.
textPaint = mSolidBackgroundEventPaint;
} else if (isDeclined) {
// Use "declined event" color.
textPaint = mDeclinedEventPaint;
} else if (allDay) {
// Text inside frame is same color as frame.
mFramedEventPaint.setColor(color);
textPaint = mFramedEventPaint;
} else {
// Use generic event text color.
textPaint = mEventPaint;
}
canvas.drawText(text.toString(), textX, textY, textPaint);
y += mEventHeight;
if (allDay) {
y += BORDER_SPACE * 2;
}
if (showTimes && !allDay) {
// show start/end time, e.g. "1pm - 2pm"
textY = y + mExtrasAscentHeight;
mStringBuilder.setLength(0);
text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL,
Utils.getTimeZone(getContext(), null)).toString();
text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END);
canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint
: mEventExtrasPaint);
y += mExtrasHeight;
}
y += EVENT_LINE_PADDING;
return y;
}
protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) {
int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING);
String text = getContext().getResources().getQuantityString(
R.plurals.month_more_events, remainingEvents);
mEventExtrasPaint.setAntiAlias(true);
mEventExtrasPaint.setFakeBoldText(true);
canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint);
mEventExtrasPaint.setFakeBoldText(false);
}
/**
* Draws a line showing busy times in each day of week The method draws
* non-conflicting times in the event color and times with conflicting
* events in the dna conflict color defined in colors.
*
* @param canvas
*/
protected void drawDNA(Canvas canvas) {
// Draw event and conflict times
if (mDna != null) {
for (Utils.DNAStrand strand : mDna.values()) {
if (strand.color == CONFLICT_COLOR || strand.points == null
|| strand.points.length == 0) {
continue;
}
mDNATimePaint.setColor(strand.color);
canvas.drawLines(strand.points, mDNATimePaint);
}
// Draw black last to make sure it's on top
Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR);
if (strand != null && strand.points != null && strand.points.length != 0) {
mDNATimePaint.setColor(strand.color);
canvas.drawLines(strand.points, mDNATimePaint);
}
if (mDayXs == null) {
return;
}
int numDays = mDayXs.length;
int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2;
if (strand != null && strand.allDays != null && strand.allDays.length == numDays) {
for (int i = 0; i < numDays; i++) {
// this adds at most 7 draws. We could sort it by color and
// build an array instead but this is easier.
if (strand.allDays[i] != 0) {
mDNAAllDayPaint.setColor(strand.allDays[i]);
canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset,
DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint);
}
}
}
}
}
@Override
protected void updateSelectionPositions() {
if (mHasSelectedDay) {
int selectedPosition = mSelectedDay - mWeekStart;
if (selectedPosition < 0) {
selectedPosition += 7;
}
int effectiveWidth = mWidth - mPadding * 2;
effectiveWidth -= SPACING_WEEK_NUMBER;
mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding;
mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding;
mSelectedLeft += SPACING_WEEK_NUMBER;
mSelectedRight += SPACING_WEEK_NUMBER;
}
}
public int getDayIndexFromLocation(float x) {
int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding;
if (x < dayStart || x > mWidth - mPadding) {
return -1;
}
// Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)));
}
@Override
public Time getDayFromLocation(float x) {
int dayPosition = getDayIndexFromLocation(x);
if (dayPosition == -1) {
return null;
}
int day = mFirstJulianDay + dayPosition;
Time time = new Time(mTimeZone);
if (mWeek == 0) {
// This week is weird...
if (day < Time.EPOCH_JULIAN_DAY) {
day++;
} else if (day == Time.EPOCH_JULIAN_DAY) {
time.set(1, 0, 1970);
time.normalize(true);
return time;
}
}
time.setJulianDay(day);
return time;
}
@Override
public boolean onHoverEvent(MotionEvent event) {
Context context = getContext();
// only send accessibility events if accessibility and exploration are
// on.
AccessibilityManager am = (AccessibilityManager) context
.getSystemService(Service.ACCESSIBILITY_SERVICE);
if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
return super.onHoverEvent(event);
}
if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
Time hover = getDayFromLocation(event.getX());
if (hover != null
&& (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) {
Long millis = hover.toMillis(true);
String date = Utils.formatDateRange(context, millis, millis,
DateUtils.FORMAT_SHOW_DATE);
AccessibilityEvent accessEvent = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
accessEvent.getText().add(date);
if (mShowDetailsInMonth && mEvents != null) {
int dayStart = SPACING_WEEK_NUMBER + mPadding;
int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth
- dayStart - mPadding));
ArrayList<Event> events = mEvents.get(dayPosition);
List<CharSequence> text = accessEvent.getText();
for (Event e : events) {
text.add(e.getTitleAndLocation() + ". ");
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
if (!e.allDay) {
flags |= DateUtils.FORMAT_SHOW_TIME;
if (DateFormat.is24HourFormat(context)) {
flags |= DateUtils.FORMAT_24HOUR;
}
} else {
flags |= DateUtils.FORMAT_UTC;
}
text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis,
flags) + ". ");
}
}
sendAccessibilityEventUnchecked(accessEvent);
mLastHoverTime = hover;
}
}
return true;
}
public void setClickedDay(float xLocation) {
mClickedDayIndex = getDayIndexFromLocation(xLocation);
invalidate();
}
public void clearClickedDay() {
mClickedDayIndex = -1;
invalidate();
}
}