88import android .graphics .PointF ;
99import android .graphics .Rect ;
1010import android .graphics .RectF ;
11+ import android .graphics .Region ;
1112import android .graphics .Typeface ;
13+ import android .os .Build ;
1214import android .support .annotation .Nullable ;
1315import android .support .v4 .view .GestureDetectorCompat ;
1416import android .support .v4 .view .ViewCompat ;
17+ import android .support .v4 .view .animation .FastOutLinearInInterpolator ;
1518import android .text .Layout ;
1619import android .text .SpannableStringBuilder ;
1720import android .text .StaticLayout ;
2730import android .view .ScaleGestureDetector ;
2831import android .view .SoundEffectConstants ;
2932import android .view .View ;
33+ import android .view .ViewConfiguration ;
3034import android .widget .OverScroller ;
3135
3236import java .text .SimpleDateFormat ;
4347 */
4448public class WeekView extends View {
4549
50+ private enum Direction {
51+ NONE , LEFT , RIGHT , VERTICAL
52+ }
53+
4654 @ Deprecated
4755 public static final int LENGTH_SHORT = 1 ;
4856 @ Deprecated
@@ -72,9 +80,9 @@ public class WeekView extends View {
7280 private Paint mEventBackgroundPaint ;
7381 private float mHeaderColumnWidth ;
7482 private List <EventRect > mEventRects ;
75- private List <WeekViewEvent > mPreviousPeriodEvents ;
76- private List <WeekViewEvent > mCurrentPeriodEvents ;
77- private List <WeekViewEvent > mNextPeriodEvents ;
83+ private List <? extends WeekViewEvent > mPreviousPeriodEvents ;
84+ private List <? extends WeekViewEvent > mCurrentPeriodEvents ;
85+ private List <? extends WeekViewEvent > mNextPeriodEvents ;
7886 private TextPaint mEventTextPaint ;
7987 private Paint mHeaderColumnBackgroundPaint ;
8088 private int mFetchedPeriod = -1 ; // the middle period the calendar has fetched.
@@ -85,7 +93,8 @@ public class WeekView extends View {
8593 private Calendar mFirstVisibleDay ;
8694 private Calendar mLastVisibleDay ;
8795 private int mDefaultEventColor ;
88-
96+ private int mMinimumFlingVelocity = 0 ;
97+ private int mScaledTouchSlop = 0 ;
8998 // Attributes and their default values.
9099 private int mHourHeight = 50 ;
91100 private int mNewHourHeight = -1 ;
@@ -151,20 +160,40 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d
151160 if (mIsZooming )
152161 return true ;
153162
154- // Calculate the direction to scroll.
155- if (mCurrentScrollDirection == Direction .NONE ) {
156- // allow scrolling only in one direction
157- if (Math .abs (distanceX ) > Math .abs (distanceY )){
158- mCurrentScrollDirection = Direction .HORIZONTAL ;
163+ switch (mCurrentScrollDirection ) {
164+ case NONE : {
165+ // Allow scrolling only in one direction.
166+ if (Math .abs (distanceX ) > Math .abs (distanceY )) {
167+ if (distanceX > 0 ) {
168+ mCurrentScrollDirection = Direction .LEFT ;
169+ } else {
170+ mCurrentScrollDirection = Direction .RIGHT ;
171+ }
172+ } else {
173+ mCurrentScrollDirection = Direction .VERTICAL ;
174+ }
175+ break ;
159176 }
160- else {
161- mCurrentScrollDirection = Direction .VERTICAL ;
177+ case LEFT : {
178+ // Change direction if there was enough change.
179+ if (Math .abs (distanceX ) > Math .abs (distanceY ) && (distanceX < -mScaledTouchSlop )) {
180+ mCurrentScrollDirection = Direction .RIGHT ;
181+ }
182+ break ;
183+ }
184+ case RIGHT : {
185+ // Change direction if there was enough change.
186+ if (Math .abs (distanceX ) > Math .abs (distanceY ) && (distanceX > mScaledTouchSlop )) {
187+ mCurrentScrollDirection = Direction .LEFT ;
188+ }
189+ break ;
162190 }
163191 }
164192
165193 // Calculate the new origin after scroll.
166194 switch (mCurrentScrollDirection ) {
167- case HORIZONTAL :
195+ case LEFT :
196+ case RIGHT :
168197 mCurrentOrigin .x -= distanceX * mXScrollingSpeed ;
169198 ViewCompat .postInvalidateOnAnimation (WeekView .this );
170199 break ;
@@ -178,14 +207,20 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d
178207
179208 @ Override
180209 public boolean onFling (MotionEvent e1 , MotionEvent e2 , float velocityX , float velocityY ) {
210+ if (mIsZooming )
211+ return true ;
212+
181213 mScroller .forceFinished (true );
182214
183215 mCurrentFlingDirection = mCurrentScrollDirection ;
184- if (mCurrentFlingDirection == Direction .HORIZONTAL ){
185- 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 );
186- }
187- else if (mCurrentFlingDirection == Direction .VERTICAL ){
188- 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 );
216+ switch (mCurrentFlingDirection ) {
217+ case LEFT :
218+ case RIGHT :
219+ 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 );
220+ break ;
221+ case VERTICAL :
222+ 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 );
223+ break ;
189224 }
190225
191226 ViewCompat .postInvalidateOnAnimation (WeekView .this );
@@ -247,10 +282,6 @@ public void onLongPress(MotionEvent e) {
247282 }
248283 };
249284
250- private enum Direction {
251- NONE , HORIZONTAL , VERTICAL
252- }
253-
254285 public WeekView (Context context ) {
255286 this (context , null );
256287 }
@@ -314,7 +345,10 @@ public WeekView(Context context, AttributeSet attrs, int defStyleAttr) {
314345 private void init () {
315346 // Scrolling initialization.
316347 mGestureDetector = new GestureDetectorCompat (mContext , mGestureListener );
317- mScroller = new OverScroller (mContext );
348+ mScroller = new OverScroller (mContext , new FastOutLinearInInterpolator ());
349+
350+ mMinimumFlingVelocity = ViewConfiguration .get (mContext ).getScaledMinimumFlingVelocity ();
351+ mScaledTouchSlop = ViewConfiguration .get (mContext ).getScaledTouchSlop ();
318352
319353 // Measure settings for time column.
320354 mTimeTextPaint = new Paint (Paint .ANTI_ALIAS_FLAG );
@@ -438,23 +472,23 @@ private void initTextTimeWidth() {
438472 protected void onDraw (Canvas canvas ) {
439473 super .onDraw (canvas );
440474
475+ // Hide everything in the first cell (top left corner).
476+ canvas .drawRect (0 , 0 , mTimeTextWidth + mHeaderColumnPadding * 2 , mHeaderTextHeight + mHeaderRowPadding * 2 , mHeaderBackgroundPaint );
477+
441478 // Draw the header row.
442479 drawHeaderRowAndEvents (canvas );
443480
444481 // Draw the time column and all the axes/separators.
445482 drawTimeColumnAndAxes (canvas );
446-
447- // Hide everything in the first cell (top left corner).
448- canvas .drawRect (0 , 0 , mTimeTextWidth + mHeaderColumnPadding * 2 , mHeaderTextHeight + mHeaderRowPadding * 2 , mHeaderBackgroundPaint );
449-
450- // Hide anything that is in the bottom margin of the header row.
451- canvas .drawRect (mHeaderColumnWidth , mHeaderTextHeight + mHeaderRowPadding * 2 , getWidth (), mHeaderRowPadding * 2 + mHeaderTextHeight + mHeaderMarginBottom + mTimeTextHeight /2 , mHeaderColumnBackgroundPaint );
452483 }
453484
454485 private void drawTimeColumnAndAxes (Canvas canvas ) {
455486 // Draw the background color for the header column.
456487 canvas .drawRect (0 , mHeaderTextHeight + mHeaderRowPadding * 2 , mHeaderColumnWidth , getHeight (), mHeaderColumnBackgroundPaint );
457488
489+ // Clip to paint in left column only.
490+ canvas .clipRect (0 , mHeaderTextHeight + mHeaderRowPadding * 2 , mHeaderColumnWidth , getHeight (), Region .Op .REPLACE );
491+
458492 for (int i = 0 ; i < 24 ; i ++) {
459493 float top = mHeaderTextHeight + mHeaderRowPadding * 2 + mCurrentOrigin .y + mHourHeight * i + mHeaderMarginBottom ;
460494
@@ -544,6 +578,9 @@ else if (mNewHourHeight > mMaxHourHeight)
544578 }
545579 }
546580
581+ // Clip to paint events only.
582+ canvas .clipRect (mHeaderColumnWidth , mHeaderTextHeight + mHeaderRowPadding * 2 + mHeaderMarginBottom + mTimeTextHeight /2 , getWidth (), getHeight (), Region .Op .REPLACE );
583+
547584 // Iterate through each day.
548585 Calendar oldFirstVisibleDay = mFirstVisibleDay ;
549586 mFirstVisibleDay = (Calendar ) today .clone ();
@@ -629,6 +666,10 @@ else if (day.before(today)) {
629666 startPixel += mWidthPerDay + mColumnGap ;
630667 }
631668
669+
670+ // Clip to paint header row only.
671+ canvas .clipRect (mHeaderColumnWidth , 0 , getWidth (), mHeaderTextHeight + mHeaderRowPadding * 2 , Region .Op .REPLACE );
672+
632673 // Draw the header background.
633674 canvas .drawRect (0 , 0 , getWidth (), mHeaderTextHeight + mHeaderRowPadding * 2 , mHeaderBackgroundPaint );
634675
@@ -845,9 +886,9 @@ private void getMoreEvents(Calendar day) {
845886 if (mWeekViewLoader != null ){
846887 int periodToFetch = (int ) mWeekViewLoader .toWeekViewPeriodIndex (day );
847888 if (!isInEditMode () && (mFetchedPeriod < 0 || mFetchedPeriod != periodToFetch || mRefreshEvents )) {
848- List <WeekViewEvent > previousPeriodEvents = null ;
849- List <WeekViewEvent > currentPeriodEvents = null ;
850- List <WeekViewEvent > nextPeriodEvents = null ;
889+ List <? extends WeekViewEvent > previousPeriodEvents = null ;
890+ List <? extends WeekViewEvent > currentPeriodEvents = null ;
891+ List <? extends WeekViewEvent > nextPeriodEvents = null ;
851892
852893 if (mPreviousPeriodEvents != null && mCurrentPeriodEvents != null && mNextPeriodEvents != null ){
853894 if (periodToFetch == mFetchedPeriod -1 ){
@@ -917,6 +958,8 @@ else if (periodToFetch == mFetchedPeriod+1){
917958 * @param event The event to cache.
918959 */
919960 private void cacheEvent (WeekViewEvent event ) {
961+ if (event .getStartTime ().compareTo (event .getEndTime ()) >= 0 )
962+ return ;
920963 if (!isSameDay (event .getStartTime (), event .getEndTime ())) {
921964 // Add first day.
922965 Calendar endTime = (Calendar ) event .getStartTime ().clone ();
@@ -961,7 +1004,7 @@ private void cacheEvent(WeekViewEvent event) {
9611004 * Sort and cache events.
9621005 * @param events The events to be sorted and cached.
9631006 */
964- private void sortAndCacheEvents (List <WeekViewEvent > events ) {
1007+ private void sortAndCacheEvents (List <? extends WeekViewEvent > events ) {
9651008 sortEvents (events );
9661009 for (WeekViewEvent event : events ) {
9671010 cacheEvent (event );
@@ -972,7 +1015,7 @@ private void sortAndCacheEvents(List<WeekViewEvent> events) {
9721015 * Sorts the events in ascending order.
9731016 * @param events The events to be sorted.
9741017 */
975- private void sortEvents (List <WeekViewEvent > events ) {
1018+ private void sortEvents (List <? extends WeekViewEvent > events ) {
9761019 Collections .sort (events , new Comparator <WeekViewEvent >() {
9771020 @ Override
9781021 public int compare (WeekViewEvent event1 , WeekViewEvent event2 ) {
@@ -1636,7 +1679,7 @@ public boolean onTouchEvent(MotionEvent event) {
16361679
16371680 // Check after call of mGestureDetector, so mCurrentFlingDirection and mCurrentScrollDirection are set.
16381681 if (event .getAction () == MotionEvent .ACTION_UP && !mIsZooming && mCurrentFlingDirection == Direction .NONE ) {
1639- if (mCurrentScrollDirection == Direction .HORIZONTAL ) {
1682+ if (mCurrentScrollDirection == Direction .RIGHT || mCurrentScrollDirection == Direction . LEFT ) {
16401683 goToNearestOrigin ();
16411684 }
16421685 mCurrentScrollDirection = Direction .NONE ;
@@ -1646,15 +1689,29 @@ public boolean onTouchEvent(MotionEvent event) {
16461689 }
16471690
16481691 private void goToNearestOrigin (){
1649- float leftDays = Math .round (mCurrentOrigin .x / (mWidthPerDay + mColumnGap ));
1692+ double leftDays = mCurrentOrigin .x / (mWidthPerDay + mColumnGap );
1693+
1694+ if (mCurrentFlingDirection != Direction .NONE ) {
1695+ // snap to nearest day
1696+ leftDays = Math .round (leftDays );
1697+ } else if (mCurrentScrollDirection == Direction .LEFT ) {
1698+ // snap to last day
1699+ leftDays = Math .floor (leftDays );
1700+ } else if (mCurrentScrollDirection == Direction .RIGHT ) {
1701+ // snap to next day
1702+ leftDays = Math .ceil (leftDays );
1703+ } else {
1704+ // snap to nearest day
1705+ leftDays = Math .round (leftDays );
1706+ }
16501707
16511708 int nearestOrigin = (int ) (mCurrentOrigin .x - leftDays * (mWidthPerDay + mColumnGap ));
16521709
16531710 if (nearestOrigin != 0 ) {
16541711 // Stop current animation.
16551712 mScroller .forceFinished (true );
16561713 // Snap to date.
1657- mScroller .startScroll ((int ) mCurrentOrigin .x , (int ) mCurrentOrigin .y , -nearestOrigin , 0 , 50 );
1714+ mScroller .startScroll ((int ) mCurrentOrigin .x , (int ) mCurrentOrigin .y , -nearestOrigin , 0 , ( int ) ( Math . abs ( nearestOrigin ) / mWidthPerDay * 500 ) );
16581715 ViewCompat .postInvalidateOnAnimation (WeekView .this );
16591716 }
16601717 // Reset scrolling and fling direction.
@@ -1672,14 +1729,29 @@ public void computeScroll() {
16721729 goToNearestOrigin ();
16731730 }
16741731 } else {
1675- if (mScroller .computeScrollOffset ()) {
1732+ if (mCurrentFlingDirection != Direction .NONE && forceFinishScroll ()) {
1733+ goToNearestOrigin ();
1734+ } else if (mScroller .computeScrollOffset ()) {
16761735 mCurrentOrigin .y = mScroller .getCurrY ();
16771736 mCurrentOrigin .x = mScroller .getCurrX ();
16781737 ViewCompat .postInvalidateOnAnimation (this );
16791738 }
16801739 }
16811740 }
16821741
1742+ /**
1743+ * Check if scrolling should be stopped.
1744+ * @return true if scrolling should be stopped before reaching the end of animation.
1745+ */
1746+ private boolean forceFinishScroll () {
1747+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .ICE_CREAM_SANDWICH ) {
1748+ // current velocity only available since api 14
1749+ return mScroller .getCurrVelocity () <= mMinimumFlingVelocity ;
1750+ } else {
1751+ return false ;
1752+ }
1753+ }
1754+
16831755
16841756 /////////////////////////////////////////////////////////////////
16851757 //
0 commit comments