From 72038d9f414f278fb5a267b03023829345127e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Bar=C3=A1nek?= Date: Wed, 2 Jan 2019 13:14:25 +0100 Subject: [PATCH] add files --- build.gradle | 43 + compactcalendarview.iml | 163 +++ gradle.properties | 3 + proguard-rules.pro | 17 + src/main/AndroidManifest.xml | 4 + .../compactcalendarview/AnimationHandler.java | 211 ++++ .../AnimationListener.java | 22 + .../compactcalendarview/AnimatorListener.java | 25 + .../CollapsingAnimation.java | 49 + .../CompactCalendarController.java | 1072 +++++++++++++++++ .../CompactCalendarView.java | 440 +++++++ .../sundeepk/compactcalendarview/Events.java | 52 + .../compactcalendarview/EventsContainer.java | 158 +++ .../compactcalendarview/WeekUtils.java | 37 + .../comparators/EventComparator.java | 13 + .../compactcalendarview/domain/Event.java | 65 + src/main/res/font/poppins.xml | 7 + src/main/res/font/poppins_bold.xml | 7 + src/main/res/font/poppins_light.xml | 7 + src/main/res/font/poppins_medium.xml | 7 + src/main/res/font/poppins_regular.xml | 7 + src/main/res/font/poppins_semibold.xml | 7 + src/main/res/font/title_goal_fontfamily.xml | 11 + src/main/res/values/attrs.xml | 33 + 24 files changed, 2460 insertions(+) create mode 100644 build.gradle create mode 100644 compactcalendarview.iml create mode 100644 gradle.properties create mode 100644 proguard-rules.pro create mode 100644 src/main/AndroidManifest.xml create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/AnimationHandler.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/AnimationListener.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/AnimatorListener.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/CollapsingAnimation.java create mode 100755 src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarController.java create mode 100755 src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarView.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/Events.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/EventsContainer.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/WeekUtils.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/comparators/EventComparator.java create mode 100644 src/main/java/com/github/sundeepk/compactcalendarview/domain/Event.java create mode 100644 src/main/res/font/poppins.xml create mode 100644 src/main/res/font/poppins_bold.xml create mode 100644 src/main/res/font/poppins_light.xml create mode 100644 src/main/res/font/poppins_medium.xml create mode 100644 src/main/res/font/poppins_regular.xml create mode 100644 src/main/res/font/poppins_semibold.xml create mode 100644 src/main/res/font/title_goal_fontfamily.xml create mode 100644 src/main/res/values/attrs.xml diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0b8de7d --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' +apply plugin: 'idea' + +idea { + module { + downloadJavadoc = false + + downloadSources = true + } +} + +repositories { + google() + jcenter() + mavenCentral() +} + + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + api fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.0.2' + //mockito dependencies + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.19.0' +} \ No newline at end of file diff --git a/compactcalendarview.iml b/compactcalendarview.iml new file mode 100644 index 0000000..ef5f65d --- /dev/null +++ b/compactcalendarview.iml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e447a07 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=CompactCalendarView Library +POM_ARTIFACT_ID=compact-calendar-view +POM_PACKAGING=aar \ No newline at end of file diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..61f9bcd --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/sundeep/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..35ec149 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/AnimationHandler.java b/src/main/java/com/github/sundeepk/compactcalendarview/AnimationHandler.java new file mode 100644 index 0000000..b53a446 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/AnimationHandler.java @@ -0,0 +1,211 @@ +package com.github.sundeepk.compactcalendarview; + + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.OvershootInterpolator; + +import androidx.annotation.NonNull; + +class AnimationHandler { + + private static final int HEIGHT_ANIM_DURATION_MILLIS = 650; + private static final int INDICATOR_ANIM_DURATION_MILLIS = 600; + private boolean isAnimating = false; + private CompactCalendarController compactCalendarController; + private CompactCalendarView compactCalendarView; + private CompactCalendarView.CompactCalendarAnimationListener compactCalendarAnimationListener; + + AnimationHandler(CompactCalendarController compactCalendarController, CompactCalendarView compactCalendarView) { + this.compactCalendarController = compactCalendarController; + this.compactCalendarView = compactCalendarView; + } + + void setCompactCalendarAnimationListener(CompactCalendarView.CompactCalendarAnimationListener compactCalendarAnimationListener){ + this.compactCalendarAnimationListener = compactCalendarAnimationListener; + } + + void openCalendar() { + if (isAnimating) { + return; + } + isAnimating = true; + Animation heightAnim = getCollapsingAnimation(true); + heightAnim.setDuration(HEIGHT_ANIM_DURATION_MILLIS); + heightAnim.setInterpolator(new AccelerateDecelerateInterpolator()); + compactCalendarController.setAnimationStatus(CompactCalendarController.EXPAND_COLLAPSE_CALENDAR); + setUpAnimationLisForOpen(heightAnim); + compactCalendarView.getLayoutParams().height = 0; + compactCalendarView.requestLayout(); + compactCalendarView.startAnimation(heightAnim); + } + + void closeCalendar() { + if (isAnimating) { + return; + } + isAnimating = true; + Animation heightAnim = getCollapsingAnimation(false); + heightAnim.setDuration(HEIGHT_ANIM_DURATION_MILLIS); + heightAnim.setInterpolator(new AccelerateDecelerateInterpolator()); + setUpAnimationLisForClose(heightAnim); + compactCalendarController.setAnimationStatus(CompactCalendarController.EXPAND_COLLAPSE_CALENDAR); + compactCalendarView.getLayoutParams().height = compactCalendarView.getHeight(); + compactCalendarView.requestLayout(); + compactCalendarView.startAnimation(heightAnim); + } + + void openCalendarWithAnimation() { + if (isAnimating) { + return; + } + isAnimating = true; + final Animator indicatorAnim = getIndicatorAnimator(1f, compactCalendarController.getDayIndicatorRadius()); + final Animation heightAnim = getExposeCollapsingAnimation(true); + compactCalendarView.getLayoutParams().height = 0; + compactCalendarView.requestLayout(); + setUpAnimationLisForExposeOpen(indicatorAnim, heightAnim); + compactCalendarView.startAnimation(heightAnim); + } + + void closeCalendarWithAnimation() { + if (isAnimating) { + return; + } + isAnimating = true; + final Animator indicatorAnim = getIndicatorAnimator(compactCalendarController.getDayIndicatorRadius(), 1f); + final Animation heightAnim = getExposeCollapsingAnimation(false); + compactCalendarView.getLayoutParams().height = compactCalendarView.getHeight(); + compactCalendarView.requestLayout(); + setUpAnimationLisForExposeClose(indicatorAnim, heightAnim); + compactCalendarView.startAnimation(heightAnim); + } + + private void setUpAnimationLisForExposeOpen(final Animator indicatorAnim, Animation heightAnim) { + heightAnim.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.EXPOSE_CALENDAR_ANIMATION); + } + + @Override + public void onAnimationEnd(Animation animation) { + indicatorAnim.start(); + } + }); + indicatorAnim.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.ANIMATE_INDICATORS); + } + + @Override + public void onAnimationEnd(Animator animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.IDLE); + onOpen(); + isAnimating = false; + } + }); + } + + private void setUpAnimationLisForExposeClose(final Animator indicatorAnim, Animation heightAnim) { + heightAnim.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.EXPOSE_CALENDAR_ANIMATION); + indicatorAnim.start(); + } + + @Override + public void onAnimationEnd(Animation animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.IDLE); + onClose(); + isAnimating = false; + } + }); + indicatorAnim.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + compactCalendarController.setAnimationStatus(CompactCalendarController.ANIMATE_INDICATORS); + } + + @Override + public void onAnimationEnd(Animator animation) { + } + }); + } + + @NonNull + private Animation getExposeCollapsingAnimation(final boolean isCollapsing) { + Animation heightAnim = getCollapsingAnimation(isCollapsing); + heightAnim.setDuration(HEIGHT_ANIM_DURATION_MILLIS); + heightAnim.setInterpolator(new AccelerateDecelerateInterpolator()); + return heightAnim; + } + + @NonNull + private Animation getCollapsingAnimation(boolean isCollapsing) { + return new CollapsingAnimation(compactCalendarView, compactCalendarController, compactCalendarController.getTargetHeight(), getTargetGrowRadius(), isCollapsing); + } + + @NonNull + private Animator getIndicatorAnimator(float from, float to) { + ValueAnimator animIndicator = ValueAnimator.ofFloat(from, to); + animIndicator.setDuration(INDICATOR_ANIM_DURATION_MILLIS); + animIndicator.setInterpolator(new OvershootInterpolator()); + animIndicator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + compactCalendarController.setGrowFactorIndicator((Float) animation.getAnimatedValue()); + compactCalendarView.invalidate(); + } + }); + return animIndicator; + } + + private int getTargetGrowRadius() { + int heightSq = compactCalendarController.getTargetHeight() * compactCalendarController.getTargetHeight(); + int widthSq = compactCalendarController.getWidth() * compactCalendarController.getWidth(); + return (int) (0.5 * Math.sqrt(heightSq + widthSq)); + } + + private void onOpen() { + if (compactCalendarAnimationListener != null) { + compactCalendarAnimationListener.onOpened(); + } + } + + private void onClose() { + if (compactCalendarAnimationListener != null) { + compactCalendarAnimationListener.onClosed(); + } + } + + private void setUpAnimationLisForOpen(Animation openAnimation) { + openAnimation.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + super.onAnimationEnd(animation); + onOpen(); + isAnimating = false; + } + }); + } + + private void setUpAnimationLisForClose(Animation openAnimation) { + openAnimation.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + super.onAnimationEnd(animation); + onClose(); + isAnimating = false; + } + }); + } + + public boolean isAnimating() { + return isAnimating; + } +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/AnimationListener.java b/src/main/java/com/github/sundeepk/compactcalendarview/AnimationListener.java new file mode 100644 index 0000000..7fd5bfb --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/AnimationListener.java @@ -0,0 +1,22 @@ +package com.github.sundeepk.compactcalendarview; + + +import android.view.animation.Animation; + +public abstract class AnimationListener implements Animation.AnimationListener{ + + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/AnimatorListener.java b/src/main/java/com/github/sundeepk/compactcalendarview/AnimatorListener.java new file mode 100644 index 0000000..d99c9f5 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/AnimatorListener.java @@ -0,0 +1,25 @@ +package com.github.sundeepk.compactcalendarview; + + +import android.animation.Animator; + +public abstract class AnimatorListener implements Animator.AnimatorListener{ + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/CollapsingAnimation.java b/src/main/java/com/github/sundeepk/compactcalendarview/CollapsingAnimation.java new file mode 100644 index 0000000..250da61 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/CollapsingAnimation.java @@ -0,0 +1,49 @@ +package com.github.sundeepk.compactcalendarview; + +import android.view.animation.Animation; +import android.view.animation.Transformation; + +class CollapsingAnimation extends Animation { + private final int targetHeight; + private final CompactCalendarView view; + private int targetGrowRadius; + private final boolean down; + private CompactCalendarController compactCalendarController; + + public CollapsingAnimation(CompactCalendarView view, CompactCalendarController compactCalendarController, int targetHeight, int targetGrowRadius, boolean down) { + this.view = view; + this.compactCalendarController = compactCalendarController; + this.targetHeight = targetHeight; + this.targetGrowRadius = targetGrowRadius; + this.down = down; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + float grow = 0; + int newHeight; + if (down) { + newHeight = (int) (targetHeight * interpolatedTime); + grow = (interpolatedTime * (targetGrowRadius * 2)); + } else { + float progress = 1 - interpolatedTime; + newHeight = (int) (targetHeight * progress); + grow = (progress * (targetGrowRadius * 2)); + } + compactCalendarController.setGrowProgress(grow); + view.getLayoutParams().height = newHeight; + view.requestLayout(); + + } + + @Override + public void initialize(int width, int height, int parentWidth, + int parentHeight) { + super.initialize(width, height, parentWidth, parentHeight); + } + + @Override + public boolean willChangeBounds() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarController.java b/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarController.java new file mode 100755 index 0000000..cbfbac3 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarController.java @@ -0,0 +1,1072 @@ +package com.github.sundeepk.compactcalendarview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.widget.OverScroller; +import com.github.sundeepk.compactcalendarview.domain.Event; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import static com.github.sundeepk.compactcalendarview.CompactCalendarView.CompactCalendarViewListener; +import static com.github.sundeepk.compactcalendarview.CompactCalendarView.FILL_LARGE_INDICATOR; +import static com.github.sundeepk.compactcalendarview.CompactCalendarView.NO_FILL_LARGE_INDICATOR; +import static com.github.sundeepk.compactcalendarview.CompactCalendarView.SMALL_INDICATOR; + + +class CompactCalendarController { + + public static final int IDLE = 0; + public static final int EXPOSE_CALENDAR_ANIMATION = 1; + public static final int EXPAND_COLLAPSE_CALENDAR = 2; + public static final int ANIMATE_INDICATORS = 3; + private static final int VELOCITY_UNIT_PIXELS_PER_SECOND = 1000; + private static final int LAST_FLING_THRESHOLD_MILLIS = 300; + private static final int DAYS_IN_WEEK = 7; + private static final float SNAP_VELOCITY_DIP_PER_SECOND = 400; + private static final float ANIMATION_SCREEN_SET_DURATION_MILLIS = 700; + + private int eventIndicatorStyle = SMALL_INDICATOR; + private int currentDayIndicatorStyle = FILL_LARGE_INDICATOR; + private int currentSelectedDayIndicatorStyle = FILL_LARGE_INDICATOR; + private int paddingWidth = 40; + private int paddingHeight = 40; + private int textHeight; + private int textWidth; + private int widthPerDay; + private int monthsScrolledSoFar; + private int heightPerDay; + private int textSize = 30; + private int width; + private int height; + private int paddingRight; + private int paddingLeft; + private int maximumVelocity; + private int densityAdjustedSnapVelocity; + private int distanceThresholdForAutoScroll; + private int targetHeight; + private int animationStatus = 0; + private int firstDayOfWeekToDraw = Calendar.MONDAY; + private float xIndicatorOffset; + private float multiDayIndicatorStrokeWidth; + private float bigCircleIndicatorRadius; + private float smallIndicatorRadius; + private float growFactor = 0f; + private float screenDensity = 1; + private float growfactorIndicator; + private float distanceX; + private long lastAutoScrollFromFling; + + private boolean useThreeLetterAbbreviation = false; + private boolean isSmoothScrolling; + private boolean isScrolling; + private boolean shouldDrawDaysHeader = true; + private boolean shouldDrawIndicatorsBelowSelectedDays = false; + private boolean displayOtherMonthDays = false; + private boolean shouldSelectFirstDayOfMonthOnScroll = true; + private boolean isRtl = false; + + private CompactCalendarViewListener listener; + private VelocityTracker velocityTracker = null; + private Direction currentDirection = Direction.NONE; + private Date currentDate = new Date(); + private Locale locale; + private Calendar currentCalender; + private Calendar todayCalender; + private Calendar calendarWithFirstDayOfMonth; + private Calendar eventsCalendar; + private EventsContainer eventsContainer; + private PointF accumulatedScrollOffset = new PointF(); + private OverScroller scroller; + private Paint dayPaint = new Paint(); + private Paint background = new Paint(); + private Rect textSizeRect; + private String[] dayColumnNames; + + // colors + private int multiEventIndicatorColor; + private int currentDayBackgroundColor; + private int currentDayTextColor; + private int calenderTextColor; + private int currentSelectedDayBackgroundColor; + private int currentSelectedDayTextColor; + private int calenderBackgroundColor = Color.WHITE; + private int otherMonthDaysTextColor; + private TimeZone timeZone; + + /** + * Only used in onDrawCurrentMonth to temporarily calculate previous month days + */ + private Calendar tempPreviousMonthCalendar; + + private enum Direction { + NONE, HORIZONTAL, VERTICAL + } + + CompactCalendarController(Paint dayPaint, OverScroller scroller, Rect textSizeRect, AttributeSet attrs, + Context context, int currentDayBackgroundColor, int calenderTextColor, + int currentSelectedDayBackgroundColor, VelocityTracker velocityTracker, + int multiEventIndicatorColor, EventsContainer eventsContainer, + Locale locale, TimeZone timeZone) { + this.dayPaint = dayPaint; + this.scroller = scroller; + this.textSizeRect = textSizeRect; + this.currentDayBackgroundColor = currentDayBackgroundColor; + this.calenderTextColor = calenderTextColor; + this.currentSelectedDayBackgroundColor = currentSelectedDayBackgroundColor; + this.otherMonthDaysTextColor = calenderTextColor; + this.velocityTracker = velocityTracker; + this.multiEventIndicatorColor = multiEventIndicatorColor; + this.eventsContainer = eventsContainer; + this.locale = locale; + this.timeZone = timeZone; + this.displayOtherMonthDays = false; + loadAttributes(attrs, context); + init(context); + } + + private void loadAttributes(AttributeSet attrs, Context context) { + if (attrs != null && context != null) { + TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CompactCalendarView, 0, 0); + try { + currentDayBackgroundColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarCurrentDayBackgroundColor, currentDayBackgroundColor); + calenderTextColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarTextColor, calenderTextColor); + currentDayTextColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarCurrentDayTextColor, calenderTextColor); + otherMonthDaysTextColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarOtherMonthDaysTextColor, otherMonthDaysTextColor); + currentSelectedDayBackgroundColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarCurrentSelectedDayBackgroundColor, currentSelectedDayBackgroundColor); + currentSelectedDayTextColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarCurrentSelectedDayTextColor, calenderTextColor); + calenderBackgroundColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarBackgroundColor, calenderBackgroundColor); + multiEventIndicatorColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarMultiEventIndicatorColor, multiEventIndicatorColor); + textSize = typedArray.getDimensionPixelSize(R.styleable.CompactCalendarView_compactCalendarTextSize, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, context.getResources().getDisplayMetrics())); + targetHeight = typedArray.getDimensionPixelSize(R.styleable.CompactCalendarView_compactCalendarTargetHeight, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, targetHeight, context.getResources().getDisplayMetrics())); + eventIndicatorStyle = typedArray.getInt(R.styleable.CompactCalendarView_compactCalendarEventIndicatorStyle, FILL_LARGE_INDICATOR); + currentDayIndicatorStyle = typedArray.getInt(R.styleable.CompactCalendarView_compactCalendarCurrentDayIndicatorStyle, FILL_LARGE_INDICATOR); + currentSelectedDayIndicatorStyle = typedArray.getInt(R.styleable.CompactCalendarView_compactCalendarCurrentSelectedDayIndicatorStyle, FILL_LARGE_INDICATOR); + displayOtherMonthDays = typedArray.getBoolean(R.styleable.CompactCalendarView_compactCalendarDisplayOtherMonthDays, displayOtherMonthDays); + shouldSelectFirstDayOfMonthOnScroll = typedArray.getBoolean(R.styleable.CompactCalendarView_compactCalendarShouldSelectFirstDayOfMonthOnScroll, shouldSelectFirstDayOfMonthOnScroll); + } finally { + typedArray.recycle(); + } + } + } + + private void init(Context context) { + currentCalender = Calendar.getInstance(timeZone, locale); + todayCalender = Calendar.getInstance(timeZone, locale); + calendarWithFirstDayOfMonth = Calendar.getInstance(timeZone, locale); + eventsCalendar = Calendar.getInstance(timeZone, locale); + tempPreviousMonthCalendar = Calendar.getInstance(timeZone, locale); + + // make setMinimalDaysInFirstWeek same across android versions + eventsCalendar.setMinimalDaysInFirstWeek(1); + calendarWithFirstDayOfMonth.setMinimalDaysInFirstWeek(1); + todayCalender.setMinimalDaysInFirstWeek(1); + currentCalender.setMinimalDaysInFirstWeek(1); + tempPreviousMonthCalendar.setMinimalDaysInFirstWeek(1); + + setFirstDayOfWeek(firstDayOfWeekToDraw); + + setUseWeekDayAbbreviation(false); + dayPaint.setTextAlign(Paint.Align.CENTER); + dayPaint.setStyle(Paint.Style.STROKE); + dayPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + dayPaint.setTypeface(Typeface.SANS_SERIF); + dayPaint.setTextSize(textSize); + dayPaint.setColor(calenderTextColor); + dayPaint.getTextBounds("31", 0, "31".length(), textSizeRect); + textHeight = textSizeRect.height() * 3; + textWidth = textSizeRect.width() * 2; + + todayCalender.setTime(new Date()); + setToMidnight(todayCalender); + + currentCalender.setTime(currentDate); + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 0); + + initScreenDensityRelatedValues(context); + + xIndicatorOffset = 3.5f * screenDensity; + + //scale small indicator by screen density + smallIndicatorRadius = 2.5f * screenDensity; + + //just set a default growFactor to draw full calendar when initialised + growFactor = Integer.MAX_VALUE; + } + + private void initScreenDensityRelatedValues(Context context) { + if (context != null) { + screenDensity = context.getResources().getDisplayMetrics().density; + final ViewConfiguration configuration = ViewConfiguration + .get(context); + densityAdjustedSnapVelocity = (int) (screenDensity * SNAP_VELOCITY_DIP_PER_SECOND); + maximumVelocity = configuration.getScaledMaximumFlingVelocity(); + + final DisplayMetrics dm = context.getResources().getDisplayMetrics() ; + multiDayIndicatorStrokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm); + } + } + + private void setCalenderToFirstDayOfMonth(Calendar calendarWithFirstDayOfMonth, Date currentDate, int scrollOffset, int monthOffset) { + setMonthOffset(calendarWithFirstDayOfMonth, currentDate, scrollOffset, monthOffset); + calendarWithFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); + } + + private void setMonthOffset(Calendar calendarWithFirstDayOfMonth, Date currentDate, int scrollOffset, int monthOffset) { + calendarWithFirstDayOfMonth.setTime(currentDate); + calendarWithFirstDayOfMonth.add(Calendar.MONTH, scrollOffset + monthOffset); + calendarWithFirstDayOfMonth.set(Calendar.HOUR_OF_DAY, 0); + calendarWithFirstDayOfMonth.set(Calendar.MINUTE, 0); + calendarWithFirstDayOfMonth.set(Calendar.SECOND, 0); + calendarWithFirstDayOfMonth.set(Calendar.MILLISECOND, 0); + } + + void setIsRtl(boolean isRtl){ + this.isRtl = isRtl; + } + + void setShouldSelectFirstDayOfMonthOnScroll(boolean shouldSelectFirstDayOfMonthOnScroll){ + this.shouldSelectFirstDayOfMonthOnScroll = shouldSelectFirstDayOfMonthOnScroll; + } + + void setDisplayOtherMonthDays(boolean displayOtherMonthDays) { + this.displayOtherMonthDays = displayOtherMonthDays; + } + + void shouldDrawIndicatorsBelowSelectedDays(boolean shouldDrawIndicatorsBelowSelectedDays){ + this.shouldDrawIndicatorsBelowSelectedDays = shouldDrawIndicatorsBelowSelectedDays; + } + + void setCurrentDayIndicatorStyle(int currentDayIndicatorStyle) { + this.currentDayIndicatorStyle = currentDayIndicatorStyle; + } + + void setEventIndicatorStyle(int eventIndicatorStyle) { + this.eventIndicatorStyle = eventIndicatorStyle; + } + + void setCurrentSelectedDayIndicatorStyle(int currentSelectedDayIndicatorStyle){ + this.currentSelectedDayIndicatorStyle = currentSelectedDayIndicatorStyle; + } + + void setTargetHeight(int targetHeight) { + this.targetHeight = targetHeight; + } + + float getScreenDensity(){ + return screenDensity; + } + + float getDayIndicatorRadius(){ + return bigCircleIndicatorRadius; + } + + void setGrowFactorIndicator(float growfactorIndicator) { + this.growfactorIndicator = growfactorIndicator; + } + + float getGrowFactorIndicator() { + return growfactorIndicator; + } + + void setAnimationStatus(int animationStatus) { + this.animationStatus = animationStatus; + } + + int getTargetHeight() { + return targetHeight; + } + + int getWidth(){ + return width; + } + + void setListener(CompactCalendarViewListener listener) { + this.listener = listener; + } + + void removeAllEvents() { + eventsContainer.removeAllEvents(); + } + + void setFirstDayOfWeek(int day){ + if (day < 1 || day > 7) { + throw new IllegalArgumentException("Day must be an int between 1 and 7 or DAY_OF_WEEK from Java Calendar class. For more information please see Calendar.DAY_OF_WEEK."); + } + this.firstDayOfWeekToDraw = day; + setUseWeekDayAbbreviation(useThreeLetterAbbreviation); + eventsCalendar.setFirstDayOfWeek(day); + calendarWithFirstDayOfMonth.setFirstDayOfWeek(day); + todayCalender.setFirstDayOfWeek(day); + currentCalender.setFirstDayOfWeek(day); + tempPreviousMonthCalendar.setFirstDayOfWeek(day); + } + + void setCurrentSelectedDayBackgroundColor(int currentSelectedDayBackgroundColor) { + this.currentSelectedDayBackgroundColor = currentSelectedDayBackgroundColor; + } + + void setCurrentSelectedDayTextColor(int currentSelectedDayTextColor) { + this.currentSelectedDayTextColor = currentSelectedDayTextColor; + } + + void setCalenderBackgroundColor(int calenderBackgroundColor) { + this.calenderBackgroundColor = calenderBackgroundColor; + } + + void setCurrentDayBackgroundColor(int currentDayBackgroundColor) { + this.currentDayBackgroundColor = currentDayBackgroundColor; + } + + void setCurrentDayTextColor(int currentDayTextColor) { + this.currentDayTextColor = currentDayTextColor; + } + + void scrollRight() { + if (isRtl) { + scrollPrev(); + } else { + scrollNext(); + } + } + + void scrollLeft() { + if (isRtl) { + scrollNext(); + } else { + scrollPrev(); + } + } + + private void scrollNext() { + monthsScrolledSoFar = monthsScrolledSoFar - 1; + accumulatedScrollOffset.x = monthsScrolledSoFar * width; + if(shouldSelectFirstDayOfMonthOnScroll){ + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentCalender.getTime(), 0, 1); + setCurrentDate(calendarWithFirstDayOfMonth.getTime()); + } + performMonthScrollCallback(); + } + + private void scrollPrev() { + monthsScrolledSoFar = monthsScrolledSoFar + 1; + accumulatedScrollOffset.x = monthsScrolledSoFar * width; + if(shouldSelectFirstDayOfMonthOnScroll){ + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentCalender.getTime(), 0, -1); + setCurrentDate(calendarWithFirstDayOfMonth.getTime()); + } + performMonthScrollCallback(); + } + + void setLocale(TimeZone timeZone, Locale locale) { + if (locale == null) { + throw new IllegalArgumentException("Locale cannot be null."); + } + if (timeZone == null) { + throw new IllegalArgumentException("TimeZone cannot be null."); + } + this.locale = locale; + this.timeZone = timeZone; + this.eventsContainer = new EventsContainer(Calendar.getInstance(this.timeZone, this.locale)); + // passing null will not re-init density related values - and that's ok + init(null); + } + + void setUseWeekDayAbbreviation(boolean useThreeLetterAbbreviation) { + this.useThreeLetterAbbreviation = useThreeLetterAbbreviation; + this.dayColumnNames = WeekUtils.getWeekdayNames(locale, firstDayOfWeekToDraw, this.useThreeLetterAbbreviation); + } + + void setDayColumnNames(String[] dayColumnNames) { + if (dayColumnNames == null || dayColumnNames.length != 7) { + throw new IllegalArgumentException("Column names cannot be null and must contain a value for each day of the week"); + } + this.dayColumnNames = dayColumnNames; + } + + void setShouldDrawDaysHeader(boolean shouldDrawDaysHeader) { + this.shouldDrawDaysHeader = shouldDrawDaysHeader; + } + + void onMeasure(int width, int height, int paddingRight, int paddingLeft) { + widthPerDay = (width) / DAYS_IN_WEEK; + heightPerDay = targetHeight > 0 ? targetHeight / 7 : height / 7; + this.width = width; + this.distanceThresholdForAutoScroll = (int) (width * 0.50); + this.height = height; + this.paddingRight = paddingRight; + this.paddingLeft = paddingLeft; + + //makes easier to find radius + bigCircleIndicatorRadius = getInterpolatedBigCircleIndicator(); + + // scale the selected day indicators slightly so that event indicators can be drawn below + bigCircleIndicatorRadius = shouldDrawIndicatorsBelowSelectedDays && eventIndicatorStyle == CompactCalendarView.SMALL_INDICATOR ? bigCircleIndicatorRadius * 0.85f : bigCircleIndicatorRadius; + } + + //assume square around each day of width and height = heightPerDay and get diagonal line length + //interpolate height and radius + //https://en.wikipedia.org/wiki/Linear_interpolation + private float getInterpolatedBigCircleIndicator() { + float x0 = textSizeRect.height(); + float x1 = heightPerDay; // take into account indicator offset + float x = (x1 + textSizeRect.height()) / 2f; // pick a point which is almost half way through heightPerDay and textSizeRect + double y1 = 0.5 * Math.sqrt((x1 * x1) + (x1 * x1)); + double y0 = 0.5 * Math.sqrt((x0 * x0) + (x0 * x0)); + + return (float) (y0 + ((y1 - y0) * ((x - x0) / (x1 - x0)))); + } + + void onDraw(Canvas canvas) { + paddingWidth = widthPerDay / 2; + paddingHeight = heightPerDay / 2; + calculateXPositionOffset(); + + if (animationStatus == EXPOSE_CALENDAR_ANIMATION) { + drawCalendarWhileAnimating(canvas); + } else if (animationStatus == ANIMATE_INDICATORS) { + drawCalendarWhileAnimatingIndicators(canvas); + } else { + drawCalenderBackground(canvas); + drawScrollableCalender(canvas); + } + } + + private void drawCalendarWhileAnimatingIndicators(Canvas canvas) { + dayPaint.setColor(calenderBackgroundColor); + dayPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(0, 0, growFactor, dayPaint); + dayPaint.setStyle(Paint.Style.STROKE); + dayPaint.setColor(Color.WHITE); + drawScrollableCalender(canvas); + } + + private void drawCalendarWhileAnimating(Canvas canvas) { + background.setColor(calenderBackgroundColor); + background.setStyle(Paint.Style.FILL); + canvas.drawCircle(0, 0, growFactor, background); + dayPaint.setStyle(Paint.Style.STROKE); + dayPaint.setColor(Color.WHITE); + drawScrollableCalender(canvas); + } + + void onSingleTapUp(MotionEvent e) { + // Don't handle single tap when calendar is scrolling and is not stationary + if (isScrolling()) { + return; + } + + int dayColumn = Math.round((paddingLeft + e.getX() - paddingWidth - paddingRight) / widthPerDay); + int dayRow = Math.round((e.getY() - paddingHeight) / heightPerDay); + + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, monthsScrolledSoFar(), 0); + + int firstDayOfMonth = getDayOfWeek(calendarWithFirstDayOfMonth); + + int dayOfMonth = ((dayRow - 1) * 7) - firstDayOfMonth; + if (isRtl) { + dayOfMonth += 6 - dayColumn; + } else { + dayOfMonth += dayColumn; + } + if (dayOfMonth < calendarWithFirstDayOfMonth.getActualMaximum(Calendar.DAY_OF_MONTH) + && dayOfMonth >= 0) { + calendarWithFirstDayOfMonth.add(Calendar.DATE, dayOfMonth); + + currentCalender.setTimeInMillis(calendarWithFirstDayOfMonth.getTimeInMillis()); + performOnDayClickCallback(currentCalender.getTime()); + } + } + + // Add a little leeway buy checking if amount scrolled is almost same as expected scroll + // as it maybe off by a few pixels + private boolean isScrolling() { + float scrolledX = Math.abs(accumulatedScrollOffset.x); + int expectedScrollX = Math.abs(width * monthsScrolledSoFar); + return scrolledX < expectedScrollX - 5 || scrolledX > expectedScrollX + 5; + } + + private void performOnDayClickCallback(Date date) { + if (listener != null) { + listener.onDayClick(date); + } + } + + boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + //ignore scrolling callback if already smooth scrolling + if (isSmoothScrolling) { + return true; + } + + if (currentDirection == Direction.NONE) { + if (Math.abs(distanceX) > Math.abs(distanceY)) { + currentDirection = Direction.HORIZONTAL; + } else { + currentDirection = Direction.VERTICAL; + } + } + + isScrolling = true; + this.distanceX = distanceX; + return true; + } + + boolean onTouch(MotionEvent event) { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } + + velocityTracker.addMovement(event); + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + + if (!scroller.isFinished()) { + scroller.abortAnimation(); + } + isSmoothScrolling = false; + + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + velocityTracker.addMovement(event); + velocityTracker.computeCurrentVelocity(500); + + } else if (event.getAction() == MotionEvent.ACTION_UP) { + handleHorizontalScrolling(); + velocityTracker.recycle(); + velocityTracker.clear(); + velocityTracker = null; + isScrolling = false; + } + return false; + } + + private void snapBackScroller() { + float remainingScrollAfterFingerLifted1 = (accumulatedScrollOffset.x - (monthsScrolledSoFar * width)); + scroller.startScroll((int) accumulatedScrollOffset.x, 0, (int) -remainingScrollAfterFingerLifted1, 0); + } + + private void handleHorizontalScrolling() { + int velocityX = computeVelocity(); + handleSmoothScrolling(velocityX); + + currentDirection = Direction.NONE; + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, monthsScrolledSoFar(), 0); + + if (calendarWithFirstDayOfMonth.get(Calendar.MONTH) != currentCalender.get(Calendar.MONTH) && shouldSelectFirstDayOfMonthOnScroll) { + setCalenderToFirstDayOfMonth(currentCalender, currentDate, monthsScrolledSoFar(), 0); + } + } + + private int computeVelocity() { + velocityTracker.computeCurrentVelocity(VELOCITY_UNIT_PIXELS_PER_SECOND, maximumVelocity); + return (int) velocityTracker.getXVelocity(); + } + + private void handleSmoothScrolling(int velocityX) { + int distanceScrolled = (int) (accumulatedScrollOffset.x - (width * monthsScrolledSoFar)); + boolean isEnoughTimeElapsedSinceLastSmoothScroll = System.currentTimeMillis() - lastAutoScrollFromFling > LAST_FLING_THRESHOLD_MILLIS; + if (velocityX > densityAdjustedSnapVelocity && isEnoughTimeElapsedSinceLastSmoothScroll) { + scrollPreviousMonth(); + } else if (velocityX < -densityAdjustedSnapVelocity && isEnoughTimeElapsedSinceLastSmoothScroll) { + scrollNextMonth(); + } else if (isScrolling && distanceScrolled > distanceThresholdForAutoScroll) { + scrollPreviousMonth(); + } else if (isScrolling && distanceScrolled < -distanceThresholdForAutoScroll) { + scrollNextMonth(); + } else { + isSmoothScrolling = false; + snapBackScroller(); + } + } + + private void scrollNextMonth() { + lastAutoScrollFromFling = System.currentTimeMillis(); + monthsScrolledSoFar = monthsScrolledSoFar - 1; + performScroll(); + isSmoothScrolling = true; + performMonthScrollCallback(); + } + + private void scrollPreviousMonth() { + lastAutoScrollFromFling = System.currentTimeMillis(); + monthsScrolledSoFar = monthsScrolledSoFar + 1; + performScroll(); + isSmoothScrolling = true; + performMonthScrollCallback(); + } + + private void performMonthScrollCallback() { + if (listener != null) { + listener.onMonthScroll(getFirstDayOfCurrentMonth()); + } + } + + private void performScroll() { + int targetScroll = (monthsScrolledSoFar ) * width; + float remainingScrollAfterFingerLifted = targetScroll - accumulatedScrollOffset.x; + scroller.startScroll((int) accumulatedScrollOffset.x, 0, (int) (remainingScrollAfterFingerLifted), 0, + (int) (Math.abs((int) (remainingScrollAfterFingerLifted)) / (float) width * ANIMATION_SCREEN_SET_DURATION_MILLIS)); + } + + int getHeightPerDay() { + return heightPerDay; + } + + int getWeekNumberForCurrentMonth() { + Calendar calendar = Calendar.getInstance(timeZone, locale); + calendar.setTime(currentDate); + return calendar.get(Calendar.WEEK_OF_MONTH); + } + + Date getFirstDayOfCurrentMonth() { + Calendar calendar = Calendar.getInstance(timeZone, locale); + calendar.setTime(currentDate); + calendar.add(Calendar.MONTH, monthsScrolledSoFar()); + calendar.set(Calendar.DAY_OF_MONTH, 1); + setToMidnight(calendar); + return calendar.getTime(); + } + + void setCurrentDate(Date dateTimeMonth) { + distanceX = 0; + monthsScrolledSoFar = 0; + accumulatedScrollOffset.x = 0; + scroller.startScroll(0, 0, 0, 0); + currentDate = new Date(dateTimeMonth.getTime()); + currentCalender.setTime(currentDate); + todayCalender = Calendar.getInstance(timeZone, locale); + setToMidnight(currentCalender); + } + + private void setToMidnight(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + } + + void addEvent(Event event) { + eventsContainer.addEvent(event); + } + + void addEvents(List events) { + eventsContainer.addEvents(events); + } + + List getCalendarEventsFor(long epochMillis) { + return eventsContainer.getEventsFor(epochMillis); + } + + List getCalendarEventsForMonth(long epochMillis) { + return eventsContainer.getEventsForMonth(epochMillis); + } + + void removeEventsFor(long epochMillis) { + eventsContainer.removeEventByEpochMillis(epochMillis); + } + + void removeEvent(Event event) { + eventsContainer.removeEvent(event); + } + + void removeEvents(List events) { + eventsContainer.removeEvents(events); + } + + void setGrowProgress(float grow) { + growFactor = grow; + } + + float getGrowFactor() { + return growFactor; + } + + boolean onDown(MotionEvent e) { + scroller.forceFinished(true); + return true; + } + + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + scroller.forceFinished(true); + return true; + } + + boolean computeScroll() { + if (scroller.computeScrollOffset()) { + accumulatedScrollOffset.x = scroller.getCurrX(); + return true; + } + return false; + } + + private void drawScrollableCalender(Canvas canvas) { + if (isRtl) { + drawNextMonth(canvas, -1); + drawCurrentMonth(canvas); + drawPreviousMonth(canvas,1); + } else { + drawPreviousMonth(canvas, -1); + drawCurrentMonth(canvas); + drawNextMonth(canvas, 1); + } + } + + private void drawNextMonth(Canvas canvas, int offset) { + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, offset); + drawMonth(canvas, calendarWithFirstDayOfMonth, (width * (-monthsScrolledSoFar + 1))); + } + + private void drawCurrentMonth(Canvas canvas) { + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, monthsScrolledSoFar(), 0); + drawMonth(canvas, calendarWithFirstDayOfMonth, width * -monthsScrolledSoFar); + } + + private int monthsScrolledSoFar() { + return isRtl? monthsScrolledSoFar : -monthsScrolledSoFar; + } + + private void drawPreviousMonth(Canvas canvas, int offset) { + setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, offset); + drawMonth(canvas, calendarWithFirstDayOfMonth, (width * (-monthsScrolledSoFar - 1))); + } + + private void calculateXPositionOffset() { + if (currentDirection == Direction.HORIZONTAL) { + accumulatedScrollOffset.x -= distanceX; + } + } + + private void drawCalenderBackground(Canvas canvas) { + dayPaint.setColor(calenderBackgroundColor); + dayPaint.setStyle(Paint.Style.FILL); + canvas.drawRect(0, 0, width, height, dayPaint); + dayPaint.setStyle(Paint.Style.STROKE); + dayPaint.setColor(calenderTextColor); + } + + void drawEvents(Canvas canvas, Calendar currentMonthToDrawCalender, int offset) { + int currentMonth = currentMonthToDrawCalender.get(Calendar.MONTH); + List uniqEvents = eventsContainer.getEventsForMonthAndYear(currentMonth, currentMonthToDrawCalender.get(Calendar.YEAR)); + + boolean shouldDrawCurrentDayCircle = currentMonth == todayCalender.get(Calendar.MONTH); + boolean shouldDrawSelectedDayCircle = currentMonth == currentCalender.get(Calendar.MONTH); + + int todayDayOfMonth = todayCalender.get(Calendar.DAY_OF_MONTH); + int currentYear = todayCalender.get(Calendar.YEAR); + int selectedDayOfMonth = currentCalender.get(Calendar.DAY_OF_MONTH); + float indicatorOffset = bigCircleIndicatorRadius / 2; + if (uniqEvents != null) { + for (int i = 0; i < uniqEvents.size(); i++) { + Events events = uniqEvents.get(i); + long timeMillis = events.getTimeInMillis(); + eventsCalendar.setTimeInMillis(timeMillis); + + int dayOfWeek = getDayOfWeek(eventsCalendar); + if (isRtl) { + dayOfWeek = 6 - dayOfWeek; + } + + int weekNumberForMonth = eventsCalendar.get(Calendar.WEEK_OF_MONTH); + float xPosition = widthPerDay * dayOfWeek + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight; + float yPosition = weekNumberForMonth * heightPerDay + paddingHeight; + + if (((animationStatus == EXPOSE_CALENDAR_ANIMATION || animationStatus == ANIMATE_INDICATORS) && xPosition >= growFactor ) || yPosition >= growFactor) { + // only draw small event indicators if enough of the calendar is exposed + continue; + } else if (animationStatus == EXPAND_COLLAPSE_CALENDAR && yPosition >= growFactor){ + // expanding animation, just draw event indicators if enough of the calendar is visible + continue; + } else if (animationStatus == EXPOSE_CALENDAR_ANIMATION && (eventIndicatorStyle == FILL_LARGE_INDICATOR || eventIndicatorStyle == NO_FILL_LARGE_INDICATOR)) { + // Don't draw large indicators during expose animation, until animation is done + continue; + } + + List eventsList = events.getEvents(); + int dayOfMonth = eventsCalendar.get(Calendar.DAY_OF_MONTH); + int eventYear = eventsCalendar.get(Calendar.YEAR); + boolean isSameDayAsCurrentDay = shouldDrawCurrentDayCircle && (todayDayOfMonth == dayOfMonth) && (eventYear == currentYear); + boolean isCurrentSelectedDay = shouldDrawSelectedDayCircle && (selectedDayOfMonth == dayOfMonth); + + if (shouldDrawIndicatorsBelowSelectedDays || (!shouldDrawIndicatorsBelowSelectedDays && !isSameDayAsCurrentDay && !isCurrentSelectedDay) || animationStatus == EXPOSE_CALENDAR_ANIMATION) { + if (eventIndicatorStyle == FILL_LARGE_INDICATOR || eventIndicatorStyle == NO_FILL_LARGE_INDICATOR) { + if (!eventsList.isEmpty()) { + Event event = eventsList.get(0); + drawEventIndicatorCircle(canvas, xPosition, yPosition, event.getColor()); + } + } else { + yPosition += indicatorOffset; + // offset event indicators to draw below selected day indicators + // this makes sure that they do no overlap + if (shouldDrawIndicatorsBelowSelectedDays && (isSameDayAsCurrentDay || isCurrentSelectedDay)) { + yPosition += indicatorOffset; + } + + if (eventsList.size() >= 3) { + drawEventsWithPlus(canvas, xPosition, yPosition, eventsList); + } else if (eventsList.size() == 2) { + drawTwoEvents(canvas, xPosition, yPosition, eventsList); + } else if (eventsList.size() == 1) { + drawSingleEvent(canvas, xPosition, yPosition, eventsList); + } + } + } + } + } + } + + private void drawSingleEvent(Canvas canvas, float xPosition, float yPosition, List eventsList) { + Event event = eventsList.get(0); + drawEventIndicatorCircle(canvas, xPosition, yPosition, event.getColor()); + } + + private void drawTwoEvents(Canvas canvas, float xPosition, float yPosition, List eventsList) { + //draw fist event just left of center + drawEventIndicatorCircle(canvas, xPosition + (xIndicatorOffset * -1), yPosition, eventsList.get(0).getColor()); + //draw second event just right of center + drawEventIndicatorCircle(canvas, xPosition + (xIndicatorOffset * 1), yPosition, eventsList.get(1).getColor()); + } + + //draw 2 eventsByMonthAndYearMap followed by plus indicator to show there are more than 2 eventsByMonthAndYearMap + private void drawEventsWithPlus(Canvas canvas, float xPosition, float yPosition, List eventsList) { + // k = size() - 1, but since we don't want to draw more than 2 indicators, we just stop after 2 iterations so we can just hard k = -2 instead + // we can use the below loop to draw arbitrary eventsByMonthAndYearMap based on the current screen size, for example, larger screens should be able to + // display more than 2 evens before displaying plus indicator, but don't draw more than 3 indicators for now + for (int j = 0, k = -2; j < 3; j++, k += 2) { + Event event = eventsList.get(j); + float xStartPosition = xPosition + (xIndicatorOffset * k); + if (j == 2) { + dayPaint.setColor(multiEventIndicatorColor); + dayPaint.setStrokeWidth(multiDayIndicatorStrokeWidth); + canvas.drawLine(xStartPosition - smallIndicatorRadius, yPosition, xStartPosition + smallIndicatorRadius, yPosition, dayPaint); + canvas.drawLine(xStartPosition, yPosition - smallIndicatorRadius, xStartPosition, yPosition + smallIndicatorRadius, dayPaint); + dayPaint.setStrokeWidth(0); + } else { + drawEventIndicatorCircle(canvas, xStartPosition, yPosition, event.getColor()); + } + } + } + + // zero based indexes used internally so instead of returning range of 1-7 like calendar class + // it returns 0-6 where 0 is Sunday instead of 1 + int getDayOfWeek(Calendar calendar) { + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - firstDayOfWeekToDraw; + dayOfWeek = dayOfWeek < 0 ? 7 + dayOfWeek: dayOfWeek; + return dayOfWeek; + } + + void drawMonth(Canvas canvas, Calendar monthToDrawCalender, int offset) { + drawEvents(canvas, monthToDrawCalender, offset); + + //offset by one because we want to start from Monday + int firstDayOfMonth = getDayOfWeek(monthToDrawCalender); + + boolean isSameMonthAsToday = monthToDrawCalender.get(Calendar.MONTH) == todayCalender.get(Calendar.MONTH); + boolean isSameYearAsToday = monthToDrawCalender.get(Calendar.YEAR) == todayCalender.get(Calendar.YEAR); + boolean isSameMonthAsCurrentCalendar = monthToDrawCalender.get(Calendar.MONTH) == currentCalender.get(Calendar.MONTH) && + monthToDrawCalender.get(Calendar.YEAR) == currentCalender.get(Calendar.YEAR); + int todayDayOfMonth = todayCalender.get(Calendar.DAY_OF_MONTH); + boolean isAnimatingWithExpose = animationStatus == EXPOSE_CALENDAR_ANIMATION; + + int maximumMonthDay = monthToDrawCalender.getActualMaximum(Calendar.DAY_OF_MONTH); + + tempPreviousMonthCalendar.setTimeInMillis(monthToDrawCalender.getTimeInMillis()); + tempPreviousMonthCalendar.add(Calendar.MONTH, -1); + int maximumPreviousMonthDay = tempPreviousMonthCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); + + for (int dayColumn = 0, colDirection = isRtl? 6 : 0, dayRow = 0; dayColumn <= 6; dayRow++) { + if (dayRow == 7) { + if (isRtl) { + colDirection--; + } else { + colDirection++; + } + dayRow = 0; + if (dayColumn <= 6) { + dayColumn++; + } + } + if (dayColumn == dayColumnNames.length) { + break; + } + float xPosition = widthPerDay * dayColumn + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight; + float yPosition = dayRow * heightPerDay + paddingHeight; + if (xPosition >= growFactor && (isAnimatingWithExpose || animationStatus == ANIMATE_INDICATORS) || yPosition >= growFactor) { + // don't draw days if animating expose or indicators + continue; + } + if (dayRow == 0) { + // first row, so draw the first letter of the day + if (shouldDrawDaysHeader) { + dayPaint.setColor(calenderTextColor); + dayPaint.setTypeface(Typeface.DEFAULT_BOLD); + dayPaint.setStyle(Paint.Style.FILL); + dayPaint.setColor(calenderTextColor); + canvas.drawText(dayColumnNames[colDirection], xPosition, paddingHeight, dayPaint); + dayPaint.setTypeface(Typeface.DEFAULT); + } + } else { + int day = ((dayRow - 1) * 7 + colDirection + 1) - firstDayOfMonth; + int defaultCalenderTextColorToUse = calenderTextColor; + if (currentCalender.get(Calendar.DAY_OF_MONTH) == day && isSameMonthAsCurrentCalendar && !isAnimatingWithExpose) { + drawSelectedDayCircleIndicator(currentSelectedDayIndicatorStyle, canvas, xPosition, yPosition, currentSelectedDayBackgroundColor); + defaultCalenderTextColorToUse = currentSelectedDayTextColor; + } else if (isSameYearAsToday && isSameMonthAsToday && todayDayOfMonth == day && !isAnimatingWithExpose) { + // TODO calculate position of circle in a more reliable way + dayPaint.setTypeface(Typeface.DEFAULT_BOLD); + drawDayCircleIndicator(currentDayIndicatorStyle, canvas, xPosition, yPosition, currentDayBackgroundColor); + defaultCalenderTextColorToUse = currentDayTextColor; + } + if (day <= 0) { + if (displayOtherMonthDays) { + // Display day month before + dayPaint.setStyle(Paint.Style.FILL); + dayPaint.setColor(otherMonthDaysTextColor); + canvas.drawText(String.valueOf(maximumPreviousMonthDay + day), xPosition, yPosition, dayPaint); + } + } else if (day > maximumMonthDay) { + if (displayOtherMonthDays) { + // Display day month after + dayPaint.setStyle(Paint.Style.FILL); + dayPaint.setColor(otherMonthDaysTextColor); + canvas.drawText(String.valueOf(day - maximumMonthDay), xPosition, yPosition, dayPaint); + } + } else { + dayPaint.setStyle(Paint.Style.FILL); + dayPaint.setColor(defaultCalenderTextColorToUse); + Calendar calendar = monthToDrawCalender; + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.get(Calendar.DAY_OF_MONTH); + + List dayEvents = eventsContainer.getEventsFor(calendar.getTimeInMillis()); + + if(dayEvents.size() > 0 && currentCalender.get(Calendar.DAY_OF_MONTH) != day) { + int tempColor = dayPaint.getColor(); + dayPaint.setColor(Color.WHITE); + canvas.drawText(String.valueOf(day), xPosition, yPosition, dayPaint); + dayPaint.setColor(tempColor); + dayPaint.setTypeface(Typeface.DEFAULT); + } else { + canvas.drawText(String.valueOf(day), xPosition, yPosition, dayPaint); + dayPaint.setTypeface(Typeface.DEFAULT); + } + } + } + } + } + + private void drawSelectedDayCircleIndicator(int indicatorStyle, Canvas canvas, float x, float y, int color) { + float strokeWidth = dayPaint.getStrokeWidth(); + if (indicatorStyle == NO_FILL_LARGE_INDICATOR) { + dayPaint.setStrokeWidth(2 * screenDensity); + dayPaint.setStyle(Paint.Style.STROKE); + } else { + dayPaint.setStyle(Paint.Style.FILL); + } + drawSelectedCircle(canvas, x, y, Color.rgb(0, 0, 0), 1); + dayPaint.setStrokeWidth(strokeWidth); + dayPaint.setStyle(Paint.Style.FILL); + } + + private void drawSelectedCircle(Canvas canvas, float x, float y, int color, float circleScale) { + if (animationStatus == ANIMATE_INDICATORS) { + float maxRadius = circleScale * bigCircleIndicatorRadius * 1.4f; + dayPaint.setShader(new LinearGradient(x, y, y + (growfactorIndicator > maxRadius ? maxRadius: growfactorIndicator), x + (growfactorIndicator > maxRadius ? maxRadius: growfactorIndicator), Color.rgb(68, 192, 222), Color.rgb(50, 42, 162), Shader.TileMode.CLAMP)); + drawSelectedCircle(canvas, growfactorIndicator > maxRadius ? maxRadius: growfactorIndicator, x, y - (textHeight / 6)); + } else { + Shader shader = new LinearGradient(x, y - (textHeight / 6), x + (circleScale * bigCircleIndicatorRadius), (y - (textHeight / 6)) + (circleScale * bigCircleIndicatorRadius), Color.rgb(68, 192, 222), Color.rgb(50, 42, 162), Shader.TileMode.CLAMP); + dayPaint.setShader(shader); + drawSelectedCircle(canvas, circleScale * bigCircleIndicatorRadius, x, y - (textHeight / 6)); + } + dayPaint.setShader(null); + } + + private void drawDayCircleIndicator(int indicatorStyle, Canvas canvas, float x, float y, int color) { + drawDayCircleIndicator(indicatorStyle, canvas, x, y, color, 1); + } + + private void drawDayCircleIndicator(int indicatorStyle, Canvas canvas, float x, float y, int color, float circleScale) { + float strokeWidth = dayPaint.getStrokeWidth(); + if (indicatorStyle == NO_FILL_LARGE_INDICATOR) { + dayPaint.setStrokeWidth(2 * screenDensity); + dayPaint.setStyle(Paint.Style.STROKE); + } else { + dayPaint.setStyle(Paint.Style.FILL); + } + drawCircle(canvas, x, y, color, circleScale); + dayPaint.setStrokeWidth(strokeWidth); + dayPaint.setStyle(Paint.Style.FILL); + } + + // Draw Circle on certain days to highlight them + private void drawCircle(Canvas canvas, float x, float y, int color, float circleScale) { + dayPaint.setColor(color); + if (animationStatus == ANIMATE_INDICATORS) { + float maxRadius = circleScale * bigCircleIndicatorRadius * 1.4f; + drawCircle(canvas, growfactorIndicator > maxRadius ? maxRadius: growfactorIndicator, x, y - (textHeight / 6)); + } else { + drawCircle(canvas, circleScale * bigCircleIndicatorRadius, x, y - (textHeight / 6)); + } + } + + private void drawEventIndicatorCircle(Canvas canvas, float x, float y, int color) { + dayPaint.setColor(color); + /*if (eventIndicatorStyle == SMALL_INDICATOR) { + dayPaint.setStyle(Paint.Style.FILL); + drawCircle(canvas, smallIndicatorRadius, x, y); + } else if (eventIndicatorStyle == NO_FILL_LARGE_INDICATOR){ + dayPaint.setStyle(Paint.Style.STROKE); + drawDayCircleIndicator(NO_FILL_LARGE_INDICATOR, canvas, x, y, color); + } else if (eventIndicatorStyle == FILL_LARGE_INDICATOR) {*/ + drawDayWithWhiteCircleIndicator(FILL_LARGE_INDICATOR, canvas, x, y, color); + //} + } + + private void drawDayWithWhiteCircleIndicator(int indicatorStyle, Canvas canvas, float x, float y, int color) { + drawDayWithWhiteCircleIndicator(indicatorStyle, canvas, x, y, color, 1); + } + + + private void drawDayWithWhiteCircleIndicator(int indicatorStyle, Canvas canvas, float x, float y, int color, float circleScale) { + dayPaint.setColor(Color.WHITE); + float strokeWidth = dayPaint.getStrokeWidth(); + if (indicatorStyle == NO_FILL_LARGE_INDICATOR) { + dayPaint.setStrokeWidth(2 * screenDensity); + dayPaint.setStyle(Paint.Style.STROKE); + } else { + dayPaint.setStyle(Paint.Style.FILL); + } + drawCircle(canvas, x, y, color, circleScale); + dayPaint.setStrokeWidth(strokeWidth); + dayPaint.setStyle(Paint.Style.FILL); + } + + private void drawCircle(Canvas canvas, float radius, float x, float y) { + canvas.drawCircle(x, y, radius, dayPaint); + } + + private void drawSelectedCircle(Canvas canvas, float radius, float x, float y) { + dayPaint.setStrokeWidth(5); + dayPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(x, y, radius, dayPaint); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarView.java b/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarView.java new file mode 100755 index 0000000..3b44a82 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/CompactCalendarView.java @@ -0,0 +1,440 @@ +package com.github.sundeepk.compactcalendarview; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.widget.OverScroller; + +import com.github.sundeepk.compactcalendarview.domain.Event; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import androidx.core.view.GestureDetectorCompat; + +public class CompactCalendarView extends View { + + public static final int FILL_LARGE_INDICATOR = 1; + public static final int NO_FILL_LARGE_INDICATOR = 2; + public static final int SMALL_INDICATOR = 3; + + private final AnimationHandler animationHandler; + private CompactCalendarController compactCalendarController; + private GestureDetectorCompat gestureDetector; + private boolean horizontalScrollEnabled = true; + + public interface CompactCalendarViewListener { + public void onDayClick(Date dateClicked); + public void onMonthScroll(Date firstDayOfNewMonth); + } + + public interface CompactCalendarAnimationListener { + public void onOpened(); + public void onClosed(); + } + + private final GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + compactCalendarController.onSingleTapUp(e); + invalidate(); + return super.onSingleTapUp(e); + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if(horizontalScrollEnabled) { + if (Math.abs(distanceX) > 0) { + getParent().requestDisallowInterceptTouchEvent(true); + + compactCalendarController.onScroll(e1, e2, distanceX, distanceY); + invalidate(); + return true; + } + } + + return false; + } + }; + + public CompactCalendarView(Context context) { + this(context, null); + } + + public CompactCalendarView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CompactCalendarView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + compactCalendarController = new CompactCalendarController(new Paint(), new OverScroller(getContext()), + new Rect(), attrs, getContext(), Color.argb(255, 233, 84, 81), + Color.argb(255, 64, 64, 64), Color.argb(255, 219, 219, 219), VelocityTracker.obtain(), + Color.argb(255, 100, 68, 65), new EventsContainer(Calendar.getInstance()), + Locale.getDefault(), TimeZone.getDefault()); + gestureDetector = new GestureDetectorCompat(getContext(), gestureListener); + animationHandler = new AnimationHandler(compactCalendarController, this); + } + + public void setAnimationListener(CompactCalendarAnimationListener compactCalendarAnimationListener){ + animationHandler.setCompactCalendarAnimationListener(compactCalendarAnimationListener); + } + + /* + Use a custom locale for compact calendar and reinitialise the view. + */ + public void setLocale(TimeZone timeZone, Locale locale){ + compactCalendarController.setLocale(timeZone, locale); + invalidate(); + } + + /* + Compact calendar will use the locale to determine the abbreviation to use as the day column names. + The default is to use the default locale and to abbreviate the day names to one character. + Setting this to true will displace the short weekday string provided by java. + */ + public void setUseThreeLetterAbbreviation(boolean useThreeLetterAbbreviation){ + compactCalendarController.setUseWeekDayAbbreviation(useThreeLetterAbbreviation); + invalidate(); + } + + public void setCalendarBackgroundColor(final int calenderBackgroundColor) { + compactCalendarController.setCalenderBackgroundColor(calenderBackgroundColor); + invalidate(); + } + + /* + Sets the name for each day of the week. No attempt is made to adjust width or text size based on the length of each day name. + Works best with 3-4 characters for each day. + */ + public void setDayColumnNames(String[] dayColumnNames){ + compactCalendarController.setDayColumnNames(dayColumnNames); + } + + public void setFirstDayOfWeek(int dayOfWeek){ + compactCalendarController.setFirstDayOfWeek(dayOfWeek); + invalidate(); + } + + public void setCurrentSelectedDayBackgroundColor(int currentSelectedDayBackgroundColor) { + compactCalendarController.setCurrentSelectedDayBackgroundColor(currentSelectedDayBackgroundColor); + invalidate(); + } + + public void setCurrentDayBackgroundColor(int currentDayBackgroundColor) { + compactCalendarController.setCurrentDayBackgroundColor(currentDayBackgroundColor); + invalidate(); + } + + public int getHeightPerDay(){ + return compactCalendarController.getHeightPerDay(); + } + + public void setListener(CompactCalendarViewListener listener){ + compactCalendarController.setListener(listener); + } + + public Date getFirstDayOfCurrentMonth(){ + return compactCalendarController.getFirstDayOfCurrentMonth(); + } + + public void shouldDrawIndicatorsBelowSelectedDays(boolean shouldDrawIndicatorsBelowSelectedDays){ + compactCalendarController.shouldDrawIndicatorsBelowSelectedDays(shouldDrawIndicatorsBelowSelectedDays); + } + + public void setCurrentDate(Date dateTimeMonth){ + compactCalendarController.setCurrentDate(dateTimeMonth); + invalidate(); + } + + public int getWeekNumberForCurrentMonth(){ + return compactCalendarController.getWeekNumberForCurrentMonth(); + } + + public void setShouldDrawDaysHeader(boolean shouldDrawDaysHeader){ + compactCalendarController.setShouldDrawDaysHeader(shouldDrawDaysHeader); + } + + public void setCurrentSelectedDayTextColor(int currentSelectedDayTextColor) { + compactCalendarController.setCurrentSelectedDayTextColor(currentSelectedDayTextColor); + } + + public void setCurrentDayTextColor(int currentDayTextColor) { + compactCalendarController.setCurrentDayTextColor(currentDayTextColor); + } + + /** + * see {@link #addEvent(Event, boolean)} when adding single events to control if calendar should redraw + * or {@link #addEvents(java.util.List)} when adding multiple events + * @param event + */ + public void addEvent(Event event){ + addEvent(event, true); + } + + /** + * Adds an event to be drawn as an indicator in the calendar. + * If adding multiple events see {@link #addEvents(List)}} method. + * @param event to be added to the calendar + * @param shouldInvalidate true if the view should invalidate + */ + public void addEvent(Event event, boolean shouldInvalidate){ + compactCalendarController.addEvent(event); + if(shouldInvalidate){ + invalidate(); + } + } + + /** + * Adds multiple events to the calendar and invalidates the view once all events are added. + */ + public void addEvents(List events){ + compactCalendarController.addEvents(events); + invalidate(); + } + + /** + * Fetches the events for the date passed in + * @param date + * @return + */ + public List getEvents(Date date){ + return compactCalendarController.getCalendarEventsFor(date.getTime()); + } + + /** + * Fetches the events for the epochMillis passed in + * @param epochMillis + * @return + */ + public List getEvents(long epochMillis){ + return compactCalendarController.getCalendarEventsFor(epochMillis); + } + + /** + * Fetches the events for the month of the epochMillis passed in and returns a sorted list of events + * @param epochMillis + * @return + */ + public List getEventsForMonth(long epochMillis){ + return compactCalendarController.getCalendarEventsForMonth(epochMillis); + } + + /** + * Fetches the events for the month of the date passed in and returns a sorted list of events + * @param date + * @return + */ + public List getEventsForMonth(Date date){ + return compactCalendarController.getCalendarEventsForMonth(date.getTime()); + } + + /** + * Remove the event associated with the Date passed in + * @param date + */ + public void removeEvents(Date date){ + compactCalendarController.removeEventsFor(date.getTime()); + } + + public void removeEvents(long epochMillis){ + compactCalendarController.removeEventsFor(epochMillis); + } + + /** + * see {@link #removeEvent(Event, boolean)} when removing single events to control if calendar should redraw + * or {@link #removeEvents(java.util.List)} (java.util.List)} when removing multiple events + * @param event + */ + public void removeEvent(Event event){ + removeEvent(event, true); + } + + /** + * Removes an event from the calendar. + * If removing multiple events see {@link #removeEvents(List)} + * + * @param event event to remove from the calendar + * @param shouldInvalidate true if the view should invalidate + */ + public void removeEvent(Event event, boolean shouldInvalidate){ + compactCalendarController.removeEvent(event); + if(shouldInvalidate){ + invalidate(); + } + } + + /** + * Removes multiple events from the calendar and invalidates the view once all events are added. + */ + public void removeEvents(List events){ + compactCalendarController.removeEvents(events); + invalidate(); + } + + /** + * Clears all Events from the calendar. + */ + public void removeAllEvents() { + compactCalendarController.removeAllEvents(); + invalidate(); + } + + public void setIsRtl(boolean isRtl) { + compactCalendarController.setIsRtl(isRtl); + } + + public void shouldSelectFirstDayOfMonthOnScroll(boolean shouldSelectFirstDayOfMonthOnScroll){ + compactCalendarController.setShouldSelectFirstDayOfMonthOnScroll(shouldSelectFirstDayOfMonthOnScroll); + } + + public void setCurrentSelectedDayIndicatorStyle(final int currentSelectedDayIndicatorStyle){ + compactCalendarController.setCurrentSelectedDayIndicatorStyle(currentSelectedDayIndicatorStyle); + invalidate(); + } + + public void setCurrentDayIndicatorStyle(final int currentDayIndicatorStyle){ + compactCalendarController.setCurrentDayIndicatorStyle(currentDayIndicatorStyle); + invalidate(); + } + + public void setEventIndicatorStyle(final int eventIndicatorStyle){ + compactCalendarController.setEventIndicatorStyle(eventIndicatorStyle); + invalidate(); + } + + private void checkTargetHeight() { + if (compactCalendarController.getTargetHeight() <= 0) { + throw new IllegalStateException("Target height must be set in xml properties in order to expand/collapse CompactCalendar."); + } + } + + public void displayOtherMonthDays(boolean displayOtherMonthDays){ + compactCalendarController.setDisplayOtherMonthDays(displayOtherMonthDays); + invalidate(); + } + + public void setTargetHeight(int targetHeight){ + compactCalendarController.setTargetHeight(targetHeight); + checkTargetHeight(); + } + + public void showCalendar(){ + checkTargetHeight(); + animationHandler.openCalendar(); + } + + public void hideCalendar(){ + checkTargetHeight(); + animationHandler.closeCalendar(); + } + + public void showCalendarWithAnimation(){ + checkTargetHeight(); + animationHandler.openCalendarWithAnimation(); + } + + public void hideCalendarWithAnimation(){ + checkTargetHeight(); + animationHandler.closeCalendarWithAnimation(); + } + + /** + * Moves the calendar to the right. This will show the next month when {@link #setIsRtl(boolean)} + * is set to false. If in rtl mode, it will show the previous month. + */ + public void scrollRight(){ + compactCalendarController.scrollRight(); + invalidate(); + } + + /** + * Moves the calendar to the left. This will show the previous month when {@link #setIsRtl(boolean)} + * is set to false. If in rtl mode, it will show the next month. + */ + public void scrollLeft(){ + compactCalendarController.scrollLeft(); + invalidate(); + } + + public boolean isAnimating(){ + return animationHandler.isAnimating(); + } + + @Override + protected void onMeasure(int parentWidth, int parentHeight) { + super.onMeasure(parentWidth, parentHeight); + int width = MeasureSpec.getSize(parentWidth); + int height = MeasureSpec.getSize(parentHeight); + if(width > 0 && height > 0) { + compactCalendarController.onMeasure(width, height, getPaddingRight(), getPaddingLeft()); + } + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(Canvas canvas) { + compactCalendarController.onDraw(canvas); + } + + @Override + public void computeScroll() { + super.computeScroll(); + if(compactCalendarController.computeScroll()){ + invalidate(); + } + } + + public void shouldScrollMonth(boolean enableHorizontalScroll){ + this.horizontalScrollEnabled = enableHorizontalScroll; + } + + public boolean onTouchEvent(MotionEvent event) { + if (horizontalScrollEnabled) { + compactCalendarController.onTouch(event); + invalidate(); + } + + // on touch action finished (CANCEL or UP), we re-allow the parent container to intercept touch events (scroll inside ViewPager + RecyclerView issue #82) + if((event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP) && horizontalScrollEnabled) { + getParent().requestDisallowInterceptTouchEvent(false); + } + + // always allow gestureDetector to detect onSingleTap and scroll events + return gestureDetector.onTouchEvent(event); + } + + @Override + public boolean canScrollHorizontally(int direction) { + if (this.getVisibility() == View.GONE) { + return false; + } + // Prevents ViewPager from scrolling horizontally by announcing that (issue #82) + return this.horizontalScrollEnabled; + } + +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/Events.java b/src/main/java/com/github/sundeepk/compactcalendarview/Events.java new file mode 100644 index 0000000..1df7f9a --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/Events.java @@ -0,0 +1,52 @@ +package com.github.sundeepk.compactcalendarview; + +import com.github.sundeepk.compactcalendarview.domain.Event; + +import java.util.List; + +class Events { + + private final List events; + private final long timeInMillis; + + Events(long timeInMillis, List events) { + this.timeInMillis = timeInMillis; + this.events = events; + } + + long getTimeInMillis() { + return timeInMillis; + } + + List getEvents() { + return events; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Events event = (Events) o; + + if (timeInMillis != event.timeInMillis) return false; + if (events != null ? !events.equals(event.events) : event.events != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = events != null ? events.hashCode() : 0; + result = 31 * result + (int) (timeInMillis ^ (timeInMillis >>> 32)); + return result; + } + + @Override + public String toString() { + return "Events{" + + "events=" + events + + ", timeInMillis=" + timeInMillis + + '}'; + } +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/EventsContainer.java b/src/main/java/com/github/sundeepk/compactcalendarview/EventsContainer.java new file mode 100644 index 0000000..f70e5c1 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/EventsContainer.java @@ -0,0 +1,158 @@ +package com.github.sundeepk.compactcalendarview; + +import com.github.sundeepk.compactcalendarview.comparators.EventComparator; +import com.github.sundeepk.compactcalendarview.domain.Event; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class EventsContainer { + + private Map> eventsByMonthAndYearMap = new HashMap<>(); + private Comparator eventsComparator = new EventComparator(); + private Calendar eventsCalendar; + + public EventsContainer(Calendar eventsCalendar) { + this.eventsCalendar = eventsCalendar; + } + + void addEvent(Event event) { + eventsCalendar.setTimeInMillis(event.getTimeInMillis()); + String key = getKeyForCalendarEvent(eventsCalendar); + List eventsForMonth = eventsByMonthAndYearMap.get(key); + if (eventsForMonth == null) { + eventsForMonth = new ArrayList<>(); + } + Events eventsForTargetDay = getEventDayEvent(event.getTimeInMillis()); + if (eventsForTargetDay == null) { + List events = new ArrayList<>(); + events.add(event); + eventsForMonth.add(new Events(event.getTimeInMillis(), events)); + } else { + eventsForTargetDay.getEvents().add(event); + } + eventsByMonthAndYearMap.put(key, eventsForMonth); + } + + void removeAllEvents() { + eventsByMonthAndYearMap.clear(); + } + + void addEvents(List events) { + int count = events.size(); + for (int i = 0; i < count; i++) { + addEvent(events.get(i)); + } + } + + List getEventsFor(long epochMillis) { + Events events = getEventDayEvent(epochMillis); + if (events == null) { + return new ArrayList<>(); + } else { + return events.getEvents(); + } + } + + List getEventsForMonthAndYear(int month, int year){ + return eventsByMonthAndYearMap.get(year + "_" + month); + } + + List getEventsForMonth(long eventTimeInMillis){ + eventsCalendar.setTimeInMillis(eventTimeInMillis); + String keyForCalendarEvent = getKeyForCalendarEvent(eventsCalendar); + List events = eventsByMonthAndYearMap.get(keyForCalendarEvent); + List allEventsForMonth = new ArrayList<>(); + if (events != null) { + for(Events eve : events){ + if (eve != null) { + allEventsForMonth.addAll(eve.getEvents()); + } + } + } + Collections.sort(allEventsForMonth, eventsComparator); + return allEventsForMonth; + } + + private Events getEventDayEvent(long eventTimeInMillis){ + eventsCalendar.setTimeInMillis(eventTimeInMillis); + int dayInMonth = eventsCalendar.get(Calendar.DAY_OF_MONTH); + String keyForCalendarEvent = getKeyForCalendarEvent(eventsCalendar); + List eventsForMonthsAndYear = eventsByMonthAndYearMap.get(keyForCalendarEvent); + if (eventsForMonthsAndYear != null) { + for (Events events : eventsForMonthsAndYear) { + eventsCalendar.setTimeInMillis(events.getTimeInMillis()); + int dayInMonthFromCache = eventsCalendar.get(Calendar.DAY_OF_MONTH); + if (dayInMonthFromCache == dayInMonth) { + return events; + } + } + } + return null; + } + + void removeEventByEpochMillis(long epochMillis) { + eventsCalendar.setTimeInMillis(epochMillis); + int dayInMonth = eventsCalendar.get(Calendar.DAY_OF_MONTH); + String key = getKeyForCalendarEvent(eventsCalendar); + List eventsForMonthAndYear = eventsByMonthAndYearMap.get(key); + if (eventsForMonthAndYear != null) { + Iterator calendarDayEventIterator = eventsForMonthAndYear.iterator(); + while (calendarDayEventIterator.hasNext()) { + Events next = calendarDayEventIterator.next(); + eventsCalendar.setTimeInMillis(next.getTimeInMillis()); + int dayInMonthFromCache = eventsCalendar.get(Calendar.DAY_OF_MONTH); + if (dayInMonthFromCache == dayInMonth) { + calendarDayEventIterator.remove(); + break; + } + } + if (eventsForMonthAndYear.isEmpty()) { + eventsByMonthAndYearMap.remove(key); + } + } + } + + void removeEvent(Event event) { + eventsCalendar.setTimeInMillis(event.getTimeInMillis()); + String key = getKeyForCalendarEvent(eventsCalendar); + List eventsForMonthAndYear = eventsByMonthAndYearMap.get(key); + if (eventsForMonthAndYear != null) { + Iterator eventsForMonthYrItr = eventsForMonthAndYear.iterator(); + while(eventsForMonthYrItr.hasNext()) { + Events events = eventsForMonthYrItr.next(); + int indexOfEvent = events.getEvents().indexOf(event); + if (indexOfEvent >= 0) { + if (events.getEvents().size() == 1) { + eventsForMonthYrItr.remove(); + } else { + events.getEvents().remove(indexOfEvent); + } + break; + } + } + if (eventsForMonthAndYear.isEmpty()) { + eventsByMonthAndYearMap.remove(key); + } + } + } + + void removeEvents(List events) { + int count = events.size(); + for (int i = 0; i < count; i++) { + removeEvent(events.get(i)); + } + } + + //E.g. 4 2016 becomes 2016_4 + private String getKeyForCalendarEvent(Calendar cal) { + return cal.get(Calendar.YEAR) + "_" + cal.get(Calendar.MONTH); + } + +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/WeekUtils.java b/src/main/java/com/github/sundeepk/compactcalendarview/WeekUtils.java new file mode 100644 index 0000000..1135459 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/WeekUtils.java @@ -0,0 +1,37 @@ +package com.github.sundeepk.compactcalendarview; + +import java.text.DateFormatSymbols; +import java.util.Arrays; +import java.util.Locale; + +public class WeekUtils { + + static String[] getWeekdayNames(Locale locale, int day, boolean useThreeLetterAbbreviation){ + DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(locale); + String[] dayNames = dateFormatSymbols.getShortWeekdays(); + if (dayNames == null) { + throw new IllegalStateException("Unable to determine weekday names from default locale"); + } + if (dayNames.length != 8) { + throw new IllegalStateException("Expected weekday names from default locale to be of size 7 but: " + + Arrays.toString(dayNames) + " with size " + dayNames.length + " was returned."); + } + + String[] weekDayNames = new String[7]; + String[] weekDaysFromSunday = {dayNames[1], dayNames[2], dayNames[3], dayNames[4], dayNames[5], dayNames[6], dayNames[7]}; + for (int currentDay = day - 1, i = 0; i <= 6; i++, currentDay++) { + currentDay = currentDay >= 7 ? 0 : currentDay; + weekDayNames[i] = weekDaysFromSunday[currentDay]; + } + + if (!useThreeLetterAbbreviation) { + for (int i = 0; i < weekDayNames.length; i++) { + weekDayNames[i] = weekDayNames[i].toUpperCase(); + } + } + + return weekDayNames; + } + + +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/comparators/EventComparator.java b/src/main/java/com/github/sundeepk/compactcalendarview/comparators/EventComparator.java new file mode 100644 index 0000000..4994b58 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/comparators/EventComparator.java @@ -0,0 +1,13 @@ +package com.github.sundeepk.compactcalendarview.comparators; + +import com.github.sundeepk.compactcalendarview.domain.Event; + +import java.util.Comparator; + +public class EventComparator implements Comparator { + + @Override + public int compare(Event lhs, Event rhs) { + return lhs.getTimeInMillis() < rhs.getTimeInMillis() ? -1 : lhs.getTimeInMillis() == rhs.getTimeInMillis() ? 0 : 1; + } +} diff --git a/src/main/java/com/github/sundeepk/compactcalendarview/domain/Event.java b/src/main/java/com/github/sundeepk/compactcalendarview/domain/Event.java new file mode 100644 index 0000000..05048c6 --- /dev/null +++ b/src/main/java/com/github/sundeepk/compactcalendarview/domain/Event.java @@ -0,0 +1,65 @@ +package com.github.sundeepk.compactcalendarview.domain; + +import androidx.annotation.Nullable; + +public class Event { + + private int color; + private long timeInMillis; + private Object data; + + public Event(int color, long timeInMillis) { + this.color = color; + this.timeInMillis = timeInMillis; + } + + public Event(int color, long timeInMillis, Object data) { + this.color = color; + this.timeInMillis = timeInMillis; + this.data = data; + } + + public int getColor() { + return color; + } + + public long getTimeInMillis() { + return timeInMillis; + } + + @Nullable + public Object getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Event event = (Event) o; + + if (color != event.color) return false; + if (timeInMillis != event.timeInMillis) return false; + if (data != null ? !data.equals(event.data) : event.data != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = color; + result = 31 * result + (int) (timeInMillis ^ (timeInMillis >>> 32)); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Event{" + + "color=" + color + + ", timeInMillis=" + timeInMillis + + ", data=" + data + + '}'; + } +} diff --git a/src/main/res/font/poppins.xml b/src/main/res/font/poppins.xml new file mode 100644 index 0000000..4eec978 --- /dev/null +++ b/src/main/res/font/poppins.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/poppins_bold.xml b/src/main/res/font/poppins_bold.xml new file mode 100644 index 0000000..43bc84b --- /dev/null +++ b/src/main/res/font/poppins_bold.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/poppins_light.xml b/src/main/res/font/poppins_light.xml new file mode 100644 index 0000000..f9ce88b --- /dev/null +++ b/src/main/res/font/poppins_light.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/poppins_medium.xml b/src/main/res/font/poppins_medium.xml new file mode 100644 index 0000000..7084cda --- /dev/null +++ b/src/main/res/font/poppins_medium.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/poppins_regular.xml b/src/main/res/font/poppins_regular.xml new file mode 100644 index 0000000..4eec978 --- /dev/null +++ b/src/main/res/font/poppins_regular.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/poppins_semibold.xml b/src/main/res/font/poppins_semibold.xml new file mode 100644 index 0000000..803c4d6 --- /dev/null +++ b/src/main/res/font/poppins_semibold.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/font/title_goal_fontfamily.xml b/src/main/res/font/title_goal_fontfamily.xml new file mode 100644 index 0000000..b506c5f --- /dev/null +++ b/src/main/res/font/title_goal_fontfamily.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml new file mode 100644 index 0000000..be80746 --- /dev/null +++ b/src/main/res/values/attrs.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +