diff --git a/README.md b/README.md index 7d3ceca73..153d08e7f 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ Usage com.github.alamkanak android-week-view - 1.2.3 + 1.2.4 aar ``` * Grab via gradle ```groovy - compile 'com.github.alamkanak:android-week-view:1.2.3' + compile 'com.github.alamkanak:android-week-view:1.2.4' ``` 2. Add WeekView in your xml layout. @@ -117,13 +117,23 @@ You can customize the look of the `WeekView` in xml. Use the following attribute - `textSize` - `todayBackgroundColor` - `todayHeaderTextColor` +- `showDistinctPastFutureColor` +- `futureBackgroundColor` +- `pastBackgroundColor` +- `showDistinctWeekendColor` +- `futureWeekendBackgroundColor` +- `pastWeekendBackgroundColor` +- `showNowLine` +- `nowLineColor` +- `nowLineThickness` Interfaces ---------- Use the following interfaces according to your need. -- `mWeekView.setMonthChangeListener()` to provide events to the calendar +- `mWeekView.setWeekViewLoader()` to provide events to the calendar +- `mWeekView.setMonthChangeListener()` to provide events to the calendar by months - `mWeekView.setOnEventClickListener()` to get a callback when an event is clicked - `mWeekView.setEventLongPressListener()` to get a callback when an event is long pressed - `mWeekView.setEmptyViewClickListener()` to get a callback when any empty space is clicked @@ -145,6 +155,15 @@ To do Changelog --------- +**Version 1.2.4** + +* **NOTE:** If you are using `WeekView.MonthChangeListener`, make sure to change it into `MonthLoader.MonthChangeListener` +* Add support to have loaders other than MonthViewLoader +* Add pinch to zoom support +* Add support for location +* Add ability to have different colors for past, future, weekend days +* Add support for "now" line + **Version 1.2.3** * Get callbacks when scrolling horizontally diff --git a/build.gradle b/build.gradle index 23167e28c..010afe941 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:1.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index eb8507362..4172128ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.2.3 +VERSION_NAME=1.2.4 GROUP=com.github.alamkanak POM_DESCRIPTION=An android library to show day view, week view, 3 day view etc. in your app. diff --git a/library/build.gradle b/library/build.gradle index 9453d1cdd..2ece4ff92 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -5,17 +5,17 @@ repositories { } android { - compileSdkVersion 22 - buildToolsVersion "22.0.1" + compileSdkVersion 23 + buildToolsVersion "23.0.1" defaultConfig { minSdkVersion 9 - targetSdkVersion 22 + targetSdkVersion 23 } } dependencies { - compile 'com.android.support:appcompat-v7:22.0.0' + compile 'com.android.support:appcompat-v7:23.0.1' } apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 1c9d5b1a7..c7707cff5 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,7 +1,3 @@ - - - - diff --git a/library/src/main/java/com/alamkanak/weekview/MonthLoader.java b/library/src/main/java/com/alamkanak/weekview/MonthLoader.java new file mode 100644 index 000000000..1768e143b --- /dev/null +++ b/library/src/main/java/com/alamkanak/weekview/MonthLoader.java @@ -0,0 +1,43 @@ +package com.alamkanak.weekview; + +import java.util.Calendar; +import java.util.List; + +public class MonthLoader implements WeekViewLoader { + + private MonthChangeListener mOnMonthChangeListener; + + public MonthLoader(MonthChangeListener listener){ + this.mOnMonthChangeListener = listener; + } + + @Override + public double toWeekViewPeriodIndex(Calendar instance){ + return instance.get(Calendar.YEAR) * 12 + instance.get(Calendar.MONTH) + (instance.get(Calendar.DAY_OF_MONTH) - 1) / 30.0; + } + + @Override + public List onLoad(int periodIndex){ + return mOnMonthChangeListener.onMonthChange(periodIndex / 12, periodIndex % 12 + 1); + } + + public MonthChangeListener getOnMonthChangeListener() { + return mOnMonthChangeListener; + } + + public void setOnMonthChangeListener(MonthChangeListener onMonthChangeListener) { + this.mOnMonthChangeListener = onMonthChangeListener; + } + + public interface MonthChangeListener { + /** + * Very important interface, it's the base to load events in the calendar. + * This method is called three times: once to load the previous month, once to load the next month and once to load the current month.
+ * That's why you can have three times the same event at the same place if you mess up with the configuration + * @param newYear: year of the events required by the view. + * @param newMonth: month of the events required by the view
1 based (not like JAVA API) --> January = 1 and December = 12. + * @return a list of the events happening during the specified month. + */ + List onMonthChange(int newYear, int newMonth); + } +} diff --git a/library/src/main/java/com/alamkanak/weekview/WeekView.java b/library/src/main/java/com/alamkanak/weekview/WeekView.java old mode 100644 new mode 100755 index 5a9281b32..e6110a05f --- a/library/src/main/java/com/alamkanak/weekview/WeekView.java +++ b/library/src/main/java/com/alamkanak/weekview/WeekView.java @@ -9,21 +9,25 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; +import android.support.annotation.Nullable; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.ViewCompat; import android.text.Layout; +import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; +import android.text.format.DateFormat; +import android.text.style.StyleSpan; import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.SoundEffectConstants; import android.view.View; import android.widget.OverScroller; -import android.widget.Scroller; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -31,6 +35,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Locale; /** * Created by Raquib-ul-Alam Kanak on 7/21/2014. @@ -43,8 +48,6 @@ public class WeekView extends View { @Deprecated public static final int LENGTH_LONG = 2; private final Context mContext; - private Calendar mToday; - private Calendar mStartDate; private Paint mTimeTextPaint; private float mTimeTextWidth; private float mTimeTextHeight; @@ -60,21 +63,35 @@ public class WeekView extends View { private Paint mHourSeparatorPaint; private float mHeaderMarginBottom; private Paint mTodayBackgroundPaint; + private Paint mFutureBackgroundPaint; + private Paint mPastBackgroundPaint; + private Paint mFutureWeekendBackgroundPaint; + private Paint mPastWeekendBackgroundPaint; + private Paint mNowLinePaint; private Paint mTodayHeaderTextPaint; private Paint mEventBackgroundPaint; private float mHeaderColumnWidth; private List mEventRects; + private List mPreviousPeriodEvents; + private List mCurrentPeriodEvents; + private List mNextPeriodEvents; private TextPaint mEventTextPaint; private Paint mHeaderColumnBackgroundPaint; - private Scroller mStickyScroller; - private int mFetchedMonths[] = new int[3]; + private int mFetchedPeriod = -1; // the middle period the calendar has fetched. private boolean mRefreshEvents = false; - private float mDistanceY = 0; - private float mDistanceX = 0; private Direction mCurrentFlingDirection = Direction.NONE; + private ScaleGestureDetector mScaleDetector; + private boolean mIsZooming; + private Calendar mFirstVisibleDay; + private Calendar mLastVisibleDay; + private int mDefaultEventColor; // Attributes and their default values. private int mHourHeight = 50; + private int mNewHourHeight = -1; + private int mMinHourHeight = 0; //no minimum specified (will be dynamic, based on screen) + private int mEffectiveMinHourHeight = mMinHourHeight; //compensates for the fact that you can't keep zooming out. + private int mMaxHourHeight = 250; private int mColumnGap = 10; private int mFirstDayOfWeek = Calendar.MONDAY; private int mTextSize = 12; @@ -84,6 +101,12 @@ public class WeekView extends View { private int mHeaderRowPadding = 10; private int mHeaderRowBackgroundColor = Color.WHITE; private int mDayBackgroundColor = Color.rgb(245, 245, 245); + private int mPastBackgroundColor = Color.rgb(227, 227, 227); + private int mFutureBackgroundColor = Color.rgb(245, 245, 245); + private int mPastWeekendBackgroundColor = 0; + private int mFutureWeekendBackgroundColor = 0; + private int mNowLineColor = Color.rgb(102, 102, 102); + private int mNowLineThickness = 5; private int mHourSeparatorColor = Color.rgb(230, 230, 230); private int mTodayBackgroundColor = Color.rgb(239, 247, 254); private int mHourSeparatorHeight = 2; @@ -92,22 +115,23 @@ public class WeekView extends View { private int mEventTextColor = Color.BLACK; private int mEventPadding = 8; private int mHeaderColumnBackgroundColor = Color.WHITE; - private int mDefaultEventColor; private boolean mIsFirstDraw = true; private boolean mAreDimensionsInvalid = true; @Deprecated private int mDayNameLength = LENGTH_LONG; private int mOverlappingEventGap = 0; private int mEventMarginVertical = 0; private float mXScrollingSpeed = 1f; - private Calendar mFirstVisibleDay; - private Calendar mLastVisibleDay; private Calendar mScrollToDay = null; private double mScrollToHour = -1; + private int mEventCornerRadius = 0; + private boolean mShowDistinctWeekendColor = false; + private boolean mShowNowLine = false; + private boolean mShowDistinctPastFutureColor = false; // Listeners. private EventClickListener mEventClickListener; private EventLongPressListener mEventLongPressListener; - private MonthChangeListener mMonthChangeListener; + private WeekViewLoader mWeekViewLoader; private EmptyViewClickListener mEmptyViewClickListener; private EmptyViewLongPressListener mEmptyViewLongPressListener; private DateTimeInterpreter mDateTimeInterpreter; @@ -117,39 +141,51 @@ public class WeekView extends View { @Override public boolean onDown(MotionEvent e) { - mScroller.forceFinished(true); - mStickyScroller.forceFinished(true); + goToNearestOrigin(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + // Check if view is zoomed. + if (mIsZooming) + return true; + + // Calculate the direction to scroll. if (mCurrentScrollDirection == Direction.NONE) { + // allow scrolling only in one direction if (Math.abs(distanceX) > Math.abs(distanceY)){ mCurrentScrollDirection = Direction.HORIZONTAL; - mCurrentFlingDirection = Direction.HORIZONTAL; } else { - mCurrentFlingDirection = Direction.VERTICAL; mCurrentScrollDirection = Direction.VERTICAL; } } - mDistanceX = distanceX * mXScrollingSpeed; - mDistanceY = distanceY; - invalidate(); + + // Calculate the new origin after scroll. + switch (mCurrentScrollDirection) { + case HORIZONTAL: + mCurrentOrigin.x -= distanceX * mXScrollingSpeed; + ViewCompat.postInvalidateOnAnimation(WeekView.this); + break; + case VERTICAL: + mCurrentOrigin.y -= distanceY; + ViewCompat.postInvalidateOnAnimation(WeekView.this); + break; + } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.forceFinished(true); - mStickyScroller.forceFinished(true); + mCurrentFlingDirection = mCurrentScrollDirection; if (mCurrentFlingDirection == Direction.HORIZONTAL){ - mScroller.fling((int) mCurrentOrigin.x, 0, (int) (velocityX * mXScrollingSpeed), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); + mScroller.fling((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, (int) (velocityX * mXScrollingSpeed), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, (int) -(mHourHeight * 24 + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight/2 - getHeight()), 0); } else if (mCurrentFlingDirection == Direction.VERTICAL){ - mScroller.fling(0, (int) mCurrentOrigin.y, 0, (int) velocityY, 0, 0, (int) -(mHourHeight * 24 + mHeaderTextHeight + mHeaderRowPadding * 2 - getHeight()), 0); + mScroller.fling((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, 0, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, (int) -(mHourHeight * 24 + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight/2 - getHeight()), 0); } ViewCompat.postInvalidateOnAnimation(WeekView.this); @@ -234,6 +270,9 @@ public WeekView(Context context, AttributeSet attrs, int defStyleAttr) { try { mFirstDayOfWeek = a.getInteger(R.styleable.WeekView_firstDayOfWeek, mFirstDayOfWeek); mHourHeight = a.getDimensionPixelSize(R.styleable.WeekView_hourHeight, mHourHeight); + mMinHourHeight = a.getDimensionPixelSize(R.styleable.WeekView_minHourHeight, mMinHourHeight); + mEffectiveMinHourHeight = mMinHourHeight; + mMaxHourHeight = a.getDimensionPixelSize(R.styleable.WeekView_maxHourHeight, mMaxHourHeight); mTextSize = a.getDimensionPixelSize(R.styleable.WeekView_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, context.getResources().getDisplayMetrics())); mHeaderColumnPadding = a.getDimensionPixelSize(R.styleable.WeekView_headerColumnPadding, mHeaderColumnPadding); mColumnGap = a.getDimensionPixelSize(R.styleable.WeekView_columnGap, mColumnGap); @@ -242,6 +281,12 @@ public WeekView(Context context, AttributeSet attrs, int defStyleAttr) { mHeaderRowPadding = a.getDimensionPixelSize(R.styleable.WeekView_headerRowPadding, mHeaderRowPadding); mHeaderRowBackgroundColor = a.getColor(R.styleable.WeekView_headerRowBackgroundColor, mHeaderRowBackgroundColor); mDayBackgroundColor = a.getColor(R.styleable.WeekView_dayBackgroundColor, mDayBackgroundColor); + mFutureBackgroundColor = a.getColor(R.styleable.WeekView_futureBackgroundColor, mFutureBackgroundColor); + mPastBackgroundColor = a.getColor(R.styleable.WeekView_pastBackgroundColor, mPastBackgroundColor); + mFutureWeekendBackgroundColor = a.getColor(R.styleable.WeekView_futureWeekendBackgroundColor, mFutureBackgroundColor); // If not set, use the same color as in the week + mPastWeekendBackgroundColor = a.getColor(R.styleable.WeekView_pastWeekendBackgroundColor, mPastBackgroundColor); + mNowLineColor = a.getColor(R.styleable.WeekView_nowLineColor, mNowLineColor); + mNowLineThickness = a.getDimensionPixelSize(R.styleable.WeekView_nowLineThickness, mNowLineThickness); mHourSeparatorColor = a.getColor(R.styleable.WeekView_hourSeparatorColor, mHourSeparatorColor); mTodayBackgroundColor = a.getColor(R.styleable.WeekView_todayBackgroundColor, mTodayBackgroundColor); mHourSeparatorHeight = a.getDimensionPixelSize(R.styleable.WeekView_hourSeparatorHeight, mHourSeparatorHeight); @@ -254,6 +299,11 @@ public WeekView(Context context, AttributeSet attrs, int defStyleAttr) { mOverlappingEventGap = a.getDimensionPixelSize(R.styleable.WeekView_overlappingEventGap, mOverlappingEventGap); mEventMarginVertical = a.getDimensionPixelSize(R.styleable.WeekView_eventMarginVertical, mEventMarginVertical); mXScrollingSpeed = a.getFloat(R.styleable.WeekView_xScrollingSpeed, mXScrollingSpeed); + mEventCornerRadius = a.getDimensionPixelSize(R.styleable.WeekView_eventCornerRadius, mEventCornerRadius); + mShowDistinctPastFutureColor = a.getBoolean(R.styleable.WeekView_showDistinctPastFutureColor, mShowDistinctPastFutureColor); + mShowDistinctWeekendColor = a.getBoolean(R.styleable.WeekView_showDistinctWeekendColor, mShowDistinctWeekendColor); + mShowNowLine = a.getBoolean(R.styleable.WeekView_showNowLine, mShowNowLine); + } finally { a.recycle(); } @@ -262,16 +312,9 @@ public WeekView(Context context, AttributeSet attrs, int defStyleAttr) { } private void init() { - // Get the date today. - mToday = Calendar.getInstance(); - mToday.set(Calendar.HOUR_OF_DAY, 0); - mToday.set(Calendar.MINUTE, 0); - mToday.set(Calendar.SECOND, 0); - // Scrolling initialization. mGestureDetector = new GestureDetectorCompat(mContext, mGestureListener); mScroller = new OverScroller(mContext); - mStickyScroller = new Scroller(mContext); // Measure settings for time column. mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -280,9 +323,9 @@ private void init() { mTimeTextPaint.setColor(mHeaderColumnTextColor); Rect rect = new Rect(); mTimeTextPaint.getTextBounds("00 PM", 0, "00 PM".length(), rect); - mTimeTextWidth = mTimeTextPaint.measureText("00 PM"); mTimeTextHeight = rect.height(); mHeaderMarginBottom = mTimeTextHeight / 2; + initTextTimeWidth(); // Measure settings for header row. mHeaderTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -300,6 +343,14 @@ private void init() { // Prepare day background color paint. mDayBackgroundPaint = new Paint(); mDayBackgroundPaint.setColor(mDayBackgroundColor); + mFutureBackgroundPaint = new Paint(); + mFutureBackgroundPaint.setColor(mFutureBackgroundColor); + mPastBackgroundPaint = new Paint(); + mPastBackgroundPaint.setColor(mPastBackgroundColor); + mFutureWeekendBackgroundPaint = new Paint(); + mFutureWeekendBackgroundPaint.setColor(mFutureWeekendBackgroundColor); + mPastWeekendBackgroundPaint = new Paint(); + mPastWeekendBackgroundPaint.setColor(mPastWeekendBackgroundColor); // Prepare hour separator color paint. mHourSeparatorPaint = new Paint(); @@ -307,6 +358,11 @@ private void init() { mHourSeparatorPaint.setStrokeWidth(mHourSeparatorHeight); mHourSeparatorPaint.setColor(mHourSeparatorColor); + // Prepare the "now" line color paint + mNowLinePaint = new Paint(); + mNowLinePaint.setStrokeWidth(mNowLineThickness); + mNowLinePaint.setColor(mNowLineColor); + // Prepare today background color paint. mTodayBackgroundPaint = new Paint(); mTodayBackgroundPaint.setColor(mTodayBackgroundColor); @@ -331,10 +387,51 @@ private void init() { mEventTextPaint.setStyle(Paint.Style.FILL); mEventTextPaint.setColor(mEventTextColor); mEventTextPaint.setTextSize(mEventTextSize); - mStartDate = (Calendar) mToday.clone(); // Set default event color. mDefaultEventColor = Color.parseColor("#9fc6e7"); + + mScaleDetector = new ScaleGestureDetector(mContext, new ScaleGestureDetector.OnScaleGestureListener() { + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mIsZooming = false; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mIsZooming = true; + goToNearestOrigin(); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + mNewHourHeight = Math.round(mHourHeight * detector.getScaleFactor()); + invalidate(); + return true; + } + }); + } + + // fix rotation changes + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mAreDimensionsInvalid = true; + } + + /** + * Initialize time column width. Calculate value with all possible hours (supposed widest text). + */ + private void initTextTimeWidth() { + mTimeTextWidth = 0; + for (int i = 0; i < 24; i++) { + // Measure time string and get max width. + String time = getDateTimeInterpreter().interpretTime(i); + if (time == null) + throw new IllegalStateException("A DateTimeInterpreter must not return null time"); + mTimeTextWidth = Math.max(mTimeTextWidth, mTimeTextPaint.measureText(time)); + } } @Override @@ -351,17 +448,10 @@ protected void onDraw(Canvas canvas) { canvas.drawRect(0, 0, mTimeTextWidth + mHeaderColumnPadding * 2, mHeaderTextHeight + mHeaderRowPadding * 2, mHeaderBackgroundPaint); // Hide anything that is in the bottom margin of the header row. - canvas.drawRect(mHeaderColumnWidth, mHeaderTextHeight + mHeaderRowPadding * 2, getWidth(), mHeaderRowPadding * 2 + mHeaderTextHeight + mHeaderMarginBottom + mTimeTextHeight/2 - mHourSeparatorHeight / 2, mHeaderColumnBackgroundPaint); + canvas.drawRect(mHeaderColumnWidth, mHeaderTextHeight + mHeaderRowPadding * 2, getWidth(), mHeaderRowPadding * 2 + mHeaderTextHeight + mHeaderMarginBottom + mTimeTextHeight/2, mHeaderColumnBackgroundPaint); } private void drawTimeColumnAndAxes(Canvas canvas) { - // Do not let the view go above/below the limit due to scrolling. Set the max and min limit of the scroll. - if (mCurrentScrollDirection == Direction.VERTICAL) { - if (mCurrentOrigin.y - mDistanceY > 0) mCurrentOrigin.y = 0; - else if (mCurrentOrigin.y - mDistanceY < -(mHourHeight * 24 + mHeaderTextHeight + mHeaderRowPadding * 2 - getHeight())) mCurrentOrigin.y = -(mHourHeight * 24 + mHeaderTextHeight + mHeaderRowPadding * 2 - getHeight()); - else mCurrentOrigin.y -= mDistanceY; - } - // Draw the background color for the header column. canvas.drawRect(0, mHeaderTextHeight + mHeaderRowPadding * 2, mHeaderColumnWidth, getHeight(), mHeaderColumnBackgroundPaint); @@ -382,7 +472,11 @@ private void drawHeaderRowAndEvents(Canvas canvas) { mWidthPerDay = getWidth() - mHeaderColumnWidth - mColumnGap * (mNumberOfVisibleDays - 1); mWidthPerDay = mWidthPerDay/mNumberOfVisibleDays; + Calendar today = today(); + if (mAreDimensionsInvalid) { + mEffectiveMinHourHeight= Math.max(mMinHourHeight, (int) ((getHeight() - mHeaderTextHeight - mHeaderRowPadding * 2 - mHeaderMarginBottom) / 24)); + mAreDimensionsInvalid = false; if(mScrollToDay != null) goToDate(mScrollToDay); @@ -399,21 +493,42 @@ private void drawHeaderRowAndEvents(Canvas canvas) { mIsFirstDraw = false; // If the week view is being drawn for the first time, then consider the first day of the week. - if(mNumberOfVisibleDays >= 7 && mToday.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { - int difference = (7 + (mToday.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)) % 7; + if(mNumberOfVisibleDays >= 7 && today.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { + int difference = 7 + (today.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek); mCurrentOrigin.x += (mWidthPerDay + mColumnGap) * difference; } } + // Calculate the new height due to the zooming. + if (mNewHourHeight > 0){ + if (mNewHourHeight < mEffectiveMinHourHeight) + mNewHourHeight = mEffectiveMinHourHeight; + else if (mNewHourHeight > mMaxHourHeight) + mNewHourHeight = mMaxHourHeight; + + mCurrentOrigin.y = (mCurrentOrigin.y/mHourHeight)*mNewHourHeight; + mHourHeight = mNewHourHeight; + mNewHourHeight = -1; + } + + // If the new mCurrentOrigin.y is invalid, make it valid. + if (mCurrentOrigin.y < getHeight() - mHourHeight * 24 - mHeaderTextHeight - mHeaderRowPadding * 2 - mHeaderMarginBottom - mTimeTextHeight/2) + mCurrentOrigin.y = getHeight() - mHourHeight * 24 - mHeaderTextHeight - mHeaderRowPadding * 2 - mHeaderMarginBottom - mTimeTextHeight/2; + + // Don't put an "else if" because it will trigger a glitch when completely zoomed out and + // scrolling vertically. + if (mCurrentOrigin.y > 0) { + mCurrentOrigin.y = 0; + } + // Consider scroll offset. - if (mCurrentScrollDirection == Direction.HORIZONTAL) mCurrentOrigin.x -= mDistanceX; int leftDaysWithGaps = (int) -(Math.ceil(mCurrentOrigin.x / (mWidthPerDay + mColumnGap))); float startFromPixel = mCurrentOrigin.x + (mWidthPerDay + mColumnGap) * leftDaysWithGaps + mHeaderColumnWidth; float startPixel = startFromPixel; // Prepare to iterate for each day. - Calendar day = (Calendar) mToday.clone(); + Calendar day = (Calendar) today.clone(); day.add(Calendar.HOUR, 6); // Prepare to iterate for each hour to draw the hour lines. @@ -431,8 +546,8 @@ private void drawHeaderRowAndEvents(Canvas canvas) { // Iterate through each day. Calendar oldFirstVisibleDay = mFirstVisibleDay; - mFirstVisibleDay = (Calendar) mToday.clone(); - mFirstVisibleDay.add(Calendar.DATE, leftDaysWithGaps); + mFirstVisibleDay = (Calendar) today.clone(); + mFirstVisibleDay.add(Calendar.DATE, -(Math.round(mCurrentOrigin.x / (mWidthPerDay + mColumnGap)))); if(!mFirstVisibleDay.equals(oldFirstVisibleDay) && mScrollListener != null){ mScrollListener.onFirstVisibleDayChanged(mFirstVisibleDay, oldFirstVisibleDay); } @@ -441,23 +556,47 @@ private void drawHeaderRowAndEvents(Canvas canvas) { dayNumber++) { // Check if the day is today. - day = (Calendar) mToday.clone(); + day = (Calendar) today.clone(); mLastVisibleDay = (Calendar) day.clone(); day.add(Calendar.DATE, dayNumber - 1); mLastVisibleDay.add(Calendar.DATE, dayNumber - 2); - boolean sameDay = isSameDay(day, mToday); + boolean sameDay = isSameDay(day, today); // Get more events if necessary. We want to store the events 3 months beforehand. Get // events only when it is the first iteration of the loop. - if (mEventRects == null || mRefreshEvents || (dayNumber == leftDaysWithGaps + 1 && mFetchedMonths[1] != day.get(Calendar.MONTH)+1 && day.get(Calendar.DAY_OF_MONTH) == 15)) { + if (mEventRects == null || mRefreshEvents || + (dayNumber == leftDaysWithGaps + 1 && mFetchedPeriod != (int) mWeekViewLoader.toWeekViewPeriodIndex(day) && + Math.abs(mFetchedPeriod - mWeekViewLoader.toWeekViewPeriodIndex(day)) > 0.5)) { getMoreEvents(day); mRefreshEvents = false; } // Draw background color for each day. float start = (startPixel < mHeaderColumnWidth ? mHeaderColumnWidth : startPixel); - if (mWidthPerDay + startPixel - start> 0) - canvas.drawRect(start, mHeaderTextHeight + mHeaderRowPadding * 2 + mTimeTextHeight/2 + mHeaderMarginBottom, startPixel + mWidthPerDay, getHeight(), sameDay ? mTodayBackgroundPaint : mDayBackgroundPaint); + if (mWidthPerDay + startPixel - start > 0){ + if (mShowDistinctPastFutureColor){ + boolean isWeekend = day.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || day.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY; + Paint pastPaint = isWeekend && mShowDistinctWeekendColor ? mPastWeekendBackgroundPaint : mPastBackgroundPaint; + Paint futurePaint = isWeekend && mShowDistinctWeekendColor ? mFutureWeekendBackgroundPaint : mFutureBackgroundPaint; + float startY = mHeaderTextHeight + mHeaderRowPadding * 2 + mTimeTextHeight/2 + mHeaderMarginBottom + mCurrentOrigin.y; + + if (sameDay){ + Calendar now = Calendar.getInstance(); + float beforeNow = (now.get(Calendar.HOUR_OF_DAY) + now.get(Calendar.MINUTE)/60.0f) * mHourHeight; + canvas.drawRect(start, startY, startPixel + mWidthPerDay, startY+beforeNow, pastPaint); + canvas.drawRect(start, startY+beforeNow, startPixel + mWidthPerDay, getHeight(), futurePaint); + } + else if (day.before(today)) { + canvas.drawRect(start, startY, startPixel + mWidthPerDay, getHeight(), pastPaint); + } + else { + canvas.drawRect(start, startY, startPixel + mWidthPerDay, getHeight(), futurePaint); + } + } + else { + canvas.drawRect(start, mHeaderTextHeight + mHeaderRowPadding * 2 + mTimeTextHeight / 2 + mHeaderMarginBottom, startPixel + mWidthPerDay, getHeight(), sameDay ? mTodayBackgroundPaint : mDayBackgroundPaint); + } + } // Prepare the separator lines for hours. int i = 0; @@ -478,6 +617,14 @@ private void drawHeaderRowAndEvents(Canvas canvas) { // Draw the events. drawEvents(day, startPixel, canvas); + // Draw the line at the current time. + if (mShowNowLine && sameDay){ + float startY = mHeaderTextHeight + mHeaderRowPadding * 2 + mTimeTextHeight/2 + mHeaderMarginBottom + mCurrentOrigin.y; + Calendar now = Calendar.getInstance(); + float beforeNow = (now.get(Calendar.HOUR_OF_DAY) + now.get(Calendar.MINUTE)/60.0f) * mHourHeight; + canvas.drawLine(start, startY + beforeNow, startPixel + mWidthPerDay, startY + beforeNow, mNowLinePaint); + } + // In the next iteration, start from the next day. startPixel += mWidthPerDay + mColumnGap; } @@ -489,9 +636,9 @@ private void drawHeaderRowAndEvents(Canvas canvas) { startPixel = startFromPixel; for (int dayNumber=leftDaysWithGaps+1; dayNumber <= leftDaysWithGaps + mNumberOfVisibleDays + 1; dayNumber++) { // Check if the day is today. - day = (Calendar) mToday.clone(); + day = (Calendar) today.clone(); day.add(Calendar.DATE, dayNumber - 1); - boolean sameDay = isSameDay(day, mToday); + boolean sameDay = isSameDay(day, today); // Draw the day labels. String dayLabel = getDateTimeInterpreter().interpretDate(day); @@ -517,9 +664,8 @@ private Calendar getTimeFromPoint(float x, float y){ dayNumber <= leftDaysWithGaps + mNumberOfVisibleDays + 1; dayNumber++) { float start = (startPixel < mHeaderColumnWidth ? mHeaderColumnWidth : startPixel); - if (mWidthPerDay + startPixel - start> 0 - && x>start && x 0 && x > start && x < startPixel + mWidthPerDay){ + Calendar day = today(); day.add(Calendar.DATE, dayNumber - 1); float pixelsFromZero = y - mCurrentOrigin.y - mHeaderTextHeight - mHeaderRowPadding * 2 - mTimeTextHeight/2 - mHeaderMarginBottom; @@ -547,9 +693,6 @@ private void drawEvents(Calendar date, float startFromPixel, Canvas canvas) { // Calculate top. float top = mHourHeight * 24 * mEventRects.get(i).top / 1440 + mCurrentOrigin.y + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight/2 + mEventMarginVertical; - float originalTop = top; - if (top < mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight/2) - top = mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight/2; // Calculate bottom. float bottom = mEventRects.get(i).bottom; @@ -559,11 +702,9 @@ private void drawEvents(Calendar date, float startFromPixel, Canvas canvas) { float left = startFromPixel + mEventRects.get(i).left * mWidthPerDay; if (left < startFromPixel) left += mOverlappingEventGap; - float originalLeft = left; float right = left + mEventRects.get(i).width * mWidthPerDay; if (right < startFromPixel + mWidthPerDay) right -= mOverlappingEventGap; - if (left < mHeaderColumnWidth) left = mHeaderColumnWidth; // Draw the event and the event name on top of it. RectF eventRectF = new RectF(left, top, right, bottom); @@ -576,8 +717,8 @@ eventRectF.top < getHeight() && ) { mEventRects.get(i).rectF = eventRectF; mEventBackgroundPaint.setColor(mEventRects.get(i).event.getColor() == 0 ? mDefaultEventColor : mEventRects.get(i).event.getColor()); - canvas.drawRect(mEventRects.get(i).rectF, mEventBackgroundPaint); - drawText(mEventRects.get(i).event.getName(), mEventRects.get(i).rectF, canvas, originalTop, originalLeft); + canvas.drawRoundRect(mEventRects.get(i).rectF, mEventCornerRadius, mEventCornerRadius, mEventBackgroundPaint); + drawEventTitle(mEventRects.get(i).event, mEventRects.get(i).rectF, canvas, top, left); } else mEventRects.get(i).rectF = null; @@ -589,37 +730,56 @@ eventRectF.top < getHeight() && /** * Draw the name of the event on top of the event rectangle. - * @param text The text to draw. + * @param event The event of which the title (and location) should be drawn. * @param rect The rectangle on which the text is to be drawn. * @param canvas The canvas to draw upon. * @param originalTop The original top position of the rectangle. The rectangle may have some of its portion outside of the visible area. * @param originalLeft The original left position of the rectangle. The rectangle may have some of its portion outside of the visible area. */ - private void drawText(String text, RectF rect, Canvas canvas, float originalTop, float originalLeft) { + private void drawEventTitle(WeekViewEvent event, RectF rect, Canvas canvas, float originalTop, float originalLeft) { if (rect.right - rect.left - mEventPadding * 2 < 0) return; + if (rect.bottom - rect.top - mEventPadding * 2 < 0) return; + + // Prepare the name of the event. + SpannableStringBuilder bob = new SpannableStringBuilder(); + if (event.getName() != null) { + bob.append(event.getName()); + bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); + bob.append(' '); + } - // Get text dimensions - StaticLayout textLayout = new StaticLayout(text, mEventTextPaint, (int) (rect.right - originalLeft - mEventPadding * 2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + // Prepare the location of the event. + if (event.getLocation() != null) { + bob.append(event.getLocation()); + } - // Crop height int availableHeight = (int) (rect.bottom - originalTop - mEventPadding * 2); + int availableWidth = (int) (rect.right - originalLeft - mEventPadding * 2); + + // Get text dimensions. + StaticLayout textLayout = new StaticLayout(bob, mEventTextPaint, availableWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + int lineHeight = textLayout.getHeight() / textLayout.getLineCount(); - if (lineHeight < availableHeight && textLayout.getHeight() > rect.height() - mEventPadding * 2) { - int lineCount = textLayout.getLineCount(); - int availableLineCount = (int) Math.floor(lineCount * availableHeight / textLayout.getHeight()); - float widthAvailable = (rect.right - originalLeft - mEventPadding * 2) * availableLineCount; - textLayout = new StaticLayout(TextUtils.ellipsize(text, mEventTextPaint, widthAvailable, TextUtils.TruncateAt.END), mEventTextPaint, (int) (rect.right - originalLeft - mEventPadding * 2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - } - else if (lineHeight >= availableHeight) { - int width = (int) (rect.right - originalLeft - mEventPadding * 2); - textLayout = new StaticLayout(TextUtils.ellipsize(text, mEventTextPaint, width, TextUtils.TruncateAt.END), mEventTextPaint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 1.0f, false); - } - // Draw text - canvas.save(); - canvas.translate(originalLeft + mEventPadding, originalTop + mEventPadding); - textLayout.draw(canvas); - canvas.restore(); + if (availableHeight >= lineHeight) { + // Calculate available number of line counts. + int availableLineCount = availableHeight / lineHeight; + do { + // Ellipsize text to fit into event rect. + textLayout = new StaticLayout(TextUtils.ellipsize(bob, mEventTextPaint, availableLineCount * availableWidth, TextUtils.TruncateAt.END), mEventTextPaint, (int) (rect.right - originalLeft - mEventPadding * 2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + + // Reduce line count. + availableLineCount--; + + // Repeat until text is short enough. + } while (textLayout.getHeight() > availableHeight); + + // Draw text. + canvas.save(); + canvas.translate(originalLeft + mEventPadding, originalTop + mEventPadding); + textLayout.draw(canvas); + canvas.restore(); + } } @@ -667,80 +827,88 @@ public EventRect(WeekViewEvent event, WeekViewEvent originalEvent, RectF rectF) */ private void getMoreEvents(Calendar day) { - // Delete all events if its not current month +- 1. - deleteFarMonths(day); - // Get more events if the month is changed. if (mEventRects == null) mEventRects = new ArrayList(); - if (mMonthChangeListener == null && !isInEditMode()) + if (mWeekViewLoader == null && !isInEditMode()) throw new IllegalStateException("You must provide a MonthChangeListener"); // If a refresh was requested then reset some variables. if (mRefreshEvents) { mEventRects.clear(); - mFetchedMonths = new int[3]; - } - - // Get events of previous month. - int previousMonth = (day.get(Calendar.MONTH) == 0?12:day.get(Calendar.MONTH)); - int nextMonth = (day.get(Calendar.MONTH)+2 == 13 ?1:day.get(Calendar.MONTH)+2); - int[] lastFetchedMonth = mFetchedMonths.clone(); - if (mFetchedMonths[0] < 1 || mFetchedMonths[0] != previousMonth || mRefreshEvents) { - if (!containsValue(lastFetchedMonth, previousMonth) && !isInEditMode()){ - List events = mMonthChangeListener.onMonthChange((previousMonth==12)?day.get(Calendar.YEAR)-1:day.get(Calendar.YEAR), previousMonth); - sortEvents(events); - for (WeekViewEvent event: events) { - cacheEvent(event); - } - } - mFetchedMonths[0] = previousMonth; + mPreviousPeriodEvents = null; + mCurrentPeriodEvents = null; + mNextPeriodEvents = null; + mFetchedPeriod = -1; } - // Get events of this month. - if (mFetchedMonths[1] < 1 || mFetchedMonths[1] != day.get(Calendar.MONTH)+1 || mRefreshEvents) { - if (!containsValue(lastFetchedMonth, day.get(Calendar.MONTH)+1) && !isInEditMode()) { - List events = mMonthChangeListener.onMonthChange(day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1); - sortEvents(events); - for (WeekViewEvent event : events) { - cacheEvent(event); - } - } - mFetchedMonths[1] = day.get(Calendar.MONTH)+1; - } - - // Get events of next month. - if (mFetchedMonths[2] < 1 || mFetchedMonths[2] != nextMonth || mRefreshEvents) { - if (!containsValue(lastFetchedMonth, nextMonth) && !isInEditMode()) { - List events = mMonthChangeListener.onMonthChange(nextMonth == 1 ? day.get(Calendar.YEAR) + 1 : day.get(Calendar.YEAR), nextMonth); - sortEvents(events); - for (WeekViewEvent event : events) { - cacheEvent(event); + if (mWeekViewLoader != null){ + int periodToFetch = (int) mWeekViewLoader.toWeekViewPeriodIndex(day); + if (!isInEditMode() && (mFetchedPeriod < 0 || mFetchedPeriod != periodToFetch || mRefreshEvents)) { + List previousPeriodEvents = null; + List currentPeriodEvents = null; + List nextPeriodEvents = null; + + if (mPreviousPeriodEvents != null && mCurrentPeriodEvents != null && mNextPeriodEvents != null){ + if (periodToFetch == mFetchedPeriod-1){ + currentPeriodEvents = mPreviousPeriodEvents; + nextPeriodEvents = mCurrentPeriodEvents; + } + else if (periodToFetch == mFetchedPeriod){ + previousPeriodEvents = mPreviousPeriodEvents; + currentPeriodEvents = mCurrentPeriodEvents; + nextPeriodEvents = mNextPeriodEvents; + } + else if (periodToFetch == mFetchedPeriod+1){ + previousPeriodEvents = mCurrentPeriodEvents; + currentPeriodEvents = mNextPeriodEvents; + } } + if (currentPeriodEvents == null) + currentPeriodEvents = mWeekViewLoader.onLoad(periodToFetch); + if (previousPeriodEvents == null) + previousPeriodEvents = mWeekViewLoader.onLoad(periodToFetch-1); + if (nextPeriodEvents == null) + nextPeriodEvents = mWeekViewLoader.onLoad(periodToFetch+1); + + + // Clear events. + mEventRects.clear(); + sortAndCacheEvents(previousPeriodEvents); + sortAndCacheEvents(currentPeriodEvents); + sortAndCacheEvents(nextPeriodEvents); + + mPreviousPeriodEvents = previousPeriodEvents; + mCurrentPeriodEvents = currentPeriodEvents; + mNextPeriodEvents = nextPeriodEvents; + mFetchedPeriod = periodToFetch; } - mFetchedMonths[2] = nextMonth; } // Prepare to calculate positions of each events. - ArrayList tempEvents = new ArrayList(mEventRects); + List tempEvents = mEventRects; mEventRects = new ArrayList(); - Calendar dayCounter = (Calendar) day.clone(); - dayCounter.add(Calendar.MONTH, -1); - dayCounter.set(Calendar.DAY_OF_MONTH, 1); - Calendar maxDay = (Calendar) day.clone(); - maxDay.add(Calendar.MONTH, 1); - maxDay.set(Calendar.DAY_OF_MONTH, maxDay.getActualMaximum(Calendar.DAY_OF_MONTH)); - - // Iterate through each day to calculate the position of the events. - while (dayCounter.getTimeInMillis() <= maxDay.getTimeInMillis()) { - ArrayList eventRects = new ArrayList(); - for (EventRect eventRect : tempEvents) { - if (isSameDay(eventRect.event.getStartTime(), dayCounter)) - eventRects.add(eventRect); - } + // Iterate through each day with events to calculate the position of the events. + while (tempEvents.size() > 0) { + ArrayList eventRects = new ArrayList<>(tempEvents.size()); + + // Get first event for a day. + EventRect eventRect1 = tempEvents.remove(0); + eventRects.add(eventRect1); + + int i = 0; + while (i < tempEvents.size()) { + // Collect all other events for same day. + EventRect eventRect2 = tempEvents.get(i); + if (isSameDay(eventRect1.event.getStartTime(), eventRect2.event.getStartTime())) { + tempEvents.remove(i); + eventRects.add(eventRect2); + } else { + i++; + } + } computePositionOfEvents(eventRects); - dayCounter.add(Calendar.DATE, 1); } } @@ -750,21 +918,54 @@ private void getMoreEvents(Calendar day) { */ private void cacheEvent(WeekViewEvent event) { if (!isSameDay(event.getStartTime(), event.getEndTime())) { + // Add first day. Calendar endTime = (Calendar) event.getStartTime().clone(); endTime.set(Calendar.HOUR_OF_DAY, 23); endTime.set(Calendar.MINUTE, 59); + WeekViewEvent event1 = new WeekViewEvent(event.getId(), event.getName(), event.getLocation(), event.getStartTime(), endTime); + event1.setColor(event.getColor()); + mEventRects.add(new EventRect(event1, event, null)); + + // Add other days. + Calendar otherDay = (Calendar) event.getStartTime().clone(); + otherDay.add(Calendar.DATE, 1); + while (!isSameDay(otherDay, event.getEndTime())) { + Calendar overDay = (Calendar) otherDay.clone(); + overDay.set(Calendar.HOUR_OF_DAY, 0); + overDay.set(Calendar.MINUTE, 0); + Calendar endOfOverDay = (Calendar) overDay.clone(); + endOfOverDay.set(Calendar.HOUR_OF_DAY, 23); + endOfOverDay.set(Calendar.MINUTE, 59); + WeekViewEvent eventMore = new WeekViewEvent(event.getId(), event.getName(), overDay, endOfOverDay); + eventMore.setColor(event.getColor()); + mEventRects.add(new EventRect(eventMore, event, null)); + + // Add next day. + otherDay.add(Calendar.DATE, 1); + } + + // Add last day. Calendar startTime = (Calendar) event.getEndTime().clone(); startTime.set(Calendar.HOUR_OF_DAY, 0); startTime.set(Calendar.MINUTE, 0); - WeekViewEvent event1 = new WeekViewEvent(event.getId(), event.getName(), event.getStartTime(), endTime); - event1.setColor(event.getColor()); - WeekViewEvent event2 = new WeekViewEvent(event.getId(), event.getName(), startTime, event.getEndTime()); + WeekViewEvent event2 = new WeekViewEvent(event.getId(), event.getName(), event.getLocation(), startTime, event.getEndTime()); event2.setColor(event.getColor()); - mEventRects.add(new EventRect(event1, event, null)); mEventRects.add(new EventRect(event2, event, null)); } - else + else { mEventRects.add(new EventRect(event, event, null)); + } + } + + /** + * Sort and cache events. + * @param events The events to be sorted and cached. + */ + private void sortAndCacheEvents(List events) { + sortEvents(events); + for (WeekViewEvent event : events) { + cacheEvent(event); + } } /** @@ -899,37 +1100,6 @@ private boolean isTimeAfterOrEquals(Calendar time1, Calendar time2) { return !(time1 == null || time2 == null) && time1.getTimeInMillis() >= time2.getTimeInMillis(); } - /** - * Deletes the events of the months that are too far away from the current month. - * @param currentDay The current day. - */ - private void deleteFarMonths(Calendar currentDay) { - - if (mEventRects == null) return; - - Calendar nextMonth = (Calendar) currentDay.clone(); - nextMonth.add(Calendar.MONTH, 1); - nextMonth.set(Calendar.DAY_OF_MONTH, nextMonth.getActualMaximum(Calendar.DAY_OF_MONTH)); - nextMonth.set(Calendar.HOUR_OF_DAY, 12); - nextMonth.set(Calendar.MINUTE, 59); - nextMonth.set(Calendar.SECOND, 59); - - Calendar prevMonth = (Calendar) currentDay.clone(); - prevMonth.add(Calendar.MONTH, -1); - prevMonth.set(Calendar.DAY_OF_MONTH, 1); - prevMonth.set(Calendar.HOUR_OF_DAY, 0); - prevMonth.set(Calendar.MINUTE, 0); - prevMonth.set(Calendar.SECOND, 0); - - List newEvents = new ArrayList(); - for (EventRect eventRect : mEventRects) { - boolean isFarMonth = eventRect.event.getStartTime().getTimeInMillis() > nextMonth.getTimeInMillis() || eventRect.event.getEndTime().getTimeInMillis() < prevMonth.getTimeInMillis(); - if (!isFarMonth) newEvents.add(eventRect); - } - mEventRects.clear(); - mEventRects.addAll(newEvents); - } - @Override public void invalidate() { super.invalidate(); @@ -950,12 +1120,34 @@ public EventClickListener getEventClickListener() { return mEventClickListener; } - public MonthChangeListener getMonthChangeListener() { - return mMonthChangeListener; + public @Nullable MonthLoader.MonthChangeListener getMonthChangeListener() { + if (mWeekViewLoader instanceof MonthLoader) + return ((MonthLoader) mWeekViewLoader).getOnMonthChangeListener(); + return null; + } + + public void setMonthChangeListener(MonthLoader.MonthChangeListener monthChangeListener) { + this.mWeekViewLoader = new MonthLoader(monthChangeListener); } - public void setMonthChangeListener(MonthChangeListener monthChangeListener) { - this.mMonthChangeListener = monthChangeListener; + /** + * Get event loader in the week view. Event loaders define the interval after which the events + * are loaded in week view. For a MonthLoader events are loaded for every month. You can define + * your custom event loader by extending WeekViewLoader. + * @return The event loader. + */ + public WeekViewLoader getWeekViewLoader(){ + return mWeekViewLoader; + } + + /** + * Set event loader in the week view. For example, a MonthLoader. Event loaders define the + * interval after which the events are loaded in week view. For a MonthLoader events are loaded + * for every month. You can define your custom event loader by extending WeekViewLoader. + * @param loader The event loader. + */ + public void setWeekViewLoader(WeekViewLoader loader){ + this.mWeekViewLoader = loader; } public EventLongPressListener getEventLongPressListener() { @@ -989,6 +1181,7 @@ public void setScrollListener(ScrollListener scrolledListener){ public ScrollListener getScrollListener(){ return mScrollListener; } + /** * Get the interpreter which provides the text to show in the header column and the header row. * @return The date, time interpreter. @@ -998,12 +1191,10 @@ public DateTimeInterpreter getDateTimeInterpreter() { mDateTimeInterpreter = new DateTimeInterpreter() { @Override public String interpretDate(Calendar date) { - SimpleDateFormat sdf; - sdf = mDayNameLength == LENGTH_SHORT ? new SimpleDateFormat("EEEEE") : new SimpleDateFormat("EEE"); - try{ - String dayName = sdf.format(date.getTime()).toUpperCase(); - return String.format("%s %d/%02d", dayName, date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH)); - }catch (Exception e){ + try { + SimpleDateFormat sdf = mDayNameLength == LENGTH_SHORT ? new SimpleDateFormat("EEEEE M/dd", Locale.getDefault()) : new SimpleDateFormat("EEE M/dd", Locale.getDefault()); + return sdf.format(date.getTime()).toUpperCase(); + } catch (Exception e) { e.printStackTrace(); return ""; } @@ -1011,12 +1202,17 @@ public String interpretDate(Calendar date) { @Override public String interpretTime(int hour) { - String amPm; - if (hour >= 0 && hour < 12) amPm = "AM"; - else amPm = "PM"; - if (hour == 0) hour = 12; - if (hour > 12) hour -= 12; - return String.format("%02d %s", hour, amPm); + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + + try { + SimpleDateFormat sdf = DateFormat.is24HourFormat(getContext()) ? new SimpleDateFormat("HH:mm", Locale.getDefault()) : new SimpleDateFormat("hh a", Locale.getDefault()); + return sdf.format(calendar.getTime()); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } } }; } @@ -1029,6 +1225,9 @@ public String interpretTime(int hour) { */ public void setDateTimeInterpreter(DateTimeInterpreter dateTimeInterpreter){ this.mDateTimeInterpreter = dateTimeInterpreter; + + // Refresh time column width. + initTextTimeWidth(); } @@ -1056,7 +1255,7 @@ public int getHourHeight() { } public void setHourHeight(int hourHeight) { - mHourHeight = hourHeight; + mNewHourHeight = hourHeight; invalidate(); } @@ -1269,6 +1468,19 @@ public void setOverlappingEventGap(int overlappingEventGap) { invalidate(); } + public int getEventCornerRadius() { + return mEventCornerRadius; + } + + /** + * Set corner radius for event rect. + * + * @param eventCornerRadius the radius in px. + */ + public void setEventCornerRadius(int eventCornerRadius) { + mEventCornerRadius = eventCornerRadius; + } + public int getEventMarginVertical() { return mEventMarginVertical; } @@ -1314,6 +1526,103 @@ public float getXScrollingSpeed() { public void setXScrollingSpeed(float xScrollingSpeed) { this.mXScrollingSpeed = xScrollingSpeed; } + + /** + * Whether weekends should have a background color different from the normal day background + * color. The weekend background colors are defined by the attributes + * `futureWeekendBackgroundColor` and `pastWeekendBackgroundColor`. + * @return True if weekends should have different background colors. + */ + public boolean isShowDistinctWeekendColor() { + return mShowDistinctWeekendColor; + } + + /** + * Set whether weekends should have a background color different from the normal day background + * color. The weekend background colors are defined by the attributes + * `futureWeekendBackgroundColor` and `pastWeekendBackgroundColor`. + * @param showDistinctWeekendColor True if weekends should have different background colors. + */ + public void setShowDistinctWeekendColor(boolean showDistinctWeekendColor) { + this.mShowDistinctWeekendColor = showDistinctWeekendColor; + invalidate(); + } + + /** + * Whether past and future days should have two different background colors. The past and + * future day colors are defined by the attributes `futureBackgroundColor` and + * `pastBackgroundColor`. + * @return True if past and future days should have two different background colors. + */ + public boolean isShowDistinctPastFutureColor() { + return mShowDistinctPastFutureColor; + } + + /** + * Set whether weekends should have a background color different from the normal day background + * color. The past and future day colors are defined by the attributes `futureBackgroundColor` + * and `pastBackgroundColor`. + * @param showDistinctPastFutureColor True if past and future should have two different + * background colors. + */ + public void setShowDistinctPastFutureColor(boolean showDistinctPastFutureColor) { + this.mShowDistinctPastFutureColor = showDistinctPastFutureColor; + invalidate(); + } + + /** + * Get whether "now" line should be displayed. "Now" line is defined by the attributes + * `nowLineColor` and `nowLineThickness`. + * @return True if "now" line should be displayed. + */ + public boolean isShowNowLine() { + return mShowNowLine; + } + + /** + * Set whether "now" line should be displayed. "Now" line is defined by the attributes + * `nowLineColor` and `nowLineThickness`. + * @param showNowLine True if "now" line should be displayed. + */ + public void setShowNowLine(boolean showNowLine) { + this.mShowNowLine = showNowLine; + invalidate(); + } + + /** + * Get the "now" line color. + * @return The color of the "now" line. + */ + public int getNowLineColor() { + return mNowLineColor; + } + + /** + * Set the "now" line color. + * @param nowLineColor The color of the "now" line. + */ + public void setNowLineColor(int nowLineColor) { + this.mNowLineColor = nowLineColor; + invalidate(); + } + + /** + * Get the "now" line thickness. + * @return The thickness of the "now" line. + */ + public int getNowLineThickness() { + return mNowLineThickness; + } + + /** + * Set the "now" line thickness. + * @param nowLineThickness The thickness of the "now" line. + */ + public void setNowLineThickness(int nowLineThickness) { + this.mNowLineThickness = nowLineThickness; + invalidate(); + } + ///////////////////////////////////////////////////////////////// // // Functions related to scrolling. @@ -1322,45 +1631,53 @@ public void setXScrollingSpeed(float xScrollingSpeed) { @Override public boolean onTouchEvent(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_UP) { + mScaleDetector.onTouchEvent(event); + boolean val = mGestureDetector.onTouchEvent(event); + // Check after call of mGestureDetector, so mCurrentFlingDirection and mCurrentScrollDirection are set. + if (event.getAction() == MotionEvent.ACTION_UP && !mIsZooming && mCurrentFlingDirection == Direction.NONE) { if (mCurrentScrollDirection == Direction.HORIZONTAL) { - float leftDays = Math.round(mCurrentOrigin.x / (mWidthPerDay + mColumnGap)); - int nearestOrigin = (int) (mCurrentOrigin.x - leftDays * (mWidthPerDay+mColumnGap)); - mStickyScroller.startScroll((int) mCurrentOrigin.x, 0, - nearestOrigin, 0); - ViewCompat.postInvalidateOnAnimation(WeekView.this); + goToNearestOrigin(); } mCurrentScrollDirection = Direction.NONE; } - return mGestureDetector.onTouchEvent(event); + + return val; + } + + private void goToNearestOrigin(){ + float leftDays = Math.round(mCurrentOrigin.x / (mWidthPerDay + mColumnGap)); + + int nearestOrigin = (int) (mCurrentOrigin.x - leftDays * (mWidthPerDay + mColumnGap)); + + if (nearestOrigin != 0) { + // Stop current animation. + mScroller.forceFinished(true); + // Snap to date. + mScroller.startScroll((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, -nearestOrigin, 0, 50); + ViewCompat.postInvalidateOnAnimation(WeekView.this); + } + // Reset scrolling and fling direction. + mCurrentScrollDirection = mCurrentFlingDirection = Direction.NONE; } @Override public void computeScroll() { super.computeScroll(); - if (mScroller.computeScrollOffset()) { - if (Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) < mWidthPerDay + mColumnGap && Math.abs(mScroller.getFinalX() - mScroller.getStartX()) != 0) { - mScroller.forceFinished(true); - float leftDays = Math.round(mCurrentOrigin.x / (mWidthPerDay + mColumnGap)); - if(mScroller.getFinalX() < mScroller.getCurrX()) - leftDays--; - else - leftDays++; - int nearestOrigin = (int) (mCurrentOrigin.x - leftDays * (mWidthPerDay+mColumnGap)); - mStickyScroller.startScroll((int) mCurrentOrigin.x, 0, - nearestOrigin, 0); - ViewCompat.postInvalidateOnAnimation(WeekView.this); + + if (mScroller.isFinished()) { + if (mCurrentFlingDirection != Direction.NONE) { + // Snap to day after fling is finished. + goToNearestOrigin(); } - else { - if (mCurrentFlingDirection == Direction.VERTICAL) mCurrentOrigin.y = mScroller.getCurrY(); - else mCurrentOrigin.x = mScroller.getCurrX(); + } else { + if (mScroller.computeScrollOffset()) { + mCurrentOrigin.y = mScroller.getCurrY(); + mCurrentOrigin.x = mScroller.getCurrX(); ViewCompat.postInvalidateOnAnimation(this); } } - if (mStickyScroller.computeScrollOffset()) { - mCurrentOrigin.x = mStickyScroller.getCurrX(); - ViewCompat.postInvalidateOnAnimation(this); - } } @@ -1384,6 +1701,8 @@ public void goToToday() { */ public void goToDate(Calendar date) { mScroller.forceFinished(true); + mCurrentScrollDirection = mCurrentFlingDirection = Direction.NONE; + date.set(Calendar.HOUR_OF_DAY, 0); date.set(Calendar.MINUTE, 0); date.set(Calendar.SECOND, 0); @@ -1402,12 +1721,11 @@ public void goToDate(Calendar date) { today.set(Calendar.SECOND, 0); today.set(Calendar.MILLISECOND, 0); + long day = 1000L * 60L * 60L * 24L; long dateInMillis = date.getTimeInMillis() + date.getTimeZone().getOffset(date.getTimeInMillis()); long todayInMillis = today.getTimeInMillis() + today.getTimeZone().getOffset(today.getTimeInMillis()); - int dateDifference = (int) ((dateInMillis - todayInMillis) / (1000 * 60 * 60 * 24)); - + long dateDifference = (dateInMillis/day) - (todayInMillis/day); mCurrentOrigin.x = - dateDifference * (mWidthPerDay + mColumnGap); - // mStickyScroller.startScroll((int) mCurrentOrigin.x, 0, (int) (-dateDifference*(mWidthPerDay + mColumnGap)-mCurrentOrigin.x), 0); invalidate(); } @@ -1424,16 +1742,18 @@ public void notifyDatasetChanged(){ * @param hour The hour to scroll to in 24-hour format. Supported values are 0-24. */ public void goToHour(double hour){ - int verticalOffset = (int) (mHourHeight * hour); - if (hour < 0) - verticalOffset = 0; - else if (hour > 24) - verticalOffset = mHourHeight * 24; - if (mAreDimensionsInvalid) { mScrollToHour = hour; return; - } else if (verticalOffset > mHourHeight * 24 - getHeight() + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom) + } + + int verticalOffset = 0; + if (hour > 24) + verticalOffset = mHourHeight * 24; + else if (hour > 0) + verticalOffset = (int) (mHourHeight * hour); + + if (verticalOffset > mHourHeight * 24 - getHeight() + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom) verticalOffset = (int)(mHourHeight * 24 - getHeight() + mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom); mCurrentOrigin.y = -verticalOffset; @@ -1457,23 +1777,37 @@ public double getFirstVisibleHour(){ ///////////////////////////////////////////////////////////////// public interface EventClickListener { - public void onEventClick(WeekViewEvent event, RectF eventRect); - } - - public interface MonthChangeListener { - public List onMonthChange(int newYear, int newMonth); + /** + * Triggered when clicked on one existing event + * @param event: event clicked. + * @param eventRect: view containing the clicked event. + */ + void onEventClick(WeekViewEvent event, RectF eventRect); } public interface EventLongPressListener { - public void onEventLongPress(WeekViewEvent event, RectF eventRect); + /** + * Similar to {@link com.alamkanak.weekview.WeekView.EventClickListener} but with a long press. + * @param event: event clicked. + * @param eventRect: view containing the clicked event. + */ + void onEventLongPress(WeekViewEvent event, RectF eventRect); } public interface EmptyViewClickListener { - public void onEmptyViewClicked(Calendar time); + /** + * Triggered when the users clicks on a empty space of the calendar. + * @param time: {@link Calendar} object set with the date and time of the clicked position on the view. + */ + void onEmptyViewClicked(Calendar time); } public interface EmptyViewLongPressListener { - public void onEmptyViewLongPress(Calendar time); + /** + * Similar to {@link com.alamkanak.weekview.WeekView.EmptyViewClickListener} but with long press. + * @param time: {@link Calendar} object set with the date and time of the long pressed position on the view. + */ + void onEmptyViewLongPress(Calendar time); } public interface ScrollListener { @@ -1484,7 +1818,7 @@ public interface ScrollListener { * @param newFirstVisibleDay The new first visible day * @param oldFirstVisibleDay The old first visible day (is null on the first call). */ - public void onFirstVisibleDayChanged(Calendar newFirstVisibleDay, Calendar oldFirstVisibleDay); + void onFirstVisibleDayChanged(Calendar newFirstVisibleDay, Calendar oldFirstVisibleDay); } @@ -1494,20 +1828,6 @@ public interface ScrollListener { // ///////////////////////////////////////////////////////////////// - /** - * Checks if an integer array contains a particular value. - * @param list The haystack. - * @param value The needle. - * @return True if the array contains the value. Otherwise returns false. - */ - private boolean containsValue(int[] list, int value) { - for (int i = 0; i < list.length; i++){ - if (list[i] == value) - return true; - } - return false; - } - /** * Checks if two times are on the same day. * @param dayOne The first day. @@ -1518,4 +1838,17 @@ private boolean isSameDay(Calendar dayOne, Calendar dayTwo) { return dayOne.get(Calendar.YEAR) == dayTwo.get(Calendar.YEAR) && dayOne.get(Calendar.DAY_OF_YEAR) == dayTwo.get(Calendar.DAY_OF_YEAR); } + /** + * Returns a calendar instance at the start of this day + * @return the calendar instance + */ + private Calendar today(){ + Calendar today = Calendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + return today; + } + } diff --git a/library/src/main/java/com/alamkanak/weekview/WeekViewEvent.java b/library/src/main/java/com/alamkanak/weekview/WeekViewEvent.java index ce1f15c76..e0d4765a8 100644 --- a/library/src/main/java/com/alamkanak/weekview/WeekViewEvent.java +++ b/library/src/main/java/com/alamkanak/weekview/WeekViewEvent.java @@ -11,6 +11,7 @@ public class WeekViewEvent { private Calendar mStartTime; private Calendar mEndTime; private String mName; + private String mLocation; private int mColor; public WeekViewEvent(){ @@ -56,16 +57,29 @@ public WeekViewEvent(long id, String name, int startYear, int startMonth, int st * Initializes the event for week view. * @param id The id of the event. * @param name Name of the event. + * @param location The location of the event. * @param startTime The time when the event starts. * @param endTime The time when the event ends. */ - public WeekViewEvent(long id, String name, Calendar startTime, Calendar endTime) { + public WeekViewEvent(long id, String name, String location, Calendar startTime, Calendar endTime) { this.mId = id; this.mName = name; + this.mLocation = location; this.mStartTime = startTime; this.mEndTime = endTime; } + /** + * Initializes the event for week view. + * @param id The id of the event. + * @param name Name of the event. + * @param startTime The time when the event starts. + * @param endTime The time when the event ends. + */ + public WeekViewEvent(long id, String name, Calendar startTime, Calendar endTime) { + this(id, name, null, startTime, endTime); + } + public Calendar getStartTime() { return mStartTime; @@ -91,6 +105,14 @@ public void setName(String name) { this.mName = name; } + public String getLocation() { + return mLocation; + } + + public void setLocation(String location) { + this.mLocation = location; + } + public int getColor() { return mColor; } @@ -106,4 +128,20 @@ public long getId() { public void setId(long id) { this.mId = id; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WeekViewEvent that = (WeekViewEvent) o; + + return mId == that.mId; + + } + + @Override + public int hashCode() { + return (int) (mId ^ (mId >>> 32)); + } } diff --git a/library/src/main/java/com/alamkanak/weekview/WeekViewLoader.java b/library/src/main/java/com/alamkanak/weekview/WeekViewLoader.java new file mode 100644 index 000000000..d19864318 --- /dev/null +++ b/library/src/main/java/com/alamkanak/weekview/WeekViewLoader.java @@ -0,0 +1,24 @@ +package com.alamkanak.weekview; + +import java.util.Calendar; +import java.util.List; + +public interface WeekViewLoader { + /** + * Convert a date into a double that will be used to reference when you're loading data. + * + * All periods that have the same integer part, define one period. Dates that are later in time + * should have a greater return value. + * + * @param instance the date + * @return The period index in which the date falls (floating point number). + */ + double toWeekViewPeriodIndex(Calendar instance); + + /** + * Load the events within the period + * @param periodIndex the period to load + * @return A list with the events of this period + */ + List onLoad(int periodIndex); +} diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index b94dd76dd..fccad1635 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -11,6 +11,8 @@ + + @@ -34,5 +36,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 9e37c882e..62228588d 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 21 - buildToolsVersion "21.1.1" + compileSdkVersion 23 + buildToolsVersion "23.0.1" defaultConfig { applicationId "com.alamkanak.weekview" minSdkVersion 9 - targetSdkVersion 21 + targetSdkVersion 23 versionCode 1 versionName "1.0" } @@ -22,5 +22,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':library') - compile 'com.android.support:appcompat-v7:19.1.0' + compile 'com.android.support:appcompat-v7:23.0.1' } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/MainActivity.java b/sample/src/main/java/com/alamkanak/weekview/sample/MainActivity.java index 724ba9c04..8ae323f34 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/MainActivity.java +++ b/sample/src/main/java/com/alamkanak/weekview/sample/MainActivity.java @@ -9,6 +9,7 @@ import android.widget.Toast; import com.alamkanak.weekview.DateTimeInterpreter; +import com.alamkanak.weekview.MonthLoader; import com.alamkanak.weekview.WeekView; import com.alamkanak.weekview.WeekViewEvent; @@ -23,7 +24,7 @@ * Created by Raquib-ul-Alam Kanak on 7/21/2014. * Website: http://alamkanak.github.io/ */ -public class MainActivity extends ActionBarActivity implements WeekView.MonthChangeListener, +public class MainActivity extends ActionBarActivity implements MonthLoader.MonthChangeListener, WeekView.EventClickListener, WeekView.EventLongPressListener { private static final int TYPE_DAY_VIEW = 1;