blob: 6716edf957ff19e8f350cebb2816e5c38b9e8034 [file] [log] [blame]
/*
* Copyright (C) 2007 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;
import static android.provider.Calendar.EVENT_BEGIN_TIME;
import static android.provider.Calendar.EVENT_END_TIME;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.Paint.Style;
import android.graphics.Path.Direction;
import android.net.Uri;
import android.os.Handler;
import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This is the base class for a set of classes that implement views (day view
* and week view to start with) that share some common code.
*/
public class CalendarView extends View
implements View.OnCreateContextMenuListener, View.OnClickListener {
private static float mScale = 0; // Used for supporting different screen densities
private boolean mOnFlingCalled;
protected CalendarApplication mCalendarApp;
protected CalendarActivity mParentActivity;
private static final String[] CALENDARS_PROJECTION = new String[] {
Calendars._ID, // 0
Calendars.ACCESS_LEVEL, // 1
Calendars.OWNER_ACCOUNT, // 2
};
private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1;
private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
private static final String[] ATTENDEES_PROJECTION = new String[] {
Attendees._ID, // 0
Attendees.ATTENDEE_RELATIONSHIP, // 1
};
private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
private static float SMALL_ROUND_RADIUS = 3.0F;
private static final int FROM_NONE = 0;
private static final int FROM_ABOVE = 1;
private static final int FROM_BELOW = 2;
private static final int FROM_LEFT = 4;
private static final int FROM_RIGHT = 8;
private static int HORIZONTAL_SCROLL_THRESHOLD = 50;
private ContinueScroll mContinueScroll = new ContinueScroll();
static private class DayHeader{
int cell;
String dateString;
}
private DayHeader[] dayHeaders = new DayHeader[32];
// Make this visible within the package for more informative debugging
Time mBaseDate;
private Typeface mBold = Typeface.DEFAULT_BOLD;
private int mFirstJulianDay;
private int mLastJulianDay;
private int mMonthLength;
private int mFirstDate;
private int[] mEarliestStartHour; // indexed by the week day offset
private boolean[] mHasAllDayEvent; // indexed by the week day offset
private String mDetailedView = CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW;
/**
* This variable helps to avoid unnecessarily reloading events by keeping
* track of the start millis parameter used for the most recent loading
* of events. If the next reload matches this, then the events are not
* reloaded. To force a reload, set this to zero (this is set to zero
* in the method clearCachedEvents()).
*/
private long mLastReloadMillis;
private ArrayList<Event> mEvents = new ArrayList<Event>();
private int mSelectionDay; // Julian day
private int mSelectionHour;
/* package private so that CalendarActivity can read it when creating new
* events
*/
boolean mSelectionAllDay;
private int mCellWidth;
// Pre-allocate these objects and re-use them
private Rect mRect = new Rect();
private RectF mRectF = new RectF();
private Rect mSrcRect = new Rect();
private Rect mDestRect = new Rect();
private Paint mPaint = new Paint();
private Paint mPaintBorder = new Paint();
private Paint mEventTextPaint = new Paint();
private Paint mSelectionPaint = new Paint();
private Path mPath = new Path();
protected boolean mDrawTextInEventRect;
private int mStartDay;
private PopupWindow mPopup;
private View mPopupView;
// The number of milliseconds to show the popup window
private static final int POPUP_DISMISS_DELAY = 3000;
private DismissPopup mDismissPopup = new DismissPopup();
// For drawing to an off-screen Canvas
private Bitmap mBitmap;
private Canvas mCanvas;
private boolean mRedrawScreen = true;
private boolean mRemeasure = true;
private final EventLoader mEventLoader;
protected final EventGeometry mEventGeometry;
private static final int DAY_GAP = 1;
private static final int HOUR_GAP = 1;
private static int SINGLE_ALLDAY_HEIGHT = 20;
private static int MAX_ALLDAY_HEIGHT = 72;
private static int ALLDAY_TOP_MARGIN = 3;
private static int MAX_ALLDAY_EVENT_HEIGHT = 18;
/* The extra space to leave above the text in all-day events */
private static final int ALL_DAY_TEXT_TOP_MARGIN = 0;
/* The extra space to leave above the text in normal events */
private static final int NORMAL_TEXT_TOP_MARGIN = 2;
private static final int HOURS_LEFT_MARGIN = 2;
private static final int HOURS_RIGHT_MARGIN = 4;
private static final int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN;
/* package */ static final int MINUTES_PER_HOUR = 60;
/* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
/* package */ static final int MILLIS_PER_MINUTE = 60 * 1000;
/* package */ static final int MILLIS_PER_HOUR = (3600 * 1000);
/* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
private static int NORMAL_FONT_SIZE = 12;
private static int EVENT_TEXT_FONT_SIZE = 12;
private static int HOURS_FONT_SIZE = 12;
private static int AMPM_FONT_SIZE = 9;
private static int MIN_CELL_WIDTH_FOR_TEXT = 27;
private static final int MAX_EVENT_TEXT_LEN = 500;
private static float MIN_EVENT_HEIGHT = 15.0F; // in pixels
private static int mSelectionColor;
private static int mPressedColor;
private static int mSelectedEventTextColor;
private static int mEventTextColor;
private static int mWeek_weekendColor;
private static int mCalendarDateBannerTextColor;
private static int mCalendarAllDayBackground;
private static int mCalendarAmPmLabel;
private static int mCalendarDateBannerBackground;
private static int mCalendarDateSelected;
private static int mCalendarGridAreaBackground;
private static int mCalendarGridAreaSelected;
private static int mCalendarGridLineHorizontalColor;
private static int mCalendarGridLineVerticalColor;
private static int mCalendarHourBackground;
private static int mCalendarHourLabel;
private static int mCalendarHourSelected;
private int mViewStartX;
private int mViewStartY;
private int mMaxViewStartY;
private int mBitmapHeight;
private int mViewHeight;
private int mViewWidth;
private int mGridAreaHeight;
private int mCellHeight;
private int mScrollStartY;
private int mPreviousDirection;
private int mPreviousDistanceX;
private int mHoursTextHeight;
private int mEventTextAscent;
private int mEventTextHeight;
private int mAllDayHeight;
private int mBannerPlusMargin;
private int mMaxAllDayEvents;
protected int mNumDays = 7;
private int mNumHours = 10;
private int mHoursWidth;
private int mDateStrWidth;
private int mFirstCell;
private int mFirstHour = -1;
private int mFirstHourOffset;
private String[] mHourStrs;
private String[] mDayStrs;
private String[] mDayStrs2Letter;
private boolean mIs24HourFormat;
private float[] mCharWidths = new float[MAX_EVENT_TEXT_LEN];
private ArrayList<Event> mSelectedEvents = new ArrayList<Event>();
private boolean mComputeSelectedEvents;
private Event mSelectedEvent;
private Event mPrevSelectedEvent;
private Rect mPrevBox = new Rect();
protected final Resources mResources;
private String mAmString;
private String mPmString;
private DeleteEventHelper mDeleteEventHelper;
private ContextMenuHandler mContextMenuHandler = new ContextMenuHandler();
/**
* The initial state of the touch mode when we enter this view.
*/
private static final int TOUCH_MODE_INITIAL_STATE = 0;
/**
* Indicates we just received the touch event and we are waiting to see if
* it is a tap or a scroll gesture.
*/
private static final int TOUCH_MODE_DOWN = 1;
/**
* Indicates the touch gesture is a vertical scroll
*/
private static final int TOUCH_MODE_VSCROLL = 0x20;
/**
* Indicates the touch gesture is a horizontal scroll
*/
private static final int TOUCH_MODE_HSCROLL = 0x40;
private int mTouchMode = TOUCH_MODE_INITIAL_STATE;
/**
* The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS.
*/
private static final int SELECTION_HIDDEN = 0;
private static final int SELECTION_PRESSED = 1;
private static final int SELECTION_SELECTED = 2;
private static final int SELECTION_LONGPRESS = 3;
private int mSelectionMode = SELECTION_HIDDEN;
private boolean mScrolling = false;
private String mDateRange;
private TextView mTitleTextView;
public CalendarView(CalendarActivity activity) {
super(activity);
if (mScale == 0) {
mScale = getContext().getResources().getDisplayMetrics().density;
if (mScale != 1) {
SINGLE_ALLDAY_HEIGHT *= mScale;
MAX_ALLDAY_HEIGHT *= mScale;
ALLDAY_TOP_MARGIN *= mScale;
MAX_ALLDAY_EVENT_HEIGHT *= mScale;
NORMAL_FONT_SIZE *= mScale;
EVENT_TEXT_FONT_SIZE *= mScale;
HOURS_FONT_SIZE *= mScale;
AMPM_FONT_SIZE *= mScale;
MIN_CELL_WIDTH_FOR_TEXT *= mScale;
MIN_EVENT_HEIGHT *= mScale;
HORIZONTAL_SCROLL_THRESHOLD *= mScale;
SMALL_ROUND_RADIUS *= mScale;
}
}
mResources = activity.getResources();
mEventLoader = activity.mEventLoader;
mEventGeometry = new EventGeometry();
mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT);
mEventGeometry.setHourGap(HOUR_GAP);
mParentActivity = activity;
mCalendarApp = (CalendarApplication) mParentActivity.getApplication();
mDeleteEventHelper = new DeleteEventHelper(activity, false /* don't exit when done */);
init(activity);
}
private void init(Context context) {
setFocusable(true);
// Allow focus in touch mode so that we can do keyboard shortcuts
// even after we've entered touch mode.
setFocusableInTouchMode(true);
setClickable(true);
setOnCreateContextMenuListener(this);
mStartDay = Calendar.getInstance().getFirstDayOfWeek();
if (mStartDay == Calendar.SATURDAY) {
mStartDay = Time.SATURDAY;
} else if (mStartDay == Calendar.MONDAY) {
mStartDay = Time.MONDAY;
} else {
mStartDay = Time.SUNDAY;
}
mWeek_weekendColor = mResources.getColor(R.color.week_weekend);
mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color);
mCalendarAllDayBackground = mResources.getColor(R.color.calendar_all_day_background);
mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label);
mCalendarDateBannerBackground = mResources.getColor(R.color.calendar_date_banner_background);
mCalendarDateSelected = mResources.getColor(R.color.calendar_date_selected);
mCalendarGridAreaBackground = mResources.getColor(R.color.calendar_grid_area_background);
mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected);
mCalendarGridLineHorizontalColor = mResources.getColor(R.color.calendar_grid_line_horizontal_color);
mCalendarGridLineVerticalColor = mResources.getColor(R.color.calendar_grid_line_vertical_color);
mCalendarHourBackground = mResources.getColor(R.color.calendar_hour_background);
mCalendarHourLabel = mResources.getColor(R.color.calendar_hour_label);
mCalendarHourSelected = mResources.getColor(R.color.calendar_hour_selected);
mSelectionColor = mResources.getColor(R.color.selection);
mPressedColor = mResources.getColor(R.color.pressed);
mSelectedEventTextColor = mResources.getColor(R.color.calendar_event_selected_text_color);
mEventTextColor = mResources.getColor(R.color.calendar_event_text_color);
mEventTextPaint.setColor(mEventTextColor);
mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE);
mEventTextPaint.setTextAlign(Paint.Align.LEFT);
mEventTextPaint.setAntiAlias(true);
int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color);
Paint p = mSelectionPaint;
p.setColor(gridLineColor);
p.setStyle(Style.STROKE);
p.setStrokeWidth(2.0f);
p.setAntiAlias(false);
p = mPaint;
p.setAntiAlias(true);
mPaintBorder.setColor(0xffc8c8c8);
mPaintBorder.setStyle(Style.STROKE);
mPaintBorder.setAntiAlias(true);
mPaintBorder.setStrokeWidth(2.0f);
// Allocate space for 2 weeks worth of weekday names so that we can
// easily start the week display at any week day.
mDayStrs = new String[14];
// Also create an array of 2-letter abbreviations.
mDayStrs2Letter = new String[14];
for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
int index = i - Calendar.SUNDAY;
// e.g. Tue for Tuesday
mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM);
mDayStrs[index + 7] = mDayStrs[index];
// e.g. Tu for Tuesday
mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT);
// If we don't have 2-letter day strings, fall back to 1-letter.
if (mDayStrs2Letter[index].equals(mDayStrs[index])) {
mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST);
}
mDayStrs2Letter[index + 7] = mDayStrs2Letter[index];
}
// Figure out how much space we need for the 3-letter abbrev names
// in the worst case.
p.setTextSize(NORMAL_FONT_SIZE);
p.setTypeface(mBold);
String[] dateStrs = {" 28", " 30"};
mDateStrWidth = computeMaxStringWidth(0, dateStrs, p);
mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p);
p.setTextSize(HOURS_FONT_SIZE);
p.setTypeface(null);
mIs24HourFormat = DateFormat.is24HourFormat(context);
mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm;
mHoursWidth = computeMaxStringWidth(0, mHourStrs, p);
mAmString = DateUtils.getAMPMString(Calendar.AM);
mPmString = DateUtils.getAMPMString(Calendar.PM);
String[] ampm = {mAmString, mPmString};
p.setTextSize(AMPM_FONT_SIZE);
mHoursWidth = computeMaxStringWidth(mHoursWidth, ampm, p);
mHoursWidth += HOURS_MARGIN;
LayoutInflater inflater;
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mPopupView = inflater.inflate(R.layout.bubble_event, null);
mPopupView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mPopup = new PopupWindow(context);
mPopup.setContentView(mPopupView);
Resources.Theme dialogTheme = getResources().newTheme();
dialogTheme.applyStyle(android.R.style.Theme_Dialog, true);
TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] {
android.R.attr.windowBackground });
mPopup.setBackgroundDrawable(ta.getDrawable(0));
ta.recycle();
// Enable touching the popup window
mPopupView.setOnClickListener(this);
mBaseDate = new Time();
long millis = System.currentTimeMillis();
mBaseDate.set(millis);
mEarliestStartHour = new int[mNumDays];
mHasAllDayEvent = new boolean[mNumDays];
mNumHours = context.getResources().getInteger(R.integer.number_of_hours);
mTitleTextView = (TextView) mParentActivity.findViewById(R.id.title);
}
/**
* This is called when the popup window is pressed.
*/
public void onClick(View v) {
if (v == mPopupView) {
// Pretend it was a trackball click because that will always
// jump to the "View event" screen.
switchViews(true /* trackball */);
}
}
/**
* Returns the start of the selected time in milliseconds since the epoch.
*
* @return selected time in UTC milliseconds since the epoch.
*/
long getSelectedTimeInMillis() {
Time time = new Time(mBaseDate);
time.setJulianDay(mSelectionDay);
time.hour = mSelectionHour;
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
return time.normalize(true /* ignore isDst */);
}
Time getSelectedTime() {
Time time = new Time(mBaseDate);
time.setJulianDay(mSelectionDay);
time.hour = mSelectionHour;
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
time.normalize(true /* ignore isDst */);
return time;
}
/**
* Returns the start of the selected time in minutes since midnight,
* local time. The derived class must ensure that this is consistent
* with the return value from getSelectedTimeInMillis().
*/
int getSelectedMinutesSinceMidnight() {
return mSelectionHour * MINUTES_PER_HOUR;
}
public void setSelectedDay(Time time) {
mBaseDate.set(time);
mSelectionHour = mBaseDate.hour;
mSelectedEvent = null;
mPrevSelectedEvent = null;
long millis = mBaseDate.toMillis(false /* use isDst */);
mSelectionDay = Time.getJulianDay(millis, mBaseDate.gmtoff);
mSelectedEvents.clear();
mComputeSelectedEvents = true;
// Force a recalculation of the first visible hour
mFirstHour = -1;
recalc();
mTitleTextView.setText(mDateRange);
// Force a redraw of the selection box.
mSelectionMode = SELECTION_SELECTED;
mRedrawScreen = true;
mRemeasure = true;
invalidate();
}
public Time getSelectedDay() {
Time time = new Time(mBaseDate);
time.setJulianDay(mSelectionDay);
time.hour = mSelectionHour;
// We ignore the "isDst" field because we want normalize() to figure
// out the correct DST value and not adjust the selected time based
// on the current setting of DST.
time.normalize(true /* ignore isDst */);
return time;
}
private void recalc() {
// Set the base date to the beginning of the week if we are displaying
// 7 days at a time.
if (mNumDays == 7) {
int dayOfWeek = mBaseDate.weekDay;
int diff = dayOfWeek - mStartDay;
if (diff != 0) {
if (diff < 0) {
diff += 7;
}
mBaseDate.monthDay -= diff;
mBaseDate.normalize(true /* ignore isDst */);
}
}
final long start = mBaseDate.toMillis(false /* use isDst */);
long end = start;
mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff);
mLastJulianDay = mFirstJulianDay + mNumDays - 1;
mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY);
mFirstDate = mBaseDate.monthDay;
int flags = DateUtils.FORMAT_SHOW_YEAR;
if (DateFormat.is24HourFormat(mContext)) {
flags |= DateUtils.FORMAT_24HOUR;
}
if (mNumDays > 1) {
mBaseDate.monthDay += mNumDays - 1;
end = mBaseDate.toMillis(true /* ignore isDst */);
mBaseDate.monthDay -= mNumDays - 1;
flags |= DateUtils.FORMAT_NO_MONTH_DAY;
} else {
flags |= DateUtils.FORMAT_SHOW_WEEKDAY
| DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
}
mDateRange = DateUtils.formatDateRange(mParentActivity, start, end, flags);
// Do not set the title here because this is called when executing
// initNextView() to prepare the Day view when sliding the finger
// horizontally but we don't always want to change the title. And
// if we change the title here and then change it back in the caller
// then we get an annoying flicker.
}
void setDetailedView(String detailedView) {
mDetailedView = detailedView;
}
@Override
protected void onSizeChanged(int width, int height, int oldw, int oldh) {
mViewWidth = width;
mViewHeight = height;
int gridAreaWidth = width - mHoursWidth;
mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays;
Paint p = new Paint();
p.setTextSize(NORMAL_FONT_SIZE);
int bannerTextHeight = (int) Math.abs(p.ascent());
p.setTextSize(HOURS_FONT_SIZE);
mHoursTextHeight = (int) Math.abs(p.ascent());
p.setTextSize(EVENT_TEXT_FONT_SIZE);
float ascent = -p.ascent();
mEventTextAscent = (int) Math.ceil(ascent);
float totalHeight = ascent + p.descent();
mEventTextHeight = (int) Math.ceil(totalHeight);
if (mNumDays > 1) {
mBannerPlusMargin = bannerTextHeight + 14;
} else {
mBannerPlusMargin = 0;
}
remeasure(width, height);
}
// Measures the space needed for various parts of the view after
// loading new events. This can change if there are all-day events.
private void remeasure(int width, int height) {
// First, clear the array of earliest start times, and the array
// indicating presence of an all-day event.
for (int day = 0; day < mNumDays; day++) {
mEarliestStartHour[day] = 25; // some big number
mHasAllDayEvent[day] = false;
}
// Compute the space needed for the all-day events, if any.
// Make a pass over all the events, and keep track of the maximum
// number of all-day events in any one day. Also, keep track of
// the earliest event in each day.
int maxAllDayEvents = 0;
ArrayList<Event> events = mEvents;
int len = events.size();
for (int ii = 0; ii < len; ii++) {
Event event = events.get(ii);
if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay)
continue;
if (event.allDay) {
int max = event.getColumn() + 1;
if (maxAllDayEvents < max) {
maxAllDayEvents = max;
}
int daynum = event.startDay - mFirstJulianDay;
int durationDays = event.endDay - event.startDay + 1;
if (daynum < 0) {
durationDays += daynum;
daynum = 0;
}
if (daynum + durationDays > mNumDays) {
durationDays = mNumDays - daynum;
}
for (int day = daynum; durationDays > 0; day++, durationDays--) {
mHasAllDayEvent[day] = true;
}
} else {
int daynum = event.startDay - mFirstJulianDay;
int hour = event.startTime / 60;
if (daynum >= 0 && hour < mEarliestStartHour[daynum]) {
mEarliestStartHour[daynum] = hour;
}
// Also check the end hour in case the event spans more than
// one day.
daynum = event.endDay - mFirstJulianDay;
hour = event.endTime / 60;
if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) {
mEarliestStartHour[daynum] = hour;
}
}
}
mMaxAllDayEvents = maxAllDayEvents;
mFirstCell = mBannerPlusMargin;
int allDayHeight = 0;
if (maxAllDayEvents > 0) {
// If there is at most one all-day event per day, then use less
// space (but more than the space for a single event).
if (maxAllDayEvents == 1) {
allDayHeight = SINGLE_ALLDAY_HEIGHT;
} else {
// Allow the all-day area to grow in height depending on the
// number of all-day events we need to show, up to a limit.
allDayHeight = maxAllDayEvents * MAX_ALLDAY_EVENT_HEIGHT;
if (allDayHeight > MAX_ALLDAY_HEIGHT) {
allDayHeight = MAX_ALLDAY_HEIGHT;
}
}
mFirstCell = mBannerPlusMargin + allDayHeight + ALLDAY_TOP_MARGIN;
} else {
mSelectionAllDay = false;
}
mAllDayHeight = allDayHeight;
mGridAreaHeight = height - mFirstCell;
mCellHeight = (mGridAreaHeight - ((mNumHours + 1) * HOUR_GAP)) / mNumHours;
int usedGridAreaHeight = (mCellHeight + HOUR_GAP) * mNumHours + HOUR_GAP;
int bottomSpace = mGridAreaHeight - usedGridAreaHeight;
mEventGeometry.setHourHeight(mCellHeight);
// Create an off-screen bitmap that we can draw into.
mBitmapHeight = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) + bottomSpace;
if ((mBitmap == null || mBitmap.getHeight() < mBitmapHeight) && width > 0 &&
mBitmapHeight > 0) {
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = Bitmap.createBitmap(width, mBitmapHeight, Bitmap.Config.RGB_565);
mCanvas = new Canvas(mBitmap);
}
mMaxViewStartY = mBitmapHeight - mGridAreaHeight;
if (mFirstHour == -1) {
initFirstHour();
mFirstHourOffset = 0;
}
// When we change the base date, the number of all-day events may
// change and that changes the cell height. When we switch dates,
// we use the mFirstHourOffset from the previous view, but that may
// be too large for the new view if the cell height is smaller.
if (mFirstHourOffset >= mCellHeight + HOUR_GAP) {
mFirstHourOffset = mCellHeight + HOUR_GAP - 1;
}
mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset;
int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP);
mPopup.dismiss();
mPopup.setWidth(eventAreaWidth - 20);
mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
}
/**
* Initialize the state for another view. The given view is one that has
* its own bitmap and will use an animation to replace the current view.
* The current view and new view are either both Week views or both Day
* views. They differ in their base date.
*
* @param view the view to initialize.
*/
private void initView(CalendarView view) {
view.mSelectionHour = mSelectionHour;
view.mSelectedEvents.clear();
view.mComputeSelectedEvents = true;
view.mFirstHour = mFirstHour;
view.mFirstHourOffset = mFirstHourOffset;
view.remeasure(getWidth(), getHeight());
view.mSelectedEvent = null;
view.mPrevSelectedEvent = null;
view.mStartDay = mStartDay;
if (view.mEvents.size() > 0) {
view.mSelectionAllDay = mSelectionAllDay;
} else {
view.mSelectionAllDay = false;
}
// Redraw the screen so that the selection box will be redrawn. We may
// have scrolled to a different part of the day in some other view
// so the selection box in this view may no longer be visible.
view.mRedrawScreen = true;
view.recalc();
}
/**
* Switch to another view based on what was selected (an event or a free
* slot) and how it was selected (by touch or by trackball).
*
* @param trackBallSelection true if the selection was made using the
* trackball.
*/
private void switchViews(boolean trackBallSelection) {
Event selectedEvent = mSelectedEvent;
mPopup.dismiss();
if (mNumDays > 1) {
// This is the Week view.
// With touch, we always switch to Day/Agenda View
// With track ball, if we selected a free slot, then create an event.
// If we selected a specific event, switch to EventInfo view.
if (trackBallSelection) {
if (selectedEvent == null) {
// Switch to the EditEvent view
long startMillis = getSelectedTimeInMillis();
long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(mContext, EditEvent.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, startMillis);
intent.putExtra(EVENT_END_TIME, endMillis);
mParentActivity.startActivity(intent);
} else {
// Switch to the EventInfo view
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
selectedEvent.id);
intent.setData(eventUri);
intent.setClassName(mContext, EventInfoActivity.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
mParentActivity.startActivity(intent);
}
} else {
// This was a touch selection. If the touch selected a single
// unambiguous event, then view that event. Otherwise go to
// Day/Agenda view.
if (mSelectedEvents.size() == 1) {
// Switch to the EventInfo view
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
selectedEvent.id);
intent.setData(eventUri);
intent.setClassName(mContext, EventInfoActivity.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
mParentActivity.startActivity(intent);
} else {
// Switch to the Day/Agenda view.
long millis = getSelectedTimeInMillis();
Utils.startActivity(mParentActivity, mDetailedView, millis);
}
}
} else {
// This is the Day view.
// If we selected a free slot, then create an event.
// If we selected an event, then go to the EventInfo view.
if (selectedEvent == null) {
// Switch to the EditEvent view
long startMillis = getSelectedTimeInMillis();
long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(mContext, EditEvent.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, startMillis);
intent.putExtra(EVENT_END_TIME, endMillis);
mParentActivity.startActivity(intent);
} else {
// Switch to the EventInfo view
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, selectedEvent.id);
intent.setData(eventUri);
intent.setClassName(mContext, EventInfoActivity.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, selectedEvent.startMillis);
intent.putExtra(EVENT_END_TIME, selectedEvent.endMillis);
mParentActivity.startActivity(intent);
}
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
mScrolling = false;
long duration = event.getEventTime() - event.getDownTime();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
if (mSelectionMode == SELECTION_HIDDEN) {
// Don't do anything unless the selection is visible.
break;
}
if (mSelectionMode == SELECTION_PRESSED) {
// This was the first press when there was nothing selected.
// Change the selection from the "pressed" state to the
// the "selected" state. We treat short-press and
// long-press the same here because nothing was selected.
mSelectionMode = SELECTION_SELECTED;
mRedrawScreen = true;
invalidate();
break;
}
// Check the duration to determine if this was a short press
if (duration < ViewConfiguration.getLongPressTimeout()) {
switchViews(true /* trackball */);
} else {
mSelectionMode = SELECTION_LONGPRESS;
mRedrawScreen = true;
invalidate();
performLongClick();
}
break;
case KeyEvent.KEYCODE_BACK:
if (event.isTracking() && !event.isCanceled()) {
mPopup.dismiss();
mParentActivity.finish();
return true;
}
break;
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (mSelectionMode == SELECTION_HIDDEN) {
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
// Display the selection box but don't move or select it
// on this key press.
mSelectionMode = SELECTION_SELECTED;
mRedrawScreen = true;
invalidate();
return true;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
// Display the selection box but don't select it
// on this key press.
mSelectionMode = SELECTION_PRESSED;
mRedrawScreen = true;
invalidate();
return true;
}
}
mSelectionMode = SELECTION_SELECTED;
mScrolling = false;
boolean redraw;
int selectionDay = mSelectionDay;
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
// Delete the selected event, if any
Event selectedEvent = mSelectedEvent;
if (selectedEvent == null) {
return false;
}
mPopup.dismiss();
long begin = selectedEvent.startMillis;
long end = selectedEvent.endMillis;
long id = selectedEvent.id;
mDeleteEventHelper.delete(begin, end, id, -1);
return true;
case KeyEvent.KEYCODE_ENTER:
switchViews(true /* trackball or keyboard */);
return true;
case KeyEvent.KEYCODE_BACK:
if (event.getRepeatCount() == 0) {
event.startTracking();
return true;
}
return super.onKeyDown(keyCode, event);
case KeyEvent.KEYCODE_DPAD_LEFT:
if (mSelectedEvent != null) {
mSelectedEvent = mSelectedEvent.nextLeft;
}
if (mSelectedEvent == null) {
selectionDay -= 1;
}
redraw = true;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (mSelectedEvent != null) {
mSelectedEvent = mSelectedEvent.nextRight;
}
if (mSelectedEvent == null) {
selectionDay += 1;
}
redraw = true;
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (mSelectedEvent != null) {
mSelectedEvent = mSelectedEvent.nextUp;
}
if (mSelectedEvent == null) {
if (!mSelectionAllDay) {
mSelectionHour -= 1;
adjustHourSelection();
mSelectedEvents.clear();
mComputeSelectedEvents = true;
}
}
redraw = true;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (mSelectedEvent != null) {
mSelectedEvent = mSelectedEvent.nextDown;
}
if (mSelectedEvent == null) {
if (mSelectionAllDay) {
mSelectionAllDay = false;
} else {
mSelectionHour++;
adjustHourSelection();
mSelectedEvents.clear();
mComputeSelectedEvents = true;
}
}
redraw = true;
break;
default:
return super.onKeyDown(keyCode, event);
}
if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) {
boolean forward;
CalendarView view = mParentActivity.getNextView();
Time date = view.mBaseDate;
date.set(mBaseDate);
if (selectionDay < mFirstJulianDay) {
date.monthDay -= mNumDays;
forward = false;
} else {
date.monthDay += mNumDays;
forward = true;
}
date.normalize(true /* ignore isDst */);
view.mSelectionDay = selectionDay;
initView(view);
mTitleTextView.setText(view.mDateRange);
mParentActivity.switchViews(forward, 0, 0);
return true;
}
mSelectionDay = selectionDay;
mSelectedEvents.clear();
mComputeSelectedEvents = true;
if (redraw) {
mRedrawScreen = true;
invalidate();
return true;
}
return super.onKeyDown(keyCode, event);
}
// This is called after scrolling stops to move the selected hour
// to the visible part of the screen.
private void resetSelectedHour() {
if (mSelectionHour < mFirstHour + 1) {
mSelectionHour = mFirstHour + 1;
mSelectedEvent = null;
mSelectedEvents.clear();
mComputeSelectedEvents = true;
} else if (mSelectionHour > mFirstHour + mNumHours - 3) {
mSelectionHour = mFirstHour + mNumHours - 3;
mSelectedEvent = null;
mSelectedEvents.clear();
mComputeSelectedEvents = true;
}
}
private void initFirstHour() {
mFirstHour = mSelectionHour - mNumHours / 2;
if (mFirstHour < 0) {
mFirstHour = 0;
} else if (mFirstHour + mNumHours > 24) {
mFirstHour = 24 - mNumHours;
}
}
/**
* Recomputes the first full hour that is visible on screen after the
* screen is scrolled.
*/
private void computeFirstHour() {
// Compute the first full hour that is visible on screen
mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP);
mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY;
}
private void adjustHourSelection() {
if (mSelectionHour < 0) {
mSelectionHour = 0;
if (mMaxAllDayEvents > 0) {
mPrevSelectedEvent = null;
mSelectionAllDay = true;
}
}
if (mSelectionHour > 23) {
mSelectionHour = 23;
}
// If the selected hour is at least 2 time slots from the top and
// bottom of the screen, then don't scroll the view.
if (mSelectionHour < mFirstHour + 1) {
// If there are all-days events for the selected day but there
// are no more normal events earlier in the day, then jump to
// the all-day event area.
// Exception 1: allow the user to scroll to 8am with the trackball
// before jumping to the all-day event area.
// Exception 2: if 12am is on screen, then allow the user to select
// 12am before going up to the all-day event area.
int daynum = mSelectionDay - mFirstJulianDay;
if (mMaxAllDayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour
&& mFirstHour > 0 && mFirstHour < 8) {
mPrevSelectedEvent = null;
mSelectionAllDay = true;
mSelectionHour = mFirstHour + 1;
return;
}
if (mFirstHour > 0) {
mFirstHour -= 1;
mViewStartY -= (mCellHeight + HOUR_GAP);
if (mViewStartY < 0) {
mViewStartY = 0;
}
return;
}
}
if (mSelectionHour > mFirstHour + mNumHours - 3) {
if (mFirstHour < 24 - mNumHours) {
mFirstHour += 1;
mViewStartY += (mCellHeight + HOUR_GAP);
if (mViewStartY > mBitmapHeight - mGridAreaHeight) {
mViewStartY = mBitmapHeight - mGridAreaHeight;
}
return;
} else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) {
mViewStartY = mBitmapHeight - mGridAreaHeight;
}
}
}
void clearCachedEvents() {
mLastReloadMillis = 0;
}
private Runnable mCancelCallback = new Runnable() {
public void run() {
clearCachedEvents();
}
};
void reloadEvents() {
// Protect against this being called before this view has been
// initialized.
if (mParentActivity == null) {
return;
}
mSelectedEvent = null;
mPrevSelectedEvent = null;
mSelectedEvents.clear();
// The start date is the beginning of the week at 12am
Time weekStart = new Time();
weekStart.set(mBaseDate);
weekStart.hour = 0;
weekStart.minute = 0;
weekStart.second = 0;
long millis = weekStart.normalize(true /* ignore isDst */);
// Avoid reloading events unnecessarily.
if (millis == mLastReloadMillis) {
return;
}
mLastReloadMillis = millis;
// load events in the background
mParentActivity.startProgressSpinner();
final ArrayList<Event> events = new ArrayList<Event>();
mEventLoader.loadEventsInBackground(mNumDays, events, millis, new Runnable() {
public void run() {
mEvents = events;
mRemeasure = true;
mRedrawScreen = true;
mComputeSelectedEvents = true;
recalc();
mParentActivity.stopProgressSpinner();
invalidate();
}
}, mCancelCallback);
}
@Override
protected void onDraw(Canvas canvas) {
if (mRemeasure) {
remeasure(getWidth(), getHeight());
mRemeasure = false;
}
if (mRedrawScreen && mCanvas != null) {
doDraw(mCanvas);
mRedrawScreen = false;
}
if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
canvas.save();
if (mViewStartX > 0) {
canvas.translate(mViewWidth - mViewStartX, 0);
} else {
canvas.translate(-(mViewWidth + mViewStartX), 0);
}
CalendarView nextView = mParentActivity.getNextView();
// Prevent infinite recursive calls to onDraw().
nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE;
nextView.onDraw(canvas);
canvas.restore();
canvas.save();
canvas.translate(-mViewStartX, 0);
}
if (mBitmap != null) {
drawCalendarView(canvas);
}
// Draw the fixed areas (that don't scroll) directly to the canvas.
drawAfterScroll(canvas);
mComputeSelectedEvents = false;
if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
canvas.restore();
}
}
private void drawCalendarView(Canvas canvas) {
// Copy the scrollable region from the big bitmap to the canvas.
Rect src = mSrcRect;
Rect dest = mDestRect;
src.top = mViewStartY;
src.bottom = mViewStartY + mGridAreaHeight;
src.left = 0;
src.right = mViewWidth;
dest.top = mFirstCell;
dest.bottom = mViewHeight;
dest.left = 0;
dest.right = mViewWidth;
canvas.save();
canvas.clipRect(dest);
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
canvas.drawBitmap(mBitmap, src, dest, null);
canvas.restore();
}
private void drawAfterScroll(Canvas canvas) {
Paint p = mPaint;
Rect r = mRect;
if (mMaxAllDayEvents != 0) {
drawAllDayEvents(mFirstJulianDay, mNumDays, r, canvas, p);
drawUpperLeftCorner(r, canvas, p);
}
if (mNumDays > 1) {
drawDayHeaderLoop(r, canvas, p);
}
// Draw the AM and PM indicators if we're in 12 hour mode
if (!mIs24HourFormat) {
drawAmPm(canvas, p);
}
// Update the popup window showing the event details, but only if
// we are not scrolling and we have focus.
if (!mScrolling && isFocused()) {
updateEventDetails();
}
}
// This isn't really the upper-left corner. It's the square area just
// below the upper-left corner, above the hours and to the left of the
// all-day area.
private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) {
p.setColor(mCalendarHourBackground);
r.top = mBannerPlusMargin;
r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
r.left = 0;
r.right = mHoursWidth;
canvas.drawRect(r, p);
}
private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) {
// Draw the horizontal day background banner
p.setColor(mCalendarDateBannerBackground);
r.top = 0;
r.bottom = mBannerPlusMargin;
r.left = 0;
r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP);
canvas.drawRect(r, p);
// Fill the extra space on the right side with the default background
r.left = r.right;
r.right = mViewWidth;
p.setColor(mCalendarGridAreaBackground);
canvas.drawRect(r, p);
// Draw a highlight on the selected day (if any), but only if we are
// displaying more than one day.
if (mSelectionMode != SELECTION_HIDDEN) {
if (mNumDays > 1) {
p.setColor(mCalendarDateSelected);
r.top = 0;
r.bottom = mBannerPlusMargin;
int daynum = mSelectionDay - mFirstJulianDay;
r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
r.right = r.left + mCellWidth;
canvas.drawRect(r, p);
}
}
p.setTextSize(NORMAL_FONT_SIZE);
p.setTextAlign(Paint.Align.CENTER);
int x = mHoursWidth;
int deltaX = mCellWidth + DAY_GAP;
int cell = mFirstJulianDay;
String[] dayNames;
if (mDateStrWidth < mCellWidth) {
dayNames = mDayStrs;
} else {
dayNames = mDayStrs2Letter;
}
p.setTypeface(mBold);
p.setAntiAlias(true);
for (int day = 0; day < mNumDays; day++, cell++) {
drawDayHeader(dayNames[day + mStartDay], day, cell, x, canvas, p);
x += deltaX;
}
}
private void drawAmPm(Canvas canvas, Paint p) {
p.setColor(mCalendarAmPmLabel);
p.setTextSize(AMPM_FONT_SIZE);
p.setTypeface(mBold);
p.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.RIGHT);
String text = mAmString;
if (mFirstHour >= 12) {
text = mPmString;
}
int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP;
int right = mHoursWidth - HOURS_RIGHT_MARGIN;
canvas.drawText(text, right, y, p);
if (mFirstHour < 12 && mFirstHour + mNumHours > 12) {
// Also draw the "PM"
text = mPmString;
y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP)
+ 2 * mHoursTextHeight + HOUR_GAP;
canvas.drawText(text, right, y, p);
}
}
private void doDraw(Canvas canvas) {
Paint p = mPaint;
Rect r = mRect;
drawGridBackground(r, canvas, p);
drawHours(r, canvas, p);
// Draw each day
int x = mHoursWidth;
int deltaX = mCellWidth + DAY_GAP;
int cell = mFirstJulianDay;
for (int day = 0; day < mNumDays; day++, cell++) {
drawEvents(cell, x, HOUR_GAP, canvas, p);
x += deltaX;
}
}
private void drawHours(Rect r, Canvas canvas, Paint p) {
// Draw the background for the hour labels
p.setColor(mCalendarHourBackground);
r.top = 0;
r.bottom = 24 * (mCellHeight + HOUR_GAP) + HOUR_GAP;
r.left = 0;
r.right = mHoursWidth;
canvas.drawRect(r, p);
// Fill the bottom left corner with the default grid background
r.top = r.bottom;
r.bottom = mBitmapHeight;
p.setColor(mCalendarGridAreaBackground);
canvas.drawRect(r, p);
// Draw a highlight on the selected hour (if needed)
if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllDay) {
p.setColor(mCalendarHourSelected);
r.top = mSelectionHour * (mCellHeight + HOUR_GAP);
r.bottom = r.top + mCellHeight + 2 * HOUR_GAP;
r.left = 0;
r.right = mHoursWidth;
canvas.drawRect(r, p);
// Also draw the highlight on the grid
p.setColor(mCalendarGridAreaSelected);
int daynum = mSelectionDay - mFirstJulianDay;
r.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
r.right = r.left + mCellWidth;
canvas.drawRect(r, p);
// Draw a border around the highlighted grid hour.
Path path = mPath;
r.top += HOUR_GAP;
r.bottom -= HOUR_GAP;
path.reset();
path.addRect(r.left, r.top, r.right, r.bottom, Direction.CW);
canvas.drawPath(path, mSelectionPaint);
saveSelectionPosition(r.left, r.top, r.right, r.bottom);
}
p.setColor(mCalendarHourLabel);
p.setTextSize(HOURS_FONT_SIZE);
p.setTypeface(mBold);
p.setTextAlign(Paint.Align.RIGHT);
p.setAntiAlias(true);
int right = mHoursWidth - HOURS_RIGHT_MARGIN;
int y = HOUR_GAP + mHoursTextHeight;
for (int i = 0; i < 24; i++) {
String time = mHourStrs[i];
canvas.drawText(time, right, y, p);
y += mCellHeight + HOUR_GAP;
}
}
private void drawDayHeader(String dateStr, int day, int cell, int x, Canvas canvas, Paint p) {
float xCenter = x + mCellWidth / 2.0f;
boolean isWeekend = false;
if ((mStartDay == Time.SUNDAY && (day == 0 || day == 6))
|| (mStartDay == Time.MONDAY && (day == 5 || day == 6))
|| (mStartDay == Time.SATURDAY && (day == 0 || day == 1))) {
isWeekend = true;
}
if (isWeekend) {
p.setColor(mWeek_weekendColor);
} else {
p.setColor(mCalendarDateBannerTextColor);
}
int dateNum = mFirstDate + day;
if (dateNum > mMonthLength) {
dateNum -= mMonthLength;
}
String dateNumStr;
// Add a leading zero if the date is a single digit
if (dateNum < 10) {
dateNumStr = "0" + dateNum;
} else {
dateNumStr = String.valueOf(dateNum);
}
DayHeader header = dayHeaders[day];
if (header == null || header.cell != cell) {
// The day header string is regenerated on every draw during drag and fling animation.
// Caching day header since formatting the string takes surprising long time.
dayHeaders[day] = new DayHeader();
dayHeaders[day].cell = cell;
dayHeaders[day].dateString = getResources().getString(
R.string.weekday_day, dateStr, dateNumStr);
}
dateStr = dayHeaders[day].dateString;
float y = mBannerPlusMargin - 7;
canvas.drawText(dateStr, xCenter, y, p);
}
private void drawGridBackground(Rect r, Canvas canvas, Paint p) {
Paint.Style savedStyle = p.getStyle();
// Clear the background
p.setColor(mCalendarGridAreaBackground);
r.top = 0;
r.bottom = mBitmapHeight;
r.left = 0;
r.right = mViewWidth;
canvas.drawRect(r, p);
// Draw the horizontal grid lines
p.setColor(mCalendarGridLineHorizontalColor);
p.setStyle(Style.STROKE);
p.setStrokeWidth(0);
p.setAntiAlias(false);
float startX = mHoursWidth;
float stopX = mHoursWidth + (mCellWidth + DAY_GAP) * mNumDays;
float y = 0;
float deltaY = mCellHeight + HOUR_GAP;
for (int hour = 0; hour <= 24; hour++) {
canvas.drawLine(startX, y, stopX, y, p);
y += deltaY;
}
// Draw the vertical grid lines
p.setColor(mCalendarGridLineVerticalColor);
float startY = 0;
float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP);
float deltaX = mCellWidth + DAY_GAP;
float x = mHoursWidth + mCellWidth;
for (int day = 0; day < mNumDays; day++) {
canvas.drawLine(x, startY, x, stopY, p);
x += deltaX;
}
// Restore the saved style.
p.setStyle(savedStyle);
p.setAntiAlias(true);
}
Event getSelectedEvent() {
if (mSelectedEvent == null) {
// There is no event at the selected hour, so create a new event.
return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
getSelectedMinutesSinceMidnight());
}
return mSelectedEvent;
}
boolean isEventSelected() {
return (mSelectedEvent != null);
}
Event getNewEvent() {
return getNewEvent(mSelectionDay, getSelectedTimeInMillis(),
getSelectedMinutesSinceMidnight());
}
static Event getNewEvent(int julianDay, long utcMillis,
int minutesSinceMidnight) {
Event event = Event.newInstance();
event.startDay = julianDay;
event.endDay = julianDay;
event.startMillis = utcMillis;
event.endMillis = event.startMillis + MILLIS_PER_HOUR;
event.startTime = minutesSinceMidnight;
event.endTime = event.startTime + MINUTES_PER_HOUR;
return event;
}
private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) {
float maxWidthF = 0.0f;
int len = strings.length;
for (int i = 0; i < len; i++) {
float width = p.measureText(strings[i]);
maxWidthF = Math.max(width, maxWidthF);
}
int maxWidth = (int) (maxWidthF + 0.5);
if (maxWidth < currentMax) {
maxWidth = currentMax;
}
return maxWidth;
}
private void saveSelectionPosition(float left, float top, float right, float bottom) {
mPrevBox.left = (int) left;
mPrevBox.right = (int) right;
mPrevBox.top = (int) top;
mPrevBox.bottom = (int) bottom;
}
private Rect getCurrentSelectionPosition() {
Rect box = new Rect();
box.top = mSelectionHour * (mCellHeight + HOUR_GAP);
box.bottom = box.top + mCellHeight + HOUR_GAP;
int daynum = mSelectionDay - mFirstJulianDay;
box.left = mHoursWidth + daynum * (mCellWidth + DAY_GAP);
box.right = box.left + mCellWidth + DAY_GAP;
return box;
}
private void drawAllDayEvents(int firstDay, int numDays,
Rect r, Canvas canvas, Paint p) {
p.setTextSize(NORMAL_FONT_SIZE);
p.setTextAlign(Paint.Align.LEFT);
Paint eventTextPaint = mEventTextPaint;
// Draw the background for the all-day events area
r.top = mBannerPlusMargin;
r.bottom = r.top + mAllDayHeight + ALLDAY_TOP_MARGIN;
r.left = mHoursWidth;
r.right = r.left + mNumDays * (mCellWidth + DAY_GAP);
p.setColor(mCalendarAllDayBackground);
canvas.drawRect(r, p);
// Fill the extra space on the right side with the default background
r.left = r.right;
r.right = mViewWidth;
p.setColor(mCalendarGridAreaBackground);
canvas.drawRect(r, p);
// Draw the vertical grid lines
p.setColor(mCalendarGridLineVerticalColor);
p.setStyle(Style.STROKE);
p.setStrokeWidth(0);
p.setAntiAlias(false);
float startY = r.top;
float stopY = r.bottom;
float deltaX = mCellWidth + DAY_GAP;
float x = mHoursWidth + mCellWidth;
for (int day = 0; day <= mNumDays; day++) {
canvas.drawLine(x, startY, x, stopY, p);
x += deltaX;
}
p.setAntiAlias(true);
p.setStyle(Style.FILL);
int y = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
float left = mHoursWidth;
int lastDay = firstDay + numDays - 1;
ArrayList<Event> events = mEvents;
int numEvents = events.size();
float drawHeight = mAllDayHeight;
float numRectangles = mMaxAllDayEvents;
for (int i = 0; i < numEvents; i++) {
Event event = events.get(i);
if (!event.allDay)
continue;
int startDay = event.startDay;
int endDay = event.endDay;
if (startDay > lastDay || endDay < firstDay)
continue;
if (startDay < firstDay)
startDay = firstDay;
if (endDay > lastDay)
endDay = lastDay;
int startIndex = startDay - firstDay;
int endIndex = endDay - firstDay;
float height = drawHeight / numRectangles;
// Prevent a single event from getting too big
if (height > MAX_ALLDAY_EVENT_HEIGHT) {
height = MAX_ALLDAY_EVENT_HEIGHT;
}
// Leave a one-pixel space between the vertical day lines and the
// event rectangle.
event.left = left + startIndex * (mCellWidth + DAY_GAP) + 2;
event.right = left + endIndex * (mCellWidth + DAY_GAP) + mCellWidth - 1;
event.top = y + height * event.getColumn();
// Multiply the height by 0.9 to leave a little gap between events
event.bottom = event.top + height * 0.9f;
RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
// Check if this all-day event intersects the selected day
if (mSelectionAllDay && mComputeSelectedEvents) {
if (startDay <= mSelectionDay && endDay >= mSelectionDay) {
mSelectedEvents.add(event);
}
}
}
if (mSelectionAllDay) {
// Compute the neighbors for the list of all-day events that
// intersect the selected day.
computeAllDayNeighbors();
if (mSelectedEvent != null) {
Event event = mSelectedEvent;
RectF rf = drawAllDayEventRect(event, canvas, p, eventTextPaint);
drawEventText(event, rf, canvas, eventTextPaint, ALL_DAY_TEXT_TOP_MARGIN);
}
// Draw the highlight on the selected all-day area
float top = mBannerPlusMargin + 1;
float bottom = top + mAllDayHeight + ALLDAY_TOP_MARGIN - 1;
int daynum = mSelectionDay - mFirstJulianDay;
left = mHoursWidth + daynum * (mCellWidth + DAY_GAP) + 1;
float right = left + mCellWidth + DAY_GAP - 1;
if (mNumDays == 1) {
// The Day view doesn't have a vertical line on the right.
right -= 1;
}
Path path = mPath;
path.reset();
path.addRect(left, top, right, bottom, Direction.CW);
canvas.drawPath(path, mSelectionPaint);
// Set the selection position to zero so that when we move down
// to the normal event area, we will highlight the topmost event.
saveSelectionPosition(0f, 0f, 0f, 0f);
}
}
private void computeAllDayNeighbors() {
int len = mSelectedEvents.size();
if (len == 0 || mSelectedEvent != null) {
return;
}
// First, clear all the links
for (int ii = 0; ii < len; ii++) {
Event ev = mSelectedEvents.get(ii);
ev.nextUp = null;
ev.nextDown = null;
ev.nextLeft = null;
ev.nextRight = null;
}
// For each event in the selected event list "mSelectedEvents", find
// its neighbors in the up and down directions. This could be done
// more efficiently by sorting on the Event.getColumn() field, but
// the list is expected to be very small.
// Find the event in the same row as the previously selected all-day
// event, if any.
int startPosition = -1;
if (mPrevSelectedEvent != null && mPrevSelectedEvent.allDay) {
startPosition = mPrevSelectedEvent.getColumn();
}
int maxPosition = -1;
Event startEvent = null;
Event maxPositionEvent = null;
for (int ii = 0; ii < len; ii++) {
Event ev = mSelectedEvents.get(ii);
int position = ev.getColumn();
if (position == startPosition) {
startEvent = ev;
} else if (position > maxPosition) {
maxPositionEvent = ev;
maxPosition = position;
}
for (int jj = 0; jj < len; jj++) {
if (jj == ii) {
continue;
}
Event neighbor = mSelectedEvents.get(jj);
int neighborPosition = neighbor.getColumn();
if (neighborPosition == position - 1) {
ev.nextUp = neighbor;
} else if (neighborPosition == position + 1) {
ev.nextDown = neighbor;
}
}
}
if (startEvent != null) {
mSelectedEvent = startEvent;
} else {
mSelectedEvent = maxPositionEvent;
}
}
RectF drawAllDayEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
// If this event is selected, then use the selection color
if (mSelectedEvent == event) {
// Also, remember the last selected event that we drew
mPrevSelectedEvent = event;
p.setColor(mSelectionColor);
eventTextPaint.setColor(mSelectedEventTextColor);
} else {
// Use the normal color for all-day events
p.setColor(event.color);
eventTextPaint.setColor(mEventTextColor);
}
RectF rf = mRectF;
rf.top = event.top;
rf.bottom = event.bottom;
rf.left = event.left;
rf.right = event.right;
canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
rf.left += 2;
rf.right -= 2;
return rf;
}
private void drawEvents(int date, int left, int top, Canvas canvas, Paint p) {
Paint eventTextPaint = mEventTextPaint;
int cellWidth = mCellWidth;
int cellHeight = mCellHeight;
// Use the selected hour as the selection region
Rect selectionArea = mRect;
selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP);
selectionArea.bottom = selectionArea.top + cellHeight;
selectionArea.left = left;
selectionArea.right = selectionArea.left + cellWidth;
ArrayList<Event> events = mEvents;
int numEvents = events.size();
EventGeometry geometry = mEventGeometry;
for (int i = 0; i < numEvents; i++) {
Event event = events.get(i);
if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
continue;
}
if (date == mSelectionDay && !mSelectionAllDay && mComputeSelectedEvents
&& geometry.eventIntersectsSelection(event, selectionArea)) {
mSelectedEvents.add(event);
}
RectF rf = drawEventRect(event, canvas, p, eventTextPaint);
drawEventText(event, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
}
if (date == mSelectionDay && !mSelectionAllDay && isFocused()
&& mSelectionMode != SELECTION_HIDDEN) {
computeNeighbors();
if (mSelectedEvent != null) {
RectF rf = drawEventRect(mSelectedEvent, canvas, p, eventTextPaint);
drawEventText(mSelectedEvent, rf, canvas, eventTextPaint, NORMAL_TEXT_TOP_MARGIN);
}
}
}
// Computes the "nearest" neighbor event in four directions (left, right,
// up, down) for each of the events in the mSelectedEvents array.
private void computeNeighbors() {
int len = mSelectedEvents.size();
if (len == 0 || mSelectedEvent != null) {
return;
}
// First, clear all the links
for (int ii = 0; ii < len; ii++) {
Event ev = mSelectedEvents.get(ii);
ev.nextUp = null;
ev.nextDown = null;
ev.nextLeft = null;
ev.nextRight = null;
}
Event startEvent = mSelectedEvents.get(0);
int startEventDistance1 = 100000; // any large number
int startEventDistance2 = 100000; // any large number
int prevLocation = FROM_NONE;
int prevTop;
int prevBottom;
int prevLeft;
int prevRight;
int prevCenter = 0;
Rect box = getCurrentSelectionPosition();
if (mPrevSelectedEvent != null) {
prevTop = (int) mPrevSelectedEvent.top;
prevBottom = (int) mPrevSelectedEvent.bottom;
prevLeft = (int) mPrevSelectedEvent.left;
prevRight = (int) mPrevSelectedEvent.right;
// Check if the previously selected event intersects the previous
// selection box. (The previously selected event may be from a
// much older selection box.)
if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top
|| prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) {
mPrevSelectedEvent = null;
prevTop = mPrevBox.top;
prevBottom = mPrevBox.bottom;
prevLeft = mPrevBox.left;
prevRight = mPrevBox.right;
} else {
// Clip the top and bottom to the previous selection box.
if (prevTop < mPrevBox.top) {
prevTop = mPrevBox.top;
}
if (prevBottom > mPrevBox.bottom) {
prevBottom = mPrevBox.bottom;
}
}
} else {
// Just use the previously drawn selection box
prevTop = mPrevBox.top;
prevBottom = mPrevBox.bottom;
prevLeft = mPrevBox.left;
prevRight = mPrevBox.right;
}
// Figure out where we came from and compute the center of that area.
if (prevLeft >= box.right) {
// The previously selected event was to the right of us.
prevLocation = FROM_RIGHT;
prevCenter = (prevTop + prevBottom) / 2;
} else if (prevRight <= box.left) {
// The previously selected event was to the left of us.
prevLocation = FROM_LEFT;
prevCenter = (prevTop + prevBottom) / 2;
} else if (prevBottom <= box.top) {
// The previously selected event was above us.
prevLocation = FROM_ABOVE;
prevCenter = (prevLeft + prevRight) / 2;
} else if (prevTop >= box.bottom) {
// The previously selected event was below us.
prevLocation = FROM_BELOW;
prevCenter = (prevLeft + prevRight) / 2;
}
// For each event in the selected event list "mSelectedEvents", search
// all the other events in that list for the nearest neighbor in 4
// directions.
for (int ii = 0; ii < len; ii++) {
Event ev = mSelectedEvents.get(ii);
int startTime = ev.startTime;
int endTime = ev.endTime;
int left = (int) ev.left;
int right = (int) ev.right;
int top = (int) ev.top;
if (top < box.top) {
top = box.top;
}
int bottom = (int) ev.bottom;
if (bottom > box.bottom) {
bottom = box.bottom;
}
if (false) {
int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
| DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
if (DateFormat.is24HourFormat(mContext)) {
flags |= DateUtils.FORMAT_24HOUR;
}
String timeRange = DateUtils.formatDateRange(mParentActivity,
ev.startMillis, ev.endMillis, flags);
Log.i("Cal", "left: " + left + " right: " + right + " top: " + top
+ " bottom: " + bottom + " ev: " + timeRange + " " + ev.title);
}
int upDistanceMin = 10000; // any large number
int downDistanceMin = 10000; // any large number
int leftDistanceMin = 10000; // any large number
int rightDistanceMin = 10000; // any large number
Event upEvent = null;
Event downEvent = null;
Event leftEvent = null;
Event rightEvent = null;
// Pick the starting event closest to the previously selected event,
// if any. distance1 takes precedence over distance2.
int distance1 = 0;
int distance2 = 0;
if (prevLocation == FROM_ABOVE) {
if (left >= prevCenter) {
distance1 = left - prevCenter;
} else if (right <= prevCenter) {
distance1 = prevCenter - right;
}
distance2 = top - prevBottom;
} else if (prevLocation == FROM_BELOW) {
if (left >= prevCenter) {
distance1 = left - prevCenter;
} else if (right <= prevCenter) {
distance1 = prevCenter - right;
}
distance2 = prevTop - bottom;
} else if (prevLocation == FROM_LEFT) {
if (bottom <= prevCenter) {
distance1 = prevCenter - bottom;
} else if (top >= prevCenter) {
distance1 = top - prevCenter;
}
distance2 = left - prevRight;
} else if (prevLocation == FROM_RIGHT) {
if (bottom <= prevCenter) {
distance1 = prevCenter - bottom;
} else if (top >= prevCenter) {
distance1 = top - prevCenter;
}
distance2 = prevLeft - right;
}
if (distance1 < startEventDistance1
|| (distance1 == startEventDistance1 && distance2 < startEventDistance2)) {
startEvent = ev;
startEventDistance1 = distance1;
startEventDistance2 = distance2;
}
// For each neighbor, figure out if it is above or below or left
// or right of me and compute the distance.
for (int jj = 0; jj < len; jj++) {
if (jj == ii) {
continue;
}
Event neighbor = mSelectedEvents.get(jj);
int neighborLeft = (int) neighbor.left;
int neighborRight = (int) neighbor.right;
if (neighbor.endTime <= startTime) {
// This neighbor is entirely above me.
// If we overlap the same column, then compute the distance.
if (neighborLeft < right && neighborRight > left) {
int distance = startTime - neighbor.endTime;
if (distance < upDistanceMin) {
upDistanceMin = distance;
upEvent = neighbor;
} else if (distance == upDistanceMin) {
int center = (left + right) / 2;
int currentDistance = 0;
int currentLeft = (int) upEvent.left;
int currentRight = (int) upEvent.right;
if (currentRight <= center) {
currentDistance = center - currentRight;
} else if (currentLeft >= center) {
currentDistance = currentLeft - center;
}
int neighborDistance = 0;
if (neighborRight <= center) {
neighborDistance = center - neighborRight;
} else if (neighborLeft >= center) {
neighborDistance = neighborLeft - center;
}
if (neighborDistance < currentDistance) {
upDistanceMin = distance;
upEvent = neighbor;
}
}
}
} else if (neighbor.startTime >= endTime) {
// This neighbor is entirely below me.
// If we overlap the same column, then compute the distance.
if (neighborLeft < right && neighborRight > left) {
int distance = neighbor.startTime - endTime;
if (distance < downDistanceMin) {
downDistanceMin = distance;
downEvent = neighbor;
} else if (distance == downDistanceMin) {
int center = (left + right) / 2;
int currentDistance = 0;
int currentLeft = (int) downEvent.left;
int currentRight = (int) downEvent.right;
if (currentRight <= center) {
currentDistance = center - currentRight;
} else if (currentLeft >= center) {
currentDistance = currentLeft - center;
}
int neighborDistance = 0;
if (neighborRight <= center) {
neighborDistance = center - neighborRight;
} else if (neighborLeft >= center) {
neighborDistance = neighborLeft - center;
}
if (neighborDistance < currentDistance) {
downDistanceMin = distance;
downEvent = neighbor;
}
}
}
}
if (neighborLeft >= right) {
// This neighbor is entirely to the right of me.
// Take the closest neighbor in the y direction.
int center = (top + bottom) / 2;
int distance = 0;
int neighborBottom = (int) neighbor.bottom;
int neighborTop = (int) neighbor.top;
if (neighborBottom <= center) {
distance = center - neighborBottom;
} else if (neighborTop >= center) {
distance = neighborTop - center;
}
if (distance < rightDistanceMin) {
rightDistanceMin = distance;
rightEvent = neighbor;
} else if (distance == rightDistanceMin) {
// Pick the closest in the x direction
int neighborDistance = neighborLeft - right;
int currentDistance = (int) rightEvent.left - right;
if (neighborDistance < currentDistance) {
rightDistanceMin = distance;
rightEvent = neighbor;
}
}
} else if (neighborRight <= left) {
// This neighbor is entirely to the left of me.
// Take the closest neighbor in the y direction.
int center = (top + bottom) / 2;
int distance = 0;
int neighborBottom = (int) neighbor.bottom;
int neighborTop = (int) neighbor.top;
if (neighborBottom <= center) {
distance = center - neighborBottom;
} else if (neighborTop >= center) {
distance = neighborTop - center;
}
if (distance < leftDistanceMin) {
leftDistanceMin = distance;
leftEvent = neighbor;
} else if (distance == leftDistanceMin) {
// Pick the closest in the x direction
int neighborDistance = left - neighborRight;
int currentDistance = left - (int) leftEvent.right;
if (neighborDistance < currentDistance) {
leftDistanceMin = distance;
leftEvent = neighbor;
}
}
}
}
ev.nextUp = upEvent;
ev.nextDown = downEvent;
ev.nextLeft = leftEvent;
ev.nextRight = rightEvent;
}
mSelectedEvent = startEvent;
}
private RectF drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint) {
int color = event.color;
// Fade visible boxes if event was declined.
boolean declined = (event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED);
if (declined) {
int alpha = color & 0xff000000;
color &= 0x00ffffff;
int red = (color & 0x00ff0000) >> 16;
int green = (color & 0x0000ff00) >> 8;
int blue = (color & 0x0000ff);
color = ((red >> 1) << 16) | ((green >> 1) << 8) | (blue >> 1);
color += 0x7F7F7F + alpha;
}
// If this event is selected, then use the selection color
if (mSelectedEvent == event) {
if (mSelectionMode == SELECTION_PRESSED) {
// Also, remember the last selected event that we drew
mPrevSelectedEvent = event;
// box = mBoxPressed;
p.setColor(mPressedColor); // FIXME:pressed
eventTextPaint.setColor(mSelectedEventTextColor);
} else if (mSelectionMode == SELECTION_SELECTED) {
// Also, remember the last selected event that we drew
mPrevSelectedEvent = event;
// box = mBoxSelected;
p.setColor(mSelectionColor);
eventTextPaint.setColor(mSelectedEventTextColor);
} else if (mSelectionMode == SELECTION_LONGPRESS) {
// box = mBoxLongPressed;
p.setColor(mPressedColor); // FIXME: longpressed (maybe -- this doesn't seem to work)
eventTextPaint.setColor(mSelectedEventTextColor);
} else {
p.setColor(color);
eventTextPaint.setColor(mEventTextColor);
}
} else {
p.setColor(color);
eventTextPaint.setColor(mEventTextColor);
}
RectF rf = mRectF;
rf.top = event.top;
rf.bottom = event.bottom;
rf.left = event.left;
rf.right = event.right - 1;
canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, p);
// Draw a darker border
float[] hsv = new float[3];
Color.colorToHSV(p.getColor(), hsv);
hsv[1] = 1.0f;
hsv[2] *= 0.75f;
mPaintBorder.setColor(Color.HSVToColor(hsv));
canvas.drawRoundRect(rf, SMALL_ROUND_RADIUS, SMALL_ROUND_RADIUS, mPaintBorder);
rf.left += 2;
rf.right -= 2;
return rf;
}
private Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],");
// Sanitize a string before passing it to drawText or else we get little
// squares. For newlines and tabs before a comma, delete the character.
// Otherwise, just replace them with a space.
private String drawTextSanitizer(String string) {
Matcher m = drawTextSanitizerFilter.matcher(string);
string = m.replaceAll(",").replace('\n', ' ').replace('\n', ' ');
return string;
}
private void drawEventText(Event event, RectF rf, Canvas canvas, Paint p, int topMargin) {
if (!mDrawTextInEventRect) {
return;
}
float width = rf.right - rf.left;
float height = rf.bottom - rf.top;
// Leave one pixel extra space between lines
int lineHeight = mEventTextHeight + 1;
// If the rectangle is too small for text, then return
if (width < MIN_CELL_WIDTH_FOR_TEXT || height <= lineHeight) {
return;
}
// Truncate the event title to a known (large enough) limit
String text = event.getTitleAndLocation();
text = drawTextSanitizer(text);
int len = text.length();
if (len > MAX_EVENT_TEXT_LEN) {
text = text.substring(0, MAX_EVENT_TEXT_LEN);
len = MAX_EVENT_TEXT_LEN;
}
// Figure out how much space the event title will take, and create a
// String fragment that will fit in the rectangle. Use multiple lines,
// if available.
p.getTextWidths(text, mCharWidths);
String fragment = text;
float top = rf.top + mEventTextAscent + topMargin;
int start = 0;
// Leave one pixel extra space at the bottom
while (start < len && height >= (lineHeight + 1)) {
boolean lastLine = (height < 2 * lineHeight + 1);
// Skip leading spaces at the beginning of each line
do {
char c = text.charAt(start);
if (c != ' ') break;
start += 1;
} while (start < len);
float sum = 0;
int end = start;
for (int ii = start; ii < len; ii++) {
char c = text.charAt(ii);
// If we found the end of a word, then remember the ending
// position.
if (c == ' ') {
end = ii;
}
sum += mCharWidths[ii];
// If adding this character would exceed the width and this
// isn't the last line, then break the line at the previous
// word. If there was no previous word, then break this word.
if (sum > width) {
if (end > start && !lastLine) {
// There was a previous word on this line.
fragment = text.substring(start, end);
start = end;
break;
}
// This is the only word and it is too long to fit on
// the line (or this is the last line), so take as many
// characters of this word as will fit.
fragment = text.substring(start, ii);
start = ii;
break;
}
}
// If sum <= width, then we can fit the rest of the text on
// this line.
if (sum <= width) {
fragment = text.substring(start, len);
start = len;
}
canvas.drawText(fragment, rf.left + 1, top, p);
top += lineHeight;
height -= lineHeight;
}
}
private void updateEventDetails() {
if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN
|| mSelectionMode == SELECTION_LONGPRESS) {
mPopup.dismiss();
return;
}
// Remove any outstanding callbacks to dismiss the popup.
getHandler().removeCallbacks(mDismissPopup);
Event event = mSelectedEvent;
TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title);
titleView.setText(event.title);
ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon);
imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE);
imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon);
imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE);
int flags;
if (event.allDay) {
flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL;
} else {
flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL
| DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
}
if (DateFormat.is24HourFormat(mContext)) {
flags |= DateUtils.FORMAT_24HOUR;
}
String timeRange = DateUtils.formatDateRange(mParentActivity,
event.startMillis, event.endMillis, flags);
TextView timeView = (TextView) mPopupView.findViewById(R.id.time);
timeView.setText(timeRange);
TextView whereView = (TextView) mPopupView.findViewById(R.id.where);
final boolean empty = TextUtils.isEmpty(event.location);
whereView.setVisibility(empty ? View.GONE : View.VISIBLE);
if (!empty) whereView.setText(event.location);
mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5);
postDelayed(mDismissPopup, POPUP_DISMISS_DELAY);
}
// The following routines are called from the parent activity when certain
// touch events occur.
void doDown(MotionEvent ev) {
mTouchMode = TOUCH_MODE_DOWN;
mViewStartX = 0;
mOnFlingCalled = false;
getHandler().removeCallbacks(mContinueScroll);
}
void doSingleTapUp(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
Event selectedEvent = mSelectedEvent;
int selectedDay = mSelectionDay;
int selectedHour = mSelectionHour;
boolean validPosition = setSelectionFromPosition(x, y);
if (!validPosition) {
// return if the touch wasn't on an area of concern
return;
}
mSelectionMode = SELECTION_SELECTED;
mRedrawScreen = true;
invalidate();
boolean launchNewView = false;
if (mSelectedEvent != null) {
// If the tap is on an event, launch the "View event" view
launchNewView = true;
} else if (mSelectedEvent == null && selectedDay == mSelectionDay
&& selectedHour == mSelectionHour) {
// If the tap is on an already selected hour slot,
// then launch the Day/Agenda view. Otherwise, just select the hour
// slot.
launchNewView = true;
}
if (launchNewView) {
switchViews(false /* not the trackball */);
}
}
void doLongPress(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
boolean validPosition = setSelectionFromPosition(x, y);
if (!validPosition) {
// return if the touch wasn't on an area of concern
return;
}
mSelectionMode = SELECTION_LONGPRESS;
mRedrawScreen = true;
invalidate();
performLongClick();
}
void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
// Use the distance from the current point to the initial touch instead
// of deltaX and deltaY to avoid accumulating floating-point rounding
// errors. Also, we don't need floats, we can use ints.
int distanceX = (int) e1.getX() - (int) e2.getX();
int distanceY = (int) e1.getY() - (int) e2.getY();
// If we haven't figured out the predominant scroll direction yet,
// then do it now.
if (mTouchMode == TOUCH_MODE_DOWN) {
int absDistanceX = Math.abs(distanceX);
int absDistanceY = Math.abs(distanceY);
mScrollStartY = mViewStartY;
mPreviousDistanceX = 0;
mPreviousDirection = 0;
// If the x distance is at least twice the y distance, then lock
// the scroll horizontally. Otherwise scroll vertically.
if (absDistanceX >= 2 * absDistanceY) {
mTouchMode = TOUCH_MODE_HSCROLL;
mViewStartX = distanceX;
initNextView(-mViewStartX);
} else {
mTouchMode = TOUCH_MODE_VSCROLL;
}
} else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
// We are already scrolling horizontally, so check if we
// changed the direction of scrolling so that the other week
// is now visible.
mViewStartX = distanceX;
if (distanceX != 0) {
int direction = (distanceX > 0) ? 1 : -1;
if (direction != mPreviousDirection) {
// The user has switched the direction of scrolling
// so re-init the next view
initNextView(-mViewStartX);
mPreviousDirection = direction;
}
}
// If we have moved at least the HORIZONTAL_SCROLL_THRESHOLD,
// then change the title to the new day (or week), but only
// if we haven't already changed the title.
if (distanceX >= HORIZONTAL_SCROLL_THRESHOLD) {
if (mPreviousDistanceX < HORIZONTAL_SCROLL_THRESHOLD) {
CalendarView view = mParentActivity.getNextView();
mTitleTextView.setText(view.mDateRange);
}
} else if (distanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
if (mPreviousDistanceX > -HORIZONTAL_SCROLL_THRESHOLD) {
CalendarView view = mParentActivity.getNextView();
mTitleTextView.setText(view.mDateRange);
}
} else {
if (mPreviousDistanceX >= HORIZONTAL_SCROLL_THRESHOLD
|| mPreviousDistanceX <= -HORIZONTAL_SCROLL_THRESHOLD) {
mTitleTextView.setText(mDateRange);
}
}
mPreviousDistanceX = distanceX;
}
if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) {
mViewStartY = mScrollStartY + distanceY;
if (mViewStartY < 0) {
mViewStartY = 0;
} else if (mViewStartY > mMaxViewStartY) {
mViewStartY = mMaxViewStartY;
}
computeFirstHour();
}
mScrolling = true;
if (mSelectionMode != SELECTION_HIDDEN) {
mSelectionMode = SELECTION_HIDDEN;
mRedrawScreen = true;
}
invalidate();
}
void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mTouchMode = TOUCH_MODE_INITIAL_STATE;
mSelectionMode = SELECTION_HIDDEN;
mOnFlingCalled = true;
int deltaX = (int) e2.getX() - (int) e1.getX();
int distanceX = Math.abs(deltaX);
int deltaY = (int) e2.getY() - (int) e1.getY();
int distanceY = Math.abs(deltaY);
if ((distanceX >= HORIZONTAL_SCROLL_THRESHOLD) && (distanceX > distanceY)) {
boolean switchForward = initNextView(deltaX);
CalendarView view = mParentActivity.getNextView();
mTitleTextView.setText(view.mDateRange);
mParentActivity.switchViews(switchForward, mViewStartX, mViewWidth);
mViewStartX = 0;
return;
}
// Continue scrolling vertically
mContinueScroll.init((int) velocityY / 20);
post(mContinueScroll);
}
private boolean initNextView(int deltaX) {
// Change the view to the previous day or week
CalendarView view = mParentActivity.getNextView();
Time date = view.mBaseDate;
date.set(mBaseDate);
boolean switchForward;
if (deltaX > 0) {
date.monthDay -= mNumDays;
view.mSelectionDay = mSelectionDay - mNumDays;
switchForward = false;
} else {
date.monthDay += mNumDays;
view.mSelectionDay = mSelectionDay + mNumDays;
switchForward = true;
}
date.normalize(true /* ignore isDst */);
initView(view);
view.setFrame(getLeft(), getTop(), getRight(), getBottom());
view.reloadEvents();
return switchForward;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mParentActivity.mGestureDetector.onTouchEvent(ev);
return true;
case MotionEvent.ACTION_MOVE:
mParentActivity.mGestureDetector.onTouchEvent(ev);
return true;
case MotionEvent.ACTION_UP:
mParentActivity.mGestureDetector.onTouchEvent(ev);
if (mOnFlingCalled) {
return true;
}
if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) {
mTouchMode = TOUCH_MODE_INITIAL_STATE;
if (Math.abs(mViewStartX) > HORIZONTAL_SCROLL_THRESHOLD) {
// The user has gone beyond the threshold so switch views
mParentActivity.switchViews(mViewStartX > 0, mViewStartX, mViewWidth);
mViewStartX = 0;
return true;
} else {
// Not beyond the threshold so invalidate which will cause
// the view to snap back. Also call recalc() to ensure
// that we have the correct starting date and title.
recalc();
mTitleTextView.setText(mDateRange);
invalidate();
mViewStartX = 0;
}
}
// If we were scrolling, then reset the selected hour so that it
// is visible.
if (mScrolling) {
mScrolling = false;
resetSelectedHour();
mRedrawScreen = true;
invalidate();
}
return true;
// This case isn't expected to happen.
case MotionEvent.ACTION_CANCEL:
mParentActivity.mGestureDetector.onTouchEvent(ev);
mScrolling = false;
resetSelectedHour();
return true;
default:
if (mParentActivity.mGestureDetector.onTouchEvent(ev)) {
return true;
}
return super.onTouchEvent(ev);
}
}
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
MenuItem item;
// If the trackball is held down, then the context menu pops up and
// we never get onKeyUp() for the long-press. So check for it here
// and change the selection to the long-press state.
if (mSelectionMode != SELECTION_LONGPRESS) {
mSelectionMode = SELECTION_LONGPRESS;
mRedrawScreen = true;
invalidate();
}
final long startMillis = getSelectedTimeInMillis();
int flags = DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_CAP_NOON_MIDNIGHT
| DateUtils.FORMAT_SHOW_WEEKDAY;
final String title = DateUtils.formatDateTime(mParentActivity, startMillis, flags);
menu.setHeaderTitle(title);
int numSelectedEvents = mSelectedEvents.size();
if (mNumDays == 1) {
// Day view.
// If there is a selected event, then allow it to be viewed and
// edited.
if (numSelectedEvents >= 1) {
item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_info_details);
if (isEventEditable(mContext, mSelectedEvent)) {
item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_edit);
item.setAlphabeticShortcut('e');
item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_delete);
}
item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_add);
item.setAlphabeticShortcut('n');
} else {
// Otherwise, if the user long-pressed on a blank hour, allow
// them to create an event. They can also do this by tapping.
item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_add);
item.setAlphabeticShortcut('n');
}
} else {
// Week view.
// If there is a selected event, then allow it to be viewed and
// edited.
if (numSelectedEvents >= 1) {
item = menu.add(0, MenuHelper.MENU_EVENT_VIEW, 0, R.string.event_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_info_details);
if (isEventEditable(mContext, mSelectedEvent)) {
item = menu.add(0, MenuHelper.MENU_EVENT_EDIT, 0, R.string.event_edit);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_edit);
item.setAlphabeticShortcut('e');
item = menu.add(0, MenuHelper.MENU_EVENT_DELETE, 0, R.string.event_delete);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_delete);
}
item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_add);
item.setAlphabeticShortcut('n');
item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_day);
item.setAlphabeticShortcut('d');
item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_agenda);
item.setAlphabeticShortcut('a');
} else {
// No events are selected
item = menu.add(0, MenuHelper.MENU_EVENT_CREATE, 0, R.string.event_create);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_add);
item.setAlphabeticShortcut('n');
item = menu.add(0, MenuHelper.MENU_DAY, 0, R.string.show_day_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_day);
item.setAlphabeticShortcut('d');
item = menu.add(0, MenuHelper.MENU_AGENDA, 0, R.string.show_agenda_view);
item.setOnMenuItemClickListener(mContextMenuHandler);
item.setIcon(android.R.drawable.ic_menu_agenda);
item.setAlphabeticShortcut('a');
}
}
mPopup.dismiss();
}
private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener {
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case MenuHelper.MENU_EVENT_VIEW: {
if (mSelectedEvent != null) {
long id = mSelectedEvent.id;
Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(eventUri);
intent.setClassName(mContext, EventInfoActivity.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
mParentActivity.startActivity(intent);
}
break;
}
case MenuHelper.MENU_EVENT_EDIT: {
if (mSelectedEvent != null) {
long id = mSelectedEvent.id;
Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
Intent intent = new Intent(Intent.ACTION_EDIT);
intent.setData(eventUri);
intent.setClassName(mContext, EditEvent.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, mSelectedEvent.startMillis);
intent.putExtra(EVENT_END_TIME, mSelectedEvent.endMillis);
mParentActivity.startActivity(intent);
}
break;
}
case MenuHelper.MENU_DAY: {
long startMillis = getSelectedTimeInMillis();
Utils.startActivity(mParentActivity, DayActivity.class.getName(), startMillis);
break;
}
case MenuHelper.MENU_AGENDA: {
long startMillis = getSelectedTimeInMillis();
Utils.startActivity(mParentActivity, AgendaActivity.class.getName(), startMillis);
break;
}
case MenuHelper.MENU_EVENT_CREATE: {
long startMillis = getSelectedTimeInMillis();
long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(mContext, EditEvent.class.getName());
intent.putExtra(EVENT_BEGIN_TIME, startMillis);
intent.putExtra(EVENT_END_TIME, endMillis);
intent.putExtra(EditEvent.EVENT_ALL_DAY, mSelectionAllDay);
mParentActivity.startActivity(intent);
break;
}
case MenuHelper.MENU_EVENT_DELETE: {
if (mSelectedEvent != null) {
Event selectedEvent = mSelectedEvent;
long begin = selectedEvent.startMillis;
long end = selectedEvent.endMillis;
long id = selectedEvent.id;
mDeleteEventHelper.delete(begin, end, id, -1);
}
break;
}
default: {
return false;
}
}
return true;
}
}
private static boolean isEventEditable(Context context, Event e) {
ContentResolver cr = context.getContentResolver();
int visibility = Calendars.NO_ACCESS;
int relationship = Attendees.RELATIONSHIP_ORGANIZER;
// Get the calendar id for this event
Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id),
new String[] { Events.CALENDAR_ID },
null /* selection */,
null /* selectionArgs */,
null /* sort */);
if ((cursor == null) || (cursor.getCount() == 0)) {
return false;
}
cursor.moveToFirst();
long calId = cursor.getLong(0);
cursor.deactivate();
Uri uri = Calendars.CONTENT_URI;
String where = String.format(CALENDARS_WHERE, calId);
cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null);
String calendarOwnerAccount = null;
if (cursor != null) {
cursor.moveToFirst();
visibility = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
cursor.close();
}
if (visibility < Calendars.CONTRIBUTOR_ACCESS) {
return false;
}
if (e.guestsCanModify) {
return true;
}
return !TextUtils.isEmpty(calendarOwnerAccount) && calendarOwnerAccount.equals(e.organizer);
}
/**
* Sets mSelectionDay and mSelectionHour based on the (x,y) touch position.
* If the touch position is not within the displayed grid, then this
* method returns false.
*
* @param x the x position of the touch
* @param y the y position of the touch
* @return true if the touch position is valid
*/
private boolean setSelectionFromPosition(int x, int y) {
if (x < mHoursWidth) {
return false;
}
int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP);
if (day >= mNumDays) {
day = mNumDays - 1;
}
day += mFirstJulianDay;
int hour;
if (y < mFirstCell + mFirstHourOffset) {
mSelectionAllDay = true;
} else {
hour = (y - mFirstCell - mFirstHourOffset) / (mCellHeight + HOUR_GAP);
hour += mFirstHour;
mSelectionHour = hour;
mSelectionAllDay = false;
}
mSelectionDay = day;
findSelectedEvent(x, y);
// Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day
// + " hour: " + hour
// + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " + mFirstHourOffset);
// if (mSelectedEvent != null) {
// Log.i("Cal", " num events: " + mSelectedEvents.size() + " event: " + mSelectedEvent.title);
// for (Event ev : mSelectedEvents) {
// int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
// | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
// String timeRange = formatDateRange(mParentActivity,
// ev.startMillis, ev.endMillis, flags);
//
// Log.i("Cal", " " + timeRange + " " + ev.title);
// }
// }
return true;
}
private void findSelectedEvent(int x, int y) {
int date = mSelectionDay;
int cellWidth = mCellWidth;
ArrayList<Event> events = mEvents;
int numEvents = events.size();
int left = mHoursWidth + (mSelectionDay - mFirstJulianDay) * (cellWidth + DAY_GAP);
int top = 0;
mSelectedEvent = null;
mSelectedEvents.clear();
if (mSelectionAllDay) {
float yDistance;
float minYdistance = 10000.0f; // any large number
Event closestEvent = null;
float drawHeight = mAllDayHeight;
int yOffset = mBannerPlusMargin + ALLDAY_TOP_MARGIN;
for (int i = 0; i < numEvents; i++) {
Event event = events.get(i);
if (!event.allDay) {
continue;
}
if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) {
float numRectangles = event.getMaxColumns();
float height = drawHeight / numRectangles;
if (height > MAX_ALLDAY_EVENT_HEIGHT) {
height = MAX_ALLDAY_EVENT_HEIGHT;
}
float eventTop = yOffset + height * event.getColumn();
float eventBottom = eventTop + height;
if (eventTop < y && eventBottom > y) {
// If the touch is inside the event rectangle, then
// add the event.
mSelectedEvents.add(event);
closestEvent = event;
break;
} else {
// Find the closest event
if (eventTop >= y) {
yDistance = eventTop - y;
} else {
yDistance = y - eventBottom;
}
if (yDistance < minYdistance) {
minYdistance = yDistance;
closestEvent = event;
}
}
}
}
mSelectedEvent = closestEvent;
return;
}
// Adjust y for the scrollable bitmap
y += mViewStartY - mFirstCell;
// Use a region around (x,y) for the selection region
Rect region = mRect;
region.left = x - 10;
region.right = x + 10;
region.top = y - 10;
region.bottom = y + 10;
EventGeometry geometry = mEventGeometry;
for (int i = 0; i < numEvents; i++) {
Event event = events.get(i);
// Compute the event rectangle.
if (!geometry.computeEventRect(date, left, top, cellWidth, event)) {
continue;
}
// If the event intersects the selection region, then add it to
// mSelectedEvents.
if (geometry.eventIntersectsSelection(event, region)) {
mSelectedEvents.add(event);
}
}
// If there are any events in the selected region, then assign the
// closest one to mSelectedEvent.
if (mSelectedEvents.size() > 0) {
int len = mSelectedEvents.size();
Event closestEvent = null;
float minDist = mViewWidth + mViewHeight; // some large distance
for (int index = 0; index < len; index++) {
Event ev = mSelectedEvents.get(index);
float dist = geometry.pointToEvent(x, y, ev);
if (dist < minDist) {
minDist = dist;
closestEvent = ev;
}
}
mSelectedEvent = closestEvent;
// Keep the selected hour and day consistent with the selected
// event. They could be different if we touched on an empty hour
// slot very close to an event in the previous hour slot. In
// that case we will select the nearby event.
int startDay = mSelectedEvent.startDay;
int endDay = mSelectedEvent.endDay;
if (mSelectionDay < startDay) {
mSelectionDay = startDay;
} else if (mSelectionDay > endDay) {
mSelectionDay = endDay;
}
int startHour = mSelectedEvent.startTime / 60;
int endHour;
if (mSelectedEvent.startTime < mSelectedEvent.endTime) {
endHour = (mSelectedEvent.endTime - 1) / 60;
} else {
endHour = mSelectedEvent.endTime / 60;
}
if (mSelectionHour < startHour) {
mSelectionHour = startHour;
} else if (mSelectionHour > endHour) {
mSelectionHour = endHour;
}
}
}
// Encapsulates the code to continue the scrolling after the
// finger is lifted. Instead of stopping the scroll immediately,
// the scroll continues to "free spin" and gradually slows down.
private class ContinueScroll implements Runnable {
int mSignDeltaY;
int mAbsDeltaY;
float mFloatDeltaY;
long mFreeSpinTime;
private static final float FRICTION_COEF = 0.7F;
private static final long FREE_SPIN_MILLIS = 180;
private static final int MAX_DELTA = 60;
private static final int SCROLL_REPEAT_INTERVAL = 30;
public void init(int deltaY) {
mSignDeltaY = 0;
if (deltaY > 0) {
mSignDeltaY = 1;
} else if (deltaY < 0) {
mSignDeltaY = -1;
}
mAbsDeltaY = Math.abs(deltaY);
// Limit the maximum speed
if (mAbsDeltaY > MAX_DELTA) {
mAbsDeltaY = MAX_DELTA;
}
mFloatDeltaY = mAbsDeltaY;
mFreeSpinTime = System.currentTimeMillis() + FREE_SPIN_MILLIS;
// Log.i("Cal", "init scroll: mAbsDeltaY: " + mAbsDeltaY
// + " mViewStartY: " + mViewStartY);
}
public void run() {
long time = System.currentTimeMillis();
// Start out with a frictionless "free spin"
if (time > mFreeSpinTime) {
// If the delta is small, then apply a fixed deceleration.
// Otherwise
if (mAbsDeltaY <= 10) {
mAbsDeltaY -= 2;
} else {
mFloatDeltaY *= FRICTION_COEF;
mAbsDeltaY = (int) mFloatDeltaY;
}
if (mAbsDeltaY < 0) {
mAbsDeltaY = 0;
}
}
if (mSignDeltaY == 1) {
mViewStartY -= mAbsDeltaY;
} else {
mViewStartY += mAbsDeltaY;
}
// Log.i("Cal", " scroll: mAbsDeltaY: " + mAbsDeltaY
// + " mViewStartY: " + mViewStartY);
if (mViewStartY < 0) {
mViewStartY = 0;
mAbsDeltaY = 0;
} else if (mViewStartY > mMaxViewStartY) {
mViewStartY = mMaxViewStartY;
mAbsDeltaY = 0;
}
computeFirstHour();
if (mAbsDeltaY > 0) {
postDelayed(this, SCROLL_REPEAT_INTERVAL);
} else {
// Done scrolling.
mScrolling = false;
resetSelectedHour();
mRedrawScreen = true;
}
invalidate();
}
}
/**
* Cleanup the pop-up.
*/
public void cleanup() {
// Protect against null-pointer exceptions
if (mPopup != null) {
mPopup.dismiss();
}
Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mDismissPopup);
}
// Turn off redraw
mRemeasure = false;
mRedrawScreen = false;
}
@Override protected void onDetachedFromWindow() {
cleanup();
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
super.onDetachedFromWindow();
}
class DismissPopup implements Runnable {
public void run() {
// Protect against null-pointer exceptions
if (mPopup != null) {
mPopup.dismiss();
}
}
}
}