package com.miris.ui.comp; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.widget.OverScroller; import com.miris.R; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public class CompactCalendarController { private int paddingWidth = 40; private int paddingHeight = 40; private Paint dayPaint = new Paint(); private Rect rect; private int textHeight; private int textWidth; private static final int DAYS_IN_WEEK = 7; private int widthPerDay; private String[] dayColumnNames; private float distanceX; private PointF accumulatedScrollOffset = new PointF(); private OverScroller scroller; private int monthsScrolledSoFar; private Date currentDate = new Date(); private Locale locale = Locale.getDefault(); private Calendar currentCalender = Calendar.getInstance(locale); private Calendar todayCalender = Calendar.getInstance(locale); private Calendar calendarWithFirstDayOfMonth = Calendar.getInstance(locale); private Calendar eventsCalendar = Calendar.getInstance(locale); private Direction currentDirection = Direction.NONE; private int heightPerDay; private int currentDayBackgroundColor; private int calenderTextColor; private int currentSelectedDayBackgroundColor; private int calenderBackgroundColor = Color.WHITE; private int textSize = 30; private int width; private int height; private int paddingRight; private int paddingLeft; private boolean shouldDrawDaysHeader = true; private Map<String, List<CalendarDayEvent>> events = new HashMap<>(); private boolean showSmallIndicator; private float smallIndicatorRadius; private enum Direction { NONE, HORIZONTAL, VERTICAL } public CompactCalendarController(Paint dayPaint, OverScroller scroller, Rect rect, AttributeSet attrs, Context context, int currentDayBackgroundColor, int calenderTextColor, int currentSelectedDayBackgroundColor) { this.dayPaint = dayPaint; this.scroller = scroller; this.rect = rect; this.currentDayBackgroundColor = currentDayBackgroundColor; this.calenderTextColor = calenderTextColor; this.currentSelectedDayBackgroundColor = currentSelectedDayBackgroundColor; 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); currentSelectedDayBackgroundColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarCurrentSelectedDayBackgroundColor, currentSelectedDayBackgroundColor); calenderBackgroundColor = typedArray.getColor(R.styleable.CompactCalendarView_compactCalendarBackgroundColor, calenderBackgroundColor); textSize = typedArray.getDimensionPixelSize(R.styleable.CompactCalendarView_compactCalendarTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, context.getResources().getDisplayMetrics())); } finally { typedArray.recycle(); } } } private void init(Context context) { 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(), rect); textHeight = rect.height() * 3; textWidth = rect.width() * 2; todayCalender.setTime(currentDate); setToMidnight(todayCalender); currentCalender.setTime(currentDate); setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 0); eventsCalendar.setFirstDayOfWeek(Calendar.MONDAY); float screenDensity = 1; if(context != null){ screenDensity = context.getResources().getDisplayMetrics().density; } //scale small indicator by screen density smallIndicatorRadius = 2.5f * screenDensity; } 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); } public void showNextMonth() { setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentCalender.getTime(), 0, 1); setCurrentDate(calendarWithFirstDayOfMonth.getTime()); } public void showPreviousMonth() { setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentCalender.getTime(), 0, -1); setCurrentDate(calendarWithFirstDayOfMonth.getTime()); } public void setLocale(Locale locale) { if (locale == null) { throw new IllegalArgumentException("Locale cannot be null"); } this.locale = locale; } public void setUseWeekDayAbbreviation(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."); } if (useThreeLetterAbbreviation) { this.dayColumnNames = new String[]{dayNames[2], dayNames[3], dayNames[4], dayNames[5], dayNames[6], dayNames[7], dayNames[1]}; } else { this.dayColumnNames = new String[]{dayNames[2].substring(0, 1), dayNames[3].substring(0, 1), dayNames[4].substring(0, 1), dayNames[5].substring(0, 1), dayNames[6].substring(0, 1), dayNames[7].substring(0, 1), dayNames[1].substring(0, 1)}; } } public 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; } public void setShouldDrawDaysHeader(boolean shouldDrawDaysHeader) { this.shouldDrawDaysHeader = shouldDrawDaysHeader; } public void showSmallIndicator(boolean showSmallIndicator) { this.showSmallIndicator = showSmallIndicator; } public void onMeasure(int width, int height, int paddingRight, int paddingLeft) { widthPerDay = (width) / DAYS_IN_WEEK; heightPerDay = height / 7; this.width = width; this.height = height; this.paddingRight = paddingRight; this.paddingLeft = paddingLeft; } public void onDraw(Canvas canvas) { paddingWidth = widthPerDay / 2; paddingHeight = heightPerDay / 2; calculateXPositionOffset(); drawCalenderBackground(canvas); drawScrollableCalender(canvas); } public boolean onTouch(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { if (currentDirection == Direction.HORIZONTAL) { monthsScrolledSoFar = Math.round(accumulatedScrollOffset.x / width); float remainingScrollAfterFingerLifted = (accumulatedScrollOffset.x - monthsScrolledSoFar * width); scroller.startScroll((int) accumulatedScrollOffset.x, 0, (int) -remainingScrollAfterFingerLifted, 0); currentDirection = Direction.NONE; setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 0); if (calendarWithFirstDayOfMonth.get(Calendar.MONTH) != currentCalender.get(Calendar.MONTH)) { setCalenderToFirstDayOfMonth(currentCalender, currentDate, -monthsScrolledSoFar, 0); } return true; } currentDirection = Direction.NONE; } return false; } public int getHeightPerDay() { return heightPerDay; } public int getWeekNumberForCurrentMonth() { Calendar calendar = Calendar.getInstance(locale); calendar.setTime(currentDate); return calendar.get(Calendar.WEEK_OF_MONTH); } public Date getFirstDayOfCurrentMonth() { Calendar calendar = Calendar.getInstance(locale); calendar.setTime(currentDate); calendar.add(Calendar.MONTH, -monthsScrolledSoFar); calendar.set(Calendar.DAY_OF_MONTH, 1); setToMidnight(calendar); return calendar.getTime(); } public void setCurrentDate(Date dateTimeMonth) { currentDate = new Date(dateTimeMonth.getTime()); currentCalender.setTime(currentDate); setToMidnight(currentCalender); monthsScrolledSoFar = 0; accumulatedScrollOffset.x = 0; } 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); } public void addEvent(CalendarDayEvent event) { eventsCalendar.setTimeInMillis(event.getTimeInMillis()); String key = getKeyForCalendarEvent(eventsCalendar); List<CalendarDayEvent> uniqCalendarDayEvents = events.get(key); if (uniqCalendarDayEvents == null) { uniqCalendarDayEvents = new ArrayList<>(); } if (!uniqCalendarDayEvents.contains(event)) { uniqCalendarDayEvents.add(event); } events.put(key, uniqCalendarDayEvents); } public void addEvents(List<CalendarDayEvent> events) { int count = events.size(); for (int i = 0; i < count; i++) { addEvent(events.get(i)); } } public void removeEvent(CalendarDayEvent event) { eventsCalendar.setTimeInMillis(event.getTimeInMillis()); String key = getKeyForCalendarEvent(eventsCalendar); List<CalendarDayEvent> uniqCalendarDayEvents = events.get(key); if (uniqCalendarDayEvents != null) { uniqCalendarDayEvents.remove(event); } } public void removeEvents(List<CalendarDayEvent> events) { int count = events.size(); for (int i = 0; i < count; i++) { removeEvent(events.get(i)); } } List<CalendarDayEvent> getEvents(Date date) { eventsCalendar.setTimeInMillis(date.getTime()); String key = getKeyForCalendarEvent(eventsCalendar); List<CalendarDayEvent> uniqEvents = events.get(key); if (events != null) { return uniqEvents; } else { return new ArrayList<>(); } } private String getKeyForCalendarEvent(Calendar cal) { return cal.get(Calendar.YEAR) + "_" + cal.get(Calendar.MONTH); } public Date onSingleTapConfirmed(MotionEvent e) { monthsScrolledSoFar = Math.round(accumulatedScrollOffset.x / width); int dayColumn = Math.round((paddingLeft + e.getX() - paddingWidth - paddingRight) / widthPerDay); int dayRow = Math.round((e.getY() - paddingHeight) / heightPerDay); setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 0); //Start Monday as day 1 and Sunday as day 7. Not Sunday as day 1 and Monday as day 2 int firstDayOfMonth = calendarWithFirstDayOfMonth.get(Calendar.DAY_OF_WEEK) - 1; firstDayOfMonth = firstDayOfMonth <= 0 ? 7 : firstDayOfMonth; int dayOfMonth = ((dayRow - 1) * 7 + dayColumn + 1) - firstDayOfMonth; if (dayOfMonth < calendarWithFirstDayOfMonth.getActualMaximum(Calendar.DAY_OF_MONTH) && dayOfMonth >= 0) { calendarWithFirstDayOfMonth.add(Calendar.DATE, dayOfMonth); currentCalender.setTimeInMillis(calendarWithFirstDayOfMonth.getTimeInMillis()); return currentCalender.getTime(); } else { return null; } } public boolean onDown(MotionEvent e) { scroller.forceFinished(true); return true; } public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { scroller.forceFinished(true); return true; } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (currentDirection == Direction.NONE) { if (Math.abs(distanceX) > Math.abs(distanceY)) { currentDirection = Direction.HORIZONTAL; } else { currentDirection = Direction.VERTICAL; } } this.distanceX = distanceX; return true; } public boolean computeScroll() { if (scroller.computeScrollOffset()) { accumulatedScrollOffset.x = scroller.getCurrX(); return true; } return false; } private void drawScrollableCalender(Canvas canvas) { monthsScrolledSoFar = (int) (accumulatedScrollOffset.x / width); drawPreviousMonth(canvas); drawCurrentMonth(canvas); drawNextMonth(canvas); } private void drawNextMonth(Canvas canvas) { setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 1); drawMonth(canvas, calendarWithFirstDayOfMonth, (width * (-monthsScrolledSoFar + 1))); } private void drawCurrentMonth(Canvas canvas) { setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, 0); drawMonth(canvas, calendarWithFirstDayOfMonth, width * -monthsScrolledSoFar); } private void drawPreviousMonth(Canvas canvas) { setCalenderToFirstDayOfMonth(calendarWithFirstDayOfMonth, currentDate, -monthsScrolledSoFar, -1); 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) { List<CalendarDayEvent> uniqCalendarDayEvents = events.get(getKeyForCalendarEvent(currentMonthToDrawCalender)); boolean shouldDrawCurrentDayCircle = currentMonthToDrawCalender.get(Calendar.MONTH) == todayCalender.get(Calendar.MONTH); int todayDayOfMonth = todayCalender.get(Calendar.DAY_OF_MONTH); if (uniqCalendarDayEvents != null) { for (int i = 0; i < uniqCalendarDayEvents.size(); i++) { CalendarDayEvent event = uniqCalendarDayEvents.get(i); long timeMillis = event.getTimeInMillis(); eventsCalendar.setTimeInMillis(timeMillis); int dayOfWeek = eventsCalendar.get(Calendar.DAY_OF_WEEK) - 1; dayOfWeek = dayOfWeek <= 0 ? 7 : dayOfWeek; dayOfWeek = dayOfWeek - 1; int weekNumberForMonth = eventsCalendar.get(Calendar.WEEK_OF_MONTH); float xPosition = widthPerDay * dayOfWeek + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight; float yPosition = weekNumberForMonth * heightPerDay + paddingHeight; int dayOfMonth = eventsCalendar.get(Calendar.DAY_OF_MONTH); boolean isSameDayAsCurrentDay = (todayDayOfMonth == dayOfMonth && shouldDrawCurrentDayCircle); if (!isSameDayAsCurrentDay) { if (showSmallIndicator) { if (event.getholiday()) { drawSmallIndicatorCircle(canvas, xPosition - 13, yPosition + 17, event.getColor()); drawSmallIndicatorCircle(canvas, xPosition + 13, yPosition + 17, Color.argb(255, 102, 0, 153)); } else { drawSmallIndicatorCircle(canvas, xPosition, yPosition + 17, event.getColor()); } } else { drawCircle(canvas, xPosition, yPosition, event.getColor()); } } } } } void drawMonth(Canvas canvas, Calendar currentMonthToDrawCalender, int offset) { drawEvents(canvas, currentMonthToDrawCalender, offset); int firstDayOfMonth = currentMonthToDrawCalender.get(Calendar.DAY_OF_WEEK) - 1; firstDayOfMonth = firstDayOfMonth <= 0 ? 7 : firstDayOfMonth; firstDayOfMonth = firstDayOfMonth - 1; boolean isSameMonth = currentMonthToDrawCalender.get(Calendar.MONTH) == todayCalender.get(Calendar.MONTH); int todayDayOfMonth = todayCalender.get(Calendar.DAY_OF_MONTH); for (int dayColumn = 0, dayRow = 0; dayColumn <= 6; dayRow++) { if (dayRow == 7) { dayRow = 0; if (dayColumn <= 6) { dayColumn++; } } if (dayColumn == 6) { dayPaint.setColor(Color.argb(255, 233, 84, 81)); } else { dayPaint.setColor(calenderTextColor); } if (dayColumn == dayColumnNames.length) { break; } float xPosition = widthPerDay * dayColumn + paddingWidth + paddingLeft + accumulatedScrollOffset.x + offset - paddingRight; if (dayRow == 0) { // first row, so draw the first letter of the day if (shouldDrawDaysHeader) { dayPaint.setTypeface(Typeface.DEFAULT_BOLD); canvas.drawText(dayColumnNames[dayColumn], xPosition, paddingHeight, dayPaint); dayPaint.setTypeface(Typeface.DEFAULT); } } else { int day = ((dayRow - 1) * 7 + dayColumn + 1) - firstDayOfMonth; float yPosition = dayRow * heightPerDay + paddingHeight; if (isSameMonth && todayDayOfMonth == day) { drawCircle(canvas, xPosition, yPosition, currentDayBackgroundColor); } else if (currentCalender.get(Calendar.DAY_OF_MONTH) == day) { drawCircle(canvas, xPosition, yPosition, currentSelectedDayBackgroundColor); } if (day <= currentMonthToDrawCalender.getActualMaximum(Calendar.DAY_OF_MONTH) && day > 0) { canvas.drawText(String.valueOf(day), xPosition, yPosition, dayPaint); } } } } private void drawCircle(Canvas canvas, float x, float y, int color) { dayPaint.setColor(color); float radius = (float) (0.5 * Math.sqrt(widthPerDay * widthPerDay + heightPerDay * heightPerDay)); drawCircle(canvas, radius / 2, x, y - (textHeight / 6)); } private void drawSmallIndicatorCircle(Canvas canvas, float x, float y, int color) { dayPaint.setColor(color); drawCircle(canvas, smallIndicatorRadius, x, y); } private void drawCircle(Canvas canvas, float radius, float x, float y) { if (radius >= 34) { radius = 58; } dayPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(x, y, radius, dayPaint); dayPaint.setStyle(Paint.Style.STROKE); dayPaint.setColor(calenderTextColor); } }