diff --git a/core/build.gradle b/core/build.gradle index b01f1943b..c9a538c24 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -15,9 +15,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - testOptions { - unitTests.includeAndroidResources = true - } } dependencies { diff --git a/core/src/main/java/com/alamkanak/weekview/CalendarExtensions.kt b/core/src/main/java/com/alamkanak/weekview/CalendarExtensions.kt index d4291c972..65c8bbf27 100644 --- a/core/src/main/java/com/alamkanak/weekview/CalendarExtensions.kt +++ b/core/src/main/java/com/alamkanak/weekview/CalendarExtensions.kt @@ -8,30 +8,6 @@ import kotlin.math.roundToInt internal const val DAY_IN_MILLIS = 1000L * 60L * 60L * 24L -internal interface Duration { - val inMillis: Int -} - -internal inline class Days(val days: Int) : Duration { - override val inMillis: Int - get() = days * (24 * 60 * 60 * 1_000) -} - -internal inline class Hours(val hours: Int) : Duration { - override val inMillis: Int - get() = hours * (60 * 60 * 1_000) -} - -internal inline class Minutes(val minutes: Int) : Duration { - override val inMillis: Int - get() = minutes * (60 * 1_000) -} - -internal inline class Millis(val millis: Int) : Duration { - override val inMillis: Int - get() = millis -} - internal var Calendar.hour: Int get() = get(Calendar.HOUR_OF_DAY) set(value) { @@ -61,82 +37,68 @@ internal val Calendar.year: Int internal fun Calendar.isEqual(other: Calendar) = timeInMillis == other.timeInMillis -internal fun Calendar.isNotEqual(other: Calendar) = isEqual(other).not() - -internal operator fun Calendar.plus(days: Days): Calendar { +internal fun Calendar.plusDays(days: Int): Calendar { return copy().apply { - add(Calendar.DATE, days.days) + add(Calendar.DATE, days) } } -internal operator fun Calendar.plusAssign(days: Days) { - add(Calendar.DATE, days.days) -} - -internal operator fun Calendar.minus(days: Days): Calendar { +internal fun Calendar.plusHours(hours: Int): Calendar { return copy().apply { - add(Calendar.DATE, days.days * (-1)) + add(Calendar.HOUR_OF_DAY, hours) } } -internal operator fun Calendar.minusAssign(days: Days) { - add(Calendar.DATE, days.days * (-1)) +internal fun Calendar.addDays(days: Int) { + add(Calendar.DATE, days) } -internal operator fun Calendar.plus(minutes: Minutes): Calendar { +internal fun Calendar.minusDays(days: Int): Calendar { return copy().apply { - add(Calendar.MINUTE, minutes.minutes) + add(Calendar.DATE, days * -1) } } -internal operator fun Calendar.minus(minutes: Minutes): Calendar { +internal fun Calendar.minusHours(hours: Int): Calendar { return copy().apply { - add(Calendar.MINUTE, minutes.minutes * (-1)) + add(Calendar.HOUR_OF_DAY, hours * -1) } } -internal operator fun Calendar.minusAssign(minutes: Minutes) { - add(Calendar.MINUTE, minutes.minutes * (-1)) -} - -internal operator fun Calendar.plus(hours: Hours): Calendar { +internal fun Calendar.plusMillis(millis: Int): Calendar { return copy().apply { - add(Calendar.HOUR_OF_DAY, hours.hours) + add(Calendar.MILLISECOND, millis) } } -internal operator fun Calendar.plusAssign(hours: Hours) { - add(Calendar.HOUR_OF_DAY, hours.hours) -} - -internal operator fun Calendar.minus(hours: Hours): Calendar { +internal fun Calendar.minusMillis(millis: Int): Calendar { return copy().apply { - add(Calendar.HOUR_OF_DAY, hours.hours * (-1)) + add(Calendar.MILLISECOND, millis * -1) } } -internal operator fun Calendar.minusAssign(hours: Hours) { - add(Calendar.HOUR_OF_DAY, hours.hours * (-1)) +internal fun Calendar.subtractMillis(millis: Int) { + add(Calendar.MILLISECOND, millis * -1) } -internal operator fun Calendar.plus(millis: Millis): Calendar { +internal fun Calendar.plusMinutes(minutes: Int): Calendar { return copy().apply { - add(Calendar.MILLISECOND, millis.millis) + add(Calendar.MINUTE, minutes) } } -internal operator fun Calendar.plusAssign(millis: Millis) { - add(Calendar.MILLISECOND, millis.millis) -} - -internal operator fun Calendar.minus(millis: Millis): Calendar { +internal fun Calendar.minusMinutes(minutes: Int): Calendar { return copy().apply { - add(Calendar.MILLISECOND, millis.millis * (-1)) + add(Calendar.MINUTE, minutes * -1) } } -internal operator fun Calendar.minusAssign(millis: Millis) { - add(Calendar.MILLISECOND, millis.millis * (-1)) +internal fun Calendar.subtractMinutes(hours: Int) { + add(Calendar.MINUTE, hours * -1) +} + +internal fun Calendar.subtractHours(hours: Int) { + add(Calendar.HOUR_OF_DAY, hours * -1) } internal fun Calendar.isBefore(other: Calendar) = timeInMillis < other.timeInMillis @@ -151,9 +113,9 @@ internal val Calendar.isToday: Boolean internal fun Calendar.toEpochDays(): Int = (atStartOfDay.timeInMillis / DAY_IN_MILLIS).toInt() -internal infix fun Calendar.minutesUntil(other: Calendar): Minutes { +internal infix fun Calendar.minutesUntil(other: Calendar): Int { val diff = (timeInMillis - other.timeInMillis) / 60_000 - return Minutes(diff.toInt()) + return diff.toInt() } internal val Calendar.lengthOfMonth: Int @@ -245,7 +207,7 @@ internal fun List.validate(viewState: ViewState): List { viewState.createDateRange(minDate!!) } mustAdjustEnd -> { - val start = maxDate!! - Days(viewState.numberOfVisibleDays - 1) + val start = maxDate!!.minusDays(viewState.numberOfVisibleDays - 1) viewState.createDateRange(start) } else -> { @@ -310,7 +272,7 @@ internal fun Calendar.format(): String { return sdf.format(time) } -fun Calendar.computeDifferenceWithFirstDayOfWeek(): Int { +internal fun Calendar.computeDifferenceWithFirstDayOfWeek(): Int { val firstDayOfWeek = firstDayOfWeek return if (firstDayOfWeek == Calendar.MONDAY && dayOfWeek == Calendar.SUNDAY) { // Special case, because Calendar.MONDAY has constant value 2 and Calendar.SUNDAY has @@ -321,18 +283,34 @@ fun Calendar.computeDifferenceWithFirstDayOfWeek(): Int { } } -fun Calendar.previousFirstDayOfWeek(): Calendar { - val result = this - Days(1) +internal fun Calendar.previousFirstDayOfWeek(): Calendar { + val result = this.minusDays(1) while (result.dayOfWeek != firstDayOfWeek) { result.add(Calendar.DATE, -1) } return result } -fun Calendar.nextFirstDayOfWeek(): Calendar { - val result = this + Days(1) +internal fun Calendar.nextFirstDayOfWeek(): Calendar { + val result = this.plusDays(1) while (result.dayOfWeek != firstDayOfWeek) { result.add(Calendar.DATE, 1) } return result } + +internal fun Calendar.limitToMinHour(minHour: Int): Calendar { + return if (hour < minHour) { + withTimeAtStartOfPeriod(hour = minHour) + } else { + this + } +} + +internal fun Calendar.limitToMaxHour(maxHour: Int): Calendar { + return if (hour >= maxHour) { + withTimeAtEndOfPeriod(hour = maxHour) + } else { + this + } +} diff --git a/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt b/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt index f4a44e94d..7bb50ac6e 100644 --- a/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt +++ b/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt @@ -57,7 +57,7 @@ private class SingleEventsUpdater( } val eventChips = chipsCache?.normalEventChipsByDate(date).orEmpty().filter { - it.event.isWithin(viewState.minHour, viewState.maxHour) + it.item.isWithin(viewState.minHour, viewState.maxHour) } eventChips.calculateBounds(startPixel = modifiedStartPixel) @@ -244,11 +244,19 @@ private class SingleEventsDrawer( return } - val sortedEventChips = eventChips.sortedBy { - it.event.id == viewState.dragState?.eventId + val (backgroundChips, foregroundChips) = eventChips.partition { + it.item.configuration.arrangement == WeekViewItem.Arrangement.Background } - for (eventChip in sortedEventChips) { + val draggedEventId = viewState.dragState?.eventId + + val sortedChips = mutableListOf().apply { + // Make sure that the currently dragged chip is rendered above all other chips + this += backgroundChips.sortedBy { it.item.id == draggedEventId } + this += foregroundChips.sortedBy { it.item.id == draggedEventId } + } + + for (eventChip in sortedChips) { val textLayout = eventLabels[eventChip.id] eventChipDrawer.draw(eventChip, canvas = this, textLayout) } diff --git a/core/src/main/java/com/alamkanak/weekview/DragHandler.kt b/core/src/main/java/com/alamkanak/weekview/DragHandler.kt index 4006a834f..d6e2ab091 100644 --- a/core/src/main/java/com/alamkanak/weekview/DragHandler.kt +++ b/core/src/main/java/com/alamkanak/weekview/DragHandler.kt @@ -18,7 +18,7 @@ internal class DragHandler( private val executor = DragScrollExecutor() - private val draggedEvent: ResolvedWeekViewEntity? + private val draggedEvent: WeekViewItem? get() { val eventsCache = eventsCacheProvider() ?: return null val eventId = viewState.dragState?.eventId ?: return null @@ -30,8 +30,8 @@ internal class DragHandler( fun startDragAndDrop(eventChip: EventChip, x: Float, y: Float) { viewState.dragState = DragState( - eventId = eventChip.eventId, - draggedEventStartTime = eventChip.event.startTime, + eventId = eventChip.itemId, + draggedEventStartTime = eventChip.item.duration.startTime, dragStartTime = requireNotNull(touchHandler.calculateTimeFromPoint(x, y)), ) @@ -64,33 +64,33 @@ internal class DragHandler( ): Calendar { val dragState = requireNotNull(viewState.dragState) val delta = currentDragLocation minutesUntil dragState.dragStartTime - return dragState.draggedEventStartTime + delta + return dragState.draggedEventStartTime.plusMinutes(delta) } private fun sanitizeEventStart( rawEventStart: Calendar, ): Calendar { val minutesBeyondQuarterHour = rawEventStart.minute % 15 - val minutesUntilNextQuarterHour = 15 - minutesBeyondQuarterHour return if (minutesBeyondQuarterHour >= 8) { // Go to next quarter hour - rawEventStart + Minutes(minutesUntilNextQuarterHour) + val minutesUntilNextQuarterHour = 15 - minutesBeyondQuarterHour + rawEventStart.plusMinutes(minutesUntilNextQuarterHour) } else { // Go to previous quarter hour - rawEventStart - Minutes(minutesBeyondQuarterHour) + rawEventStart.minusMinutes(minutesBeyondQuarterHour) } } private fun updateDraggedEvent(newStartTime: Calendar) { val originalEvent = draggedEvent ?: return - val updatedEvent = originalEvent.createCopy( + val updatedEvent = originalEvent.copyWith( startTime = newStartTime, - endTime = newStartTime + Minutes(originalEvent.durationInMinutes), + endTime = newStartTime.plusMinutes(originalEvent.durationInMinutes), ) val eventsProcessor = eventsProcessorProvider() ?: return - eventsProcessor.updateDraggedEntity(updatedEvent, viewState) + eventsProcessor.updateDraggedItem(updatedEvent, viewState) } private fun scrollIfNecessary(e: MotionEvent) { @@ -116,7 +116,7 @@ internal class DragHandler( } val draggedEvent = draggedEvent ?: return@execute - updateDraggedEvent(newStartTime = draggedEvent.startTime - Minutes(15)) + updateDraggedEvent(newStartTime = draggedEvent.duration.startTime.minusMinutes(15)) val distance = viewState.hourHeight / 4f navigator.scrollVerticallyBy(distance = distance * (-1)) @@ -132,7 +132,7 @@ internal class DragHandler( } val draggedEvent = draggedEvent ?: return@execute - updateDraggedEvent(newStartTime = draggedEvent.startTime + Minutes(15)) + updateDraggedEvent(newStartTime = draggedEvent.duration.startTime.plusMinutes(15)) val distance = viewState.hourHeight / 4f navigator.scrollVerticallyBy(distance = distance) @@ -140,22 +140,30 @@ internal class DragHandler( } private fun scrollLeft() { + if (!viewState.horizontalScrollingEnabled) { + return + } + executor.execute(delay = 600) { val draggedEvent = draggedEvent ?: return@execute - updateDraggedEvent(newStartTime = draggedEvent.startTime - Days(1)) + updateDraggedEvent(newStartTime = draggedEvent.duration.startTime.minusDays(1)) - val date = draggedEvent.startTime.atStartOfDay - navigator.scrollHorizontallyTo(date - Days(1)) + val date = draggedEvent.duration.startTime.atStartOfDay + navigator.scrollHorizontallyTo(date.minusDays(1)) } } private fun scrollRight() { + if (!viewState.horizontalScrollingEnabled) { + return + } + executor.execute(delay = 600) { val draggedEvent = draggedEvent ?: return@execute - updateDraggedEvent(newStartTime = draggedEvent.startTime + Days(1)) + updateDraggedEvent(newStartTime = draggedEvent.duration.startTime.plusDays(1)) - val date = draggedEvent.startTime.atStartOfDay - navigator.scrollHorizontallyTo(date + Days(1)) + val date = draggedEvent.duration.startTime.atStartOfDay + navigator.scrollHorizontallyTo(date.plusDays(1)) } } diff --git a/core/src/main/java/com/alamkanak/weekview/EventChip.kt b/core/src/main/java/com/alamkanak/weekview/EventChip.kt index 5bf78ce7e..e3652f4ba 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventChip.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventChip.kt @@ -4,12 +4,12 @@ import android.graphics.RectF import java.util.Calendar /** - * This class encapsulates a [ResolvedWeekViewEntity] and its visual representation, a [RectF] which - * is drawn to the screen. There may be more than one [EventChip] for any [ResolvedWeekViewEntity], - * for instance in the case of multi-day events. + * This class encapsulates a [WeekViewItem] and its visual representation, a [RectF] which is drawn + * to the screen. There may be more than one [EventChip] for any [WeekViewItem], for instance in the + * case of multi-day events. */ internal data class EventChip( - val event: ResolvedWeekViewEntity, + val item: WeekViewItem, val index: Int, val startTime: Calendar, val endTime: Calendar, @@ -18,12 +18,12 @@ internal data class EventChip( /** * A unique ID of this [EventChip]. */ - val id: String = "${event.id}-$index" + val id: String = "${item.id}-$index" /** - * The ID of this [EventChip]'s [ResolvedWeekViewEntity]. + * The ID of this [EventChip]'s [WeekViewItem]. */ - val eventId: Long = event.id + val itemId: Long = item.id /** * The bounds in which [EventChip] will be drawn. @@ -31,7 +31,7 @@ internal data class EventChip( var bounds: RectF = RectF() val durationInMinutes: Int by lazy { - (endTime minutesUntil startTime).minutes + endTime minutesUntil startTime } /** @@ -62,10 +62,10 @@ internal data class EventChip( var minutesFromStartHour: Int = 0 val startsOnEarlierDay: Boolean - get() = event.startTime < startTime + get() = item.duration.startTime < startTime val endsOnLaterDay: Boolean - get() = event.endTime > endTime + get() = item.duration.endTime > endTime fun setEmpty() { bounds.setEmpty() diff --git a/core/src/main/java/com/alamkanak/weekview/EventChipBoundsCalculator.kt b/core/src/main/java/com/alamkanak/weekview/EventChipBoundsCalculator.kt index c1a84f680..2cc5d48c4 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventChipBoundsCalculator.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventChipBoundsCalculator.kt @@ -10,13 +10,10 @@ internal class EventChipBoundsCalculator( eventChip: EventChip, startPixel: Float ): RectF { - val drawableWidth = when (eventChip.event) { - is ResolvedWeekViewEntity.Event<*> -> viewState.drawableDayWidth - is ResolvedWeekViewEntity.BlockedTime -> viewState.dayWidth - } + val respectDayGap = eventChip.item.configuration.respectDayGap + val drawableWidth = if (respectDayGap) viewState.drawableDayWidth else viewState.dayWidth - val isBlockedTime = eventChip.event is ResolvedWeekViewEntity.BlockedTime - val leftOffset = if (viewState.isLtr || isBlockedTime) 0 else viewState.columnGap + val leftOffset = if (viewState.isLtr || !respectDayGap) 0 else viewState.columnGap val minutesFromStart = eventChip.minutesFromStartHour val top = calculateDistanceFromTop(minutesFromStart) @@ -27,7 +24,7 @@ internal class EventChipBoundsCalculator( val partialEventEndsAtEndOfDay = eventChip.endTime.isAtEndOfPeriod(hour = viewState.maxHour) val fullEventContinuesOnNextDay = eventChip.endsOnLaterDay - if (!(partialEventEndsAtEndOfDay && fullEventContinuesOnNextDay) && !isBlockedTime) { + if (!(partialEventEndsAtEndOfDay && fullEventContinuesOnNextDay)) { // There's only one case where we don't render a vertical margin: The partial event ends // at midnight, but the full event continues continues on the next day. bottom -= viewState.eventMarginVertical @@ -49,7 +46,7 @@ internal class EventChipBoundsCalculator( right -= viewState.singleDayHorizontalPadding * 2 } - val isBeingDragged = eventChip.eventId == viewState.dragState?.eventId + val isBeingDragged = eventChip.itemId == viewState.dragState?.eventId if (isBeingDragged) { left = startPixel + leftOffset right = left + drawableWidth diff --git a/core/src/main/java/com/alamkanak/weekview/EventChipDrawer.kt b/core/src/main/java/com/alamkanak/weekview/EventChipDrawer.kt index af0bf1564..7bf5b6254 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventChipDrawer.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventChipDrawer.kt @@ -17,39 +17,27 @@ internal class EventChipDrawer( private val backgroundPaint = Paint() private val borderPaint = Paint() - private val patternPaint = Paint(Paint.ANTI_ALIAS_FLAG) - internal fun draw( eventChip: EventChip, canvas: Canvas, textLayout: StaticLayout? ) = with(canvas) { - val entity = eventChip.event + val item = eventChip.item val bounds = eventChip.bounds - val cornerRadius = (entity.style.cornerRadius ?: viewState.eventCornerRadius).toFloat() + val cornerRadius = (item.style.cornerRadius ?: viewState.eventCornerRadius).toFloat() - val isBeingDragged = entity.id == viewState.dragState?.eventId - updateBackgroundPaint(entity, isBeingDragged, backgroundPaint) + val isBeingDragged = item.id == viewState.dragState?.eventId + updateBackgroundPaint(item, isBeingDragged, backgroundPaint) drawRoundRect(bounds, cornerRadius, cornerRadius, backgroundPaint) - val pattern = entity.style.pattern - if (pattern != null) { - drawPattern( - pattern = pattern, - bounds = eventChip.bounds, - isLtr = viewState.isLtr, - paint = patternPaint - ) - } - - val borderWidth = entity.style.borderWidth + val borderWidth = item.style.borderWidth if (borderWidth != null && borderWidth > 0) { - updateBorderPaint(entity, borderPaint) + updateBorderPaint(item, borderPaint) val borderBounds = bounds.insetBy(borderWidth / 2f) drawRoundRect(borderBounds, cornerRadius, cornerRadius, borderPaint) } - if (entity.isMultiDay && entity.isNotAllDay) { + if (item.isMultiDay && item.isNotAllDay) { drawCornersForMultiDayEvents(eventChip, cornerRadius) } @@ -62,11 +50,11 @@ internal class EventChipDrawer( eventChip: EventChip, cornerRadius: Float ) { - val event = eventChip.event + val item = eventChip.item val bounds = eventChip.bounds - val isBeingDragged = event.id == viewState.dragState?.eventId - updateBackgroundPaint(event, isBeingDragged, backgroundPaint) + val isBeingDragged = item.id == viewState.dragState?.eventId + updateBackgroundPaint(item, isBeingDragged, backgroundPaint) if (eventChip.startsOnEarlierDay) { val topRect = RectF(bounds) @@ -80,7 +68,7 @@ internal class EventChipDrawer( drawRect(bottomRect, backgroundPaint) } - if (event.style.borderWidth != null) { + if (item.style.borderWidth != null) { drawMultiDayBorderStroke(eventChip, cornerRadius) } } @@ -89,14 +77,14 @@ internal class EventChipDrawer( eventChip: EventChip, cornerRadius: Float ) { - val event = eventChip.event + val item = eventChip.item val bounds = eventChip.bounds - val borderWidth = event.style.borderWidth ?: 0 + val borderWidth = item.style.borderWidth ?: 0 val borderStart = bounds.left + borderWidth / 2 val borderEnd = bounds.right - borderWidth / 2 - updateBorderPaint(event, backgroundPaint) + updateBorderPaint(item, backgroundPaint) if (eventChip.startsOnEarlierDay) { drawVerticalLine( @@ -143,7 +131,7 @@ internal class EventChipDrawer( bounds.right - viewState.eventPaddingHorizontal } - val verticalOffset = if (eventChip.event.isAllDay) { + val verticalOffset = if (eventChip.item.isAllDay) { (bounds.height() - textLayout.height) / 2f } else { viewState.eventPaddingVertical.toFloat() @@ -155,11 +143,11 @@ internal class EventChipDrawer( } private fun updateBackgroundPaint( - entity: ResolvedWeekViewEntity, + item: WeekViewItem, isBeingDragged: Boolean, paint: Paint ) = with(paint) { - color = entity.style.backgroundColor ?: viewState.defaultEventColor + color = item.style.backgroundColor ?: viewState.defaultEventColor isAntiAlias = true strokeWidth = 0f style = Paint.Style.FILL @@ -172,12 +160,12 @@ internal class EventChipDrawer( } private fun updateBorderPaint( - entity: ResolvedWeekViewEntity, + item: WeekViewItem, paint: Paint ) = with(paint) { - color = entity.style.borderColor ?: viewState.defaultEventColor + color = item.style.borderColor ?: viewState.defaultEventColor isAntiAlias = true - strokeWidth = entity.style.borderWidth?.toFloat() ?: 0f + strokeWidth = item.style.borderWidth?.toFloat() ?: 0f style = Paint.Style.STROKE } } diff --git a/core/src/main/java/com/alamkanak/weekview/EventChipsCache.kt b/core/src/main/java/com/alamkanak/weekview/EventChipsCache.kt index fc49509ab..0f1b575ee 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventChipsCache.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventChipsCache.kt @@ -9,36 +9,44 @@ internal typealias EventChipsCacheProvider = () -> EventChipsCache? internal class EventChipsCache { val allEventChips: List - get() = normalEventChipsByDate.values.flatten() + allDayEventChipsByDate.values.flatten() + get() = chipsByDate.values.flatten() - private val normalEventChipsByDate = ConcurrentHashMap>() - private val allDayEventChipsByDate = ConcurrentHashMap>() + private val chipsByDate = ConcurrentHashMap>() fun allEventChipsInDateRange( - dateRange: List + dateRange: List, ): List { val results = mutableListOf() for (date in dateRange) { - results += allDayEventChipsByDate[date.atStartOfDay.timeInMillis].orEmpty() - results += normalEventChipsByDate[date.atStartOfDay.timeInMillis].orEmpty() + results += chipsByDate[date.atStartOfDay.timeInMillis].orEmpty() } return results } fun normalEventChipsByDate( - date: Calendar - ): List = normalEventChipsByDate[date.atStartOfDay.timeInMillis].orEmpty() + date: Calendar, + ): List = chipsByDate[date.atStartOfDay.timeInMillis].orEmpty().filter { it.item.isNotAllDay } fun allDayEventChipsByDate( - date: Calendar - ): List = allDayEventChipsByDate[date.atStartOfDay.timeInMillis].orEmpty() + date: Calendar, + ): List = chipsByDate[date.atStartOfDay.timeInMillis].orEmpty().filter { it.item.isAllDay } + + fun normalEventChipsInDateRange( + dateRange: List, + ): List { + val results = mutableListOf() + for (date in dateRange) { + results += normalEventChipsByDate(date) + } + return results + } fun allDayEventChipsInDateRange( - dateRange: List + dateRange: List, ): List { val results = mutableListOf() for (date in dateRange) { - results += allDayEventChipsByDate[date.atStartOfDay.timeInMillis].orEmpty() + results += allDayEventChipsByDate(date) } return results } @@ -50,20 +58,15 @@ internal class EventChipsCache { fun addAll(eventChips: List) { for (eventChip in eventChips) { - val isExistingEvent = allEventChips.any { it.eventId == eventChip.eventId } - if (isExistingEvent) { - remove(eventId = eventChip.eventId) + val isExistingChip = allEventChips.any { it.itemId == eventChip.itemId } + if (isExistingChip) { + remove(eventId = eventChip.itemId) } } for (eventChip in eventChips) { val key = eventChip.startTime.atStartOfDay.timeInMillis - - if (eventChip.event.isAllDay) { - allDayEventChipsByDate.addOrReplace(key, eventChip) - } else { - normalEventChipsByDate.addOrReplace(key, eventChip) - } + chipsByDate.add(key, eventChip) } } @@ -73,56 +76,42 @@ internal class EventChipsCache { candidates.isEmpty() -> null // Two events hit. This is most likely because an all-day event was clicked, but a // single event is rendered underneath it. We return the all-day event. - candidates.size == 2 -> candidates.first { it.event.isAllDay } + candidates.size == 2 -> candidates.first { it.item.isAllDay } else -> candidates.first() } } fun remove(eventId: Long) { - val eventChip = allEventChips.firstOrNull { it.eventId == eventId } ?: return + val eventChip = allEventChips.firstOrNull { it.itemId == eventId } ?: return remove(eventChip) } - fun removeAll(events: List) { + fun removeAll(events: List) { val eventIds = events.map { it.id } - val eventChips = allEventChips.filter { it.event.id in eventIds } + val eventChips = allEventChips.filter { it.item.id in eventIds } eventChips.forEach(this::remove) } private fun remove(eventChip: EventChip) { val key = eventChip.startTime.atStartOfDay.timeInMillis - val eventId = eventChip.eventId - - if (eventChip.event.isAllDay) { - allDayEventChipsByDate[key]?.removeAll { it.event.id == eventId } - } else { - normalEventChipsByDate[key]?.removeAll { it.event.id == eventId } - } + val itemId = eventChip.itemId + chipsByDate[key]?.removeAll { it.itemId == itemId } } fun clearSingleEventsCache() { - allEventChips.filter { it.event.isNotAllDay }.forEach(EventChip::setEmpty) + allEventChips.filter { it.item.isNotAllDay }.forEach(EventChip::setEmpty) } fun clear() { - allDayEventChipsByDate.clear() - normalEventChipsByDate.clear() + chipsByDate.clear() } +} - private fun ConcurrentHashMap>.addOrReplace( - key: Long, - eventChip: EventChip - ) { - val results = getOrElse(key) { CopyOnWriteArrayList() } - val indexOfExisting = results.indexOfFirst { it.event.id == eventChip.event.id } - if (indexOfExisting != -1) { - // If an event with the same ID already exists, replace it. The new event will likely be - // more up-to-date. - results.removeAt(indexOfExisting) - results.add(indexOfExisting, eventChip) - } else { - results.add(eventChip) - } - this[key] = results - } +private fun ConcurrentHashMap>.add( + key: Long, + eventChip: EventChip, +) { + val results = getOrElse(key) { CopyOnWriteArrayList() } + results.add(eventChip) + this[key] = results } diff --git a/core/src/main/java/com/alamkanak/weekview/EventChipsFactory.kt b/core/src/main/java/com/alamkanak/weekview/EventChipsFactory.kt index 89515cbec..bc319fd81 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventChipsFactory.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventChipsFactory.kt @@ -5,10 +5,24 @@ import java.util.Calendar internal class EventChipsFactory { fun create( - events: List, - viewState: ViewState + items: List, + viewState: ViewState, + ): List { + val (backgroundItems, foregroundItems) = items.partition { + it.configuration.arrangement == WeekViewItem.Arrangement.Background + } + + val backgroundChips = createInternal(backgroundItems, viewState) + val foregroundChips = createInternal(foregroundItems, viewState) + + return backgroundChips + foregroundChips + } + + private fun createInternal( + items: List, + viewState: ViewState, ): List { - val eventChips = convertEventsToEventChips(events, viewState) + val eventChips = convertEventsToEventChips(items, viewState) val groups = eventChips.groupedByDate().values for (group in groups) { @@ -19,32 +33,28 @@ internal class EventChipsFactory { } private fun convertEventsToEventChips( - events: List, + items: List, viewState: ViewState ): List { - return events.sortedByTime().sanitize(viewState).toEventChips(viewState) + return items.sanitize(viewState).toEventChips(viewState) } - private fun List.sortedByTime(): List { - return sortedWith(compareBy({ it.startTime }, { it.endTime })) - } - - private fun List.sanitize(viewState: ViewState): List { + private fun List.sanitize(viewState: ViewState): List { return map { it.sanitize(viewState) } } - private fun List.toEventChips(viewState: ViewState): List { - return map { event -> - val eventParts = event.split(viewState) + private fun List.toEventChips(viewState: ViewState): List { + return flatMap { item -> + val eventParts = item.split(viewState) eventParts.mapIndexed { index, eventPart -> EventChip( - event = event, + item = item, index = index, - startTime = eventPart.startTime, - endTime = eventPart.endTime, + startTime = eventPart.duration.startTime, + endTime = eventPart.duration.endTime, ) } - }.flatten() + } } /** @@ -54,8 +64,8 @@ internal class EventChipsFactory { * @param eventChips A list of [EventChip]s */ private fun computePositionOfEvents(eventChips: List, viewState: ViewState) { - val singleEventChips = eventChips.filter { it.event.isNotAllDay } - val allDayEventChips = eventChips.filter { it.event.isAllDay } + val singleEventChips = eventChips.filter { it.item.isNotAllDay } + val allDayEventChips = eventChips.filter { it.item.isAllDay } val singleEventGroups = singleEventChips.toMultiColumnCollisionGroups() val allDayGroups = if (viewState.arrangeAllDayEventsVertically) { @@ -145,7 +155,7 @@ internal class EventChipsFactory { } private fun calculateMinutesFromStart(eventChip: EventChip, viewState: ViewState) { - if (eventChip.event.isAllDay) { + if (eventChip.item.isAllDay) { return } @@ -197,7 +207,7 @@ internal class EventChipsFactory { * @return Whether a collision exists */ fun collidesWith(eventChip: EventChip): Boolean { - return eventChips.any { it.event.collidesWith(eventChip.event) } + return eventChips.any { it.item.collidesWith(eventChip.item) } } fun add(eventChip: EventChip) { @@ -230,7 +240,7 @@ internal class EventChipsFactory { operator fun get(index: Int): EventChip = eventChips[index] fun fits(eventChip: EventChip): Boolean { - return isEmpty || !eventChips.last().event.collidesWith(eventChip.event) + return isEmpty || !eventChips.last().item.collidesWith(eventChip.item) } } @@ -255,9 +265,21 @@ internal class EventChipsFactory { } } -private fun ResolvedWeekViewEntity.sanitize(viewState: ViewState): ResolvedWeekViewEntity { - return if (endTime.isAtStartOfPeriod(hour = viewState.minHour)) { - createCopy(endTime = endTime - Millis(1)) +private fun WeekViewItem.sanitize(viewState: ViewState): WeekViewItem { + return if (isNotAllDay) { + val shouldAdjustEndTime = duration.endTime.isAtStartOfPeriod(viewState.minHour) + val newEndTime = if (shouldAdjustEndTime) { + duration.endTime.minusMillis(1) + } else { + duration.endTime + } + + copy( + duration = WeekViewItem.Duration.Bounded( + startTime = duration.startTime, + endTime = newEndTime, + ) + ) } else { this } diff --git a/core/src/main/java/com/alamkanak/weekview/EventsCache.kt b/core/src/main/java/com/alamkanak/weekview/EventsCache.kt index 6bd08702d..1abea9642 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventsCache.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventsCache.kt @@ -1,87 +1,71 @@ package com.alamkanak.weekview import androidx.collection.ArrayMap -import java.util.Calendar internal typealias EventsCacheProvider = () -> EventsCache? /** - * An abstract class that provides functionality to cache [ResolvedWeekViewEntity] elements. + * An abstract class that provides functionality to cache [WeekViewItem] elements. */ internal abstract class EventsCache { - abstract val allEvents: List - abstract fun update(events: List) - abstract fun update(event: ResolvedWeekViewEntity) - abstract fun clear() + abstract val allEvents: List - operator fun get(id: Long): ResolvedWeekViewEntity? = allEvents.firstOrNull { it.id == id } + abstract fun update(events: List) - operator fun get( - dateRange: List - ): List { - val startDate = checkNotNull(dateRange.minOrNull()) - val endDate = checkNotNull(dateRange.maxOrNull()) - return allEvents.filter { it.endTime >= startDate || it.startTime <= endDate } - } + abstract fun update(event: WeekViewItem) - operator fun get( - fetchRange: FetchRange - ): List { - val startTime = fetchRange.previous.startDate - val endTime = fetchRange.next.endDate - return allEvents.filter { it.endTime >= startTime && it.startTime <= endTime } - } + abstract fun clear() + + operator fun get(id: Long): WeekViewItem? = allEvents.firstOrNull { it.id == id } } /** - * Represents an [EventsCache] that relies on a simple list of [ResolvedWeekViewEntity] objects. - * When updated with new [ResolvedWeekViewEntity] objects, all existing ones are replaced. + * Represents an [EventsCache] that relies on a simple list of [WeekViewItem] objects. When updated + * with new [WeekViewItem] objects, all existing ones are replaced. */ internal class SimpleEventsCache : EventsCache() { - private var _allEvents: MutableList? = null - - override val allEvents: List - get() = _allEvents.orEmpty() + override val allEvents = mutableListOf() - override fun update(events: List) { - _allEvents = events.toMutableList() + override fun update(events: List) { + allEvents.clear() + allEvents.addAll(events) } - override fun update(event: ResolvedWeekViewEntity) { - val index = _allEvents?.indexOfFirst { it.id == event.id }?.takeIf { it != -1 } + override fun update(event: WeekViewItem) { + val index = allEvents.indexOfFirst { it.id == event.id } - if (index != null) { - _allEvents?.removeAt(index) - _allEvents?.add(index, event) + if (index != -1) { + allEvents.removeAt(index) + allEvents.add(index, event) } } override fun clear() { - _allEvents = null + allEvents.clear() } } /** - * Represents an [EventsCache] that caches [ResolvedWeekViewEntity]s for their respective [Period] - * and allows retrieval based on that [Period]. + * Represents an [EventsCache] that caches [WeekViewItem]s for their respective [Period] and allows + * retrieval based on that [Period]. */ internal class PaginatedEventsCache : EventsCache() { - override val allEvents: List - get() = eventsByPeriod.values.flatten() + private val eventsByPeriod: ArrayMap> = ArrayMap() - private val eventsByPeriod: ArrayMap> = ArrayMap() + override val allEvents: List + get() = eventsByPeriod.values.flatten() - override fun update(events: List) { + override fun update(events: List) { val groupedEvents = events.groupBy { it.period } for ((period, periodEvents) in groupedEvents) { eventsByPeriod[period] = periodEvents.toMutableList() } } - override fun update(event: ResolvedWeekViewEntity) { + override fun update(event: WeekViewItem) { val existingEvent = allEvents.firstOrNull { it.id == event.id } ?: return eventsByPeriod[existingEvent.period]?.removeAll { it.id == event.id } eventsByPeriod[event.period]?.add(event) diff --git a/core/src/main/java/com/alamkanak/weekview/EventsProcessor.kt b/core/src/main/java/com/alamkanak/weekview/EventsProcessor.kt index e63f8feac..af38cabf6 100644 --- a/core/src/main/java/com/alamkanak/weekview/EventsProcessor.kt +++ b/core/src/main/java/com/alamkanak/weekview/EventsProcessor.kt @@ -20,88 +20,142 @@ internal class EventsProcessor( ) { /** - * Updates the [EventsCache] with the provided [WeekViewEntity] elements and creates - * [EventChip]s. + * Updates the [EventsCache] with the provided [WeekViewItem] object and creates [EventChip]s. * - * @param entities The list of new [WeekViewEntity] elements + * @param items The list of new [WeekViewItem] objects * @param viewState The current [ViewState] of [WeekView] * @param onFinished Callback to inform the caller whether [WeekView] should invalidate. */ fun submit( - entities: List, + items: List, viewState: ViewState, onFinished: () -> Unit ) { backgroundExecutor.execute { - submitEntities(entities, viewState) + submitItems(items, viewState) mainThreadExecutor.execute { onFinished() } } } - internal fun updateDraggedEntity( - event: ResolvedWeekViewEntity, - viewState: ViewState + /** + * Updates the [EventsCache] with the provided [WeekViewItem] object and creates [EventChip]s. + * + * @param items The list of new [WeekViewItem] objects + * @param viewState The current [ViewState] of [WeekView] + * @param onFinished Callback to inform the caller whether [WeekView] should invalidate. + */ + @Deprecated( + message = "Remove once WeekViewEntity is fully deprecated.", + ) + fun submitEntities( + items: List, + viewState: ViewState, + onFinished: () -> Unit, ) { - eventsCache.update(event) - eventChipsCache.remove(eventId = event.id) + backgroundExecutor.execute { + submitEntities(items, viewState) + mainThreadExecutor.execute { + onFinished() + } + } + } - val eventChips = eventChipsFactory.create(listOf(event), viewState) - eventChipsCache.addAll(eventChips) + internal fun updateDraggedItem( + updatedItem: WeekViewItem, + viewState: ViewState, + ) { + eventsCache.update(updatedItem) + + val newEventChips = eventChipsFactory.create(listOf(updatedItem), viewState) + eventChipsCache.remove(eventId = updatedItem.id) + eventChipsCache.addAll(newEventChips) + } + + @WorkerThread + private fun submitItems( + items: List, + viewState: ViewState, + ) { + val processedItems = processItems(items) + eventsCache.update(processedItems) + + if (eventsCache is SimpleEventsCache) { + submitEntitiesToSimpleCache(processedItems, viewState) + } else { + submitEntitiesToPagedCache(processedItems, viewState) + } } @WorkerThread private fun submitEntities( entities: List, - viewState: ViewState + viewState: ViewState, ) { - val resolvedEntities = entities.map { it.resolve(context) } - eventsCache.update(resolvedEntities) + val processedItems = entities + .map { it.resolve(context).toWeekViewItem() } + + eventsCache.update(processedItems) if (eventsCache is SimpleEventsCache) { - submitEntitiesToSimpleCache(resolvedEntities, viewState) + submitEntitiesToSimpleCache(processedItems, viewState) } else { - submitEntitiesToPagedCache(resolvedEntities, viewState) + submitEntitiesToPagedCache(processedItems, viewState) + } + } + + private fun processItems(items: List): List { + return items.map { item -> + item.copy( + title = item.title.processed, + subtitle = item.subtitle?.processed, + duration = when (val duration = item.duration) { + is WeekViewItem.Duration.AllDay -> duration // Keep all-day events as-is + is WeekViewItem.Duration.Bounded -> WeekViewItem.Duration.Bounded( + startTime = duration.startTime.withLocalTimeZone(), + endTime = duration.endTime.withLocalTimeZone(), + ) + } + ) } } private fun submitEntitiesToSimpleCache( - entities: List, + items: List, viewState: ViewState, ) { - val eventChips = eventChipsFactory.create(entities, viewState) + val eventChips = eventChipsFactory.create(items, viewState) eventChipsCache.replaceAll(eventChips) } private fun submitEntitiesToPagedCache( - entities: List, + items: List, viewState: ViewState, ) { - val diffResult = performDiff(entities) + val diffResult = performDiff(items) eventChipsCache.removeAll(diffResult.itemsToRemove) val eventChips = eventChipsFactory.create(diffResult.itemsToAddOrUpdate, viewState) eventChipsCache.addAll(eventChips) } - private fun performDiff(newEntities: List): DiffResult { - val existingEventChips = eventChipsCache.allEventChips - val existingEntities = existingEventChips.map { it.event } + private fun performDiff(items: List): DiffResult { + val existingItems = eventChipsCache.allEventChips.map { it.item } return DiffResult.calculateDiff( - existingEntities = existingEntities, - newEntities = newEntities, + existingEntities = existingItems, + newEntities = items, ) } data class DiffResult( - val itemsToAddOrUpdate: List, - val itemsToRemove: List, + val itemsToAddOrUpdate: List, + val itemsToRemove: List, ) { companion object { fun calculateDiff( - existingEntities: List, - newEntities: List, + existingEntities: List, + newEntities: List, ): DiffResult { val existingEntityIds = existingEntities.map { it.id } diff --git a/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt b/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt index 211aebe14..8f4b49c22 100644 --- a/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt +++ b/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt @@ -235,7 +235,7 @@ private class AllDayEventsUpdater( viewState.currentAllDayEventHeight = maximumChipHeight val maximumChipsPerDay = eventsLabelLayouts.keys - .groupBy { it.event.startTime.toEpochDays() } + .groupBy { it.item.duration.startTime.toEpochDays() } .values .maxByOrNull { it.size }?.size ?: 0 @@ -272,7 +272,7 @@ internal class AllDayEventsDrawer( override fun draw(canvas: Canvas) = canvas.drawInBounds(viewState.headerBounds) { for (date in viewState.dateRange) { val events = allDayEventLayouts - .filter { it.key.event.startTime.isSameDate(date) } + .filter { it.key.item.duration.startTime.isSameDate(date) } .toList() if (viewState.arrangeAllDayEventsVertically) { diff --git a/core/src/main/java/com/alamkanak/weekview/PatternDrawing.kt b/core/src/main/java/com/alamkanak/weekview/PatternDrawing.kt deleted file mode 100644 index adcf12261..000000000 --- a/core/src/main/java/com/alamkanak/weekview/PatternDrawing.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.alamkanak.weekview - -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF -import com.alamkanak.weekview.WeekViewEntity.Style.Pattern -import com.alamkanak.weekview.WeekViewEntity.Style.Pattern.Lined.Direction -import com.alamkanak.weekview.WeekViewEntity.Style.Pattern.Lined.Direction.EndToStart -import com.alamkanak.weekview.WeekViewEntity.Style.Pattern.Lined.Direction.StartToEnd -import kotlin.math.sqrt - -private data class Line(val startX: Float, val startY: Float, val stopX: Float, val stopY: Float) - -internal fun Canvas.drawPattern( - pattern: Pattern, - bounds: RectF, - isLtr: Boolean, - paint: Paint -) { - paint.color = pattern.color - paint.strokeWidth = pattern.strokeWidth.toFloat() - - when (pattern) { - is Pattern.Lined -> drawDiagonalLines(bounds, pattern.spacing, isLtr, pattern.direction, paint) - is Pattern.Dotted -> drawDots(bounds, pattern.spacing, paint) - } -} - -internal fun Canvas.drawDiagonalLines( - bounds: RectF, - spacing: Int, - isLtr: Boolean, - direction: Direction, - paint: Paint -) { - val mustStartLeft = (isLtr && direction == StartToEnd) || (!isLtr && direction == EndToStart) - val lines = mutableListOf() - var startX = if (mustStartLeft) bounds.left else bounds.right - - // Draw all the lines to the right of the top-left corner (flipped for RTL) - if (mustStartLeft) { - while (startX <= bounds.right) { - lines += calculateDiagonalLine( - startX = startX, - startY = bounds.top, - stopY = bounds.bottom, - drawLeftToRight = mustStartLeft - ) - startX += spacing - } - } else { - while (startX >= bounds.left) { - lines += calculateDiagonalLine( - startX = startX, - startY = bounds.top, - stopY = bounds.bottom, - drawLeftToRight = mustStartLeft - ) - startX -= spacing - } - } - - // Now, draw the lines to the left of the top-left corner (flipped for RTL) - var endX = if (mustStartLeft) bounds.right else bounds.left - startX = if (mustStartLeft) bounds.left else bounds.right - - if (mustStartLeft) { - while (endX >= bounds.left) { - lines += calculateDiagonalLine( - startX = startX, - startY = bounds.top, - stopY = bounds.bottom, - drawLeftToRight = mustStartLeft - ) - startX -= spacing - endX -= spacing - } - } else { - while (endX <= bounds.right) { - lines += calculateDiagonalLine( - startX = startX, - startY = bounds.top, - stopY = bounds.bottom, - drawLeftToRight = mustStartLeft - ) - startX += spacing - endX += spacing - } - } - - paint.style = Paint.Style.STROKE - for (line in lines) { - drawLine(line.startX, line.startY, line.stopX, line.stopY, paint) - } -} - -private fun calculateDiagonalLine( - startX: Float, - startY: Float, - stopY: Float, - drawLeftToRight: Boolean -): Line { - val height = stopY - startY - val width = height / sqrt(2f) - val stopX = if (drawLeftToRight) startX + width else startX - width - return Line(startX, startY, stopX, stopY) -} - -internal fun Canvas.drawDots(bounds: RectF, spacing: Int, paint: Paint) { - paint.style = Paint.Style.FILL - val strokeWidth = paint.strokeWidth - - val paddedDot = strokeWidth + spacing - val horizontalDots = (bounds.width() / paddedDot).toInt() - val verticalDots = (bounds.height() / paddedDot).toInt() - - val dotsWidth = horizontalDots * paddedDot - val dotsHeight = verticalDots * paddedDot - - val horizontalPadding = bounds.width() - dotsWidth - val verticalPadding = bounds.height() - dotsHeight - - val left = bounds.left + horizontalPadding / 2 - val top = bounds.top + verticalPadding / 2 - - for (horizontalDot in 0 until horizontalDots) { - for (verticalDot in 0 until verticalDots) { - val leftBound = left + horizontalDot * paddedDot - val topBound = top + verticalDot * paddedDot - val radius = paint.strokeWidth / 2 - val x = leftBound + paddedDot / 2 - val y = topBound + paddedDot / 2 - drawCircle(x, y, radius, paint) - } - } -} diff --git a/core/src/main/java/com/alamkanak/weekview/ResolvedWeekViewEvent.kt b/core/src/main/java/com/alamkanak/weekview/ResolvedWeekViewEvent.kt index 0b432646d..64b615814 100644 --- a/core/src/main/java/com/alamkanak/weekview/ResolvedWeekViewEvent.kt +++ b/core/src/main/java/com/alamkanak/weekview/ResolvedWeekViewEvent.kt @@ -4,6 +4,7 @@ import android.content.Context import java.util.Calendar import kotlin.math.roundToInt +@Deprecated(message = "Remove this soon.") internal sealed class ResolvedWeekViewEntity { internal abstract val id: Long @@ -18,6 +19,7 @@ internal sealed class ResolvedWeekViewEntity { Period.fromDate(startTime) } + @Deprecated(message = "Remove this soon.") data class Event( override val id: Long, override val title: CharSequence, @@ -29,6 +31,7 @@ internal sealed class ResolvedWeekViewEntity { val data: T? ) : ResolvedWeekViewEntity() + @Deprecated(message = "Remove this soon.") data class BlockedTime( override val id: Long, override val title: CharSequence, @@ -40,6 +43,7 @@ internal sealed class ResolvedWeekViewEntity { override val isAllDay: Boolean = false } + @Deprecated(message = "Remove this soon.") data class Style( val textColor: Int? = null, val backgroundColor: Int? = null, @@ -75,10 +79,10 @@ internal sealed class ResolvedWeekViewEntity { // Resolve collisions by shortening the preceding event by 1 ms if (endTime.isEqual(other.startTime)) { - endTime -= Millis(1) + endTime.subtractMillis(1) return false } else if (startTime.isEqual(other.endTime)) { - other.endTime -= Millis(1) + other.endTime.subtractMillis(1) } return !startTime.isAfter(other.endTime) && !endTime.isBefore(other.startTime) @@ -126,3 +130,37 @@ internal fun WeekViewEntity.Style.resolve( borderWidth = borderWidthResource?.resolve(context), cornerRadius = cornerRadiusResource?.resolve(context) ) + +internal fun ResolvedWeekViewEntity.toWeekViewItem(): WeekViewItem { + return WeekViewItem( + id = id, + title = title, + subtitle = subtitle, + duration = if (isAllDay) { + WeekViewItem.Duration.AllDay( + date = startTime.atStartOfDay, + ) + } else { + WeekViewItem.Duration.Bounded( + startTime = startTime, + endTime = endTime, + ) + }, + style = WeekViewItem.Style( + textColor = style.textColor, + backgroundColor = style.backgroundColor, + borderColor = style.borderColor, + borderWidth = style.borderWidth, + cornerRadius = style.cornerRadius, + ), + configuration = WeekViewItem.Configuration( + respectDayGap = this is ResolvedWeekViewEntity.Event<*>, + arrangement = when (this) { + is ResolvedWeekViewEntity.Event<*> -> WeekViewItem.Arrangement.Foreground + is ResolvedWeekViewEntity.BlockedTime -> WeekViewItem.Arrangement.Background + }, + canBeDragged = this is ResolvedWeekViewEntity.Event<*>, + ), + data = (this as? ResolvedWeekViewEntity.Event<*>)?.data ?: Unit, + ) +} diff --git a/core/src/main/java/com/alamkanak/weekview/Resources.kt b/core/src/main/java/com/alamkanak/weekview/Resources.kt index 889f91c47..b97b729ff 100644 --- a/core/src/main/java/com/alamkanak/weekview/Resources.kt +++ b/core/src/main/java/com/alamkanak/weekview/Resources.kt @@ -9,6 +9,9 @@ import androidx.annotation.DimenRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat +@Deprecated( + message = "Remove this soon.", +) internal sealed class ColorResource { data class Value(@ColorInt val color: Int) : ColorResource() data class Id(@ColorRes val resId: Int) : ColorResource() @@ -19,6 +22,9 @@ internal sealed class ColorResource { } } +@Deprecated( + message = "Remove this soon.", +) internal sealed class TextResource { data class Value(val text: CharSequence) : TextResource() data class Id(@StringRes val resId: Int) : TextResource() @@ -37,6 +43,9 @@ internal sealed class TextResource { } } +@Deprecated( + message = "Remove this soon.", +) internal sealed class DimenResource { data class Value(val value: Int) : DimenResource() data class Id(@DimenRes val resId: Int) : DimenResource() diff --git a/core/src/main/java/com/alamkanak/weekview/TextExtensions.kt b/core/src/main/java/com/alamkanak/weekview/TextExtensions.kt index 23f460d82..d348805af 100644 --- a/core/src/main/java/com/alamkanak/weekview/TextExtensions.kt +++ b/core/src/main/java/com/alamkanak/weekview/TextExtensions.kt @@ -41,11 +41,11 @@ internal fun CharSequence.semibold() = SpannableString(this).apply { setSpan(TypefaceSpan("sans-serif-medium"), 0, length, 0) } -internal fun ViewState.getTextPaint(event: ResolvedWeekViewEntity): TextPaint { - val textPaint = TextPaint(if (event.isAllDay) allDayEventTextPaint else eventTextPaint) +internal fun ViewState.getTextPaint(item: WeekViewItem): TextPaint { + val textPaint = TextPaint(if (item.isAllDay) allDayEventTextPaint else eventTextPaint) textPaint.textAlign = if (isLtr) Paint.Align.LEFT else Paint.Align.RIGHT - val textColor = event.style.textColor + val textColor = item.style.textColor if (textColor != null) { textPaint.color = textColor } diff --git a/core/src/main/java/com/alamkanak/weekview/TextFitter.kt b/core/src/main/java/com/alamkanak/weekview/TextFitter.kt index cba04a201..53fc01487 100644 --- a/core/src/main/java/com/alamkanak/weekview/TextFitter.kt +++ b/core/src/main/java/com/alamkanak/weekview/TextFitter.kt @@ -10,7 +10,7 @@ internal class TextFitter( private val spannableStringBuilder = SpannableStringBuilder() fun fitAllDayEvent(eventChip: EventChip): StaticLayout { - val textPaint = viewState.getTextPaint(eventChip.event) + val textPaint = viewState.getTextPaint(eventChip.item) return eventChip.getText(includeSubtitle = false).toTextLayout(textPaint, width = Int.MAX_VALUE) } @@ -19,7 +19,7 @@ internal class TextFitter( } private fun EventChip.fitText(availableWidth: Int, availableHeight: Int): StaticLayout { - val textPaint = viewState.getTextPaint(event) + val textPaint = viewState.getTextPaint(item) var text = getText(includeSubtitle = true) var textLayout = text.toTextLayout(textPaint, width = availableWidth) @@ -47,11 +47,11 @@ internal class TextFitter( } private fun EventChip.getText(includeSubtitle: Boolean): CharSequence { - val subtitle = event.subtitle?.takeIf { event.isNotAllDay && includeSubtitle } + val subtitle = item.subtitle?.takeIf { item.isNotAllDay && includeSubtitle } return combineTitleAndSubtitle( - title = event.title, + title = item.title, subtitle = subtitle, - isMultiLine = event.isNotAllDay + isMultiLine = item.isNotAllDay, ) } diff --git a/core/src/main/java/com/alamkanak/weekview/ViewState.kt b/core/src/main/java/com/alamkanak/weekview/ViewState.kt index 8c7181fed..dd68d2f3a 100644 --- a/core/src/main/java/com/alamkanak/weekview/ViewState.kt +++ b/core/src/main/java/com/alamkanak/weekview/ViewState.kt @@ -200,7 +200,7 @@ internal class ViewState { val minX: Float get() = maxDate?.let { - val date = it - Days(numberOfVisibleDays - 1) + val date = it.minusDays(numberOfVisibleDays - 1) getXOriginForDate(date) } ?: run { if (isLtr) Float.NEGATIVE_INFINITY else Float.POSITIVE_INFINITY @@ -316,7 +316,7 @@ internal class ViewState { get() { val factor = if (isLtr) -1f else 1f val daysFromToday = currentOrigin.x / (dayWidth * factor) - return today() + Days(floor(daysFromToday).toInt()) + return today().plusDays(floor(daysFromToday).toInt()) } private fun scrollToFirstDayOfWeek(navigationListener: Navigator.NavigationListener) { @@ -339,9 +339,9 @@ internal class ViewState { val desired = now() if (desired.hour > minHour) { // Add some padding above the current time (and thus: the now line) - desired -= Hours(1) + desired.subtractHours(1) } else { - desired -= Minutes(desired.minute) + desired.subtractMinutes(desired.minute) } desired.hour = desired.hour.coerceIn(minimumValue = minHour, maximumValue = maxHour) @@ -366,10 +366,10 @@ internal class ViewState { return if (candidate.isBefore(minDate)) { minDate } else if (candidate.isAfter(maxDate)) { - maxDate - Days(numberOfVisibleDays - 1) + maxDate.minusDays(numberOfVisibleDays - 1) } else if (numberOfVisibleDays >= 7 && stickToActualWeek) { val diff = candidate.computeDifferenceWithFirstDayOfWeek() - candidate - Days(diff) + candidate.minusDays(diff) } else { candidate } @@ -559,9 +559,9 @@ internal class ViewState { dateRange.clear() val startDate = if (isLtr) { - today() + Days(daysFromOrigin) + today().plusDays(daysFromOrigin) } else { - today() + Days(numberOfVisibleDays - 1 - daysFromOrigin) + today().plusDays(numberOfVisibleDays - 1 - daysFromOrigin) } val newDateRange = createDateRange(startDate, visibleDays) @@ -578,9 +578,9 @@ internal class ViewState { startDate: Calendar, visibleDays: Int = numberOfVisibleDays ) = if (isLtr) { - (0 until visibleDays).map { startDate + Days(it) } + (0 until visibleDays).map { startDate.plusDays(it) } } else { - (0 until visibleDays).map { startDate - Days(it) } + (0 until visibleDays).map { startDate.minusDays(it) } } fun onSizeChanged(width: Int, height: Int) { diff --git a/core/src/main/java/com/alamkanak/weekview/WeekView.kt b/core/src/main/java/com/alamkanak/weekview/WeekView.kt index 8104bf7e7..8287102f2 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekView.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekView.kt @@ -13,6 +13,8 @@ import android.view.View import android.view.accessibility.AccessibilityManager import androidx.annotation.RequiresApi import androidx.core.view.ViewCompat +import java.lang.ClassCastException +import java.lang.Exception import java.util.Calendar import kotlin.math.abs import kotlin.math.min @@ -178,9 +180,9 @@ class WeekView @JvmOverloads constructor( val delta = daysScrolled.roundToInt() * (-1) val firstVisibleDate = if (viewState.isLtr) { - today() + Days(delta) + today().plusDays(delta) } else { - today() - Days(delta) + today().minusDays(delta) } val dateRange = viewState.createDateRange(firstVisibleDate) @@ -352,6 +354,7 @@ class WeekView @JvmOverloads constructor( var headerBottomShadowColor: Int @RequiresApi(api = 29) get() = viewState.headerBackgroundWithShadowPaint.shadowLayerColor + @RequiresApi(api = 29) set(value) { val paint = viewState.headerBackgroundWithShadowPaint paint.setShadowLayer(headerBottomShadowRadius.toFloat(), 0f, 0f, value) @@ -365,6 +368,7 @@ class WeekView @JvmOverloads constructor( var headerBottomShadowRadius: Int @RequiresApi(api = 29) get() = viewState.headerBackgroundWithShadowPaint.shadowLayerRadius.roundToInt() + @RequiresApi(api = 29) set(value) { val paint = viewState.headerBackgroundWithShadowPaint paint.setShadowLayer(value.toFloat(), 0f, 0f, headerBottomShadowColor) @@ -1225,9 +1229,9 @@ class WeekView @JvmOverloads constructor( if (desired.hour > minHour) { // Add some padding above the current time (and thus: the now line) - desired -= Hours(1) + desired.subtractHours(1) } else { - desired -= Minutes(desired.minute) + desired.subtractMinutes(desired.minute) } val fraction = desired.minute / 60f @@ -1404,15 +1408,33 @@ class WeekView @JvmOverloads constructor( internal fun handleClick(x: Float, y: Float): Boolean { val eventChip = findHitEvent(x, y) ?: return false - val data = findEventData(id = eventChip.eventId) ?: return false - onEventClick(data, eventChip.bounds) + val data = findEventData(id = eventChip.itemId) ?: return false + + try { + // Needed while WeekViewEntity is still around, because WeekViewEntity.BlockedTimeSlot + // doesn't have any data associated with it. Since WeekViewItem requires data, we + // are currently using Unit. However, Unit can't be cast to T. + onEventClick(data, eventChip.bounds) + } catch (e: ClassCastException) { + // Ignored + } + return true } internal fun handleLongClick(x: Float, y: Float): LongClickResult? { val eventChip = findHitEvent(x, y) ?: return null - val data = findEventData(id = eventChip.eventId) ?: return null - val handled = onEventLongClick(data, eventChip.bounds) + val data = findEventData(id = eventChip.itemId) ?: return null + + val handled = try { + // Needed while WeekViewEntity is still around, because WeekViewEntity.BlockedTimeSlot + // doesn't have any data associated with it. Since WeekViewItem requires data, we + // are currently using Unit. However, Unit can't be cast to T. + onEventLongClick(data, eventChip.bounds) + } catch (e: ClassCastException) { + true + } + return LongClickResult(eventChip = eventChip, handled = handled) } @@ -1422,7 +1444,7 @@ class WeekView @JvmOverloads constructor( candidates.isEmpty() -> null // Two events hit. This is most likely because an all-day event was clicked, but a // single event is rendered underneath it. We return the all-day event. - candidates.size == 2 -> candidates.first { it.event.isAllDay }.takeUnless { it.isHidden } + candidates.size == 2 -> candidates.first { it.item.isAllDay }.takeUnless { it.isHidden } else -> candidates.first().takeUnless { it.isHidden } } } @@ -1448,25 +1470,35 @@ class WeekView @JvmOverloads constructor( @Suppress("UNCHECKED_CAST") private fun findEventData(id: Long): T? { val match = eventsCache[id] - return (match as? ResolvedWeekViewEntity.Event)?.data + return match?.data as? T } /** * Called for each element of type [T] that was submitted to this adapter. This method must - * return a [WeekViewEntity] that will be rendered in the [WeekView] that is associated with + * return a [WeekViewItem] that will be rendered in the [WeekView] that is associated with * this adapter. * * @param item The item of type [T] that was submitted to [WeekView] - * @return A [WeekViewEntity] that will be rendered in [WeekView] + * @return A [WeekViewItem] that will be rendered in [WeekView] */ - abstract fun onCreateEntity(item: T): WeekViewEntity + abstract fun onCreateItem(item: T): WeekViewItem /** - * Returns the data of the [WeekViewEntity.Event] that the user clicked on. + * Called for each element of type [T] that was submitted to this adapter. This method must + * return a [WeekViewEntity] that will be rendered in the [WeekView] that is associated with + * this adapter. * - * @param data The data of the [WeekViewEntity.Event] + * @param item The item of type [T] that was submitted to [WeekView] + * @return A [WeekViewEntity] that will be rendered in [WeekView] */ - open fun onEventClick(data: T) = Unit + @Deprecated( + message = "Use onCreateItem instead to create a WeekViewItem.", + ) + open fun onCreateEntity(item: T): WeekViewEntity { + throw RuntimeException( + "You called submitList() on WeekView's adapter, but didn't implement onCreateEntity()." + ) + } /** * Returns the data of the [WeekViewEntity.Event] that the user clicked on as well as the @@ -1477,13 +1509,6 @@ class WeekView @JvmOverloads constructor( */ open fun onEventClick(data: T, bounds: RectF) = Unit - /** - * Returns the data of the [WeekViewEntity.Event] that the user long-clicked on. - * - * @param data The data of the [WeekViewEntity.Event] - */ - open fun onEventLongClick(data: T) = Unit - /** * Returns the data of the [WeekViewEntity.Event] that the user long-clicked on as well as * the bounds of the [EventChip] in which it is displayed. @@ -1501,8 +1526,8 @@ class WeekView @JvmOverloads constructor( onDragAndDropFinished( data = data, - newStartTime = match.startTime, - newEndTime = match.endTime, + newStartTime = match.duration.startTime, + newEndTime = match.duration.endTime, ) } @@ -1582,8 +1607,16 @@ class WeekView @JvmOverloads constructor( @PublicApi fun submitList(elements: List) { val viewState = weekView?.viewState ?: return - val entities = elements.map(this::onCreateEntity) - eventsProcessor.submit(entities, viewState, onFinished = this::updateObserver) + + try { + // The consumer is still using onCreateEntity instead of onCreateItem. Remove this + // try-catch once onCreateEntity is fully deprecated. + val items = elements.map(this::onCreateItem) + eventsProcessor.submit(items, viewState, onFinished = this::updateObserver) + } catch (e: Exception) { + val entities = elements.map(this::onCreateEntity) + eventsProcessor.submitEntities(entities, viewState, onFinished = this::updateObserver) + } } } @@ -1614,8 +1647,16 @@ class WeekView @JvmOverloads constructor( @PublicApi fun submitList(elements: List) { val viewState = weekView?.viewState ?: return - val entities = elements.map(this::onCreateEntity) - eventsProcessor.submit(entities, viewState, onFinished = this::updateObserver) + + try { + // The consumer is still using onCreateEntity instead of onCreateItem. Remove this + // try-catch once onCreateEntity is fully deprecated. + val items = elements.map(this::onCreateItem) + eventsProcessor.submit(items, viewState, onFinished = this::updateObserver) + } catch (e: Exception) { + val entities = elements.map(this::onCreateEntity) + eventsProcessor.submitEntities(entities, viewState, onFinished = this::updateObserver) + } } /** diff --git a/core/src/main/java/com/alamkanak/weekview/WeekViewAccessibilityTouchHelper.kt b/core/src/main/java/com/alamkanak/weekview/WeekViewAccessibilityTouchHelper.kt index 57fe17a86..aa4b551a3 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekViewAccessibilityTouchHelper.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekViewAccessibilityTouchHelper.kt @@ -72,7 +72,7 @@ internal class WeekViewAccessibilityTouchHelper( ): Boolean = when (action) { AccessibilityNodeInfoCompat.ACTION_CLICK -> { touchHandler.adapter?.onEventClick( - id = eventChip.eventId, + id = eventChip.itemId, bounds = eventChip.bounds ) sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED) @@ -80,7 +80,7 @@ internal class WeekViewAccessibilityTouchHelper( } AccessibilityNodeInfoCompat.ACTION_LONG_CLICK -> { touchHandler.adapter?.handleLongClick( - id = eventChip.eventId, + id = eventChip.itemId, bounds = eventChip.bounds ) sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) @@ -130,7 +130,7 @@ internal class WeekViewAccessibilityTouchHelper( eventChip: EventChip, node: AccessibilityNodeInfoCompat ) { - node.contentDescription = createDescriptionForVirtualView(eventChip.event) + node.contentDescription = createDescriptionForVirtualView(eventChip.item) node.addAction(AccessibilityActionCompat.ACTION_CLICK) node.addAction(AccessibilityActionCompat.ACTION_LONG_CLICK) @@ -160,9 +160,9 @@ internal class WeekViewAccessibilityTouchHelper( node.setBoundsInParent(bounds) } - private fun createDescriptionForVirtualView(event: ResolvedWeekViewEntity): String { - val date = dateTimeFormatter.format(event.startTime.time) - return "$date: ${event.title}, ${event.subtitle}" + private fun createDescriptionForVirtualView(item: WeekViewItem): String { + val date = dateTimeFormatter.format(item.duration.startTime.time) + return "$date: ${item.title}, ${item.subtitle}" } private fun createDescriptionForVirtualView(date: Calendar): String { diff --git a/core/src/main/java/com/alamkanak/weekview/WeekViewEntitiesSplitter.kt b/core/src/main/java/com/alamkanak/weekview/WeekViewEntitiesSplitter.kt index 0c7ca6915..b25d62ef6 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekViewEntitiesSplitter.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekViewEntitiesSplitter.kt @@ -1,9 +1,7 @@ package com.alamkanak.weekview -import java.util.Calendar - -internal fun ResolvedWeekViewEntity.split(viewState: ViewState): List { - if (startTime >= endTime) { +internal fun WeekViewItem.split(viewState: ViewState): List { + if (duration.startTime >= duration.endTime) { return emptyList() } @@ -13,59 +11,38 @@ internal fun ResolvedWeekViewEntity.split(viewState: ViewState): List { - val firstEvent = createCopy( - startTime = startTime.limitToMinHour(minHour), - endTime = startTime.atEndOfDay.limitToMaxHour(maxHour) +): List { + val firstEvent = copyWith( + startTime = duration.startTime.limitToMinHour(minHour), + endTime = duration.startTime.atEndOfDay.limitToMaxHour(maxHour) ) - val results = mutableListOf() + val results = mutableListOf() results += firstEvent - val daysInBetween = endTime.toEpochDays() - startTime.toEpochDays() - 1 + val daysInBetween = duration.endTime.toEpochDays() - duration.startTime.toEpochDays() - 1 if (daysInBetween > 0) { - val currentDate = startTime.atStartOfDay + Days(1) - while (currentDate.toEpochDays() < endTime.toEpochDays()) { + val currentDate = duration.startTime.atStartOfDay.plusDays(1) + while (currentDate.toEpochDays() < duration.endTime.toEpochDays()) { val intermediateStart = currentDate.withTimeAtStartOfPeriod(minHour) val intermediateEnd = currentDate.withTimeAtEndOfPeriod(maxHour) - results += createCopy(startTime = intermediateStart, endTime = intermediateEnd) - currentDate += Days(1) + results += copyWith(startTime = intermediateStart, endTime = intermediateEnd) + currentDate.addDays(1) } } - val lastEvent = createCopy( - startTime = endTime.atStartOfDay.limitToMinHour(minHour), - endTime = endTime.limitToMaxHour(maxHour) + val lastEvent = copyWith( + startTime = duration.endTime.atStartOfDay.limitToMinHour(minHour), + endTime = duration.endTime.limitToMaxHour(maxHour) ) results += lastEvent - return results.sortedWith(compareBy({ it.startTime }, { it.endTime })) -} - -private fun ResolvedWeekViewEntity.limitTo(minHour: Int, maxHour: Int) = createCopy( - startTime = startTime.limitToMinHour(minHour), - endTime = endTime.limitToMaxHour(maxHour) -) - -private fun Calendar.limitToMinHour(minHour: Int): Calendar { - return if (hour < minHour) { - withTimeAtStartOfPeriod(hour = minHour) - } else { - this - } -} - -private fun Calendar.limitToMaxHour(maxHour: Int): Calendar { - return if (hour >= maxHour) { - withTimeAtEndOfPeriod(hour = maxHour) - } else { - this - } + return results.sortedWith(compareBy({ it.duration.startTime }, { it.duration.endTime })) } diff --git a/core/src/main/java/com/alamkanak/weekview/WeekViewEntity.kt b/core/src/main/java/com/alamkanak/weekview/WeekViewEntity.kt index d5a847a9b..c390b33ae 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekViewEntity.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekViewEntity.kt @@ -10,8 +10,14 @@ import java.util.Calendar * Encapsulates all information necessary to render an entity in [WeekView]. This entity can be * either a [WeekViewEntity.Event] or a [WeekViewEntity.BlockedTime]. */ +@Deprecated( + message = "Use WeekViewItem instead. This class will be removed in a future release.", +) sealed class WeekViewEntity { + @Deprecated( + message = "Use WeekViewItem instead. This class will be removed in a future release.", + ) data class Event internal constructor( internal val id: Long = 0L, internal val titleResource: TextResource, @@ -23,6 +29,9 @@ sealed class WeekViewEntity { internal val data: T ) : WeekViewEntity() { + @Deprecated( + message = "Use WeekViewItem instead. This class will be removed in a future release.", + ) class Builder(private val data: T) { private var id: Long? = null @@ -100,6 +109,9 @@ sealed class WeekViewEntity { } } + @Deprecated( + message = "Use WeekViewItem instead. This class will be removed in a future release.", + ) data class BlockedTime internal constructor( internal val id: Long = 0L, internal val titleResource: TextResource, @@ -109,6 +121,9 @@ sealed class WeekViewEntity { internal val style: Style = Style() ) : WeekViewEntity() { + @Deprecated( + message = "Use WeekViewItem instead. This class will be removed in a future release.", + ) class Builder { private var id: Long? = null @@ -178,6 +193,9 @@ sealed class WeekViewEntity { } } + @Deprecated( + message = "Use WeekViewItem and WeekViewItem.Style instead. This class will be removed in a future release.", + ) class Style internal constructor() { internal var textColorResource: ColorResource? = null @@ -187,11 +205,13 @@ sealed class WeekViewEntity { internal var cornerRadiusResource: DimenResource? = null internal var pattern: Pattern? = null + @Deprecated(message = "Pattern is no longer rendered and will be removed in a future release.") sealed class Pattern { abstract val color: Int abstract val strokeWidth: Int + @Deprecated(message = "Pattern is no longer rendered and will be removed in a future release.") data class Lined( @ColorInt override val color: Int, @Dimension override val strokeWidth: Int, @@ -203,6 +223,7 @@ sealed class WeekViewEntity { } } + @Deprecated(message = "Pattern is no longer rendered and will be removed in a future release.") data class Dotted( @ColorInt override val color: Int, @Dimension override val strokeWidth: Int, @@ -210,6 +231,9 @@ sealed class WeekViewEntity { ) : Pattern() } + @Deprecated( + message = "Use WeekViewItem and WeekViewItem.Style instead. This class will be removed in a future release.", + ) class Builder { private val style = Style() diff --git a/core/src/main/java/com/alamkanak/weekview/WeekViewGestureHandler.kt b/core/src/main/java/com/alamkanak/weekview/WeekViewGestureHandler.kt index afbfcad64..0334e69ca 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekViewGestureHandler.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekViewGestureHandler.kt @@ -241,20 +241,20 @@ internal class WeekViewGestureHandler( } private fun Calendar.performFling(direction: Direction, viewState: ViewState): Calendar { - val daysDelta = Days(viewState.numberOfVisibleDays) + val delta = viewState.numberOfVisibleDays return when (direction) { Left -> { if (viewState.isLtr) { - this + daysDelta + this.plusDays(delta) } else { - this - daysDelta + this.minusDays(delta) } } Right -> { if (viewState.isLtr) { - this - daysDelta + this.minusDays(delta) } else { - this + daysDelta + this.plusDays(delta) } } else -> throw IllegalStateException() diff --git a/core/src/main/java/com/alamkanak/weekview/WeekViewItem.kt b/core/src/main/java/com/alamkanak/weekview/WeekViewItem.kt new file mode 100644 index 000000000..db2047dc0 --- /dev/null +++ b/core/src/main/java/com/alamkanak/weekview/WeekViewItem.kt @@ -0,0 +1,224 @@ +package com.alamkanak.weekview + +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import java.util.Calendar + +/** + * Represents an item that is displayed in [WeekView]. The corresponding domain object can be passed + * as the [data] property and will be passed to the caller's implementation of + * [WeekView.Adapter.onEventClick]. + * + * The item is rendered based on the information provided in its [style] and [configuration] + * properties. + */ +data class WeekViewItem internal constructor( + val id: Long = 0L, + val title: CharSequence, + val subtitle: CharSequence? = null, + val duration: Duration, + val style: Style = Style(), + val configuration: Configuration = Configuration(), + val data: Any, +) { + + /** + * Encapsulates styling information for a [WeekViewItem]. If a property is not provided, + * [WeekView] will use its global default values (such as [WeekView.defaultEventTextColor]). + */ + data class Style( + @ColorInt val textColor: Int? = null, + @ColorInt val backgroundColor: Int? = null, + @ColorInt val borderColor: Int? = null, + @Dimension val borderWidth: Int? = null, + @Dimension val cornerRadius: Int? = null, + ) + + /** + * Encapsulates information about a [WeekViewItem]. + * + * @param respectDayGap Whether the item should respect [WeekView.columnGap]. If not, it will + * occupy the full width of a day. + * @param arrangement Whether the item should be rendered in the foreground or background. + * @param canBeDragged Whether the item can be dragged around by the user. + */ + data class Configuration( + val arrangement: Arrangement = Arrangement.Foreground, + val respectDayGap: Boolean = true, + val canBeDragged: Boolean = true, + ) { + companion object { + + fun foreground( + respectDayGap: Boolean = true, + canBeDragged: Boolean = true, + ) = Configuration( + respectDayGap = respectDayGap, + canBeDragged = canBeDragged, + arrangement = Arrangement.Foreground, + ) + + fun background( + respectDayGap: Boolean = false, + canBeDragged: Boolean = false, + ) = Configuration( + respectDayGap = respectDayGap, + canBeDragged = canBeDragged, + arrangement = Arrangement.Background, + ) + } + } + + /** + * The arrangement of a [WeekViewItem], specifying whether it should be rendered in the + * foreground or background. + */ + enum class Arrangement { + Background, + Foreground, + } + + /** + * The duration information of a [WeekViewItem], which can either be an [AllDay] event or a + * [Bounded] event. + */ + sealed class Duration { + + abstract val startTime: Calendar + abstract val endTime: Calendar + + data class Bounded( + override val startTime: Calendar, + override val endTime: Calendar, + ) : Duration() + + data class AllDay( + val date: Calendar, + ) : Duration() { + override val startTime: Calendar = date.atStartOfDay + override val endTime: Calendar = date.atEndOfDay + } + } + + companion object { + fun of(data: Any): Builder = Builder(data) + } + + /** + * Builder to construct a [WeekViewItem]. A [WeekViewItem] needs an [id], a [title], and + * a [duration]. The latter can be a [WeekViewItem.Duration.AllDay] or a + * [WeekViewItem.Duration.Bounded]. + */ + class Builder internal constructor(private val data: Any) { + + private var id: Long? = null + private var title: CharSequence? = null + private var subtitle: CharSequence? = null + private var duration: Duration? = null + private var style: Style = Style() + private var configuration = Configuration() + + fun setId(id: Long): Builder { + this.id = id + return this + } + + fun setTitle(title: CharSequence): Builder { + this.title = title + return this + } + + fun setSubtitle(subtitle: CharSequence): Builder { + this.subtitle = subtitle + return this + } + + fun setAllDayDuration(date: Calendar): Builder { + this.duration = Duration.AllDay(date) + return this + } + + fun setBoundedDuration(startTime: Calendar, endTime: Calendar): Builder { + this.duration = Duration.Bounded(startTime, endTime) + return this + } + + fun setStyle(style: Style): Builder { + this.style = style + return this + } + + fun setConfiguration(configuration: Configuration): Builder { + this.configuration = configuration + return this + } + + fun build(): WeekViewItem { + val id = requireNotNull(id) { "id == null" } + val title = requireNotNull(title) { "title == null" } + val duration = requireNotNull(duration) { "duration == null" } + return WeekViewItem(id, title, subtitle, duration, style, configuration, data) + } + } + + internal val isAllDay: Boolean = duration is Duration.AllDay + + internal val isNotAllDay: Boolean = !isAllDay + + internal val isMultiDay: Boolean by lazy { + !duration.startTime.isSameDate(duration.endTime) + } + + internal val period: Period by lazy { + when (duration) { + is Duration.Bounded -> Period.fromDate(duration.startTime) + is Duration.AllDay -> Period.fromDate(duration.date) + } + } + + internal val durationInMinutes: Int by lazy { + duration.endTime minutesUntil duration.startTime + } + + internal fun isWithin( + minHour: Int, + maxHour: Int + ): Boolean = duration.startTime.hour >= minHour && duration.endTime.hour <= maxHour + + internal fun copyWith(startTime: Calendar, endTime: Calendar): WeekViewItem { + return when (duration) { + is Duration.Bounded -> copy(duration = duration.copy(startTime = startTime, endTime = endTime)) + is Duration.AllDay -> this + } + } + + internal fun limitTo(minHour: Int, maxHour: Int): WeekViewItem { + return copyWith( + startTime = duration.startTime.limitToMinHour(minHour), + endTime = duration.endTime.limitToMaxHour(maxHour), + ) + } + + internal fun collidesWith(other: WeekViewItem): Boolean = duration.overlapsWith(other.duration) +} + +private fun WeekViewItem.Duration.overlapsWith(other: WeekViewItem.Duration): Boolean { + if (this is WeekViewItem.Duration.AllDay || other is WeekViewItem.Duration.AllDay) { + return false + } + + if (startTime.isEqual(other.startTime) && endTime.isEqual(other.endTime)) { + // Complete overlap + return true + } + + // Resolve collisions by shortening the preceding event by 1 ms + if (endTime.isEqual(other.startTime)) { + endTime.subtractMillis(1) + return false + } else if (startTime.isEqual(other.endTime)) { + other.endTime.subtractMillis(1) + } + + return !startTime.isAfter(other.endTime) && !endTime.isBefore(other.startTime) +} diff --git a/core/src/test/java/com/alamkanak/weekview/DateExtensionsTest.kt b/core/src/test/java/com/alamkanak/weekview/DateExtensionsTest.kt index e33902223..826b75f9e 100644 --- a/core/src/test/java/com/alamkanak/weekview/DateExtensionsTest.kt +++ b/core/src/test/java/com/alamkanak/weekview/DateExtensionsTest.kt @@ -1,10 +1,13 @@ package com.alamkanak.weekview +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Test +import org.junit.runner.RunWith import java.util.Calendar +@RunWith(AndroidJUnit4::class) class DateExtensionsTest { @Test @@ -21,17 +24,17 @@ class DateExtensionsTest { val second = firstDayOfYear().withYear(2019) assertEquals(first, second) - val newSecond = second + Millis(1) + val newSecond = second.plusMillis(1) assertNotEquals(first, newSecond) } @Test fun `adds days correctly`() { val date = firstDayOfYear().withYear(2019) - val result = date + Days(2) + val result = date.plusDays(2) assertEquals(3, result.dayOfMonth) - val secondResult = date + Days(31) + val secondResult = date.plusDays(31) assertEquals(1, secondResult.dayOfMonth) assertEquals(Calendar.FEBRUARY, secondResult.month) } diff --git a/core/src/test/java/com/alamkanak/weekview/DiffResultTest.kt b/core/src/test/java/com/alamkanak/weekview/DiffResultTest.kt index 433296224..fec91bc65 100644 --- a/core/src/test/java/com/alamkanak/weekview/DiffResultTest.kt +++ b/core/src/test/java/com/alamkanak/weekview/DiffResultTest.kt @@ -1,22 +1,21 @@ package com.alamkanak.weekview +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.alamkanak.weekview.EventsProcessor.DiffResult -import com.alamkanak.weekview.util.MockFactory +import com.alamkanak.weekview.util.Mocks import com.alamkanak.weekview.util.withDifferentId import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config +import java.util.Calendar -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [27]) +@RunWith(AndroidJUnit4::class) class DiffResultTest { @Test fun `DiffResult for empty existing and new entities contains no elements`() { - val existingEntities = emptyList() - val newEntities = emptyList() + val existingEntities = emptyList() + val newEntities = emptyList() val result = DiffResult.calculateDiff( existingEntities = existingEntities, @@ -29,8 +28,8 @@ class DiffResultTest { @Test fun `New entities are correctly recognized as new`() { - val existingEntities = emptyList() - val newEntities = MockFactory.resolvedWeekViewEntities(count = 2) + val existingEntities = emptyList() + val newEntities = Mocks.weekViewItems(count = 2) val result = DiffResult.calculateDiff( existingEntities = existingEntities, @@ -43,9 +42,10 @@ class DiffResultTest { @Test fun `Updated entities are correctly recognized as new`() { - val existingEntity = MockFactory.resolvedWeekViewEntity() - val newEntity = existingEntity.createCopy( - endTime = existingEntity.startTime + Hours(2) + val existingEntity = Mocks.weekViewItem() + val newEntity = existingEntity.copyWith( + startTime = existingEntity.duration.startTime, + endTime = existingEntity.duration.endTime.plusMinutes(60), ) val result = DiffResult.calculateDiff( @@ -59,11 +59,15 @@ class DiffResultTest { @Test fun `New and updated entities are correctly recognized together`() { - val existingEntity = MockFactory.resolvedWeekViewEntity() - val updatedEntity = existingEntity.createCopy( - endTime = existingEntity.startTime + Hours(2) + val startTime = Calendar.getInstance() + val endTime = startTime.plusMinutes(60) + + val existingEntity = Mocks.weekViewItem(startTime, endTime) + val updatedEntity = existingEntity.copyWith( + startTime = existingEntity.duration.startTime, + endTime = existingEntity.duration.endTime.plusMinutes(60), ) - val newEntity = MockFactory.resolvedWeekViewEntity() + val newEntity = Mocks.weekViewItem(startTime, endTime) val result = DiffResult.calculateDiff( existingEntities = listOf(existingEntity), @@ -76,7 +80,7 @@ class DiffResultTest { @Test fun `Removed entities are correctly recognized as to-remove`() { - val entityToRemove = MockFactory.resolvedWeekViewEntity() + val entityToRemove = Mocks.weekViewItem() val result = DiffResult.calculateDiff( existingEntities = listOf(entityToRemove), @@ -89,7 +93,7 @@ class DiffResultTest { @Test fun `Otherwise equal entities with different IDs are treated as separate elements`() { - val existingEntity = MockFactory.resolvedWeekViewEntity() + val existingEntity = Mocks.weekViewItem() val newEntity = existingEntity.withDifferentId() val result = DiffResult.calculateDiff( diff --git a/core/src/test/java/com/alamkanak/weekview/PeriodTest.kt b/core/src/test/java/com/alamkanak/weekview/PeriodTest.kt index 6e627b7be..7ac720306 100644 --- a/core/src/test/java/com/alamkanak/weekview/PeriodTest.kt +++ b/core/src/test/java/com/alamkanak/weekview/PeriodTest.kt @@ -1,9 +1,12 @@ package com.alamkanak.weekview +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import java.util.Calendar +@RunWith(AndroidJUnit4::class) class PeriodTest { @Test diff --git a/core/src/test/java/com/alamkanak/weekview/WeekViewEventSplitterTest.kt b/core/src/test/java/com/alamkanak/weekview/WeekViewEventSplitterTest.kt index 1edc90e49..982dd2a17 100644 --- a/core/src/test/java/com/alamkanak/weekview/WeekViewEventSplitterTest.kt +++ b/core/src/test/java/com/alamkanak/weekview/WeekViewEventSplitterTest.kt @@ -1,13 +1,16 @@ package com.alamkanak.weekview +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.alamkanak.weekview.util.Event -import com.alamkanak.weekview.util.createResolvedWeekViewEvent +import com.alamkanak.weekview.util.Mocks +import com.google.common.truth.Truth.assertThat import com.nhaarman.mockitokotlin2.mock import org.junit.Assert.assertEquals import org.junit.Test -import org.mockito.Mockito.mock +import org.junit.runner.RunWith import org.mockito.Mockito.`when` as whenever +@RunWith(AndroidJUnit4::class) class WeekViewEventSplitterTest { private val viewState: ViewState = mock() @@ -18,8 +21,8 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(24) val startTime = today().withHour(11) - val endTime = startTime + Hours(2) - val event = createResolvedWeekViewEvent(startTime, endTime) + val endTime = startTime.plusHours(2) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf(event) @@ -32,13 +35,13 @@ class WeekViewEventSplitterTest { whenever(viewState.minHour).thenReturn(7) whenever(viewState.maxHour).thenReturn(21) - val event = createResolvedWeekViewEvent( + val event = Mocks.weekViewItem( startTime = today().withHour(1), endTime = today().withHour(2) ) val results = event.split(viewState) - assertEquals(emptyList(), results) + assertThat(results).isEmpty() } @Test @@ -46,14 +49,14 @@ class WeekViewEventSplitterTest { whenever(viewState.minHour).thenReturn(7) whenever(viewState.maxHour).thenReturn(21) - val event = createResolvedWeekViewEvent( + val event = Mocks.weekViewItem( startTime = today().withHour(22), - endTime = today().plus(Days(1)).withHour(6) + endTime = today().plusDays(1).withHour(6) ) val results = event.split(viewState) - assertEquals(emptyList(), results) + assertThat(results).isEmpty() } @Test @@ -64,7 +67,7 @@ class WeekViewEventSplitterTest { val startTime = today().withHour(8) val endTime = today().withHour(12) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf( @@ -72,7 +75,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -85,7 +88,7 @@ class WeekViewEventSplitterTest { val startTime = today().withHour(18) val endTime = today().withHour(23) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf( @@ -93,7 +96,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -104,9 +107,9 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(24) val startTime = today().withHour(11) - val endTime = (startTime + Days(1)).withHour(2) + val endTime = (startTime.plusDays(1)).withHour(2) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf( @@ -115,7 +118,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -129,12 +132,12 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(maxHour) val startTime = today().withHour(5) - val endTime = (startTime + Days(2)).withHour(23) + val endTime = (startTime.plusDays(2)).withHour(23) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) - val tomorrow = today() + Days(1) + val tomorrow = today().plusDays(1) val expected = listOf( Event(startTime.withHour(minHour), startTime.withTimeAtEndOfPeriod(maxHour)), Event(tomorrow.withHour(minHour), tomorrow.withTimeAtEndOfPeriod(maxHour)), @@ -142,7 +145,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -156,9 +159,9 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(maxHour) val startTime = today().withHour(8) - val endTime = (startTime + Days(1)).withHour(5) + val endTime = (startTime.plusDays(1)).withHour(5) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf( @@ -166,7 +169,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -180,9 +183,9 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(maxHour) val startTime = today().withHour(22) - val endTime = (startTime + Days(1)).withHour(9) + val endTime = (startTime.plusDays(1)).withHour(9) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) val expected = listOf( @@ -190,7 +193,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } @@ -201,12 +204,12 @@ class WeekViewEventSplitterTest { whenever(viewState.maxHour).thenReturn(24) val startTime = today().withHour(11) - val endTime = (startTime + Days(2)).withHour(2) + val endTime = (startTime.plusDays(2)).withHour(2) - val event = createResolvedWeekViewEvent(startTime, endTime) + val event = Mocks.weekViewItem(startTime, endTime) val results = event.split(viewState) - val intermediateDate = startTime + Days(1) + val intermediateDate = startTime.plusDays(1) val expected = listOf( Event(startTime, startTime.atEndOfDay), Event(intermediateDate.atStartOfDay, intermediateDate.atEndOfDay), @@ -214,7 +217,7 @@ class WeekViewEventSplitterTest { ) val expectedTimes = expected.map { it.startTime.timeInMillis to it.endTime.timeInMillis } - val resultTimes = results.map { it.startTime.timeInMillis to it.endTime.timeInMillis } + val resultTimes = results.map { it.duration.startTime.timeInMillis to it.duration.endTime.timeInMillis } assertEquals(expectedTimes, resultTimes) } diff --git a/core/src/test/java/com/alamkanak/weekview/WeekViewEventTest.kt b/core/src/test/java/com/alamkanak/weekview/WeekViewEventTest.kt index 4735cf403..cea3584bd 100644 --- a/core/src/test/java/com/alamkanak/weekview/WeekViewEventTest.kt +++ b/core/src/test/java/com/alamkanak/weekview/WeekViewEventTest.kt @@ -1,13 +1,16 @@ package com.alamkanak.weekview -import com.alamkanak.weekview.util.createResolvedWeekViewEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.alamkanak.weekview.util.Mocks import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +@RunWith(AndroidJUnit4::class) class WeekViewEventTest { private val viewState = Mockito.mock(ViewState::class.java) @@ -21,10 +24,10 @@ class WeekViewEventTest { @Test fun `single-day event is recognized correctly`() { - val startTime = (today() + Days(1)).withHour(6).withMinutes(0) - val endTime = startTime + Hours(10) + val startTime = (today().plusDays(1)).withHour(6).withMinutes(0) + val endTime = startTime.plusHours(10) - val originalEvent = createResolvedWeekViewEvent(startTime, endTime) + val originalEvent = Mocks.weekViewItem(startTime, endTime) val eventChips = factory.create(listOf(originalEvent), viewState) assertTrue(eventChips.size == 1) @@ -36,10 +39,10 @@ class WeekViewEventTest { @Test fun `two-day event is recognized correctly`() { - val startTime = (today() + Days(1)).withHour(14).withMinutes(0) - val endTime = (today() + Days(2)).withHour(14).withMinutes(0) + val startTime = (today().plusDays(1)).withHour(14).withMinutes(0) + val endTime = (today().plusDays(2)).withHour(14).withMinutes(0) - val originalEvent = createResolvedWeekViewEvent(startTime, endTime) + val originalEvent = Mocks.weekViewItem(startTime, endTime) val eventChips = factory.create(listOf(originalEvent), viewState) assertTrue(eventChips.size == 2) @@ -55,10 +58,10 @@ class WeekViewEventTest { @Test fun `multi-day event is recognized correctly`() { - val startTime = (today() + Days(1)).withHour(14).withMinutes(0) - val endTime = (today() + Days(3)).withHour(1).withMinutes(0) + val startTime = (today().plusDays(1)).withHour(14).withMinutes(0) + val endTime = (today().plusDays(3)).withHour(1).withMinutes(0) - val originalEvent = createResolvedWeekViewEvent(startTime, endTime) + val originalEvent = Mocks.weekViewItem(startTime, endTime) val eventChips = factory.create(listOf(originalEvent), viewState) assertTrue(eventChips.size == 3) @@ -76,12 +79,12 @@ class WeekViewEventTest { @Test fun `non-colliding events are recognized correctly`() { val firstStartTime = now() - val firstEndTime = firstStartTime + Hours(1) - val first = createResolvedWeekViewEvent(firstStartTime, firstEndTime) + val firstEndTime = firstStartTime.plusHours(1) + val first = Mocks.weekViewItem(firstStartTime, firstEndTime) - val secondStartTime = firstStartTime + Hours(2) - val secondEndTime = secondStartTime + Hours(1) - val second = createResolvedWeekViewEvent(secondStartTime, secondEndTime) + val secondStartTime = firstStartTime.plusHours(2) + val secondEndTime = secondStartTime.plusHours(1) + val second = Mocks.weekViewItem(secondStartTime, secondEndTime) assertFalse(first.collidesWith(second)) } @@ -89,12 +92,12 @@ class WeekViewEventTest { @Test fun `overlapping events are recognized as colliding`() { val firstStartTime = now() - val firstEndTime = firstStartTime + Hours(1) - val first = createResolvedWeekViewEvent(firstStartTime, firstEndTime) + val firstEndTime = firstStartTime.plusHours(1) + val first = Mocks.weekViewItem(firstStartTime, firstEndTime) - val secondStartTime = firstStartTime - Hours(1) - val secondEndTime = firstEndTime + Hours(1) - val second = createResolvedWeekViewEvent(secondStartTime, secondEndTime) + val secondStartTime = firstStartTime.minusHours(1) + val secondEndTime = firstEndTime.plusHours(1) + val second = Mocks.weekViewItem(secondStartTime, secondEndTime) assertTrue(first.collidesWith(second)) } @@ -102,12 +105,12 @@ class WeekViewEventTest { @Test fun `partly-overlapping events are recognized as colliding`() { val firstStartTime = now().withMinutes(0) - val firstEndTime = firstStartTime + Hours(1) - val first = createResolvedWeekViewEvent(firstStartTime, firstEndTime) + val firstEndTime = firstStartTime.plusHours(1) + val first = Mocks.weekViewItem(firstStartTime, firstEndTime) val secondStartTime = firstStartTime.withMinutes(30) - val secondEndTime = secondStartTime + Hours(1) - val second = createResolvedWeekViewEvent(secondStartTime, secondEndTime) + val secondEndTime = secondStartTime.plusHours(1) + val second = Mocks.weekViewItem(secondStartTime, secondEndTime) assertTrue(first.collidesWith(second)) } diff --git a/core/src/test/java/com/alamkanak/weekview/util/Event.kt b/core/src/test/java/com/alamkanak/weekview/util/Event.kt index df83d8a7d..6b531f4b2 100644 --- a/core/src/test/java/com/alamkanak/weekview/util/Event.kt +++ b/core/src/test/java/com/alamkanak/weekview/util/Event.kt @@ -4,5 +4,5 @@ import java.util.Calendar data class Event( val startTime: Calendar, - val endTime: Calendar + val endTime: Calendar, ) diff --git a/core/src/test/java/com/alamkanak/weekview/util/MockFactory.kt b/core/src/test/java/com/alamkanak/weekview/util/MockFactory.kt deleted file mode 100644 index 60310d5ae..000000000 --- a/core/src/test/java/com/alamkanak/weekview/util/MockFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.alamkanak.weekview.util - -import com.alamkanak.weekview.Hours -import com.alamkanak.weekview.ResolvedWeekViewEntity -import com.alamkanak.weekview.plus -import java.util.Calendar -import kotlin.random.Random - -internal object MockFactory { - - fun resolvedWeekViewEntities(count: Int): List> { - return (0 until count).map { resolvedWeekViewEntity() } - } - - fun resolvedWeekViewEntity( - startTime: Calendar = Calendar.getInstance(), - endTime: Calendar = Calendar.getInstance() + Hours(1), - isAllDay: Boolean = false, - ): ResolvedWeekViewEntity.Event { - val id = Random.nextLong() - return ResolvedWeekViewEntity.Event( - id = id, - title = "Title $id", - startTime = startTime, - endTime = endTime, - subtitle = null, - isAllDay = isAllDay, - style = ResolvedWeekViewEntity.Style(), - data = Event(startTime, endTime), - ) - } -} diff --git a/core/src/test/java/com/alamkanak/weekview/util/Mocks.kt b/core/src/test/java/com/alamkanak/weekview/util/Mocks.kt new file mode 100644 index 000000000..a6af692a8 --- /dev/null +++ b/core/src/test/java/com/alamkanak/weekview/util/Mocks.kt @@ -0,0 +1,32 @@ +package com.alamkanak.weekview.util + +import com.alamkanak.weekview.WeekViewItem +import com.alamkanak.weekview.plusMinutes +import java.util.Calendar +import kotlin.random.Random + +internal object Mocks { + + fun weekViewItems(count: Int): List { + return (0 until count).map { weekViewItem() } + } + + fun weekViewItem( + startTime: Calendar = Calendar.getInstance(), + endTime: Calendar = Calendar.getInstance().plusMinutes(60), + ): WeekViewItem { + val id = Random.nextLong() + return WeekViewItem( + id = id, + title = "Title $id", + duration = WeekViewItem.Duration.Bounded( + startTime = startTime, + endTime = endTime, + ), + subtitle = null, + data = Event(startTime, endTime), + ) + } +} + +internal fun WeekViewItem.withDifferentId() = copy(id = Random.nextLong()) diff --git a/core/src/test/java/com/alamkanak/weekview/util/TestingUtils.kt b/core/src/test/java/com/alamkanak/weekview/util/TestingUtils.kt deleted file mode 100644 index 98ee2b010..000000000 --- a/core/src/test/java/com/alamkanak/weekview/util/TestingUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.alamkanak.weekview.util - -import com.alamkanak.weekview.ResolvedWeekViewEntity -import java.util.Calendar -import kotlin.random.Random - -internal fun createResolvedWeekViewEvent( - startTime: Calendar, - endTime: Calendar -): ResolvedWeekViewEntity = ResolvedWeekViewEntity.Event( - id = 0, - title = "Title", - startTime = startTime, - endTime = endTime, - subtitle = null, - isAllDay = false, - style = ResolvedWeekViewEntity.Style(), - data = Unit -) - -internal fun ResolvedWeekViewEntity.Event<*>.withDifferentId() = copy(id = Random.nextLong()) diff --git a/jodatime/src/main/java/com/alamkanak/weekview/jodatime/WeekViewItemBuilderExtensions.kt b/jodatime/src/main/java/com/alamkanak/weekview/jodatime/WeekViewItemBuilderExtensions.kt new file mode 100644 index 000000000..017156787 --- /dev/null +++ b/jodatime/src/main/java/com/alamkanak/weekview/jodatime/WeekViewItemBuilderExtensions.kt @@ -0,0 +1,16 @@ +package com.alamkanak.weekview.jodatime + +import com.alamkanak.weekview.WeekViewItem +import org.joda.time.LocalDate +import org.joda.time.LocalDateTime + +fun WeekViewItem.Builder.setAllDayDuration(date: LocalDate): WeekViewItem.Builder { + return setAllDayDuration(date.toCalendar()) +} + +fun WeekViewItem.Builder.setBoundedDuration( + startTime: LocalDateTime, + endTime: LocalDateTime, +): WeekViewItem.Builder { + return setBoundedDuration(startTime.toCalendar(), endTime.toCalendar()) +} diff --git a/jsr310/src/main/java/com/alamkanak/weekview/jsr310/WeekViewItemBuilderExtensions.kt b/jsr310/src/main/java/com/alamkanak/weekview/jsr310/WeekViewItemBuilderExtensions.kt new file mode 100644 index 000000000..fe84e4aea --- /dev/null +++ b/jsr310/src/main/java/com/alamkanak/weekview/jsr310/WeekViewItemBuilderExtensions.kt @@ -0,0 +1,16 @@ +package com.alamkanak.weekview.jsr310 + +import com.alamkanak.weekview.WeekViewItem +import java.time.LocalDate +import java.time.LocalDateTime + +fun WeekViewItem.Builder.setAllDayDuration(date: LocalDate): WeekViewItem.Builder { + return setAllDayDuration(date.toCalendar()) +} + +fun WeekViewItem.Builder.setBoundedDuration( + startTime: LocalDateTime, + endTime: LocalDateTime, +): WeekViewItem.Builder { + return setBoundedDuration(startTime.toCalendar(), endTime.toCalendar()) +} diff --git a/sample/build.gradle b/sample/build.gradle index bd6e0d110..57cae27ad 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -16,9 +16,6 @@ android { targetCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled true } - lintOptions { - abortOnError false - } buildFeatures { viewBinding true } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/data/EventsRepository.kt b/sample/src/main/java/com/alamkanak/weekview/sample/data/EventsRepository.kt index 57b48b54b..f57ff6083 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/data/EventsRepository.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/data/EventsRepository.kt @@ -7,7 +7,7 @@ import android.os.Looper import com.alamkanak.weekview.sample.data.model.ApiBlockedTime import com.alamkanak.weekview.sample.data.model.ApiEvent import com.alamkanak.weekview.sample.data.model.ApiResult -import com.alamkanak.weekview.sample.data.model.CalendarEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.time.YearMonth @@ -21,7 +21,7 @@ class EventsRepository(private val context: Context) { fun fetch( yearMonths: List, - onSuccess: (List) -> Unit + onSuccess: (List) -> Unit, ) { val handlerThread = HandlerThread("events-fetching") handlerThread.start() @@ -34,7 +34,7 @@ class EventsRepository(private val context: Context) { val calendarEntities = yearMonths.flatMap { yearMonth -> apiEntities.mapIndexedNotNull { index, apiResult -> - apiResult.toCalendarEntity(yearMonth, index) + apiResult.toCalendarItem(yearMonth, index) } } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/data/model/ApiEvent.kt b/sample/src/main/java/com/alamkanak/weekview/sample/data/model/ApiEvent.kt index bd988579c..9410a9918 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/data/model/ApiEvent.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/data/model/ApiEvent.kt @@ -7,7 +7,7 @@ import java.time.LocalTime import java.time.YearMonth interface ApiResult { - fun toCalendarEntity(yearMonth: YearMonth, index: Int): CalendarEntity? + fun toCalendarItem(yearMonth: YearMonth, index: Int): CalendarItem? } data class ApiEvent( @@ -18,16 +18,16 @@ data class ApiEvent( @SerializedName("duration") val duration: Int, @SerializedName("color") val color: String, @SerializedName("is_canceled") val isCanceled: Boolean, - @SerializedName("is_all_day") val isAllDay: Boolean + @SerializedName("is_all_day") val isAllDay: Boolean, ) : ApiResult { - override fun toCalendarEntity(yearMonth: YearMonth, index: Int): CalendarEntity? { + override fun toCalendarItem(yearMonth: YearMonth, index: Int): CalendarItem? { return try { val startTime = LocalTime.parse(startTime) val startDateTime = yearMonth.atDay(dayOfMonth).atTime(startTime) val endDateTime = startDateTime.plusMinutes(duration.toLong()) - CalendarEntity.Event( - id = "100${yearMonth.year}00${yearMonth.monthValue}00$index".toLong(), + CalendarItem.Event( + id = generateId(yearMonth, index), title = title, location = location, startTime = startDateTime, @@ -45,16 +45,16 @@ data class ApiEvent( data class ApiBlockedTime( @SerializedName("day_of_month") val dayOfMonth: Int, @SerializedName("start_time") val startTime: String, - @SerializedName("duration") val duration: Int + @SerializedName("duration") val duration: Int, ) : ApiResult { - override fun toCalendarEntity(yearMonth: YearMonth, index: Int): CalendarEntity? { + override fun toCalendarItem(yearMonth: YearMonth, index: Int): CalendarItem? { return try { val startTime = LocalTime.parse(startTime) val startDateTime = yearMonth.atDay(dayOfMonth).atTime(startTime) val endDateTime = startDateTime.plusMinutes(duration.toLong()) - CalendarEntity.BlockedTimeSlot( - id = "200${yearMonth.year}00${yearMonth.monthValue}00$index".toLong(), + CalendarItem.BlockedTimeSlot( + id = generateId(yearMonth, index), startTime = startDateTime, endTime = endDateTime ) @@ -63,3 +63,10 @@ data class ApiBlockedTime( } } } + +private fun generateId(yearMonth: YearMonth, index: Int): Long { + val eventNumber = index.toString().padStart(length = 4, padChar = '0') + val year = yearMonth.year * 1_000_000 + val month = yearMonth.monthValue * 1_000 + return "$year$month$eventNumber".toLong() +} diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarEntity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarEntity.kt deleted file mode 100644 index af0a96788..000000000 --- a/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarEntity.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.alamkanak.weekview.sample.data.model - -import android.graphics.Color -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.style.StrikethroughSpan -import android.text.style.TypefaceSpan -import com.alamkanak.weekview.WeekViewEntity -import com.alamkanak.weekview.jsr310.setEndTime -import com.alamkanak.weekview.jsr310.setStartTime -import com.alamkanak.weekview.sample.R -import java.time.LocalDateTime - -sealed class CalendarEntity { - - data class Event( - val id: Long, - val title: CharSequence, - val startTime: LocalDateTime, - val endTime: LocalDateTime, - val location: CharSequence, - val color: Int, - val isAllDay: Boolean, - val isCanceled: Boolean - ) : CalendarEntity() - - data class BlockedTimeSlot( - val id: Long, - val startTime: LocalDateTime, - val endTime: LocalDateTime - ) : CalendarEntity() -} - -fun CalendarEntity.toWeekViewEntity(): WeekViewEntity { - return when (this) { - is CalendarEntity.Event -> toWeekViewEntity() - is CalendarEntity.BlockedTimeSlot -> toWeekViewEntity() - } -} - -fun CalendarEntity.Event.toWeekViewEntity(): WeekViewEntity { - val backgroundColor = if (!isCanceled) color else Color.WHITE - val textColor = if (!isCanceled) Color.WHITE else color - val borderWidthResId = if (!isCanceled) R.dimen.no_border_width else R.dimen.border_width - - val style = WeekViewEntity.Style.Builder() - .setTextColor(textColor) - .setBackgroundColor(backgroundColor) - .setBorderWidthResource(borderWidthResId) - .setBorderColor(color) - .build() - - val title = SpannableStringBuilder(title).apply { - val titleSpan = TypefaceSpan("sans-serif-medium") - setSpan(titleSpan, 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - if (isCanceled) { - setSpan(StrikethroughSpan(), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - - val subtitle = SpannableStringBuilder(location).apply { - if (isCanceled) { - setSpan(StrikethroughSpan(), 0, location.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - - return WeekViewEntity.Event.Builder(this) - .setId(id) - .setTitle(title) - .setStartTime(startTime) - .setEndTime(endTime) - .setSubtitle(subtitle) - .setAllDay(isAllDay) - .setStyle(style) - .build() -} - -fun CalendarEntity.BlockedTimeSlot.toWeekViewEntity(): WeekViewEntity { - val style = WeekViewEntity.Style.Builder() - .setBackgroundColorResource(R.color.gray_alpha10) - .setCornerRadius(0) - .build() - - return WeekViewEntity.BlockedTime.Builder() - .setId(id) - .setStartTime(startTime) - .setEndTime(endTime) - .setStyle(style) - .build() -} diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarItem.kt b/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarItem.kt new file mode 100644 index 000000000..c6dfdb1db --- /dev/null +++ b/sample/src/main/java/com/alamkanak/weekview/sample/data/model/CalendarItem.kt @@ -0,0 +1,164 @@ +package com.alamkanak.weekview.sample.data.model + +import android.content.Context +import android.graphics.Color +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StrikethroughSpan +import android.text.style.TypefaceSpan +import androidx.core.content.ContextCompat +import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem +import com.alamkanak.weekview.jsr310.setAllDayDuration +import com.alamkanak.weekview.jsr310.setBoundedDuration +import com.alamkanak.weekview.jsr310.setEndTime +import com.alamkanak.weekview.jsr310.setStartTime +import com.alamkanak.weekview.sample.R +import java.time.LocalDateTime + +sealed class CalendarItem { + + data class Event( + val id: Long, + val title: CharSequence, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + val location: CharSequence, + val color: Int, + val isAllDay: Boolean, + val isCanceled: Boolean, + ) : CalendarItem() + + data class BlockedTimeSlot( + val id: Long, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + ) : CalendarItem() +} + +fun CalendarItem.toWeekViewItem(context: Context): WeekViewItem { + return when (this) { + is CalendarItem.Event -> toWeekViewItem(context) + is CalendarItem.BlockedTimeSlot -> toWeekViewItem(context) + } +} + +fun CalendarItem.Event.toWeekViewItem(context: Context): WeekViewItem { + val backgroundColor = if (!isCanceled) color else Color.WHITE + val textColor = if (!isCanceled) Color.WHITE else color + val borderWidthResId = if (!isCanceled) R.dimen.no_border_width else R.dimen.border_width + val borderWidth = context.resources.getDimensionPixelSize(borderWidthResId) + + val title = SpannableStringBuilder(title).apply { + val titleSpan = TypefaceSpan("sans-serif-medium") + setSpan(titleSpan, 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + if (isCanceled) { + setSpan(StrikethroughSpan(), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + val subtitle = SpannableStringBuilder(location).apply { + if (isCanceled) { + setSpan(StrikethroughSpan(), 0, location.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + val style = WeekViewItem.Style( + textColor = textColor, + backgroundColor = backgroundColor, + borderWidth = borderWidth, + borderColor = color, + ) + + val config = WeekViewItem.Configuration.foreground() + + return WeekViewItem.of(this) + .setId(id) + .setTitle(title) + .setSubtitle(subtitle) + .apply { + if (isAllDay) { + setAllDayDuration(startTime.toLocalDate()) + } else { + setBoundedDuration(startTime, endTime) + } + } + .setStyle(style) + .setConfiguration(config) + .build() +} + +fun CalendarItem.BlockedTimeSlot.toWeekViewItem(context: Context): WeekViewItem { + val style = WeekViewItem.Style( + backgroundColor = ContextCompat.getColor(context, R.color.gray_alpha10), + cornerRadius = context.resources.getDimensionPixelSize(R.dimen.no_corner_radius), + ) + + val config = WeekViewItem.Configuration.background() + + return WeekViewItem.of(this) + .setId(id) + .setTitle("Unavailable") + .setBoundedDuration(startTime, endTime) + .setStyle(style) + .setConfiguration(config) + .build() +} + +fun CalendarItem.toWeekViewEntity(): WeekViewEntity { + return when (this) { + is CalendarItem.Event -> toWeekViewEntity() + is CalendarItem.BlockedTimeSlot -> toWeekViewEntity() + } +} + +fun CalendarItem.Event.toWeekViewEntity(): WeekViewEntity { + val backgroundColor = if (!isCanceled) color else Color.WHITE + val textColor = if (!isCanceled) Color.WHITE else color + val borderWidthResId = if (!isCanceled) R.dimen.no_border_width else R.dimen.border_width + + val style = WeekViewEntity.Style.Builder() + .setTextColor(textColor) + .setBackgroundColor(backgroundColor) + .setBorderWidthResource(borderWidthResId) + .setBorderColor(color) + .build() + + val title = SpannableStringBuilder(title).apply { + val titleSpan = TypefaceSpan("sans-serif-medium") + setSpan(titleSpan, 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + if (isCanceled) { + setSpan(StrikethroughSpan(), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + val subtitle = SpannableStringBuilder(location).apply { + if (isCanceled) { + setSpan(StrikethroughSpan(), 0, location.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + return WeekViewEntity.Event.Builder(this) + .setId(id) + .setTitle(title) + .setStartTime(startTime) + .setEndTime(endTime) + .setSubtitle(subtitle) + .setAllDay(isAllDay) + .setStyle(style) + .build() +} + +fun CalendarItem.BlockedTimeSlot.toWeekViewEntity(): WeekViewEntity { + val style = WeekViewEntity.Style.Builder() + .setBackgroundColorResource(R.color.gray_alpha10) + .setCornerRadius(0) + .build() + + return WeekViewEntity.BlockedTime.Builder() + .setId(id) + .setStartTime(startTime) + .setEndTime(endTime) + .setStyle(style) + .build() +} diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/BasicActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/BasicActivity.kt index eee669504..bda4b470a 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/BasicActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/BasicActivity.kt @@ -4,14 +4,15 @@ import android.graphics.RectF import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity -import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem import com.alamkanak.weekview.jsr310.WeekViewPagingAdapterJsr310 import com.alamkanak.weekview.jsr310.setDateFormatter -import com.alamkanak.weekview.sample.data.model.CalendarEntity -import com.alamkanak.weekview.sample.data.model.toWeekViewEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem +import com.alamkanak.weekview.sample.data.model.toWeekViewItem import com.alamkanak.weekview.sample.databinding.ActivityBasicBinding import com.alamkanak.weekview.sample.util.GenericAction.ShowSnackbar import com.alamkanak.weekview.sample.util.defaultDateTimeFormatter +import com.alamkanak.weekview.sample.util.defaultTimeFormatter import com.alamkanak.weekview.sample.util.genericViewModel import com.alamkanak.weekview.sample.util.setupWithWeekView import com.alamkanak.weekview.sample.util.showToast @@ -54,7 +55,7 @@ class BasicActivity : AppCompatActivity() { } viewModel.viewState.observe(this) { viewState -> - adapter.submitList(viewState.entities) + adapter.submitList(viewState.items) } viewModel.actions.subscribeToEvents(this) { action -> @@ -70,29 +71,31 @@ class BasicActivity : AppCompatActivity() { } } -private class BasicActivityWeekViewAdapter( +class BasicActivityWeekViewAdapter( private val dragHandler: (Long, LocalDateTime, LocalDateTime) -> Unit, private val loadMoreHandler: (List) -> Unit -) : WeekViewPagingAdapterJsr310() { +) : WeekViewPagingAdapterJsr310() { - override fun onCreateEntity(item: CalendarEntity): WeekViewEntity = item.toWeekViewEntity() + override fun onCreateItem(item: CalendarItem): WeekViewItem = item.toWeekViewItem(context) - override fun onEventClick(data: CalendarEntity, bounds: RectF) { - if (data is CalendarEntity.Event) { - context.showToast("Clicked ${data.title}") + override fun onEventClick(data: CalendarItem, bounds: RectF) { + val message = when (data) { + is CalendarItem.Event -> { + "Clicked event ${data.title}" + } + is CalendarItem.BlockedTimeSlot -> { + val formattedStart = defaultTimeFormatter.format(data.startTime) + val formattedEnd = defaultTimeFormatter.format(data.endTime) + "Clicked blocked time ($formattedStart–$formattedEnd)" + } } + context.showToast(message) } override fun onEmptyViewClick(time: LocalDateTime) { context.showToast("Empty view clicked at ${defaultDateTimeFormatter.format(time)}") } - override fun onDragAndDropFinished(data: CalendarEntity, newStartTime: LocalDateTime, newEndTime: LocalDateTime) { - if (data is CalendarEntity.Event) { - dragHandler(data.id, newStartTime, newEndTime) - } - } - override fun onEmptyViewLongClick(time: LocalDateTime) { context.showToast("Empty view long-clicked at ${defaultDateTimeFormatter.format(time)}") } @@ -101,6 +104,12 @@ private class BasicActivityWeekViewAdapter( loadMoreHandler(yearMonthsBetween(startDate, endDate)) } + override fun onDragAndDropFinished(data: CalendarItem, newStartTime: LocalDateTime, newEndTime: LocalDateTime) { + if (data is CalendarItem.Event) { + dragHandler(data.id, newStartTime, newEndTime) + } + } + override fun onVerticalScrollPositionChanged(currentOffset: Float, distance: Float) { Log.d("BasicActivity", "Scrolling vertically (distance: ${distance.toInt()}, current offset ${currentOffset.toInt()})") } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomFontActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomFontActivity.kt index 96b154619..56b714f02 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomFontActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomFontActivity.kt @@ -1,11 +1,12 @@ package com.alamkanak.weekview.sample.ui +import android.graphics.RectF import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem import com.alamkanak.weekview.jsr310.WeekViewPagingAdapterJsr310 -import com.alamkanak.weekview.sample.data.model.CalendarEntity -import com.alamkanak.weekview.sample.data.model.toWeekViewEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem +import com.alamkanak.weekview.sample.data.model.toWeekViewItem import com.alamkanak.weekview.sample.databinding.ActivityCustomFontBinding import com.alamkanak.weekview.sample.util.defaultDateTimeFormatter import com.alamkanak.weekview.sample.util.genericViewModel @@ -22,20 +23,18 @@ class CustomFontActivity : AppCompatActivity() { ActivityCustomFontBinding.inflate(layoutInflater) } - private val adapter: CustomFontActivityWeekViewAdapter by lazy { - CustomFontActivityWeekViewAdapter(loadMoreHandler = this::onLoadMore) - } - private val viewModel by genericViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.toolbarContainer.toolbar.setupWithWeekView(binding.weekView) + + val adapter = CustomFontActivityWeekViewAdapter(loadMoreHandler = this::onLoadMore) binding.weekView.adapter = adapter viewModel.viewState.observe(this) { viewState -> - adapter.submitList(viewState.entities) + adapter.submitList(viewState.items) } } @@ -46,12 +45,12 @@ class CustomFontActivity : AppCompatActivity() { private class CustomFontActivityWeekViewAdapter( private val loadMoreHandler: (List) -> Unit -) : WeekViewPagingAdapterJsr310() { +) : WeekViewPagingAdapterJsr310() { - override fun onCreateEntity(item: CalendarEntity): WeekViewEntity = item.toWeekViewEntity() + override fun onCreateItem(item: CalendarItem): WeekViewItem = item.toWeekViewItem(context) - override fun onEventClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { + override fun onEventClick(data: CalendarItem, bounds: RectF) { + if (data is CalendarItem.Event) { context.showToast("Clicked ${data.title}") } } @@ -60,10 +59,13 @@ private class CustomFontActivityWeekViewAdapter( context.showToast("Empty view clicked at ${defaultDateTimeFormatter.format(time)}") } - override fun onEventLongClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { + override fun onEventLongClick(data: CalendarItem, bounds: RectF): Boolean { + if (data is CalendarItem.Event) { context.showToast("Long-clicked ${data.title}") } + + // Disabling drag-&-drop by considering the long-click handled. + return true } override fun onEmptyViewLongClick(time: LocalDateTime) { diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/LimitedActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/LimitedActivity.kt index d81bbc87d..ae4a76e94 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/LimitedActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/LimitedActivity.kt @@ -1,13 +1,14 @@ package com.alamkanak.weekview.sample.ui +import android.graphics.RectF import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem import com.alamkanak.weekview.jsr310.WeekViewPagingAdapterJsr310 import com.alamkanak.weekview.jsr310.maxDateAsLocalDate import com.alamkanak.weekview.jsr310.minDateAsLocalDate -import com.alamkanak.weekview.sample.data.model.CalendarEntity -import com.alamkanak.weekview.sample.data.model.toWeekViewEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem +import com.alamkanak.weekview.sample.data.model.toWeekViewItem import com.alamkanak.weekview.sample.databinding.ActivityLimitedBinding import com.alamkanak.weekview.sample.util.defaultDateTimeFormatter import com.alamkanak.weekview.sample.util.genericViewModel @@ -41,7 +42,7 @@ class LimitedActivity : AppCompatActivity() { binding.weekView.adapter = adapter viewModel.viewState.observe(this) { viewState -> - adapter.submitList(viewState.entities) + adapter.submitList(viewState.items) } } @@ -52,12 +53,12 @@ class LimitedActivity : AppCompatActivity() { private class LimitedActivityWeekViewAdapter( private val loadMoreHandler: (List) -> Unit -) : WeekViewPagingAdapterJsr310() { +) : WeekViewPagingAdapterJsr310() { - override fun onCreateEntity(item: CalendarEntity): WeekViewEntity = item.toWeekViewEntity() + override fun onCreateItem(item: CalendarItem): WeekViewItem = item.toWeekViewItem(context) - override fun onEventClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { + override fun onEventClick(data: CalendarItem, bounds: RectF) { + if (data is CalendarItem.Event) { context.showToast("Clicked ${data.title}") } } @@ -66,12 +67,6 @@ private class LimitedActivityWeekViewAdapter( context.showToast("Empty view clicked at ${defaultDateTimeFormatter.format(time)}") } - override fun onEventLongClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { - context.showToast("Long-clicked ${data.title}") - } - } - override fun onEmptyViewLongClick(time: LocalDateTime) { context.showToast("Empty view long-clicked at ${defaultDateTimeFormatter.format(time)}") } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/StaticActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/StaticActivity.kt index 74b05d6dc..759813d68 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/StaticActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/StaticActivity.kt @@ -1,14 +1,15 @@ package com.alamkanak.weekview.sample.ui +import android.graphics.RectF import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem import com.alamkanak.weekview.jsr310.WeekViewPagingAdapterJsr310 import com.alamkanak.weekview.jsr310.firstVisibleDateAsLocalDate import com.alamkanak.weekview.jsr310.scrollToDate import com.alamkanak.weekview.sample.R -import com.alamkanak.weekview.sample.data.model.CalendarEntity -import com.alamkanak.weekview.sample.data.model.toWeekViewEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem +import com.alamkanak.weekview.sample.data.model.toWeekViewItem import com.alamkanak.weekview.sample.databinding.ActivityStaticBinding import com.alamkanak.weekview.sample.util.defaultDateTimeFormatter import com.alamkanak.weekview.sample.util.genericViewModel @@ -57,7 +58,7 @@ class StaticActivity : AppCompatActivity() { } viewModel.viewState.observe(this) { viewState -> - adapter.submitList(viewState.entities) + adapter.submitList(viewState.items) } } @@ -79,12 +80,12 @@ class StaticActivity : AppCompatActivity() { private class StaticActivityWeekViewAdapter( private val rangeChangeHandler: (LocalDate, LocalDate) -> Unit, private val loadMoreHandler: (List) -> Unit -) : WeekViewPagingAdapterJsr310() { +) : WeekViewPagingAdapterJsr310() { - override fun onCreateEntity(item: CalendarEntity): WeekViewEntity = item.toWeekViewEntity() + override fun onCreateItem(item: CalendarItem): WeekViewItem = item.toWeekViewItem(context) - override fun onEventClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { + override fun onEventClick(data: CalendarItem, bounds: RectF) { + if (data is CalendarItem.Event) { context.showToast("Clicked ${data.title}") } } @@ -93,12 +94,6 @@ private class StaticActivityWeekViewAdapter( context.showToast("Empty view clicked at ${defaultDateTimeFormatter.format(time)}") } - override fun onEventLongClick(data: CalendarEntity) { - if (data is CalendarEntity.Event) { - context.showToast("Long-clicked ${data.title}") - } - } - override fun onEmptyViewLongClick(time: LocalDateTime) { context.showToast("Empty view long-clicked at ${defaultDateTimeFormatter.format(time)}") } diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/WithFragmentActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/WithFragmentActivity.kt index 489940241..b04120541 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/WithFragmentActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/WithFragmentActivity.kt @@ -6,13 +6,13 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import com.alamkanak.weekview.WeekViewEntity +import com.alamkanak.weekview.WeekViewItem import com.alamkanak.weekview.jsr310.WeekViewPagingAdapterJsr310 import com.alamkanak.weekview.jsr310.maxDateAsLocalDate import com.alamkanak.weekview.jsr310.minDateAsLocalDate import com.alamkanak.weekview.sample.R -import com.alamkanak.weekview.sample.data.model.CalendarEntity -import com.alamkanak.weekview.sample.data.model.toWeekViewEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem +import com.alamkanak.weekview.sample.data.model.toWeekViewItem import com.alamkanak.weekview.sample.databinding.FragmentWeekBinding import com.alamkanak.weekview.sample.util.genericViewModel import com.alamkanak.weekview.sample.util.setupWithWeekView @@ -59,7 +59,7 @@ class WeekFragment : Fragment(R.layout.fragment_week) { binding.weekView.maxDateAsLocalDate = YearMonth.now().atEndOfMonth() viewModel.viewState.observe(viewLifecycleOwner) { viewState -> - adapter.submitList(viewState.entities) + adapter.submitList(viewState.items) } } @@ -70,9 +70,9 @@ class WeekFragment : Fragment(R.layout.fragment_week) { private class FragmentWeekViewAdapter( private val loadMoreHandler: (List) -> Unit -) : WeekViewPagingAdapterJsr310() { +) : WeekViewPagingAdapterJsr310() { - override fun onCreateEntity(item: CalendarEntity): WeekViewEntity = item.toWeekViewEntity() + override fun onCreateItem(item: CalendarItem): WeekViewItem = item.toWeekViewItem(context) override fun onLoadMore( startDate: LocalDate, diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/util/DateFormatting.kt b/sample/src/main/java/com/alamkanak/weekview/sample/util/DateExtensions.kt similarity index 75% rename from sample/src/main/java/com/alamkanak/weekview/sample/util/DateFormatting.kt rename to sample/src/main/java/com/alamkanak/weekview/sample/util/DateExtensions.kt index d7a778345..930eb0cc9 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/util/DateFormatting.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/util/DateExtensions.kt @@ -5,3 +5,5 @@ import java.time.format.FormatStyle.MEDIUM import java.time.format.FormatStyle.SHORT val defaultDateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(MEDIUM, SHORT) + +val defaultTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedTime(SHORT) diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/util/GenericViewModel.kt b/sample/src/main/java/com/alamkanak/weekview/sample/util/GenericViewModel.kt index b912e6aab..14f7697e4 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/util/GenericViewModel.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/util/GenericViewModel.kt @@ -8,14 +8,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelLazy import androidx.lifecycle.ViewModelProvider import com.alamkanak.weekview.sample.data.EventsRepository -import com.alamkanak.weekview.sample.data.model.CalendarEntity +import com.alamkanak.weekview.sample.data.model.CalendarItem import java.time.LocalDateTime import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.format.FormatStyle.MEDIUM data class GenericViewState( - val entities: List = emptyList() + val items: List = emptyList() ) sealed class GenericAction { @@ -32,19 +32,19 @@ class GenericViewModel( private val _actions = MutableLiveData>() val actions: LiveData> = _actions - private val currentEntities: List - get() = _viewState.value?.entities.orEmpty() + private val currentItems: List + get() = _viewState.value?.items.orEmpty() fun fetchEvents(yearMonths: List) { eventsRepository.fetch(yearMonths = yearMonths) { entities -> - val existingEntities = _viewState.value?.entities.orEmpty() - _viewState.value = GenericViewState(entities = existingEntities + entities) + val existingEntities = _viewState.value?.items.orEmpty() + _viewState.value = GenericViewState(items = existingEntities + entities) } } fun handleDrag(id: Long, newStartTime: LocalDateTime, newEndTime: LocalDateTime) { - val existingEntity = currentEntities - .filterIsInstance() + val existingEntity = currentItems + .filterIsInstance() .first { it.id == id } val newEntity = existingEntity.copy( @@ -57,28 +57,28 @@ class GenericViewModel( } private fun postDragNotification( - existingEntity: CalendarEntity.Event, - updatedEntity: CalendarEntity.Event, + existingItem: CalendarItem.Event, + updatedItem: CalendarItem.Event, ) { - val newDateTime = updatedEntity.startTime.format(DateTimeFormatter.ofLocalizedDateTime(MEDIUM)) + val newDateTime = updatedItem.startTime.format(DateTimeFormatter.ofLocalizedDateTime(MEDIUM)) val action = GenericAction.ShowSnackbar( - message = "Moved ${updatedEntity.title} to $newDateTime", - undoAction = { updateEntity(existingEntity) }, + message = "Moved ${updatedItem.title} to $newDateTime", + undoAction = { updateEntity(existingItem) }, ) _actions.postEvent(action) } - private fun updateEntity(newEntity: CalendarEntity.Event) { - val updatedEntities = currentEntities.map { entity -> - if (entity is CalendarEntity.Event && entity.id == newEntity.id) { - newEntity + private fun updateEntity(newItem: CalendarItem.Event) { + val updatedEntities = currentItems.map { entity -> + if (entity is CalendarItem.Event && entity.id == newItem.id) { + newItem } else { entity } } - _viewState.value = GenericViewState(entities = updatedEntities) + _viewState.value = GenericViewState(items = updatedEntities) } class Factory(private val eventsRepository: EventsRepository) : ViewModelProvider.Factory { diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml index e86c9ccc4..881e294f6 100644 --- a/sample/src/main/res/values/dimens.xml +++ b/sample/src/main/res/values/dimens.xml @@ -4,6 +4,7 @@ 1dp 0dp 8dp + 0dp 2dp 3dp 1dp diff --git a/scripts/publish-mavencentral.gradle b/scripts/publish-mavencentral.gradle new file mode 100644 index 000000000..2bf08431e --- /dev/null +++ b/scripts/publish-mavencentral.gradle @@ -0,0 +1,124 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' +apply plugin: 'org.jetbrains.dokka' + +ext { + PUBLISH_GROUP_ID = 'com.thellmund.android-week-view' + PUBLISH_VERSION = '5.3.0' +} + +task androidSourcesJar(type: Jar) { + archiveClassifier.set('sources') + if (project.plugins.findPlugin("com.android.library")) { + from android.sourceSets.main.java.srcDirs + from android.sourceSets.main.kotlin.srcDirs + } else { + from sourceSets.main.java.srcDirs + from sourceSets.main.kotlin.srcDirs + } +} + +task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { + archiveClassifier.set('javadoc') + from dokkaJavadoc.outputDirectory +} + +artifacts { + archives androidSourcesJar + archives javadocJar +} + +group = PUBLISH_GROUP_ID +version = PUBLISH_VERSION + +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.secretKeyRingFile"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' + +File secretPropsFile = project.rootProject.file('local.properties') +if (secretPropsFile.exists()) { + Properties p = new Properties() + p.load(new FileInputStream(secretPropsFile)) + p.each { name, value -> + ext[name] = value + } +} else { + ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') + ext["signing.password"] = System.getenv('SIGNING_PASSWORD') + ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') + ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') + ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') +} + +publishing { + publications { + release(MavenPublication) { + groupId PUBLISH_GROUP_ID + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION + + if (project.plugins.findPlugin("com.android.library")) { + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + } else { + artifact("$buildDir/libs/${project.getName()}-${version}.jar") + } + + artifact androidSourcesJar + artifact javadocJar + + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Display highly customizable calendar views in your Android app' + url = 'https://github.com/thellmund/Android-Week-View' + packaging = 'aar' + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'thellmund' + name = 'Till Hellmund' + email = 'thellmund@gmail.com' + } + } + scm { + connection = 'scm:git:github.com/thellmund/Android-Week-View.git' + developerConnection = 'scm:git:ssh://github.com/thellmund/Android-Week-View.git' + url = 'https://github.com/thellmund/Android-Week-View/tree/main' + } + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } + repositories { + maven { + name = "sonatype" + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + + credentials { + username ossrhUsername + password ossrhPassword + } + } + } +} + +signing { + sign publishing.publications +} diff --git a/threetenabp/src/main/java/com/alamkanak/weekview/threetenabp/WeekViewItemBuilderExtensions.kt b/threetenabp/src/main/java/com/alamkanak/weekview/threetenabp/WeekViewItemBuilderExtensions.kt new file mode 100644 index 000000000..bbad35104 --- /dev/null +++ b/threetenabp/src/main/java/com/alamkanak/weekview/threetenabp/WeekViewItemBuilderExtensions.kt @@ -0,0 +1,16 @@ +package com.alamkanak.weekview.threetenabp + +import com.alamkanak.weekview.WeekViewItem +import org.threeten.bp.LocalDate +import org.threeten.bp.LocalDateTime + +fun WeekViewItem.Builder.setAllDayDuration(date: LocalDate): WeekViewItem.Builder { + return setAllDayDuration(date.toCalendar()) +} + +fun WeekViewItem.Builder.setBoundedDuration( + startTime: LocalDateTime, + endTime: LocalDateTime, +): WeekViewItem.Builder { + return setBoundedDuration(startTime.toCalendar(), endTime.toCalendar()) +}