| /* |
| * 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 android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.app.AlertDialog; |
| import android.app.Service; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.database.Cursor; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Align; |
| import android.graphics.Paint.Style; |
| import android.graphics.Rect; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.provider.CalendarContract.Attendees; |
| import android.provider.CalendarContract.Calendars; |
| import android.provider.CalendarContract.Events; |
| import android.text.Layout.Alignment; |
| import android.text.SpannableStringBuilder; |
| import android.text.StaticLayout; |
| 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.text.style.StyleSpan; |
| import android.util.Log; |
| import android.view.ContextMenu; |
| import android.view.ContextMenu.ContextMenuInfo; |
| import android.view.GestureDetector; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.ScaleGestureDetector; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.animation.AccelerateDecelerateInterpolator; |
| import android.view.animation.Animation; |
| import android.view.animation.Interpolator; |
| import android.view.animation.TranslateAnimation; |
| import android.widget.EdgeEffect; |
| import android.widget.ImageView; |
| import android.widget.OverScroller; |
| import android.widget.PopupWindow; |
| import android.widget.TextView; |
| import android.widget.ViewSwitcher; |
| |
| import com.android.calendar.CalendarController.EventType; |
| import com.android.calendar.CalendarController.ViewType; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Formatter; |
| import java.util.Locale; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * View for multi-day view. So far only 1 and 7 day have been tested. |
| */ |
| public class DayView extends View implements View.OnCreateContextMenuListener, |
| ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener |
| { |
| private static String TAG = "DayView"; |
| private static boolean DEBUG = false; |
| private static boolean DEBUG_SCALING = false; |
| private static final String PERIOD_SPACE = ". "; |
| |
| private static float mScale = 0; // Used for supporting different screen densities |
| private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event |
| // Duration of the allday expansion |
| private static final long ANIMATION_DURATION = 400; |
| // duration of the more allday event text fade |
| private static final long ANIMATION_SECONDARY_DURATION = 200; |
| // duration of the scroll to go to a specified time |
| private static final int GOTO_SCROLL_DURATION = 200; |
| // duration for events' cross-fade animation |
| private static final int EVENTS_CROSS_FADE_DURATION = 400; |
| // duration to show the event clicked |
| private static final int CLICK_DISPLAY_DURATION = 50; |
| |
| private static final int MENU_AGENDA = 2; |
| private static final int MENU_DAY = 3; |
| private static final int MENU_EVENT_VIEW = 5; |
| private static final int MENU_EVENT_CREATE = 6; |
| private static final int MENU_EVENT_EDIT = 7; |
| private static final int MENU_EVENT_DELETE = 8; |
| |
| private static int DEFAULT_CELL_HEIGHT = 64; |
| private static int MAX_CELL_HEIGHT = 150; |
| private static int MIN_Y_SPAN = 100; |
| |
| private boolean mOnFlingCalled; |
| private boolean mStartingScroll = false; |
| protected boolean mPaused = true; |
| private Handler mHandler; |
| /** |
| * ID of the last event which was displayed with the toast popup. |
| * |
| * This is used to prevent popping up multiple quick views for the same event, especially |
| * during calendar syncs. This becomes valid when an event is selected, either by default |
| * on starting calendar or by scrolling to an event. It becomes invalid when the user |
| * explicitly scrolls to an empty time slot, changes views, or deletes the event. |
| */ |
| private long mLastPopupEventID; |
| |
| protected Context mContext; |
| |
| private static final String[] CALENDARS_PROJECTION = new String[] { |
| Calendars._ID, // 0 |
| Calendars.CALENDAR_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 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 final int ACCESS_LEVEL_NONE = 0; |
| private static final int ACCESS_LEVEL_DELETE = 1; |
| private static final int ACCESS_LEVEL_EDIT = 2; |
| |
| private static int mHorizontalSnapBackThreshold = 128; |
| |
| private final ContinueScroll mContinueScroll = new ContinueScroll(); |
| |
| // Make this visible within the package for more informative debugging |
| Time mBaseDate; |
| private Time mCurrentTime; |
| //Update the current time line every five minutes if the window is left open that long |
| private static final int UPDATE_CURRENT_TIME_DELAY = 300000; |
| private final UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); |
| private int mTodayJulianDay; |
| |
| private final Typeface mBold = Typeface.DEFAULT_BOLD; |
| private int mFirstJulianDay; |
| private int mLoadedFirstJulianDay = -1; |
| private int mLastJulianDay; |
| |
| private int mMonthLength; |
| private int mFirstVisibleDate; |
| private int mFirstVisibleDayOfWeek; |
| private int[] mEarliestStartHour; // indexed by the week day offset |
| private boolean[] mHasAllDayEvent; // indexed by the week day offset |
| private String mEventCountTemplate; |
| private final CharSequence[] mLongPressItems; |
| private String mLongPressTitle; |
| private Event mClickedEvent; // The event the user clicked on |
| private Event mSavedClickedEvent; |
| private static int mOnDownDelay; |
| private int mClickedYLocation; |
| private long mDownTouchTime; |
| |
| private int mEventsAlpha = 255; |
| private ObjectAnimator mEventsCrossFadeAnimation; |
| |
| protected static StringBuilder mStringBuilder = new StringBuilder(50); |
| // TODO recreate formatter when locale changes |
| protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); |
| |
| private final Runnable mTZUpdater = new Runnable() { |
| @Override |
| public void run() { |
| String tz = Utils.getTimeZone(mContext, this); |
| mBaseDate.timezone = tz; |
| mBaseDate.normalize(true); |
| mCurrentTime.switchTimezone(tz); |
| invalidate(); |
| } |
| }; |
| |
| // Sets the "clicked" color from the clicked event |
| private final Runnable mSetClick = new Runnable() { |
| @Override |
| public void run() { |
| mClickedEvent = mSavedClickedEvent; |
| mSavedClickedEvent = null; |
| DayView.this.invalidate(); |
| } |
| }; |
| |
| // Clears the "clicked" color from the clicked event and launch the event |
| private final Runnable mClearClick = new Runnable() { |
| @Override |
| public void run() { |
| if (mClickedEvent != null) { |
| mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mClickedEvent.id, |
| mClickedEvent.startMillis, mClickedEvent.endMillis, |
| DayView.this.getWidth() / 2, mClickedYLocation, |
| getSelectedTimeInMillis()); |
| } |
| mClickedEvent = null; |
| DayView.this.invalidate(); |
| } |
| }; |
| |
| private final TodayAnimatorListener mTodayAnimatorListener = 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(DayView.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; |
| } |
| |
| } |
| |
| AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mScrolling = true; |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| mScrolling = false; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mScrolling = false; |
| resetSelectedHour(); |
| invalidate(); |
| } |
| }; |
| |
| /** |
| * 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 ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); |
| private StaticLayout[] mLayouts = null; |
| private StaticLayout[] mAllDayLayouts = null; |
| private int mSelectionDay; // Julian day |
| private int mSelectionHour; |
| |
| boolean mSelectionAllday; |
| |
| // Current selection info for accessibility |
| private int mSelectionDayForAccessibility; // Julian day |
| private int mSelectionHourForAccessibility; |
| private Event mSelectedEventForAccessibility; |
| // Last selection info for accessibility |
| private int mLastSelectionDayForAccessibility; |
| private int mLastSelectionHourForAccessibility; |
| private Event mLastSelectedEventForAccessibility; |
| |
| |
| /** Width of a day or non-conflicting event */ |
| private int mCellWidth; |
| |
| // Pre-allocate these objects and re-use them |
| private final Rect mRect = new Rect(); |
| private final Rect mDestRect = new Rect(); |
| private final Rect mSelectionRect = new Rect(); |
| // This encloses the more allDay events icon |
| private final Rect mExpandAllDayRect = new Rect(); |
| // TODO Clean up paint usage |
| private final Paint mPaint = new Paint(); |
| private final Paint mEventTextPaint = new Paint(); |
| private final Paint mSelectionPaint = new Paint(); |
| private float[] mLines; |
| |
| private int mFirstDayOfWeek; // First day of the week |
| |
| private PopupWindow mPopup; |
| private View mPopupView; |
| |
| // The number of milliseconds to show the popup window |
| private static final int POPUP_DISMISS_DELAY = 3000; |
| private final DismissPopup mDismissPopup = new DismissPopup(); |
| |
| private boolean mRemeasure = true; |
| |
| private final EventLoader mEventLoader; |
| protected final EventGeometry mEventGeometry; |
| |
| private static float GRID_LINE_LEFT_MARGIN = 0; |
| private static final float GRID_LINE_INNER_WIDTH = 1; |
| |
| private static final int DAY_GAP = 1; |
| private static final int HOUR_GAP = 1; |
| // This is the standard height of an allday event with no restrictions |
| private static int SINGLE_ALLDAY_HEIGHT = 34; |
| /** |
| * This is the minimum desired height of a allday event. |
| * When unexpanded, allday events will use this height. |
| * When expanded allDay events will attempt to grow to fit all |
| * events at this height. |
| */ |
| private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels |
| /** |
| * This is how big the unexpanded allday height is allowed to be. |
| * It will get adjusted based on screen size |
| */ |
| private static int MAX_UNEXPANDED_ALLDAY_HEIGHT = |
| (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); |
| /** |
| * This is the minimum size reserved for displaying regular events. |
| * The expanded allDay region can't expand into this. |
| */ |
| private static int MIN_HOURS_HEIGHT = 180; |
| private static int ALLDAY_TOP_MARGIN = 1; |
| // The largest a single allDay event will become. |
| private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; |
| |
| private static int HOURS_TOP_MARGIN = 2; |
| private static int HOURS_LEFT_MARGIN = 2; |
| private static int HOURS_RIGHT_MARGIN = 4; |
| private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; |
| private static int NEW_EVENT_MARGIN = 4; |
| private static int NEW_EVENT_WIDTH = 2; |
| private static int NEW_EVENT_MAX_LENGTH = 16; |
| |
| private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4; |
| private static int CURRENT_TIME_LINE_TOP_OFFSET = 2; |
| |
| /* 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; |
| |
| // More events text will transition between invisible and this alpha |
| private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; |
| private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; |
| private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; |
| private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; |
| private static int DAY_HEADER_RIGHT_MARGIN = 4; |
| private static int DAY_HEADER_BOTTOM_MARGIN = 3; |
| private static float DAY_HEADER_FONT_SIZE = 14; |
| private static float DATE_HEADER_FONT_SIZE = 32; |
| private static float NORMAL_FONT_SIZE = 12; |
| private static float EVENT_TEXT_FONT_SIZE = 12; |
| private static float HOURS_TEXT_SIZE = 12; |
| private static float AMPM_TEXT_SIZE = 9; |
| private static int MIN_HOURS_WIDTH = 96; |
| private static int MIN_CELL_WIDTH_FOR_TEXT = 20; |
| private static final int MAX_EVENT_TEXT_LEN = 500; |
| // smallest height to draw an event with |
| private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels |
| private static int CALENDAR_COLOR_SQUARE_SIZE = 10; |
| private static int EVENT_RECT_TOP_MARGIN = 1; |
| private static int EVENT_RECT_BOTTOM_MARGIN = 0; |
| private static int EVENT_RECT_LEFT_MARGIN = 1; |
| private static int EVENT_RECT_RIGHT_MARGIN = 0; |
| private static int EVENT_RECT_STROKE_WIDTH = 2; |
| private static int EVENT_TEXT_TOP_MARGIN = 2; |
| private static int EVENT_TEXT_BOTTOM_MARGIN = 2; |
| private static int EVENT_TEXT_LEFT_MARGIN = 6; |
| private static int EVENT_TEXT_RIGHT_MARGIN = 6; |
| private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; |
| private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; |
| private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; |
| private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; |
| private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; |
| // margins and sizing for the expand allday icon |
| private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; |
| // sizing for "box +n" in allDay events |
| private static int EVENT_SQUARE_WIDTH = 10; |
| private static int EVENT_LINE_PADDING = 4; |
| private static int NEW_EVENT_HINT_FONT_SIZE = 12; |
| |
| private static int mPressedColor; |
| private static int mClickedColor; |
| private static int mEventTextColor; |
| private static int mMoreEventsTextColor; |
| |
| private static int mWeek_saturdayColor; |
| private static int mWeek_sundayColor; |
| private static int mCalendarDateBannerTextColor; |
| private static int mCalendarAmPmLabel; |
| private static int mCalendarGridAreaSelected; |
| private static int mCalendarGridLineInnerHorizontalColor; |
| private static int mCalendarGridLineInnerVerticalColor; |
| private static int mFutureBgColor; |
| private static int mFutureBgColorRes; |
| private static int mBgColor; |
| private static int mNewEventHintColor; |
| private static int mCalendarHourLabelColor; |
| private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; |
| |
| private float mAnimationDistance = 0; |
| private int mViewStartX; |
| private int mViewStartY; |
| private int mMaxViewStartY; |
| private int mViewHeight; |
| private int mViewWidth; |
| private int mGridAreaHeight = -1; |
| private static int mCellHeight = 0; // shared among all DayViews |
| private static int mMinCellHeight = 32; |
| private int mScrollStartY; |
| private int mPreviousDirection; |
| private static int mScaledPagingTouchSlop = 0; |
| |
| /** |
| * Vertical distance or span between the two touch points at the start of a |
| * scaling gesture |
| */ |
| private float mStartingSpanY = 0; |
| /** Height of 1 hour in pixels at the start of a scaling gesture */ |
| private int mCellHeightBeforeScaleGesture; |
| /** The hour at the center two touch points */ |
| private float mGestureCenterHour = 0; |
| |
| private boolean mRecalCenterHour = false; |
| |
| /** |
| * Flag to decide whether to handle the up event. Cases where up events |
| * should be ignored are 1) right after a scale gesture and 2) finger was |
| * down before app launch |
| */ |
| private boolean mHandleActionUp = true; |
| |
| private int mHoursTextHeight; |
| /** |
| * The height of the area used for allday events |
| */ |
| private int mAlldayHeight; |
| /** |
| * The height of the allday event area used during animation |
| */ |
| private int mAnimateDayHeight = 0; |
| /** |
| * The height of an individual allday event during animation |
| */ |
| private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; |
| /** |
| * Whether to use the expand or collapse icon. |
| */ |
| private static boolean mUseExpandIcon = true; |
| /** |
| * The height of the day names/numbers |
| */ |
| private static int DAY_HEADER_HEIGHT = 45; |
| /** |
| * The height of the day names/numbers for multi-day views |
| */ |
| private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; |
| /** |
| * The height of the day names/numbers when viewing a single day |
| */ |
| private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; |
| /** |
| * Max of all day events in a given day in this view. |
| */ |
| private int mMaxAlldayEvents; |
| /** |
| * A count of the number of allday events that were not drawn for each day |
| */ |
| private int[] mSkippedAlldayEvents; |
| /** |
| * The number of allDay events at which point we start hiding allDay events. |
| */ |
| private int mMaxUnexpandedAlldayEventCount = 4; |
| /** |
| * Whether or not to expand the allDay area to fill the screen |
| */ |
| private static boolean mShowAllAllDayEvents = false; |
| |
| protected int mNumDays = 7; |
| private int mNumHours = 10; |
| |
| /** Width of the time line (list of hours) to the left. */ |
| private int mHoursWidth; |
| private int mDateStrWidth; |
| /** Top of the scrollable region i.e. below date labels and all day events */ |
| private int mFirstCell; |
| /** First fully visibile hour */ |
| private int mFirstHour = -1; |
| /** Distance between the mFirstCell and the top of first fully visible hour. */ |
| private int mFirstHourOffset; |
| private String[] mHourStrs; |
| private String[] mDayStrs; |
| private String[] mDayStrs2Letter; |
| private boolean mIs24HourFormat; |
| |
| private final ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); |
| private boolean mComputeSelectedEvents; |
| private boolean mUpdateToast; |
| private Event mSelectedEvent; |
| private Event mPrevSelectedEvent; |
| private final Rect mPrevBox = new Rect(); |
| protected final Resources mResources; |
| protected final Drawable mCurrentTimeLine; |
| protected final Drawable mCurrentTimeAnimateLine; |
| protected final Drawable mTodayHeaderDrawable; |
| protected final Drawable mExpandAlldayDrawable; |
| protected final Drawable mCollapseAlldayDrawable; |
| protected Drawable mAcceptedOrTentativeEventBoxDrawable; |
| private String mAmString; |
| private String mPmString; |
| private final DeleteEventHelper mDeleteEventHelper; |
| private static int sCounter = 0; |
| |
| private final ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); |
| |
| ScaleGestureDetector mScaleGestureDetector; |
| |
| /** |
| * 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; // D-pad down but not up yet |
| private static final int SELECTION_SELECTED = 2; |
| private static final int SELECTION_LONGPRESS = 3; |
| |
| private int mSelectionMode = SELECTION_HIDDEN; |
| |
| private boolean mScrolling = false; |
| |
| // Pixels scrolled |
| private float mInitialScrollX; |
| private float mInitialScrollY; |
| |
| private boolean mAnimateToday = false; |
| private int mAnimateTodayAlpha = 0; |
| |
| // Animates the height of the allday region |
| ObjectAnimator mAlldayAnimator; |
| // Animates the height of events in the allday region |
| ObjectAnimator mAlldayEventAnimator; |
| // Animates the transparency of the more events text |
| ObjectAnimator mMoreAlldayEventsAnimator; |
| // Animates the current time marker when Today is pressed |
| ObjectAnimator mTodayAnimator; |
| // whether or not an event is stopping because it was cancelled |
| private boolean mCancellingAnimations = false; |
| // tracks whether a touch originated in the allday area |
| private boolean mTouchStartedInAlldayArea = false; |
| |
| private final CalendarController mController; |
| private final ViewSwitcher mViewSwitcher; |
| private final GestureDetector mGestureDetector; |
| private final OverScroller mScroller; |
| private final EdgeEffect mEdgeEffectTop; |
| private final EdgeEffect mEdgeEffectBottom; |
| private boolean mCallEdgeEffectOnAbsorb; |
| private final int OVERFLING_DISTANCE; |
| private float mLastVelocity; |
| |
| private final ScrollInterpolator mHScrollInterpolator; |
| private AccessibilityManager mAccessibilityMgr = null; |
| private boolean mIsAccessibilityEnabled = false; |
| private boolean mTouchExplorationEnabled = false; |
| private final String mCreateNewEventString; |
| private final String mNewEventHintString; |
| |
| public DayView(Context context, CalendarController controller, |
| ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { |
| super(context); |
| mContext = context; |
| initAccessibilityVariables(); |
| |
| mResources = context.getResources(); |
| mCreateNewEventString = mResources.getString(R.string.event_create); |
| mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint); |
| mNumDays = numDays; |
| |
| DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); |
| DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); |
| ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); |
| DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); |
| EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); |
| HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); |
| AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); |
| MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); |
| HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); |
| HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); |
| MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); |
| int eventTextSizeId; |
| if (mNumDays == 1) { |
| eventTextSizeId = R.dimen.day_view_event_text_size; |
| } else { |
| eventTextSizeId = R.dimen.week_view_event_text_size; |
| } |
| EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(eventTextSizeId); |
| NEW_EVENT_HINT_FONT_SIZE = (int) mResources.getDimension(R.dimen.new_event_hint_text_size); |
| MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height); |
| MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT; |
| EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin); |
| EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; |
| EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; |
| EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; |
| |
| EVENT_TEXT_LEFT_MARGIN = (int) mResources |
| .getDimension(R.dimen.event_text_horizontal_margin); |
| EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; |
| EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; |
| EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; |
| |
| if (mScale == 0) { |
| |
| mScale = mResources.getDisplayMetrics().density; |
| if (mScale != 1) { |
| SINGLE_ALLDAY_HEIGHT *= mScale; |
| ALLDAY_TOP_MARGIN *= mScale; |
| MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; |
| |
| NORMAL_FONT_SIZE *= mScale; |
| GRID_LINE_LEFT_MARGIN *= mScale; |
| HOURS_TOP_MARGIN *= mScale; |
| MIN_CELL_WIDTH_FOR_TEXT *= mScale; |
| MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; |
| mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; |
| |
| CURRENT_TIME_LINE_SIDE_BUFFER *= mScale; |
| CURRENT_TIME_LINE_TOP_OFFSET *= mScale; |
| |
| MIN_Y_SPAN *= mScale; |
| MAX_CELL_HEIGHT *= mScale; |
| DEFAULT_CELL_HEIGHT *= mScale; |
| DAY_HEADER_HEIGHT *= mScale; |
| DAY_HEADER_RIGHT_MARGIN *= mScale; |
| DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; |
| DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; |
| DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; |
| CALENDAR_COLOR_SQUARE_SIZE *= mScale; |
| EVENT_RECT_TOP_MARGIN *= mScale; |
| EVENT_RECT_BOTTOM_MARGIN *= mScale; |
| ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; |
| EVENT_RECT_LEFT_MARGIN *= mScale; |
| EVENT_RECT_RIGHT_MARGIN *= mScale; |
| EVENT_RECT_STROKE_WIDTH *= mScale; |
| EVENT_SQUARE_WIDTH *= mScale; |
| EVENT_LINE_PADDING *= mScale; |
| NEW_EVENT_MARGIN *= mScale; |
| NEW_EVENT_WIDTH *= mScale; |
| NEW_EVENT_MAX_LENGTH *= mScale; |
| } |
| } |
| HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; |
| DAY_HEADER_HEIGHT = mNumDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; |
| |
| mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light); |
| mCurrentTimeAnimateLine = mResources |
| .getDrawable(R.drawable.timeline_indicator_activated_holo_light); |
| mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); |
| mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light); |
| mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light); |
| mNewEventHintColor = mResources.getColor(R.color.new_event_hint_text_color); |
| mAcceptedOrTentativeEventBoxDrawable = mResources |
| .getDrawable(R.drawable.panel_month_event_holo_light); |
| |
| mEventLoader = eventLoader; |
| mEventGeometry = new EventGeometry(); |
| mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); |
| mEventGeometry.setHourGap(HOUR_GAP); |
| mEventGeometry.setCellMargin(DAY_GAP); |
| mLongPressItems = new CharSequence[] { |
| mResources.getString(R.string.new_event_dialog_option) |
| }; |
| mLongPressTitle = mResources.getString(R.string.new_event_dialog_label); |
| mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); |
| mLastPopupEventID = INVALID_EVENT_ID; |
| mController = controller; |
| mViewSwitcher = viewSwitcher; |
| mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); |
| mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); |
| if (mCellHeight == 0) { |
| mCellHeight = Utils.getSharedPreference(mContext, |
| GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); |
| } |
| mScroller = new OverScroller(context); |
| mHScrollInterpolator = new ScrollInterpolator(); |
| mEdgeEffectTop = new EdgeEffect(context); |
| mEdgeEffectBottom = new EdgeEffect(context); |
| ViewConfiguration vc = ViewConfiguration.get(context); |
| mScaledPagingTouchSlop = vc.getScaledPagingTouchSlop(); |
| mOnDownDelay = ViewConfiguration.getTapTimeout(); |
| OVERFLING_DISTANCE = vc.getScaledOverflingDistance(); |
| |
| init(context); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| if (mHandler == null) { |
| mHandler = getHandler(); |
| mHandler.post(mUpdateCurrentTime); |
| } |
| } |
| |
| 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); |
| |
| mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); |
| |
| mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); |
| long currentTime = System.currentTimeMillis(); |
| mCurrentTime.set(currentTime); |
| mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); |
| |
| mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); |
| mWeek_sundayColor = mResources.getColor(R.color.week_sunday); |
| mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); |
| mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color); |
| mBgColor = mResources.getColor(R.color.calendar_hour_background); |
| mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); |
| mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); |
| mCalendarGridLineInnerHorizontalColor = mResources |
| .getColor(R.color.calendar_grid_line_inner_horizontal_color); |
| mCalendarGridLineInnerVerticalColor = mResources |
| .getColor(R.color.calendar_grid_line_inner_vertical_color); |
| mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); |
| mPressedColor = mResources.getColor(R.color.pressed); |
| mClickedColor = mResources.getColor(R.color.day_event_clicked_background_color); |
| mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); |
| mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color); |
| |
| 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.FILL); |
| p.setAntiAlias(false); |
| |
| p = mPaint; |
| p.setAntiAlias(true); |
| |
| // 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) |
| .toUpperCase(); |
| mDayStrs[index + 7] = mDayStrs[index]; |
| // e.g. Tu for Tuesday |
| mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) |
| .toUpperCase(); |
| |
| // 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(DATE_HEADER_FONT_SIZE); |
| p.setTypeface(mBold); |
| String[] dateStrs = {" 28", " 30"}; |
| mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); |
| p.setTextSize(DAY_HEADER_FONT_SIZE); |
| mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); |
| |
| p.setTextSize(HOURS_TEXT_SIZE); |
| p.setTypeface(null); |
| handleOnResume(); |
| |
| mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); |
| mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); |
| String[] ampm = {mAmString, mPmString}; |
| p.setTextSize(AMPM_TEXT_SIZE); |
| mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) |
| + HOURS_RIGHT_MARGIN); |
| mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); |
| |
| 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.MATCH_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); |
| // Catch long clicks for creating a new event |
| setOnLongClickListener(this); |
| |
| mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); |
| long millis = System.currentTimeMillis(); |
| mBaseDate.set(millis); |
| |
| mEarliestStartHour = new int[mNumDays]; |
| mHasAllDayEvent = new boolean[mNumDays]; |
| |
| // mLines is the array of points used with Canvas.drawLines() in |
| // drawGridBackground() and drawAllDayEvents(). Its size depends |
| // on the max number of lines that can ever be drawn by any single |
| // drawLines() call in either of those methods. |
| final int maxGridLines = (24 + 1) // max horizontal lines we might draw |
| + (mNumDays + 1); // max vertical lines we might draw |
| mLines = new float[maxGridLines * 4]; |
| } |
| |
| /** |
| * 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 */); |
| } |
| } |
| |
| public void handleOnResume() { |
| initAccessibilityVariables(); |
| if(Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) { |
| mFutureBgColor = 0; |
| } else { |
| mFutureBgColor = mFutureBgColorRes; |
| } |
| mIs24HourFormat = DateFormat.is24HourFormat(mContext); |
| mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; |
| mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); |
| mLastSelectionDayForAccessibility = 0; |
| mLastSelectionHourForAccessibility = 0; |
| mLastSelectedEventForAccessibility = null; |
| mSelectionMode = SELECTION_HIDDEN; |
| } |
| |
| private void initAccessibilityVariables() { |
| mAccessibilityMgr = (AccessibilityManager) mContext |
| .getSystemService(Service.ACCESSIBILITY_SERVICE); |
| mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr.isEnabled(); |
| mTouchExplorationEnabled = isTouchExplorationEnabled(); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| Time getSelectedTimeForAccessibility() { |
| Time time = new Time(mBaseDate); |
| time.setJulianDay(mSelectionDayForAccessibility); |
| time.hour = mSelectionHourForAccessibility; |
| |
| // 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; |
| } |
| |
| int getFirstVisibleHour() { |
| return mFirstHour; |
| } |
| |
| void setFirstVisibleHour(int firstHour) { |
| mFirstHour = firstHour; |
| mFirstHourOffset = 0; |
| } |
| |
| public void setSelected(Time time, boolean ignoreTime, boolean animateToday) { |
| mBaseDate.set(time); |
| setSelectedHour(mBaseDate.hour); |
| setSelectedEvent(null); |
| mPrevSelectedEvent = null; |
| long millis = mBaseDate.toMillis(false /* use isDst */); |
| setSelectedDay(Time.getJulianDay(millis, mBaseDate.gmtoff)); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| |
| int gotoY = Integer.MIN_VALUE; |
| |
| if (!ignoreTime && mGridAreaHeight != -1) { |
| int lastHour = 0; |
| |
| if (mBaseDate.hour < mFirstHour) { |
| // Above visible region |
| gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); |
| } else { |
| lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) |
| + mFirstHour; |
| |
| if (mBaseDate.hour >= lastHour) { |
| // Below visible region |
| |
| // target hour + 1 (to give it room to see the event) - |
| // grid height (to get the y of the top of the visible |
| // region) |
| gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) |
| * (mCellHeight + HOUR_GAP) - mGridAreaHeight); |
| } |
| } |
| |
| if (DEBUG) { |
| Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " |
| + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight |
| + " ymax " + mMaxViewStartY); |
| } |
| |
| if (gotoY > mMaxViewStartY) { |
| gotoY = mMaxViewStartY; |
| } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { |
| gotoY = 0; |
| } |
| } |
| |
| recalc(); |
| |
| mRemeasure = true; |
| invalidate(); |
| |
| boolean delayAnimateToday = false; |
| if (gotoY != Integer.MIN_VALUE) { |
| ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); |
| scrollAnim.setDuration(GOTO_SCROLL_DURATION); |
| scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); |
| scrollAnim.addListener(mAnimatorListener); |
| scrollAnim.start(); |
| delayAnimateToday = true; |
| } |
| if (animateToday) { |
| synchronized (mTodayAnimatorListener) { |
| if (mTodayAnimator != null) { |
| mTodayAnimator.removeAllListeners(); |
| mTodayAnimator.cancel(); |
| } |
| mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", |
| mAnimateTodayAlpha, 255); |
| mAnimateToday = true; |
| mTodayAnimatorListener.setFadingIn(true); |
| mTodayAnimatorListener.setAnimator(mTodayAnimator); |
| mTodayAnimator.addListener(mTodayAnimatorListener); |
| mTodayAnimator.setDuration(150); |
| if (delayAnimateToday) { |
| mTodayAnimator.setStartDelay(GOTO_SCROLL_DURATION); |
| } |
| mTodayAnimator.start(); |
| } |
| } |
| sendAccessibilityEventAsNeeded(false); |
| } |
| |
| // Called from animation framework via reflection. Do not remove |
| public void setViewStartY(int viewStartY) { |
| if (viewStartY > mMaxViewStartY) { |
| viewStartY = mMaxViewStartY; |
| } |
| |
| mViewStartY = viewStartY; |
| |
| computeFirstHour(); |
| invalidate(); |
| } |
| |
| public void setAnimateTodayAlpha(int todayAlpha) { |
| mAnimateTodayAlpha = todayAlpha; |
| 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; |
| } |
| |
| public void updateTitle() { |
| Time start = new Time(mBaseDate); |
| start.normalize(true); |
| Time end = new Time(start); |
| end.monthDay += mNumDays - 1; |
| // Move it forward one minute so the formatter doesn't lose a day |
| end.minute += 1; |
| end.normalize(true); |
| |
| long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; |
| if (mNumDays != 1) { |
| // Don't show day of the month if for multi-day view |
| formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; |
| |
| // Abbreviate the month if showing multiple months |
| if (start.month != end.month) { |
| formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; |
| } |
| } |
| |
| mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, |
| formatFlags, null, null); |
| } |
| |
| /** |
| * return a negative number if "time" is comes before the visible time |
| * range, a positive number if "time" is after the visible time range, and 0 |
| * if it is in the visible time range. |
| */ |
| public int compareToVisibleTimeRange(Time time) { |
| |
| int savedHour = mBaseDate.hour; |
| int savedMinute = mBaseDate.minute; |
| int savedSec = mBaseDate.second; |
| |
| mBaseDate.hour = 0; |
| mBaseDate.minute = 0; |
| mBaseDate.second = 0; |
| |
| if (DEBUG) { |
| Log.d(TAG, "Begin " + mBaseDate.toString()); |
| Log.d(TAG, "Diff " + time.toString()); |
| } |
| |
| // Compare beginning of range |
| int diff = Time.compare(time, mBaseDate); |
| if (diff > 0) { |
| // Compare end of range |
| mBaseDate.monthDay += mNumDays; |
| mBaseDate.normalize(true); |
| diff = Time.compare(time, mBaseDate); |
| |
| if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); |
| |
| mBaseDate.monthDay -= mNumDays; |
| mBaseDate.normalize(true); |
| if (diff < 0) { |
| // in visible time |
| diff = 0; |
| } else if (diff == 0) { |
| // Midnight of following day |
| diff = 1; |
| } |
| } |
| |
| if (DEBUG) Log.d(TAG, "Diff: " + diff); |
| |
| mBaseDate.hour = savedHour; |
| mBaseDate.minute = savedMinute; |
| mBaseDate.second = savedSec; |
| return diff; |
| } |
| |
| 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) { |
| adjustToBeginningOfWeek(mBaseDate); |
| } |
| |
| final long start = mBaseDate.toMillis(false /* use isDst */); |
| mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); |
| mLastJulianDay = mFirstJulianDay + mNumDays - 1; |
| |
| mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); |
| mFirstVisibleDate = mBaseDate.monthDay; |
| mFirstVisibleDayOfWeek = mBaseDate.weekDay; |
| } |
| |
| private void adjustToBeginningOfWeek(Time time) { |
| int dayOfWeek = time.weekDay; |
| int diff = dayOfWeek - mFirstDayOfWeek; |
| if (diff != 0) { |
| if (diff < 0) { |
| diff += 7; |
| } |
| time.monthDay -= diff; |
| time.normalize(true /* ignore isDst */); |
| } |
| } |
| |
| @Override |
| protected void onSizeChanged(int width, int height, int oldw, int oldh) { |
| mViewWidth = width; |
| mViewHeight = height; |
| mEdgeEffectTop.setSize(mViewWidth, mViewHeight); |
| mEdgeEffectBottom.setSize(mViewWidth, mViewHeight); |
| int gridAreaWidth = width - mHoursWidth; |
| mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; |
| |
| // This would be about 1 day worth in a 7 day view |
| mHorizontalSnapBackThreshold = width / 7; |
| |
| Paint p = new Paint(); |
| p.setTextSize(HOURS_TEXT_SIZE); |
| mHoursTextHeight = (int) Math.abs(p.ascent()); |
| 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) { |
| // Shrink to fit available space but make sure we can display at least two events |
| MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); |
| MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); |
| MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, |
| (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); |
| mMaxUnexpandedAlldayEventCount = |
| (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_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; |
| } |
| |
| int maxAllDayEvents = mMaxAlldayEvents; |
| |
| // The min is where 24 hours cover the entire visible area |
| mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); |
| if (mCellHeight < mMinCellHeight) { |
| mCellHeight = mMinCellHeight; |
| } |
| |
| // Calculate mAllDayHeight |
| mFirstCell = DAY_HEADER_HEIGHT; |
| int allDayHeight = 0; |
| if (maxAllDayEvents > 0) { |
| int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; |
| // 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 if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ |
| // 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_HEIGHT_OF_ONE_ALLDAY_EVENT; |
| if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { |
| allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; |
| } |
| } else { |
| // if we have more than the magic number, check if we're animating |
| // and if not adjust the sizes appropriately |
| if (mAnimateDayHeight != 0) { |
| // Don't shrink the space past the final allDay space. The animation |
| // continues to hide the last event so the more events text can |
| // fade in. |
| allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); |
| } else { |
| // Try to fit all the events in |
| allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); |
| // But clip the area depending on which mode we're in |
| if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { |
| allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * |
| MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); |
| } else if (allDayHeight > maxAllAllDayHeight) { |
| allDayHeight = maxAllAllDayHeight; |
| } |
| } |
| } |
| mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; |
| } else { |
| mSelectionAllday = false; |
| } |
| mAlldayHeight = allDayHeight; |
| |
| mGridAreaHeight = height - mFirstCell; |
| |
| // Set up the expand icon position |
| int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); |
| mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, |
| EVENT_ALL_DAY_TEXT_LEFT_MARGIN); |
| mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth |
| - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); |
| mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; |
| mExpandAllDayRect.top = mExpandAllDayRect.bottom |
| - mExpandAlldayDrawable.getIntrinsicHeight(); |
| |
| mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); |
| mEventGeometry.setHourHeight(mCellHeight); |
| |
| final long minimumDurationMillis = (long) |
| (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); |
| Event.computePositions(mEvents, minimumDurationMillis); |
| |
| // Compute the top of our reachable view |
| mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; |
| if (DEBUG) { |
| Log.e(TAG, "mViewStartY: " + mViewStartY); |
| Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); |
| } |
| if (mViewStartY > mMaxViewStartY) { |
| mViewStartY = mMaxViewStartY; |
| computeFirstHour(); |
| } |
| |
| 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; |
| |
| final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); |
| //When we get new events we don't want to dismiss the popup unless the event changes |
| if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { |
| 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(DayView view) { |
| view.setSelectedHour(mSelectionHour); |
| view.mSelectedEvents.clear(); |
| view.mComputeSelectedEvents = true; |
| view.mFirstHour = mFirstHour; |
| view.mFirstHourOffset = mFirstHourOffset; |
| view.remeasure(getWidth(), getHeight()); |
| view.initAllDayHeights(); |
| |
| view.setSelectedEvent(null); |
| view.mPrevSelectedEvent = null; |
| view.mFirstDayOfWeek = mFirstDayOfWeek; |
| 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.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(); |
| mLastPopupEventID = INVALID_EVENT_ID; |
| 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; |
| long extraLong = 0; |
| if (mSelectionAllday) { |
| extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; |
| } |
| mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, |
| startMillis, endMillis, -1, -1, extraLong, -1); |
| } else { |
| if (mIsAccessibilityEnabled) { |
| mAccessibilityMgr.interrupt(); |
| } |
| // Switch to the EventInfo view |
| mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, |
| selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, |
| getSelectedTimeInMillis()); |
| } |
| } 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) { |
| if (mIsAccessibilityEnabled) { |
| mAccessibilityMgr.interrupt(); |
| } |
| mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, |
| selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, |
| getSelectedTimeInMillis()); |
| } |
| } |
| } 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; |
| long extraLong = 0; |
| if (mSelectionAllday) { |
| extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; |
| } |
| mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, |
| startMillis, endMillis, -1, -1, extraLong, -1); |
| } else { |
| if (mIsAccessibilityEnabled) { |
| mAccessibilityMgr.interrupt(); |
| } |
| mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, |
| selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, |
| getSelectedTimeInMillis()); |
| } |
| } |
| } |
| |
| @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; |
| invalidate(); |
| break; |
| } |
| |
| // Check the duration to determine if this was a short press |
| if (duration < ViewConfiguration.getLongPressTimeout()) { |
| switchViews(true /* trackball */); |
| } else { |
| mSelectionMode = SELECTION_LONGPRESS; |
| invalidate(); |
| performLongClick(); |
| } |
| break; |
| // case KeyEvent.KEYCODE_BACK: |
| // if (event.isTracking() && !event.isCanceled()) { |
| // mPopup.dismiss(); |
| // mContext.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; |
| 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; |
| 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(); |
| mLastPopupEventID = INVALID_EVENT_ID; |
| |
| 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) { |
| setSelectedEvent(mSelectedEvent.nextLeft); |
| } |
| if (mSelectedEvent == null) { |
| mLastPopupEventID = INVALID_EVENT_ID; |
| selectionDay -= 1; |
| } |
| redraw = true; |
| break; |
| |
| case KeyEvent.KEYCODE_DPAD_RIGHT: |
| if (mSelectedEvent != null) { |
| setSelectedEvent(mSelectedEvent.nextRight); |
| } |
| if (mSelectedEvent == null) { |
| mLastPopupEventID = INVALID_EVENT_ID; |
| selectionDay += 1; |
| } |
| redraw = true; |
| break; |
| |
| case KeyEvent.KEYCODE_DPAD_UP: |
| if (mSelectedEvent != null) { |
| setSelectedEvent(mSelectedEvent.nextUp); |
| } |
| if (mSelectedEvent == null) { |
| mLastPopupEventID = INVALID_EVENT_ID; |
| if (!mSelectionAllday) { |
| setSelectedHour(mSelectionHour - 1); |
| adjustHourSelection(); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| } |
| } |
| redraw = true; |
| break; |
| |
| case KeyEvent.KEYCODE_DPAD_DOWN: |
| if (mSelectedEvent != null) { |
| setSelectedEvent(mSelectedEvent.nextDown); |
| } |
| if (mSelectedEvent == null) { |
| mLastPopupEventID = INVALID_EVENT_ID; |
| if (mSelectionAllday) { |
| mSelectionAllday = false; |
| } else { |
| setSelectedHour(mSelectionHour + 1); |
| adjustHourSelection(); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| } |
| } |
| redraw = true; |
| break; |
| |
| default: |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { |
| DayView view = (DayView) mViewSwitcher.getNextView(); |
| Time date = view.mBaseDate; |
| date.set(mBaseDate); |
| if (selectionDay < mFirstJulianDay) { |
| date.monthDay -= mNumDays; |
| } else { |
| date.monthDay += mNumDays; |
| } |
| date.normalize(true /* ignore isDst */); |
| view.setSelectedDay(selectionDay); |
| |
| initView(view); |
| |
| Time end = new Time(date); |
| end.monthDay += mNumDays - 1; |
| mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); |
| return true; |
| } |
| if (mSelectionDay != selectionDay) { |
| Time date = new Time(mBaseDate); |
| date.setJulianDay(selectionDay); |
| date.hour = mSelectionHour; |
| mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); |
| } |
| setSelectedDay(selectionDay); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| mUpdateToast = true; |
| |
| if (redraw) { |
| invalidate(); |
| return true; |
| } |
| |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| |
| @Override |
| public boolean onHoverEvent(MotionEvent event) { |
| if (DEBUG) { |
| int action = event.getAction(); |
| switch (action) { |
| case MotionEvent.ACTION_HOVER_ENTER: |
| Log.e(TAG, "ACTION_HOVER_ENTER"); |
| break; |
| case MotionEvent.ACTION_HOVER_MOVE: |
| Log.e(TAG, "ACTION_HOVER_MOVE"); |
| break; |
| case MotionEvent.ACTION_HOVER_EXIT: |
| Log.e(TAG, "ACTION_HOVER_EXIT"); |
| break; |
| default: |
| Log.e(TAG, "Unknown hover event action. " + event); |
| } |
| } |
| |
| // Mouse also generates hover events |
| // Send accessibility events if accessibility and exploration are on. |
| if (!mTouchExplorationEnabled) { |
| return super.onHoverEvent(event); |
| } |
| if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { |
| setSelectionFromPosition((int) event.getX(), (int) event.getY(), true); |
| invalidate(); |
| } |
| return true; |
| } |
| |
| private boolean isTouchExplorationEnabled() { |
| return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled(); |
| } |
| |
| private void sendAccessibilityEventAsNeeded(boolean speakEvents) { |
| if (!mIsAccessibilityEnabled) { |
| return; |
| } |
| boolean dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility; |
| boolean hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility; |
| if (dayChanged || hourChanged || |
| mLastSelectedEventForAccessibility != mSelectedEventForAccessibility) { |
| mLastSelectionDayForAccessibility = mSelectionDayForAccessibility; |
| mLastSelectionHourForAccessibility = mSelectionHourForAccessibility; |
| mLastSelectedEventForAccessibility = mSelectedEventForAccessibility; |
| |
| StringBuilder b = new StringBuilder(); |
| |
| // Announce only the changes i.e. day or hour or both |
| if (dayChanged) { |
| b.append(getSelectedTimeForAccessibility().format("%A ")); |
| } |
| if (hourChanged) { |
| b.append(getSelectedTimeForAccessibility().format(mIs24HourFormat ? "%k" : "%l%p")); |
| } |
| if (dayChanged || hourChanged) { |
| b.append(PERIOD_SPACE); |
| } |
| |
| if (speakEvents) { |
| if (mEventCountTemplate == null) { |
| mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); |
| } |
| |
| // Read out the relevant event(s) |
| int numEvents = mSelectedEvents.size(); |
| if (numEvents > 0) { |
| if (mSelectedEventForAccessibility == null) { |
| // Read out all the events |
| int i = 1; |
| for (Event calEvent : mSelectedEvents) { |
| if (numEvents > 1) { |
| // Read out x of numEvents if there are more than one event |
| mStringBuilder.setLength(0); |
| b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); |
| b.append(" "); |
| } |
| appendEventAccessibilityString(b, calEvent); |
| } |
| } else { |
| if (numEvents > 1) { |
| // Read out x of numEvents if there are more than one event |
| mStringBuilder.setLength(0); |
| b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents |
| .indexOf(mSelectedEventForAccessibility) + 1, numEvents)); |
| b.append(" "); |
| } |
| appendEventAccessibilityString(b, mSelectedEventForAccessibility); |
| } |
| } else { |
| b.append(mCreateNewEventString); |
| } |
| } |
| |
| if (dayChanged || hourChanged || speakEvents) { |
| AccessibilityEvent event = AccessibilityEvent |
| .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); |
| CharSequence msg = b.toString(); |
| event.getText().add(msg); |
| event.setAddedCount(msg.length()); |
| sendAccessibilityEventUnchecked(event); |
| } |
| } |
| } |
| |
| /** |
| * @param b |
| * @param calEvent |
| */ |
| private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { |
| b.append(calEvent.getTitleAndLocation()); |
| b.append(PERIOD_SPACE); |
| String when; |
| int flags = DateUtils.FORMAT_SHOW_DATE; |
| if (calEvent.allDay) { |
| flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; |
| } else { |
| flags |= DateUtils.FORMAT_SHOW_TIME; |
| if (DateFormat.is24HourFormat(mContext)) { |
| flags |= DateUtils.FORMAT_24HOUR; |
| } |
| } |
| when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); |
| b.append(when); |
| b.append(PERIOD_SPACE); |
| } |
| |
| private class GotoBroadcaster implements Animation.AnimationListener { |
| private final int mCounter; |
| private final Time mStart; |
| private final Time mEnd; |
| |
| public GotoBroadcaster(Time start, Time end) { |
| mCounter = ++sCounter; |
| mStart = start; |
| mEnd = end; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animation animation) { |
| DayView view = (DayView) mViewSwitcher.getCurrentView(); |
| view.mViewStartX = 0; |
| view = (DayView) mViewSwitcher.getNextView(); |
| view.mViewStartX = 0; |
| |
| if (mCounter == sCounter) { |
| mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, |
| ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); |
| } |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animation animation) { |
| } |
| |
| @Override |
| public void onAnimationStart(Animation animation) { |
| } |
| } |
| |
| private View switchViews(boolean forward, float xOffSet, float width, float velocity) { |
| mAnimationDistance = width - xOffSet; |
| if (DEBUG) { |
| Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); |
| } |
| |
| float progress = Math.abs(xOffSet) / width; |
| if (progress > 1.0f) { |
| progress = 1.0f; |
| } |
| |
| float inFromXValue, inToXValue; |
| float outFromXValue, outToXValue; |
| if (forward) { |
| inFromXValue = 1.0f - progress; |
| inToXValue = 0.0f; |
| outFromXValue = -progress; |
| outToXValue = -1.0f; |
| } else { |
| inFromXValue = progress - 1.0f; |
| inToXValue = 0.0f; |
| outFromXValue = progress; |
| outToXValue = 1.0f; |
| } |
| |
| final Time start = new Time(mBaseDate.timezone); |
| start.set(mController.getTime()); |
| if (forward) { |
| start.monthDay += mNumDays; |
| } else { |
| start.monthDay -= mNumDays; |
| } |
| mController.setTime(start.normalize(true)); |
| |
| Time newSelected = start; |
| |
| if (mNumDays == 7) { |
| newSelected = new Time(start); |
| adjustToBeginningOfWeek(start); |
| } |
| |
| final Time end = new Time(start); |
| end.monthDay += mNumDays - 1; |
| |
| // We have to allocate these animation objects each time we switch views |
| // because that is the only way to set the animation parameters. |
| TranslateAnimation inAnimation = new TranslateAnimation( |
| Animation.RELATIVE_TO_SELF, inFromXValue, |
| Animation.RELATIVE_TO_SELF, inToXValue, |
| Animation.ABSOLUTE, 0.0f, |
| Animation.ABSOLUTE, 0.0f); |
| |
| TranslateAnimation outAnimation = new TranslateAnimation( |
| Animation.RELATIVE_TO_SELF, outFromXValue, |
| Animation.RELATIVE_TO_SELF, outToXValue, |
| Animation.ABSOLUTE, 0.0f, |
| Animation.ABSOLUTE, 0.0f); |
| |
| long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); |
| inAnimation.setDuration(duration); |
| inAnimation.setInterpolator(mHScrollInterpolator); |
| outAnimation.setInterpolator(mHScrollInterpolator); |
| outAnimation.setDuration(duration); |
| outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); |
| mViewSwitcher.setInAnimation(inAnimation); |
| mViewSwitcher.setOutAnimation(outAnimation); |
| |
| DayView view = (DayView) mViewSwitcher.getCurrentView(); |
| view.cleanup(); |
| mViewSwitcher.showNext(); |
| view = (DayView) mViewSwitcher.getCurrentView(); |
| view.setSelected(newSelected, true, false); |
| view.requestFocus(); |
| view.reloadEvents(); |
| view.updateTitle(); |
| view.restartCurrentTimeUpdates(); |
| |
| return view; |
| } |
| |
| // 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) { |
| setSelectedHour(mFirstHour + 1); |
| setSelectedEvent(null); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| } else if (mSelectionHour > mFirstHour + mNumHours - 3) { |
| setSelectedHour(mFirstHour + mNumHours - 3); |
| setSelectedEvent(null); |
| mSelectedEvents.clear(); |
| mComputeSelectedEvents = true; |
| } |
| } |
| |
| private void initFirstHour() { |
| mFirstHour = mSelectionHour - mNumHours / 5; |
| 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) { |
| setSelectedHour(0); |
| if (mMaxAlldayEvents > 0) { |
| mPrevSelectedEvent = null; |
| mSelectionAllday = true; |
| } |
| } |
| |
| if (mSelectionHour > 23) { |
| setSelectedHour(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 (daynum < mEarliestStartHour.length && daynum >= 0 |
| && mMaxAlldayEvents > 0 |
| && mEarliestStartHour[daynum] > mSelectionHour |
| && mFirstHour > 0 && mFirstHour < 8) { |
| mPrevSelectedEvent = null; |
| mSelectionAllday = true; |
| setSelectedHour(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 > mMaxViewStartY) { |
| mViewStartY = mMaxViewStartY; |
| } |
| return; |
| } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { |
| mViewStartY = mMaxViewStartY; |
| } |
| } |
| } |
| |
| void clearCachedEvents() { |
| mLastReloadMillis = 0; |
| } |
| |
| private final Runnable mCancelCallback = new Runnable() { |
| public void run() { |
| clearCachedEvents(); |
| } |
| }; |
| |
| /* package */ void reloadEvents() { |
| // Protect against this being called before this view has been |
| // initialized. |
| // if (mContext == null) { |
| // return; |
| // } |
| |
| // Make sure our time zones are up to date |
| mTZUpdater.run(); |
| |
| setSelectedEvent(null); |
| mPrevSelectedEvent = null; |
| mSelectedEvents.clear(); |
| |
| // The start date is the beginning of the week at 12am |
| Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); |
| 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 |
| // mContext.startProgressSpinner(); |
| final ArrayList<Event> events = new ArrayList<Event>(); |
| mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { |
| |
| public void run() { |
| boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay; |
| mEvents = events; |
| mLoadedFirstJulianDay = mFirstJulianDay; |
| if (mAllDayEvents == null) { |
| mAllDayEvents = new ArrayList<Event>(); |
| } else { |
| mAllDayEvents.clear(); |
| } |
| |
| // Create a shorter array for all day events |
| for (Event e : events) { |
| if (e.drawAsAllday()) { |
| mAllDayEvents.add(e); |
| } |
| } |
| |
| // New events, new layouts |
| if (mLayouts == null || mLayouts.length < events.size()) { |
| mLayouts = new StaticLayout[events.size()]; |
| } else { |
| Arrays.fill(mLayouts, null); |
| } |
| |
| if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { |
| mAllDayLayouts = new StaticLayout[events.size()]; |
| } else { |
| Arrays.fill(mAllDayLayouts, null); |
| } |
| |
| computeEventRelations(); |
| |
| mRemeasure = true; |
| mComputeSelectedEvents = true; |
| recalc(); |
| |
| // Start animation to cross fade the events |
| if (fadeinEvents) { |
| if (mEventsCrossFadeAnimation == null) { |
| mEventsCrossFadeAnimation = |
| ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255); |
| mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION); |
| } |
| mEventsCrossFadeAnimation.start(); |
| } else{ |
| invalidate(); |
| } |
| } |
| }, mCancelCallback); |
| } |
| |
| public void setEventsAlpha(int alpha) { |
| mEventsAlpha = alpha; |
| invalidate(); |
| } |
| |
| public int getEventsAlpha() { |
| return mEventsAlpha; |
| } |
| |
| public void stopEventsAnimation() { |
| if (mEventsCrossFadeAnimation != null) { |
| mEventsCrossFadeAnimation.cancel(); |
| } |
| mEventsAlpha = 255; |
| } |
| |
| private void computeEventRelations() { |
| // Compute the layout relation between each event before measuring cell |
| // width, as the cell width should be adjusted along with the relation. |
| // |
| // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) |
| // We should mark them as "overwapped". Though they are not overwapped logically, but |
| // minimum cell height implicitly expands the cell height of A and it should look like |
| // (1:00pm - 1:15pm) after the cell height adjustment. |
| |
| // 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; |
| final ArrayList<Event> events = mEvents; |
| final int len = events.size(); |
| // Num of all-day-events on each day. |
| final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; |
| Arrays.fill(eventsCount, 0); |
| for (int ii = 0; ii < len; ii++) { |
| Event event = events.get(ii); |
| if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { |
| continue; |
| } |
| if (event.drawAsAllday()) { |
| // Count all the events being drawn as allDay events |
| final int firstDay = Math.max(event.startDay, mFirstJulianDay); |
| final int lastDay = Math.min(event.endDay, mLastJulianDay); |
| for (int day = firstDay; day <= lastDay; day++) { |
| final int count = ++eventsCount[day - mFirstJulianDay]; |
| if (maxAllDayEvents < count) { |
| maxAllDayEvents = count; |
| } |
| } |
| |
| 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; |
| initAllDayHeights(); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| if (mRemeasure) { |
| remeasure(getWidth(), getHeight()); |
| mRemeasure = false; |
| } |
| canvas.save(); |
| |
| float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; |
| // offset canvas by the current drag and header position |
| canvas.translate(-mViewStartX, yTranslate); |
| // clip to everything below the allDay area |
| Rect dest = mDestRect; |
| dest.top = (int) (mFirstCell - yTranslate); |
| dest.bottom = (int) (mViewHeight - yTranslate); |
| dest.left = 0; |
| dest.right = mViewWidth; |
| canvas.save(); |
| canvas.clipRect(dest); |
| // Draw the movable part of the view |
| doDraw(canvas); |
| // restore to having no clip |
| canvas.restore(); |
| |
| if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { |
| float xTranslate; |
| if (mViewStartX > 0) { |
| xTranslate = mViewWidth; |
| } else { |
| xTranslate = -mViewWidth; |
| } |
| // Move the canvas around to prep it for the next view |
| // specifically, shift it by a screen and undo the |
| // yTranslation which will be redone in the nextView's onDraw(). |
| canvas.translate(xTranslate, -yTranslate); |
| DayView nextView = (DayView) mViewSwitcher.getNextView(); |
| |
| // Prevent infinite recursive calls to onDraw(). |
| nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; |
| |
| nextView.onDraw(canvas); |
| // Move it back for this view |
| canvas.translate(-xTranslate, 0); |
| } else { |
| // If we drew another view we already translated it back |
| // If we didn't draw another view we should be at the edge of the |
| // screen |
| canvas.translate(mViewStartX, -yTranslate); |
| } |
| |
| // Draw the fixed areas (that don't scroll) directly to the canvas. |
| drawAfterScroll(canvas); |
| if (mComputeSelectedEvents && mUpdateToast) { |
| updateEventDetails(); |
| mUpdateToast = false; |
| } |
| mComputeSelectedEvents = false; |
| |
| // Draw overscroll glow |
| if (!mEdgeEffectTop.isFinished()) { |
| if (DAY_HEADER_HEIGHT != 0) { |
| canvas.translate(0, DAY_HEADER_HEIGHT); |
| } |
| if (mEdgeEffectTop.draw(canvas)) { |
| invalidate(); |
| } |
| if (DAY_HEADER_HEIGHT != 0) { |
| canvas.translate(0, -DAY_HEADER_HEIGHT); |
| } |
| } |
| if (!mEdgeEffectBottom.isFinished()) { |
| canvas.rotate(180, mViewWidth/2, mViewHeight/2); |
| if (mEdgeEffectBottom.draw(canvas)) { |
| invalidate(); |
| } |
| } |
| canvas.restore(); |
| } |
| |
| private void drawAfterScroll(Canvas canvas) { |
| Paint p = mPaint; |
| Rect r = mRect; |
| |
| drawAllDayHighlights(r, canvas, p); |
| if (mMaxAlldayEvents != 0) { |
| drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); |
| drawUpperLeftCorner(r, canvas, p); |
| } |
| |
| drawScrollLine(r, canvas, p); |
| drawDayHeaderLoop(r, canvas, p); |
| |
| // Draw the AM and PM indicators if we're in 12 hour mode |
| if (!mIs24HourFormat) { |
| drawAmPm(canvas, p); |
| } |
| } |
| |
| // 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) { |
| setupHourTextPaint(p); |
| if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { |
| // Draw the allDay expand/collapse icon |
| if (mUseExpandIcon) { |
| mExpandAlldayDrawable.setBounds(mExpandAllDayRect); |
| mExpandAlldayDrawable.draw(canvas); |
| } else { |
| mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); |
| mCollapseAlldayDrawable.draw(canvas); |
| } |
| } |
| } |
| |
| private void drawScrollLine(Rect r, Canvas canvas, Paint p) { |
| final int right = computeDayLeftPosition(mNumDays); |
| final int y = mFirstCell - 1; |
| |
| p.setAntiAlias(false); |
| p.setStyle(Style.FILL); |
| |
| p.setColor(mCalendarGridLineInnerHorizontalColor); |
| p.setStrokeWidth(GRID_LINE_INNER_WIDTH); |
| canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); |
| p.setAntiAlias(true); |
| } |
| |
| // Computes the x position for the left side of the given day (base 0) |
| private int computeDayLeftPosition(int day) { |
| int effectiveWidth = mViewWidth - mHoursWidth; |
| return day * effectiveWidth / mNumDays + mHoursWidth; |
| } |
| |
| private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { |
| if (mFutureBgColor != 0) { |
| // First, color the labels area light gray |
| r.top = 0; |
| r.bottom = DAY_HEADER_HEIGHT; |
| r.left = 0; |
| r.right = mViewWidth; |
| p.setColor(mBgColor); |
| p.setStyle(Style.FILL); |
| canvas.drawRect(r, p); |
| // and the area that says All day |
| r.top = DAY_HEADER_HEIGHT; |
| r.bottom = mFirstCell - 1; |
| r.left = 0; |
| r.right = mHoursWidth; |
| canvas.drawRect(r, p); |
| |
| int startIndex = -1; |
| |
| int todayIndex = mTodayJulianDay - mFirstJulianDay; |
| if (todayIndex < 0) { |
| // Future |
| startIndex = 0; |
| } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { |
| // Multiday - tomorrow is visible. |
| startIndex = todayIndex + 1; |
| } |
| |
| if (startIndex >= 0) { |
| // Draw the future highlight |
| r.top = 0; |
| r.bottom = mFirstCell - 1; |
| r.left = computeDayLeftPosition(startIndex) + 1; |
| r.right = computeDayLeftPosition(mNumDays); |
| p.setColor(mFutureBgColor); |
| p.setStyle(Style.FILL); |
| canvas.drawRect(r, p); |
| } |
| } |
| |
| if (mSelectionAllday && mSelectionMode != SELECTION_HIDDEN) { |
| // Draw the selection highlight on the selected all-day area |
| mRect.top = DAY_HEADER_HEIGHT + 1; |
| mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; |
| int daynum = mSelectionDay - mFirstJulianDay; |
| mRect.left = computeDayLeftPosition(daynum) + 1; |
| mRect.right = computeDayLeftPosition(daynum + 1); |
| p.setColor(mCalendarGridAreaSelected); |
| canvas.drawRect(mRect, p); |
| } |
| } |
| |
| private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { |
| // Draw the horizontal day background banner |
| // p.setColor(mCalendarDateBannerBackground); |
| // r.top = 0; |
| // r.bottom = DAY_HEADER_HEIGHT; |
| // 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); |
| if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { |
| return; |
| } |
| |
| p.setTypeface(mBold); |
| p.setTextAlign(Paint.Align.RIGHT); |
| int cell = mFirstJulianDay; |
| |
| String[] dayNames; |
| if (mDateStrWidth < mCellWidth) { |
| dayNames = mDayStrs; |
| } else { |
| dayNames = mDayStrs2Letter; |
| } |
| |
| p.setAntiAlias(true); |
| for (int day = 0; day < mNumDays; day++, cell++) { |
| int dayOfWeek = day + mFirstVisibleDayOfWeek; |
| if (dayOfWeek >= 14) { |
| dayOfWeek -= 14; |
| } |
| |
| int color = mCalendarDateBannerTextColor; |
| if (mNumDays == 1) { |
| if (dayOfWeek == Time.SATURDAY) { |
| color = mWeek_saturdayColor; |
| } else if (dayOfWeek == Time.SUNDAY) { |
| color = mWeek_sundayColor; |
| } |
| } else { |
| final int column = day % 7; |
| if (Utils.isSaturday(column, mFirstDayOfWeek)) { |
| color = mWeek_saturdayColor; |
| } else if (Utils.isSunday(column, mFirstDayOfWeek)) { |
| color = mWeek_sundayColor; |
| } |
| } |
| |
| p.setColor(color); |
| drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); |
| } |
| p.setTypeface(null); |
| } |
| |
| private void drawAmPm(Canvas canvas, Paint p) { |
| p.setColor(mCalendarAmPmLabel); |
| p.setTextSize(AMPM_TEXT_SIZE); |
| p.setTypeface(mBold); |
| p.setAntiAlias(true); |
| p.setTextAlign(Paint.Align.RIGHT); |
| String text = mAmString; |
| if (mFirstHour >= 12) { |
| text = mPmString; |
| } |
| int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; |
| canvas.drawText(text, HOURS_LEFT_MARGIN, 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, HOURS_LEFT_MARGIN, y, p); |
| } |
| } |
| |
| private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, |
| Paint p) { |
| r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; |
| r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; |
| |
| r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; |
| r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); |
| |
| mCurrentTimeLine.setBounds(r); |
| mCurrentTimeLine.draw(canvas); |
| if (mAnimateToday) { |
| mCurrentTimeAnimateLine.setBounds(r); |
| mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha); |
| mCurrentTimeAnimateLine.draw(canvas); |
| } |
| } |
| |
| private void doDraw(Canvas canvas) { |
| Paint p = mPaint; |
| Rect r = mRect; |
| |
| if (mFutureBgColor != 0) { |
| drawBgColors(r, canvas, p); |
| } |
| drawGridBackground(r, canvas, p); |
| drawHours(r, canvas, p); |
| |
| // Draw each day |
| int cell = mFirstJulianDay; |
| p.setAntiAlias(false); |
| int alpha = p.getAlpha(); |
| p.setAlpha(mEventsAlpha); |
| for (int day = 0; day < mNumDays; day++, cell++) { |
| // TODO Wow, this needs cleanup. drawEvents loop through all the |
| // events on every call. |
| drawEvents(cell, day, HOUR_GAP, canvas, p); |
| // If this is today |
| if (cell == mTodayJulianDay) { |
| int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) |
| + ((mCurrentTime.minute * mCellHeight) / 60) + 1; |
| |
| // And the current time shows up somewhere on the screen |
| if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { |
| drawCurrentTimeLine(r, day, lineY, canvas, p); |
| } |
| } |
| } |
| p.setAntiAlias(true); |
| p.setAlpha(alpha); |
| |
| drawSelectedRect(r, canvas, p); |
| } |
| |
| private void drawSelectedRect(Rect r, Canvas canvas, Paint p) { |
| // Draw a highlight on the selected hour (if needed) |
| if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { |
| int daynum = mSelectionDay - mFirstJulianDay; |
| r.top = mSelectionHour * (mCellHeight + HOUR_GAP); |
| r.bottom = r.top + mCellHeight + HOUR_GAP; |
| r.left = computeDayLeftPosition(daynum) + 1; |
| r.right = computeDayLeftPosition(daynum + 1) + 1; |
| |
| saveSelectionPosition(r.left, r.top, r.right, r.bottom); |
| |
| // Draw the highlight on the grid |
| p.setColor(mCalendarGridAreaSelected); |
| r.top += HOUR_GAP; |
| r.right -= DAY_GAP; |
| p.setAntiAlias(false); |
| canvas.drawRect(r, p); |
| |
| // Draw a "new event hint" on top of the highlight |
| // For the week view, show a "+", for day view, show "+ New event" |
| p.setColor(mNewEventHintColor); |
| if (mNumDays > 1) { |
| p.setStrokeWidth(NEW_EVENT_WIDTH); |
| int width = r.right - r.left; |
| int midX = r.left + width / 2; |
| int midY = r.top + mCellHeight / 2; |
| int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2; |
| length = Math.min(length, NEW_EVENT_MAX_LENGTH); |
| int verticalPadding = (mCellHeight - length) / 2; |
| int horizontalPadding = (width - length) / 2; |
| canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, |
| midY, p); |
| canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p); |
| } else { |
| p.setStyle(Paint.Style.FILL); |
| p.setTextSize(NEW_EVENT_HINT_FONT_SIZE); |
| p.setTextAlign(Paint.Align.LEFT); |
| p.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); |
| canvas.drawText(mNewEventHintString, r.left + EVENT_TEXT_LEFT_MARGIN, |
| r.top + Math.abs(p.getFontMetrics().ascent) + EVENT_TEXT_TOP_MARGIN , p); |
| } |
| } |
| } |
| |
| private void drawHours(Rect r, Canvas canvas, Paint p) { |
| setupHourTextPaint(p); |
| |
| int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; |
| |
| for (int i = 0; i < 24; i++) { |
| String time = mHourStrs[i]; |
| canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); |
| y += mCellHeight + HOUR_GAP; |
| } |
| } |
| |
| private void setupHourTextPaint(Paint p) { |
| p.setColor(mCalendarHourLabelColor); |
| p.setTextSize(HOURS_TEXT_SIZE); |
| p.setTypeface(Typeface.DEFAULT); |
| p.setTextAlign(Paint.Align.RIGHT); |
| p.setAntiAlias(true); |
| } |
| |
| private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { |
| int dateNum = mFirstVisibleDate + day; |
| int x; |
| if (dateNum > mMonthLength) { |
| dateNum -= mMonthLength; |
| } |
| p.setAntiAlias(true); |
| |
| int todayIndex = mTodayJulianDay - mFirstJulianDay; |
| // Draw day of the month |
| String dateNumStr = String.valueOf(dateNum); |
| if (mNumDays > 1) { |
| float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; |
| |
| // Draw day of the month |
| x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; |
| p.setTextAlign(Align.RIGHT); |
| p.setTextSize(DATE_HEADER_FONT_SIZE); |
| |
| p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); |
| canvas.drawText(dateNumStr, x, y, p); |
| |
| // Draw day of the week |
| x -= p.measureText(" " + dateNumStr); |
| p.setTextSize(DAY_HEADER_FONT_SIZE); |
| p.setTypeface(Typeface.DEFAULT); |
| canvas.drawText(dayStr, x, y, p); |
| } else { |
| float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; |
| p.setTextAlign(Align.LEFT); |
| |
| |
| // Draw day of the week |
| x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; |
| p.setTextSize(DAY_HEADER_FONT_SIZE); |
| p.setTypeface(Typeface.DEFAULT); |
| canvas.drawText(dayStr, x, y, p); |
| |
| // Draw day of the month |
| x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; |
| p.setTextSize(DATE_HEADER_FONT_SIZE); |
| p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); |
| canvas.drawText(dateNumStr, x, y, p); |
| } |
| } |
| |
| private void drawGridBackground(Rect r, Canvas canvas, Paint p) { |
| Paint.Style savedStyle = p.getStyle(); |
| |
| final float stopX = computeDayLeftPosition(mNumDays); |
| float y = 0; |
| final float deltaY = mCellHeight + HOUR_GAP; |
| int linesIndex = 0; |
| final float startY = 0; |
| final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); |
| float x = mHoursWidth; |
| |
| // Draw the inner horizontal grid lines |
| p.setColor(mCalendarGridLineInnerHorizontalColor); |
| p.setStrokeWidth(GRID_LINE_INNER_WIDTH); |
| p.setAntiAlias(false); |
| y = 0; |
| linesIndex = 0; |
| for (int hour = 0; hour <= 24; hour++) { |
| mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; |
| mLines[linesIndex++] = y; |
| mLines[linesIndex++] = stopX; |
| mLines[linesIndex++] = y; |
| y += deltaY; |
| } |
| if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { |
| canvas.drawLines(mLines, 0, linesIndex, p); |
| linesIndex = 0; |
| p.setColor(mCalendarGridLineInnerVerticalColor); |
| } |
| |
| // Draw the inner vertical grid lines |
| for (int day = 0; day <= mNumDays; day++) { |
| x = computeDayLeftPosition(day); |
| mLines[linesIndex++] = x; |
| mLines[linesIndex++] = startY; |
| mLines[linesIndex++] = x; |
| mLines[linesIndex++] = stopY; |
| } |
| canvas.drawLines(mLines, 0, linesIndex, p); |
| |
| // Restore the saved style. |
| p.setStyle(savedStyle); |
| p.setAntiAlias(true); |
| } |
| |
| /** |
| * @param r |
| * @param canvas |
| * @param p |
| */ |
| private void drawBgColors(Rect r, Canvas canvas, Paint p) { |
| int todayIndex = mTodayJulianDay - mFirstJulianDay; |
| // Draw the hours background color |
| r.top = mDestRect.top; |
| r.bottom = mDestRect.bottom; |
| r.left = 0; |
| r.right = mHoursWidth; |
| p.setColor(mBgColor); |
| p.setStyle(Style.FILL); |
| p.setAntiAlias(false); |
| canvas.drawRect(r, p); |
| |
| // Draw background for grid area |
| if (mNumDays == 1 && todayIndex == 0) { |
| // Draw a white background for the time later than current time |
| int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) |
| + ((mCurrentTime.minute * mCellHeight) / 60) + 1; |
| if (lineY < mViewStartY + mViewHeight) { |
| lineY = Math.max(lineY, mViewStartY); |
| r.left = mHoursWidth; |
| r.right = mViewWidth; |
| r.top = lineY; |
| r.bottom = mViewStartY + mViewHeight; |
| p.setColor(mFutureBgColor); |
| canvas.drawRect(r, p); |
| } |
| } else if (todayIndex >= 0 && todayIndex < mNumDays) { |
| // Draw today with a white background for the time later than current time |
| int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) |
| + ((mCurrentTime.minute * mCellHeight) / 60) + 1; |
| if (lineY < mViewStartY + mViewHeight) { |
| lineY = Math.max(lineY, mViewStartY); |
| r.left = computeDayLeftPosition(todayIndex) + 1; |
| r.right = computeDayLeftPosition(todayIndex + 1); |
| r.top = lineY; |
| r.bottom = mViewStartY + mViewHeight; |
| p.setColor(mFutureBgColor); |
| canvas.drawRect(r, p); |
| } |
| |
| // Paint Tomorrow and later days with future color |
| if (todayIndex + 1 < mNumDays) { |
| r.left = computeDayLeftPosition(todayIndex + 1) + 1; |
| r.right = computeDayLeftPosition(mNumDays); |
| r.top = mDestRect.top; |
| r.bottom = mDestRect.bottom; |
| p.setColor(mFutureBgColor); |
| canvas.drawRect(r, p); |
| } |
| } else if (todayIndex < 0) { |
| // Future |
| r.left = computeDayLeftPosition(0) + 1; |
| r.right = computeDayLeftPosition(mNumDays); |
| r.top = mDestRect.top; |
| r.bottom = mDestRect.bottom; |
| p.setColor(mFutureBgColor); |
| canvas.drawRect(r, p); |
| } |
| 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 = computeDayLeftPosition(daynum) + 1; |
| box.right = computeDayLeftPosition(daynum + 1); |
| return box; |
| } |
| |
| private void setupTextRect(Rect r) { |
| if (r.bottom <= r.top || r.right <= r.left) { |
| r.bottom = r.top; |
| r.right = r.left; |
| return; |
| } |
| |
| if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { |
| r.top += EVENT_TEXT_TOP_MARGIN; |
| r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; |
| } |
| if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { |
| r.left += EVENT_TEXT_LEFT_MARGIN; |
| r.right -= EVENT_TEXT_RIGHT_MARGIN; |
| } |
| } |
| |
| private void setupAllDayTextRect(Rect r) { |
| if (r.bottom <= r.top || r.right <= r.left) { |
| r.bottom = r.top; |
| r.right = r.left; |
| return; |
| } |
| |
| if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { |
| r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; |
| r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; |
| } |
| if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { |
| r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; |
| r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; |
| } |
| } |
| |
| /** |
| * Return the layout for a numbered event. Create it if not already existing |
| */ |
| private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, |
| Rect r) { |
| if (i < 0 || i >= layouts.length) { |
| return null; |
| } |
| |
| StaticLayout layout = layouts[i]; |
| // Check if we have already initialized the StaticLayout and that |
| // the width hasn't changed (due to vertical resizing which causes |
| // re-layout of events at min height) |
| if (layout == null || r.width() != layout.getWidth()) { |
| SpannableStringBuilder bob = new SpannableStringBuilder(); |
| if (event.title != null) { |
| // MAX - 1 since we add a space |
| bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1)); |
| bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); |
| bob.append(' '); |
| } |
| if (event.location != null) { |
| bob.append(drawTextSanitizer(event.location.toString(), |
| MAX_EVENT_TEXT_LEN - bob.length())); |
| } |
| |
| switch (event.selfAttendeeStatus) { |
| case Attendees.ATTENDEE_STATUS_INVITED: |
| paint.setColor(event.color); |
| break; |
| case Attendees.ATTENDEE_STATUS_DECLINED: |
| paint.setColor(mEventTextColor); |
| paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA); |
| break; |
| case Attendees.ATTENDEE_STATUS_NONE: // Your own events |
| case Attendees.ATTENDEE_STATUS_ACCEPTED: |
| case Attendees.ATTENDEE_STATUS_TENTATIVE: |
| default: |
| paint.setColor(mEventTextColor); |
| break; |
| } |
| |
| // Leave a one pixel boundary on the left and right of the rectangle for the event |
| layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), |
| Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); |
| |
| layouts[i] = layout; |
| } |
| layout.getPaint().setAlpha(mEventsAlpha); |
| return layout; |
| } |
| |
| private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { |
| |
| p.setTextSize(NORMAL_FONT_SIZE); |
| p.setTextAlign(Paint.Align.LEFT); |
| Paint eventTextPaint = mEventTextPaint; |
| |
| final float startY = DAY_HEADER_HEIGHT; |
| final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; |
| float x = 0; |
| int linesIndex = 0; |
| |
| // Draw the inner vertical grid lines |
| p.setColor(mCalendarGridLineInnerVerticalColor); |
| x = mHoursWidth; |
| p.setStrokeWidth(GRID_LINE_INNER_WIDTH); |
| // Line bounding the top of the all day area |
| mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; |
| mLines[linesIndex++] = startY; |
| mLines[linesIndex++] = computeDayLeftPosition(mNumDays); |
| mLines[linesIndex++] = startY; |
| |
| for (int day = 0; day <= mNumDays; day++) { |
| x = computeDayLeftPosition(day); |
| mLines[linesIndex++] = x; |
| mLines[linesIndex++] = startY; |
| mLines[linesIndex++] = x; |
| mLines[linesIndex++] = stopY; |
| } |
| p.setAntiAlias(false); |
| canvas.drawLines(mLines, 0, linesIndex, p); |
| p.setStyle(Style.FILL); |
| |
| int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; |
| int lastDay = firstDay + numDays - 1; |
| final ArrayList<Event> events = mAllDayEvents; |
| int numEvents = events.size(); |
| // Whether or not we should draw the more events text |
| boolean hasMoreEvents = false; |
| // size of the allDay area |
| float drawHeight = mAlldayHeight; |
| // max number of events being drawn in one day of the allday area |
| float numRectangles = mMaxAlldayEvents; |
| // Where to cut off drawn allday events |
| int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; |
| // The number of events that weren't drawn in each day |
| mSkippedAlldayEvents = new int[numDays]; |
| if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && |
| mAnimateDayHeight == 0) { |
| // We draw one fewer event than will fit so that more events text |
| // can be drawn |
| numRectangles = mMaxUnexpandedAlldayEventCount - 1; |
| // We also clip the events above the more events text |
| allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; |
| hasMoreEvents = true; |
| } else if (mAnimateDayHeight != 0) { |
| // clip at the end of the animating space |
| allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; |
| } |
| |
| int alpha = eventTextPaint.getAlpha(); |
| eventTextPaint.setAlpha(mEventsAlpha); |
| for (int i = 0; i < numEvents; i++) { |
| Event event = events.get(i); |
| 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 = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : |
| drawHeight / numRectangles; |
| |
| // Prevent a single event from getting too big |
| if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { |
| height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; |
| } |
| |
| // Leave a one-pixel space between the vertical day lines and the |
| // event rectangle. |
| event.left = computeDayLeftPosition(startIndex); |
| event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; |
| event.top = y + height * event.getColumn(); |
| event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; |
| if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { |
| // check if we should skip this event. We skip if it starts |
| // after the clip bound or ends after the skip bound and we're |
| // not animating. |
| if (event.top >= allDayEventClip) { |
| incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); |
| continue; |
| } else if (event.bottom > allDayEventClip) { |
| if (hasMoreEvents) { |
| incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); |
| continue; |
| } |
| event.bottom = allDayEventClip; |
| } |
| } |
| Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, |
| (int) event.bottom); |
| setupAllDayTextRect(r); |
| StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); |
| drawEventText(layout, r, canvas, r.top, r.bottom, true); |
| |
| // Check if this all-day event intersects the selected day |
| if (mSelectionAllday && mComputeSelectedEvents) { |
| if (startDay <= mSelectionDay && endDay >= mSelectionDay) { |
| mSelectedEvents.add(event); |
| } |
| } |
| } |
| eventTextPaint.setAlpha(alpha); |
| |
| if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { |
| // If the more allday text should be visible, draw it. |
| alpha = p.getAlpha(); |
| p.setAlpha(mEventsAlpha); |
| p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); |
| for (int i = 0; i < mSkippedAlldayEvents.length; i++) { |
| if (mSkippedAlldayEvents[i] > 0) { |
| drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); |
| } |
| } |
| p.setAlpha(alpha); |
| } |
| |
| if (mSelectionAllday) { |
| // Compute the neighbors for the list of all-day events that |
| // intersect the selected day. |
| computeAllDayNeighbors(); |
| |
| // 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); |
| } |
| } |
| |
| // Helper method for counting the number of allday events skipped on each day |
| private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { |
| if (counts == null || startIndex < 0 || endIndex > counts.length) { |
| return; |
| } |
| for (int i = startIndex; i <= endIndex; i++) { |
| counts[i]++; |
| } |
| } |
| |
| // Draws the "box +n" text for hidden allday events |
| protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { |
| int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; |
| int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f |
| * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); |
| Rect r = mRect; |
| r.top = y; |
| r.left = x; |
| r.bottom = y + EVENT_SQUARE_WIDTH; |
| r.right = x + EVENT_SQUARE_WIDTH; |
| p.setColor(mMoreEventsTextColor); |
| p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); |
| p.setStyle(Style.STROKE); |
| p.setAntiAlias(false); |
| canvas.drawRect(r, p); |
| p.setAntiAlias(true); |
| p.setStyle(Style.FILL); |
| p.setTextSize(EVENT_TEXT_FONT_SIZE); |
| String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); |
| y += EVENT_SQUARE_WIDTH; |
| x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; |
| canvas.drawText(String.format(text, remainingEvents), x, y, p); |
| } |
| |
| 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.drawAsAllday()) { |
| 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) { |
| setSelectedEvent(startEvent); |
| } else { |
| setSelectedEvent(maxPositionEvent); |
| } |
| } |
| |
| private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { |
| Paint eventTextPaint = mEventTextPaint; |
| int left = computeDayLeftPosition(dayIndex) + 1; |
| int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; |
| int cellHeight = mCellHeight; |
| |
| // Use the selected hour as the selection region |
| Rect selectionArea = mSelectionRect; |
| selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); |
| selectionArea.bottom = selectionArea.top + cellHeight; |
| selectionArea.left = left; |
| selectionArea.right = selectionArea.left + cellWidth; |
| |
| final ArrayList<Event> events = mEvents; |
| int numEvents = events.size(); |
| EventGeometry geometry = mEventGeometry; |
| |
| final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; |
| |
| int alpha = eventTextPaint.getAlpha(); |
| eventTextPaint.setAlpha(mEventsAlpha); |
| for (int i = 0; i < numEvents; i++) { |
| Event event = events.get(i); |
| if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { |
| continue; |
| } |
| |
| // Don't draw it if it is not visible |
| if (event.bottom < mViewStartY || event.top > viewEndY) { |
| continue; |
| } |
| |
| if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents |
| && geometry.eventIntersectsSelection(event, selectionArea)) { |
| mSelectedEvents.add(event); |
| } |
| |
| Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); |
| setupTextRect(r); |
| |
| // Don't draw text if it is not visible |
| if (r.top > viewEndY || r.bottom < mViewStartY) { |
| continue; |
| } |
| StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); |
| // TODO: not sure why we are 4 pixels off |
| drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight |
| - DAY_HEADER_HEIGHT - mAlldayHeight, false); |
| } |
| eventTextPaint.setAlpha(alpha); |
| |
| if (date == mSelectionDay && !mSelectionAllday && isFocused() |
| && mSelectionMode != SELECTION_HIDDEN) { |
| computeNeighbors(); |
| } |
| } |
| |
| // 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(mContext, 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; |
| } |
| setSelectedEvent(startEvent); |
| } |
| |
| private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, |
| int visibleTop, int visibleBot) { |
| // Draw the Event Rect |
| Rect r = mRect; |
| r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); |
| r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); |
| r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; |
| r.right = (int) event.right; |
| |
| int color; |
| if (event == mClickedEvent) { |
| color = mClickedColor; |
| } else { |
| color = event.color; |
| } |
| |
| switch (event.selfAttendeeStatus) { |
| case Attendees.ATTENDEE_STATUS_INVITED: |
| if (event != mClickedEvent) { |
| p.setStyle(Style.STROKE); |
| } |
| break; |
| case Attendees.ATTENDEE_STATUS_DECLINED: |
| if (event != mClickedEvent) { |
| color = Utils.getDeclinedColorFromColor(color); |
| } |
| case Attendees.ATTENDEE_STATUS_NONE: // Your own events |
| case Attendees.ATTENDEE_STATUS_ACCEPTED: |
| case Attendees.ATTENDEE_STATUS_TENTATIVE: |
| default: |
| p.setStyle(Style.FILL_AND_STROKE); |
| break; |
| } |
| |
| p.setAntiAlias(false); |
| |
| int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); |
| int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); |
| r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); |
| r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, |
| visibleBot); |
| r.left += floorHalfStroke; |
| r.right -= ceilHalfStroke; |
| p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); |
| p.setColor(color); |
| int alpha = p.getAlpha(); |
| p.setAlpha(mEventsAlpha); |
| canvas.drawRect(r, p); |
| p.setAlpha(alpha); |
| p.setStyle(Style.FILL); |
| |
| // If this event is selected, then use the selection color |
| if (mSelectedEvent == event && mClickedEvent != null) { |
| boolean paintIt = false; |
| color = 0; |
| if (mSelectionMode == SELECTION_PRESSED) { |
| // Also, remember the last selected event that we drew |
| mPrevSelectedEvent = event; |
| color = mPressedColor; |
| paintIt = true; |
| } else if (mSelectionMode == SELECTION_SELECTED) { |
| // Also, remember the last selected event that we drew |
| mPrevSelectedEvent = event; |
| color = mPressedColor; |
| paintIt = true; |
| } |
| |
| if (paintIt) { |
| p.setColor(color); |
| canvas.drawRect(r, p); |
| } |
| p.setAntiAlias(true); |
| } |
| |
| // Draw cal color square border |
| // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; |
| // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; |
| // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; |
| // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; |
| // p.setColor(0xFFFFFFFF); |
| // canvas.drawRect(r, p); |
| |
| // Draw cal color |
| // r.top++; |
| // r.left++; |
| // r.bottom--; |
| // r.right--; |
| // p.setColor(event.color); |
| // canvas.drawRect(r, p); |
| |
| // Setup rect for drawEventText which follows |
| r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; |
| r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; |
| r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; |
| r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; |
| return r; |
| } |
| |
| private final 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, int maxEventTextLen) { |
| Matcher m = drawTextSanitizerFilter.matcher(string); |
| string = m.replaceAll(","); |
| |
| int len = string.length(); |
| if (maxEventTextLen <= 0) { |
| string = ""; |
| len = 0; |
| } else if (len > maxEventTextLen) { |
| string = string.substring(0, maxEventTextLen); |
| len = maxEventTextLen; |
| } |
| |
| return string.replace('\n', ' '); |
| } |
| |
| private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, |
| int bottom, boolean center) { |
| // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging |
| |
| int width = rect.right - rect.left; |
| int height = rect.bottom - rect.top; |
| |
| // If the rectangle is too small for text, then return |
| if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { |
| return; |
| } |
| |
| int totalLineHeight = 0; |
| int lineCount = eventLayout.getLineCount(); |
| for (int i = 0; i < lineCount; i++) { |
| int lineBottom = eventLayout.getLineBottom(i); |
| if (lineBottom <= height) { |
| totalLineHeight = lineBottom; |
| } else { |
| break; |
| } |
| } |
| |
| if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { |
| return; |
| } |
| |
| // Use a StaticLayout to format the string. |
| canvas.save(); |
| // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); |
| int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0; |
| canvas.translate(rect.left, rect.top + padding); |
| rect.left = 0; |
| rect.right = width; |
| rect.top = 0; |
| rect.bottom = totalLineHeight; |
| |
| // There's a bug somewhere. If this rect is outside of a previous |
| // cliprect, this becomes a no-op. What happens is that the text draw |
| // past the event rect. The current fix is to not draw the staticLayout |
| // at all if it is completely out of bound. |
| canvas.clipRect(rect); |
| eventLayout.draw(canvas); |
| canvas.restore(); |
| } |
| |
| // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it |
| // doesn't work well with hardware acceleration |
| // private void drawEmptyRect(Canvas canvas, Rect r, int color) { |
| // int linesIndex = 0; |
| // mLines[linesIndex++] = r.left; |
| // mLines[linesIndex++] = r.top; |
| // mLines[linesIndex++] = r.right; |
| // mLines[linesIndex++] = r.top; |
| // |
| // mLines[linesIndex++] = r.left; |
| // mLines[linesIndex++] = r.bottom; |
| // mLines[linesIndex++] = r.right; |
| // mLines[linesIndex++] = r.bottom; |
| // |
| // mLines[linesIndex++] = r.left; |
| // mLines[linesIndex++] = r.top; |
| // mLines[linesIndex++] = r.left; |
| // mLines[linesIndex++] = r.bottom; |
| // |
| // mLines[linesIndex++] = r.right; |
| // mLines[linesIndex++] = r.top; |
| // mLines[linesIndex++] = r.right; |
| // mLines[linesIndex++] = r.bottom; |
| // mPaint.setColor(color); |
| // canvas.drawLines(mLines, 0, linesIndex, mPaint); |
| // } |
| |
| private void updateEventDetails() { |
| if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN |
| || mSelectionMode == SELECTION_LONGPRESS) { |
| mPopup.dismiss(); |
| return; |
| } |
| if (mLastPopupEventID == mSelectedEvent.id) { |
| return; |
| } |
| |
| mLastPopupEventID = mSelectedEvent.id; |
| |
| // Remove any outstanding callbacks to dismiss the popup. |
| mHandler.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 = Utils.formatDateRange(mContext, 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); |
| mHandler.postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); |
| } |
| |
| // The following routines are called from the parent activity when certain |
| // touch events occur. |
| private void doDown(MotionEvent ev) { |
| mTouchMode = TOUCH_MODE_DOWN; |
| mViewStartX = 0; |
| mOnFlingCalled = false; |
| mHandler.removeCallbacks(mContinueScroll); |
| int x = (int) ev.getX(); |
| int y = (int) ev.getY(); |
| |
| // Save selection information: we use setSelectionFromPosition to find the selected event |
| // in order to show the "clicked" color. But since it is also setting the selected info |
| // for new events, we need to restore the old info after calling the function. |
| Event oldSelectedEvent = mSelectedEvent; |
| int oldSelectionDay = mSelectionDay; |
| int oldSelectionHour = mSelectionHour; |
| if (setSelectionFromPosition(x, y, false)) { |
| // If a time was selected (a blue selection box is visible) and the click location |
| // is in the selected time, do not show a click on an event to prevent a situation |
| // of both a selection and an event are clicked when they overlap. |
| boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN) |
| && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour; |
| if (!pressedSelected && mSelectedEvent != null) { |
| mSavedClickedEvent = mSelectedEvent; |
| mDownTouchTime = System.currentTimeMillis(); |
| postDelayed (mSetClick,mOnDownDelay); |
| } else { |
| eventClickCleanup(); |
| } |
| } |
| mSelectedEvent = oldSelectedEvent; |
| mSelectionDay = oldSelectionDay; |
| mSelectionHour = oldSelectionHour; |
| invalidate(); |
| } |
| |
| // Kicks off all the animations when the expand allday area is tapped |
| private void doExpandAllDayClick() { |
| mShowAllAllDayEvents = !mShowAllAllDayEvents; |
| |
| ObjectAnimator.setFrameDelay(0); |
| |
| // Determine the starting height |
| if (mAnimateDayHeight == 0) { |
| mAnimateDayHeight = mShowAllAllDayEvents ? |
| mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; |
| } |
| // Cancel current animations |
| mCancellingAnimations = true; |
| if (mAlldayAnimator != null) { |
| mAlldayAnimator.cancel(); |
| } |
| if (mAlldayEventAnimator != null) { |
| mAlldayEventAnimator.cancel(); |
| } |
| if (mMoreAlldayEventsAnimator != null) { |
| mMoreAlldayEventsAnimator.cancel(); |
| } |
| mCancellingAnimations = false; |
| // get new animators |
| mAlldayAnimator = getAllDayAnimator(); |
| mAlldayEventAnimator = getAllDayEventAnimator(); |
| mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, |
| "moreAllDayEventsTextAlpha", |
| mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, |
| mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); |
| |
| // Set up delays and start the animators |
| mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); |
| mAlldayAnimator.start(); |
| mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); |
| mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); |
| mMoreAlldayEventsAnimator.start(); |
| if (mAlldayEventAnimator != null) { |
| // This is the only animator that can return null, so check it |
| mAlldayEventAnimator |
| .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); |
| mAlldayEventAnimator.start(); |
| } |
| } |
| |
| /** |
| * Figures out the initial heights for allDay events and space when |
| * a view is being set up. |
| */ |
| public void initAllDayHeights() { |
| if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { |
| return; |
| } |
| if (mShowAllAllDayEvents) { |
| int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; |
| maxADHeight = Math.min(maxADHeight, |
| (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); |
| mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; |
| } else { |
| mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; |
| } |
| } |
| |
| // Sets up an animator for changing the height of allday events |
| private ObjectAnimator getAllDayEventAnimator() { |
| // First calculate the absolute max height |
| int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; |
| // Now expand to fit but not beyond the absolute max |
| maxADHeight = |
| Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); |
| // calculate the height of individual events in order to fit |
| int fitHeight = maxADHeight / mMaxAlldayEvents; |
| int currentHeight = mAnimateDayEventHeight; |
| int desiredHeight = |
| mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; |
| // if there's nothing to animate just return |
| if (currentHeight == desiredHeight) { |
| return null; |
| } |
| |
| // Set up the animator with the calculated values |
| ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", |
| currentHeight, desiredHeight); |
| animator.setDuration(ANIMATION_DURATION); |
| return animator; |
| } |
| |
| // Sets up an animator for changing the height of the allday area |
| private ObjectAnimator getAllDayAnimator() { |
| // Calculate the absolute max height |
| int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; |
| // Find the desired height but don't exceed abs max |
| maxADHeight = |
| Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); |
| // calculate the current and desired heights |
| int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; |
| int desiredHeight = mShowAllAllDayEvents ? maxADHeight : |
| (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); |
| |
| // Set up the animator with the calculated values |
| ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", |
| currentHeight, desiredHeight); |
| animator.setDuration(ANIMATION_DURATION); |
| |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (!mCancellingAnimations) { |
| // when finished, set this to 0 to signify not animating |
| mAnimateDayHeight = 0; |
| mUseExpandIcon = !mShowAllAllDayEvents; |
| } |
| mRemeasure = true; |
| invalidate(); |
| } |
| }); |
| return animator; |
| } |
| |
| // setter for the 'box +n' alpha text used by the animator |
| public void setMoreAllDayEventsTextAlpha(int alpha) { |
| mMoreAlldayEventsTextAlpha = alpha; |
| invalidate(); |
| } |
| |
| // setter for the height of the allday area used by the animator |
| public void setAnimateDayHeight(int height) { |
| mAnimateDayHeight = height; |
| mRemeasure = true; |
| invalidate(); |
| } |
| |
| // setter for the height of allday events used by the animator |
| public void setAnimateDayEventHeight(int height) { |
| mAnimateDayEventHeight = height; |
| mRemeasure = true; |
| invalidate(); |
| } |
| |
| private void doSingleTapUp(MotionEvent ev) { |
| if (!mHandleActionUp || mScrolling) { |
| return; |
| } |
| |
| int x = (int) ev.getX(); |
| int y = (int) ev.getY(); |
| int selectedDay = mSelectionDay; |
| int selectedHour = mSelectionHour; |
| |
| if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { |
| // check if the tap was in the allday expansion area |
| int bottom = mFirstCell; |
| if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) |
| || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && |
| y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { |
| doExpandAllDayClick(); |
| return; |
| } |
| } |
| |
| boolean validPosition = setSelectionFromPosition(x, y, false); |
| if (!validPosition) { |
| if (y < DAY_HEADER_HEIGHT) { |
| Time selectedTime = new Time(mBaseDate); |
| selectedTime.setJulianDay(mSelectionDay); |
| selectedTime.hour = mSelectionHour; |
| selectedTime.normalize(true /* ignore isDst */); |
| mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, |
| ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); |
| } |
| return; |
| } |
| |
| boolean hasSelection = mSelectionMode != SELECTION_HIDDEN; |
| boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) |
| && selectedDay == mSelectionDay && selectedHour == mSelectionHour; |
| |
| if (pressedSelected && mSavedClickedEvent == null) { |
| // If the tap is on an already selected hour slot, then create a new |
| // event |
| long extraLong = 0; |
| if (mSelectionAllday) { |
| extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; |
| } |
| mSelectionMode = SELECTION_SELECTED; |
| mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, |
| getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), |
| extraLong, -1); |
| } else if (mSelectedEvent != null) { |
| // If the tap is on an event, launch the "View event" view |
| if (mIsAccessibilityEnabled) { |
| mAccessibilityMgr.interrupt(); |
| } |
| |
| mSelectionMode = SELECTION_HIDDEN; |
| |
| int yLocation = |
| (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2); |
| // Y location is affected by the position of the event in the scrolling |
| // view (mViewStartY) and the presence of all day events (mFirstCell) |
| if (!mSelectedEvent.allDay) { |
| yLocation += (mFirstCell - mViewStartY); |
| } |
| mClickedYLocation = yLocation; |
| long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) - |
| (System.currentTimeMillis() - mDownTouchTime); |
| if (clearDelay > 0) { |
| this.postDelayed(mClearClick, clearDelay); |
| } else { |
| this.post(mClearClick); |
| } |
| } else { |
| // Select time |
| Time startTime = new Time(mBaseDate); |
| startTime.setJulianDay(mSelectionDay); |
| startTime.hour = mSelectionHour; |
| startTime.normalize(true /* ignore isDst */); |
| |
| Time endTime = new Time(startTime); |
| endTime.hour++; |
| |
| mSelectionMode = SELECTION_SELECTED; |
| mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, |
| CalendarController.EXTRA_GOTO_TIME, null, null); |
| } |
| invalidate(); |
| } |
| |
| private void doLongPress(MotionEvent ev) { |
| eventClickCleanup(); |
| if (mScrolling) { |
| return; |
| } |
| |
| // Scale gesture in progress |
| if (mStartingSpanY != 0) { |
| return; |
| } |
| |
| int x = (int) ev.getX(); |
| int y = (int) ev.getY(); |
| |
| boolean validPosition = setSelectionFromPosition(x, y, false); |
| if (!validPosition) { |
| // return if the touch wasn't on an area of concern |
| return; |
| } |
| |
| mSelectionMode = SELECTION_LONGPRESS; |
| invalidate(); |
| performLongClick(); |
| } |
| |
| private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { |
| cancelAnimation(); |
| if (mStartingScroll) { |
| mInitialScrollX = 0; |
| mInitialScrollY = 0; |
| mStartingScroll = false; |
| } |
| |
| mInitialScrollX += deltaX; |
| mInitialScrollY += deltaY; |
| int distanceX = (int) mInitialScrollX; |
| int distanceY = (int) mInitialScrollY; |
| |
| final float focusY = getAverageY(e2); |
| if (mRecalCenterHour) { |
| // Calculate the hour that correspond to the average of the Y touch points |
| mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) |
| / (mCellHeight + DAY_GAP); |
| mRecalCenterHour = false; |
| } |
| |
| // 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; |
| mPreviousDirection = 0; |
| |
| if (absDistanceX > absDistanceY) { |
| int slopFactor = mScaleGestureDetector.isInProgress() ? 20 : 2; |
| if (absDistanceX > mScaledPagingTouchSlop * slopFactor) { |
| 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 ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { |
| // Calculate the top of the visible region in the calendar grid. |
| // Increasing/decrease this will scroll the calendar grid up/down. |
| mViewStartY = (int) ((mGestureCenterHour * (mCellHeight + DAY_GAP)) |
| - focusY + DAY_HEADER_HEIGHT + mAlldayHeight); |
| |
| // If dragging while already at the end, do a glow |
| final int pulledToY = (int) (mScrollStartY + deltaY); |
| if (pulledToY < 0) { |
| mEdgeEffectTop.onPull(deltaY / mViewHeight); |
| if (!mEdgeEffectBottom.isFinished()) { |
| mEdgeEffectBottom.onRelease(); |
| } |
| } else if (pulledToY > mMaxViewStartY) { |
| mEdgeEffectBottom.onPull(deltaY / mViewHeight); |
| if (!mEdgeEffectTop.isFinished()) { |
| mEdgeEffectTop.onRelease(); |
| } |
| } |
| |
| if (mViewStartY < 0) { |
| mViewStartY = 0; |
| mRecalCenterHour = true; |
| } else if (mViewStartY > mMaxViewStartY) { |
| mViewStartY = mMaxViewStartY; |
| mRecalCenterHour = true; |
| } |
| if (mRecalCenterHour) { |
| // Calculate the hour that correspond to the average of the Y touch points |
| mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) |
| / (mCellHeight + DAY_GAP); |
| mRecalCenterHour = false; |
| } |
| computeFirstHour(); |
| } |
| |
| mScrolling = true; |
| |
| mSelectionMode = SELECTION_HIDDEN; |
| invalidate(); |
| } |
| |
| private float getAverageY(MotionEvent me) { |
| int count = me.getPointerCount(); |
| float focusY = 0; |
| for (int i = 0; i < count; i++) { |
| focusY += me.getY(i); |
| } |
| focusY /= count; |
| return focusY; |
| } |
| |
| private void cancelAnimation() { |
| Animation in = mViewSwitcher.getInAnimation(); |
| if (in != null) { |
| // cancel() doesn't terminate cleanly. |
| in.scaleCurrentDuration(0); |
| } |
| Animation out = mViewSwitcher.getOutAnimation(); |
| if (out != null) { |
| // cancel() doesn't terminate cleanly. |
| out.scaleCurrentDuration(0); |
| } |
| } |
| |
| private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
| cancelAnimation(); |
| |
| mSelectionMode = SELECTION_HIDDEN; |
| eventClickCleanup(); |
| |
| mOnFlingCalled = true; |
| |
| if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { |
| // Horizontal fling. |
| // initNextView(deltaX); |
| mTouchMode = TOUCH_MODE_INITIAL_STATE; |
| if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); |
| int deltaX = (int) e2.getX() - (int) e1.getX(); |
| switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); |
| mViewStartX = 0; |
| return; |
| } |
| |
| if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { |
| if (DEBUG) Log.d(TAG, "doFling: no fling"); |
| return; |
| } |
| |
| // Vertical fling. |
| mTouchMode = TOUCH_MODE_INITIAL_STATE; |
| mViewStartX = 0; |
| |
| if (DEBUG) { |
| Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); |
| } |
| |
| // Continue scrolling vertically |
| mScrolling = true; |
| mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, |
| (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, |
| mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); |
| |
| // When flinging down, show a glow when it hits the end only if it |
| // wasn't started at the top |
| if (velocityY > 0 && mViewStartY != 0) { |
| mCallEdgeEffectOnAbsorb = true; |
| } |
| // When flinging up, show a glow when it hits the end only if it wasn't |
| // started at the bottom |
| else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { |
| mCallEdgeEffectOnAbsorb = true; |
| } |
| mHandler.post(mContinueScroll); |
| } |
| |
| private boolean initNextView(int deltaX) { |
| // Change the view to the previous day or week |
| DayView view = (DayView) mViewSwitcher.getNextView(); |
| Time date = view.mBaseDate; |
| date.set(mBaseDate); |
| boolean switchForward; |
| if (deltaX > 0) { |
| date.monthDay -= mNumDays; |
| view.setSelectedDay(mSelectionDay - mNumDays); |
| switchForward = false; |
| } else { |
| date.monthDay += mNumDays; |
| view.setSelectedDay(mSelectionDay + mNumDays); |
| switchForward = true; |
| } |
| date.normalize(true /* ignore isDst */); |
| initView(view); |
| view.layout(getLeft(), getTop(), getRight(), getBottom()); |
| view.reloadEvents(); |
| return switchForward; |
| } |
| |
| // ScaleGestureDetector.OnScaleGestureListener |
| public boolean onScaleBegin(ScaleGestureDetector detector) { |
| mHandleActionUp = false; |
| float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; |
| mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); |
| |
| mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); |
| mCellHeightBeforeScaleGesture = mCellHeight; |
| |
| if (DEBUG_SCALING) { |
| float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); |
| Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour |
| + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY |
| + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); |
| } |
| |
| return true; |
| } |
| |
| // ScaleGestureDetector.OnScaleGestureListener |
| public boolean onScale(ScaleGestureDetector detector) { |
| float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); |
| |
| mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); |
| |
| if (mCellHeight < mMinCellHeight) { |
| // If mStartingSpanY is too small, even a small increase in the |
| // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT |
| mStartingSpanY = spanY; |
| mCellHeight = mMinCellHeight; |
| mCellHeightBeforeScaleGesture = mMinCellHeight; |
| } else if (mCellHeight > MAX_CELL_HEIGHT) { |
| mStartingSpanY = spanY; |
| mCellHeight = MAX_CELL_HEIGHT; |
| mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; |
| } |
| |
| int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; |
| mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; |
| mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; |
| |
| if (DEBUG_SCALING) { |
| float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); |
| Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " |
| + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" |
| + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); |
| } |
| |
| if (mViewStartY < 0) { |
| mViewStartY = 0; |
| mGestureCenterHour = (mViewStartY + gestureCenterInPixels) |
| / (float) (mCellHeight + DAY_GAP); |
| } else if (mViewStartY > mMaxViewStartY) { |
| mViewStartY = mMaxViewStartY; |
| mGestureCenterHour = (mViewStartY + gestureCenterInPixels) |
| / (float) (mCellHeight + DAY_GAP); |
| } |
| computeFirstHour(); |
| |
| mRemeasure = true; |
| invalidate(); |
| return true; |
| } |
| |
| // ScaleGestureDetector.OnScaleGestureListener |
| public void onScaleEnd(ScaleGestureDetector detector) { |
| mScrollStartY = mViewStartY; |
| mInitialScrollY = 0; |
| mInitialScrollX = 0; |
| mStartingSpanY = 0; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| int action = ev.getAction(); |
| if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); |
| |
| if ((ev.getActionMasked() == MotionEvent.ACTION_DOWN) || |
| (ev.getActionMasked() == MotionEvent.ACTION_UP) || |
| (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) || |
| (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN)) { |
| mRecalCenterHour = true; |
| } |
| |
| if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { |
| mScaleGestureDetector.onTouchEvent(ev); |
| } |
| |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mStartingScroll = true; |
| if (DEBUG) { |
| Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" |
| + ev.getPointerCount()); |
| } |
| |
| int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; |
| if (ev.getY() < bottom) { |
| mTouchStartedInAlldayArea = true; |
| } else { |
| mTouchStartedInAlldayArea = false; |
| } |
| mHandleActionUp = true; |
| mGestureDetector.onTouchEvent(ev); |
| return true; |
| |
| case MotionEvent.ACTION_MOVE: |
| if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); |
| mGestureDetector.onTouchEvent(ev); |
| return true; |
| |
| case MotionEvent.ACTION_UP: |
| if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); |
| mEdgeEffectTop.onRelease(); |
| mEdgeEffectBottom.onRelease(); |
| mStartingScroll = false; |
| mGestureDetector.onTouchEvent(ev); |
| if (!mHandleActionUp) { |
| mHandleActionUp = true; |
| mViewStartX = 0; |
| invalidate(); |
| return true; |
| } |
| |
| if (mOnFlingCalled) { |
| return true; |
| } |
| |
| // If we were scrolling, then reset the selected hour so that it |
| // is visible. |
| if (mScrolling) { |
| mScrolling = false; |
| resetSelectedHour(); |
| invalidate(); |
| } |
| |
| if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { |
| mTouchMode = TOUCH_MODE_INITIAL_STATE; |
| if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { |
| // The user has gone beyond the threshold so switch views |
| if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); |
| switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); |
| 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. |
| if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); |
| recalc(); |
| invalidate(); |
| mViewStartX = 0; |
| } |
| } |
| |
| return true; |
| |
| // This case isn't expected to happen. |
| case MotionEvent.ACTION_CANCEL: |
| if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); |
| mGestureDetector.onTouchEvent(ev); |
| mScrolling = false; |
| resetSelectedHour(); |
| return true; |
| |
| default: |
| if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); |
| if (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; |
| invalidate(); |
| } |
| |
| final long startMillis = getSelectedTimeInMillis(); |
| int flags = DateUtils.FORMAT_SHOW_TIME |
| | DateUtils.FORMAT_CAP_NOON_MIDNIGHT |
| | DateUtils.FORMAT_SHOW_WEEKDAY; |
| final String title = Utils.formatDateRange(mContext, startMillis, 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, MENU_EVENT_VIEW, 0, R.string.event_view); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_info_details); |
| |
| int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); |
| if (accessLevel == ACCESS_LEVEL_EDIT) { |
| item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_edit); |
| item.setAlphabeticShortcut('e'); |
| } |
| |
| if (accessLevel >= ACCESS_LEVEL_DELETE) { |
| item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_delete); |
| } |
| |
| item = menu.add(0, 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, 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, MENU_EVENT_VIEW, 0, R.string.event_view); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_info_details); |
| |
| int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); |
| if (accessLevel == ACCESS_LEVEL_EDIT) { |
| item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_edit); |
| item.setAlphabeticShortcut('e'); |
| } |
| |
| if (accessLevel >= ACCESS_LEVEL_DELETE) { |
| item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_delete); |
| } |
| } |
| |
| item = menu.add(0, 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, MENU_DAY, 0, R.string.show_day_view); |
| item.setOnMenuItemClickListener(mContextMenuHandler); |
| item.setIcon(android.R.drawable.ic_menu_day); |
| item.setAlphabeticShortcut('d'); |
| } |
| |
| mPopup.dismiss(); |
| } |
| |
| private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { |
| |
| public boolean onMenuItemClick(MenuItem item) { |
| switch (item.getItemId()) { |
| case MENU_EVENT_VIEW: { |
| if (mSelectedEvent != null) { |
| mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, |
| mSelectedEvent.id, mSelectedEvent.startMillis, |
| mSelectedEvent.endMillis, 0, 0, -1); |
| } |
| break; |
| } |
| case MENU_EVENT_EDIT: { |
| if (mSelectedEvent != null) { |
| mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, |
| mSelectedEvent.id, mSelectedEvent.startMillis, |
| mSelectedEvent.endMillis, 0, 0, -1); |
| } |
| break; |
| } |
| case MENU_DAY: { |
| mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, |
| ViewType.DAY); |
| break; |
| } |
| case MENU_AGENDA: { |
| mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, |
| ViewType.AGENDA); |
| break; |
| } |
| case MENU_EVENT_CREATE: { |
| long startMillis = getSelectedTimeInMillis(); |
| long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; |
| mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, |
| startMillis, endMillis, 0, 0, -1); |
| break; |
| } |
| case MENU_EVENT_DELETE: { |
| if (mSelectedEvent != null) { |
| Event selectedEvent = mSelectedEvent; |
| long begin = selectedEvent.startMillis; |
| long end = selectedEvent.endMillis; |
| long id = selectedEvent.id; |
| mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin, |
| end, 0, 0, -1); |
| } |
| break; |
| } |
| default: { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| |
| private static int getEventAccessLevel(Context context, Event e) { |
| ContentResolver cr = context.getContentResolver(); |
| |
| int accessLevel = Calendars.CAL_ACCESS_NONE; |
| |
| // 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) { |
| return ACCESS_LEVEL_NONE; |
| } |
| |
| if (cursor.getCount() == 0) { |
| cursor.close(); |
| return ACCESS_LEVEL_NONE; |
| } |
| |
| cursor.moveToFirst(); |
| long calId = cursor.getLong(0); |
| cursor.close(); |
| |
| 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(); |
| accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); |
| calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); |
| cursor.close(); |
| } |
| |
| if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) { |
| return ACCESS_LEVEL_NONE; |
| } |
| |
| if (e.guestsCanModify) { |
| return ACCESS_LEVEL_EDIT; |
| } |
| |
| if (!TextUtils.isEmpty(calendarOwnerAccount) |
| && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { |
| return ACCESS_LEVEL_EDIT; |
| } |
| |
| return ACCESS_LEVEL_DELETE; |
| } |
| |
| /** |
| * 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 |
| * @param keepOldSelection - do not change the selection info (used for invoking accessibility |
| * messages) |
| * @return true if the touch position is valid |
| */ |
| private boolean setSelectionFromPosition(int x, final int y, boolean keepOldSelection) { |
| |
| Event savedEvent = null; |
| int savedDay = 0; |
| int savedHour = 0; |
| boolean savedAllDay = false; |
| if (keepOldSelection) { |
| // Store selection info and restore it at the end. This way, we can invoke the |
| // right accessibility message without affecting the selection. |
| savedEvent = mSelectedEvent; |
| savedDay = mSelectionDay; |
| savedHour = mSelectionHour; |
| savedAllDay = mSelectionAllday; |
| } |
| if (x < mHoursWidth) { |
| x = mHoursWidth; |
| } |
| |
| int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); |
| if (day >= mNumDays) { |
| day = mNumDays - 1; |
| } |
| day += mFirstJulianDay; |
| setSelectedDay(day); |
| |
| if (y < DAY_HEADER_HEIGHT) { |
| sendAccessibilityEventAsNeeded(false); |
| return false; |
| } |
| |
| setSelectedHour(mFirstHour); /* First fully visible hour */ |
| |
| if (y < mFirstCell) { |
| mSelectionAllday = true; |
| } else { |
| // y is now offset from top of the scrollable region |
| int adjustedY = y - mFirstCell; |
| |
| if (adjustedY < mFirstHourOffset) { |
| setSelectedHour(mSelectionHour - 1); /* In the partially visible hour */ |
| } else { |
| setSelectedHour(mSelectionHour + |
| (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP)); |
| } |
| |
| mSelectionAllday = false; |
| } |
| |
| findSelectedEvent(x, y); |
| |
| // Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: " |
| // + mSelectionHour + " 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(mContext, ev.startMillis, ev.endMillis, flags); |
| // |
| // Log.i("Cal", " " + timeRange + " " + ev.title); |
| // } |
| // } |
| sendAccessibilityEventAsNeeded(true); |
| |
| // Restore old values |
| if (keepOldSelection) { |
| mSelectedEvent = savedEvent; |
| mSelectionDay = savedDay; |
| mSelectionHour = savedHour; |
| mSelectionAllday = savedAllDay; |
| } |
| 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 = computeDayLeftPosition(mSelectionDay - mFirstJulianDay); |
| int top = 0; |
| setSelectedEvent(null); |
| |
| mSelectedEvents.clear(); |
| if (mSelectionAllday) { |
| float yDistance; |
| float minYdistance = 10000.0f; // any large number |
| Event closestEvent = null; |
| float drawHeight = mAlldayHeight; |
| int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; |
| int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount; |
| if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { |
| // Leave a gap for the 'box +n' text |
| maxUnexpandedColumn--; |
| } |
| events = mAllDayEvents; |
| numEvents = events.size(); |
| for (int i = 0; i < numEvents; i++) { |
| Event event = events.get(i); |
| if (!event.drawAsAllday() || |
| (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { |
| // Don't check non-allday events or events that aren't shown |
| continue; |
| } |
| |
| if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { |
| float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents |
| : mMaxUnexpandedAlldayEventCount; |
| float height = drawHeight / numRectangles; |
| if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { |
| height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; |
| } |
| 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; |
| } |
| } |
| } |
| } |
| setSelectedEvent(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; |
| } |
| } |
| setSelectedEvent(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) { |
| setSelectedDay(startDay); |
| } else if (mSelectionDay > endDay) { |
| setSelectedDay(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 && mSelectionDay == startDay) { |
| setSelectedHour(startHour); |
| } else if (mSelectionHour > endHour && mSelectionDay == endDay) { |
| setSelectedHour(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 { |
| |
| public void run() { |
| mScrolling = mScrolling && mScroller.computeScrollOffset(); |
| if (!mScrolling || mPaused) { |
| resetSelectedHour(); |
| invalidate(); |
| return; |
| } |
| |
| mViewStartY = mScroller.getCurrY(); |
| |
| if (mCallEdgeEffectOnAbsorb) { |
| if (mViewStartY < 0) { |
| mEdgeEffectTop.onAbsorb((int) mLastVelocity); |
| mCallEdgeEffectOnAbsorb = false; |
| } else if (mViewStartY > mMaxViewStartY) { |
| mEdgeEffectBottom.onAbsorb((int) mLastVelocity); |
| mCallEdgeEffectOnAbsorb = false; |
| } |
| mLastVelocity = mScroller.getCurrVelocity(); |
| } |
| |
| if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { |
| // Allow overscroll/springback only on a fling, |
| // not a pull/fling from the end |
| if (mViewStartY < 0) { |
| mViewStartY = 0; |
| } else if (mViewStartY > mMaxViewStartY) { |
| mViewStartY = mMaxViewStartY; |
| } |
| } |
| |
| computeFirstHour(); |
| mHandler.post(this); |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Cleanup the pop-up and timers. |
| */ |
| public void cleanup() { |
| // Protect against null-pointer exceptions |
| if (mPopup != null) { |
| mPopup.dismiss(); |
| } |
| mPaused = true; |
| mLastPopupEventID = INVALID_EVENT_ID; |
| if (mHandler != null) { |
| mHandler.removeCallbacks(mDismissPopup); |
| mHandler.removeCallbacks(mUpdateCurrentTime); |
| } |
| |
| Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, |
| mCellHeight); |
| // Clear all click animations |
| eventClickCleanup(); |
| // Turn off redraw |
| mRemeasure = false; |
| // Turn off scrolling to make sure the view is in the correct state if we fling back to it |
| mScrolling = false; |
| } |
| |
| private void eventClickCleanup() { |
| this.removeCallbacks(mClearClick); |
| this.removeCallbacks(mSetClick); |
| mClickedEvent = null; |
| mSavedClickedEvent = null; |
| } |
| |
| private void setSelectedEvent(Event e) { |
| mSelectedEvent = e; |
| mSelectedEventForAccessibility = e; |
| } |
| |
| private void setSelectedHour(int h) { |
| mSelectionHour = h; |
| mSelectionHourForAccessibility = h; |
| } |
| private void setSelectedDay(int d) { |
| mSelectionDay = d; |
| mSelectionDayForAccessibility = d; |
| } |
| |
| /** |
| * Restart the update timer |
| */ |
| public void restartCurrentTimeUpdates() { |
| mPaused = false; |
| if (mHandler != null) { |
| mHandler.removeCallbacks(mUpdateCurrentTime); |
| mHandler.post(mUpdateCurrentTime); |
| } |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| cleanup(); |
| super.onDetachedFromWindow(); |
| } |
| |
| class DismissPopup implements Runnable { |
| |
| public void run() { |
| // Protect against null-pointer exceptions |
| if (mPopup != null) { |
| mPopup.dismiss(); |
| } |
| } |
| } |
| |
| class UpdateCurrentTime implements Runnable { |
| |
| public void run() { |
| long currentTime = System.currentTimeMillis(); |
| mCurrentTime.set(currentTime); |
| //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) |
| if (!DayView.this.mPaused) { |
| mHandler.postDelayed(mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY |
| - (currentTime % UPDATE_CURRENT_TIME_DELAY)); |
| } |
| mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); |
| invalidate(); |
| } |
| } |
| |
| class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { |
| @Override |
| public boolean onSingleTapUp(MotionEvent ev) { |
| if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp"); |
| DayView.this.doSingleTapUp(ev); |
| return true; |
| } |
| |
| @Override |
| public void onLongPress(MotionEvent ev) { |
| if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress"); |
| DayView.this.doLongPress(ev); |
| } |
| |
| @Override |
| public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { |
| if (DEBUG) Log.e(TAG, "GestureDetector.onScroll"); |
| eventClickCleanup(); |
| if (mTouchStartedInAlldayArea) { |
| if (Math.abs(distanceX) < Math.abs(distanceY)) { |
| // Make sure that click feedback is gone when you scroll from the |
| // all day area |
| invalidate(); |
| return false; |
| } |
| // don't scroll vertically if this started in the allday area |
| distanceY = 0; |
| } |
| DayView.this.doScroll(e1, e2, distanceX, distanceY); |
| return true; |
| } |
| |
| @Override |
| public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
| if (DEBUG) Log.e(TAG, "GestureDetector.onFling"); |
| |
| if (mTouchStartedInAlldayArea) { |
| if (Math.abs(velocityX) < Math.abs(velocityY)) { |
| return false; |
| } |
| // don't fling vertically if this started in the allday area |
| velocityY = 0; |
| } |
| DayView.this.doFling(e1, e2, velocityX, velocityY); |
| return true; |
| } |
| |
| @Override |
| public boolean onDown(MotionEvent ev) { |
| if (DEBUG) Log.e(TAG, "GestureDetector.onDown"); |
| DayView.this.doDown(ev); |
| return true; |
| } |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| int flags = DateUtils.FORMAT_SHOW_WEEKDAY; |
| long time = getSelectedTimeInMillis(); |
| if (!mSelectionAllday) { |
| flags |= DateUtils.FORMAT_SHOW_TIME; |
| } |
| if (DateFormat.is24HourFormat(mContext)) { |
| flags |= DateUtils.FORMAT_24HOUR; |
| } |
| mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags); |
| new AlertDialog.Builder(mContext).setTitle(mLongPressTitle) |
| .setItems(mLongPressItems, new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| if (which == 0) { |
| long extraLong = 0; |
| if (mSelectionAllday) { |
| extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; |
| } |
| mController.sendEventRelatedEventWithExtra(this, |
| EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1, |
| -1, extraLong, -1); |
| } |
| } |
| }).show().setCanceledOnTouchOutside(true); |
| return true; |
| } |
| |
| // The rest of this file was borrowed from Launcher2 - PagedView.java |
| private static final int MINIMUM_SNAP_VELOCITY = 2200; |
| |
| private class ScrollInterpolator implements Interpolator { |
| public ScrollInterpolator() { |
| } |
| |
| public float getInterpolation(float t) { |
| t -= 1.0f; |
| t = t * t * t * t * t + 1; |
| |
| if ((1 - t) * mAnimationDistance < 1) { |
| cancelAnimation(); |
| } |
| |
| return t; |
| } |
| } |
| |
| private long calculateDuration(float delta, float width, float velocity) { |
| /* |
| * Here we compute a "distance" that will be used in the computation of |
| * the overall snap duration. This is a function of the actual distance |
| * that needs to be traveled; we keep this value close to half screen |
| * size in order to reduce the variance in snap duration as a function |
| * of the distance the page needs to travel. |
| */ |
| final float halfScreenSize = width / 2; |
| float distanceRatio = delta / width; |
| float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); |
| float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; |
| |
| velocity = Math.abs(velocity); |
| velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); |
| |
| /* |
| * we want the page's snap velocity to approximately match the velocity |
| * at which the user flings, so we scale the duration by a value near to |
| * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to |
| * make it a little slower. |
| */ |
| long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); |
| if (DEBUG) { |
| Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" |
| + distanceRatio + " distance:" + distance + " velocity:" + velocity |
| + " duration:" + duration + " distanceInfluenceForSnapDuration:" |
| + distanceInfluenceForSnapDuration); |
| } |
| return duration; |
| } |
| |
| /* |
| * We want the duration of the page snap animation to be influenced by the |
| * distance that the screen has to travel, however, we don't want this |
| * duration to be effected in a purely linear fashion. Instead, we use this |
| * method to moderate the effect that the distance of travel has on the |
| * overall snap duration. |
| */ |
| private float distanceInfluenceForSnapDuration(float f) { |
| f -= 0.5f; // center the values about 0. |
| f *= 0.3f * Math.PI / 2.0f; |
| return (float) Math.sin(f); |
| } |
| } |