diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e7a4bcdc..a80641ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Overlapping events in the weekly view are now stacked on top of each other instead of being squeezed side by side ## [1.12.1] - 2026-10-22 ### Changed diff --git a/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt b/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt index 1f15edd71..1fc535b37 100644 --- a/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt +++ b/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt @@ -5,7 +5,9 @@ import android.content.ClipData import android.content.ClipDescription import android.content.Intent import android.content.res.Resources +import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.os.Handler import android.util.Range @@ -18,6 +20,7 @@ import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout import androidx.collection.LongSparseArray +import androidx.core.graphics.ColorUtils import androidx.fragment.app.Fragment import org.fossify.calendar.R import org.fossify.calendar.activities.MainActivity @@ -54,6 +57,7 @@ import org.fossify.calendar.helpers.TYPE_EVENT import org.fossify.calendar.helpers.TYPE_TASK import org.fossify.calendar.helpers.WEEK_START_TIMESTAMP import org.fossify.calendar.helpers.WeeklyCalendarImpl +import org.fossify.calendar.helpers.WeeklyEventLayout import org.fossify.calendar.helpers.getActivityToOpen import org.fossify.calendar.helpers.isWeekend import org.fossify.calendar.interfaces.WeekFragmentListener @@ -69,6 +73,7 @@ import org.fossify.commons.extensions.beGone import org.fossify.commons.extensions.beVisible import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.getContrastColor +import org.fossify.commons.extensions.getProperBackgroundColor import org.fossify.commons.extensions.getProperPrimaryColor import org.fossify.commons.extensions.getProperTextColor import org.fossify.commons.extensions.hideKeyboard @@ -668,70 +673,20 @@ class WeekFragment : Fragment(), WeeklyCalendar { } while (currentDayCode.toInt() <= endDayCode.toInt()) } + // make sure the title of the event below stays visible, even with a bigger minimal event height + val closeStartThreshold = max( + WeeklyEventLayout.CLOSE_START_THRESHOLD_MINUTES, + (minimalHeight / minuteHeight).roundToInt() + ) for ((_, eventDayList) in eventTimeRanges) { - val eventsCollisionChecked = ArrayList() - for ((eventId, eventWeeklyView) in eventDayList) { - if (eventWeeklyView.slot == 0) { - eventWeeklyView.slot = 1 - eventWeeklyView.slotMax = 1 - } - - eventsCollisionChecked.add(eventId) - val eventWeeklyViewsToCheck = - eventDayList.filterNot { eventsCollisionChecked.contains(it.key) } - for ((toCheckId, eventWeeklyViewToCheck) in eventWeeklyViewsToCheck) { - val areTouching = eventWeeklyView.range.intersects(eventWeeklyViewToCheck.range) - val doHaveCommonMinutes = if (areTouching) { - eventWeeklyView.range.upper > eventWeeklyViewToCheck.range.lower || (eventWeeklyView.range.lower == eventWeeklyView.range.upper && - eventWeeklyView.range.upper == eventWeeklyViewToCheck.range.lower) - } else { - false - } - - if (areTouching && doHaveCommonMinutes) { - if (eventWeeklyViewToCheck.slot == 0) { - val collisionEventWeeklyViews = eventDayList - .filter { eventWeeklyView.collisions.contains(it.key) } - var currentSlotMax = max( - eventWeeklyView.slotMax.coerceAtLeast(1), - eventWeeklyView.slot.coerceAtLeast(1) - ) - val maxCollisionSlot = collisionEventWeeklyViews - .maxOfOrNull { it.value.slot.coerceAtLeast(1) } ?: 1 - currentSlotMax = max(currentSlotMax, maxCollisionSlot) - val nextSlot = currentSlotMax + 1 - val slotRange = IntArray(currentSlotMax) { it + 1 } - for ((_, collisionEventWeeklyView) in collisionEventWeeklyViews) { - if ( - collisionEventWeeklyView.range.intersects(eventWeeklyViewToCheck.range) - && collisionEventWeeklyView.slot in 1..currentSlotMax - ) { - slotRange[collisionEventWeeklyView.slot - 1] = nextSlot - } - } - if (eventWeeklyView.slot in 1..currentSlotMax) { - slotRange[eventWeeklyView.slot - 1] = nextSlot - } - - val slot = slotRange.minOrNull() ?: nextSlot - eventWeeklyViewToCheck.slot = slot + WeeklyEventLayout.arrange(eventDayList.values, closeStartThreshold) + } - if (slot == nextSlot) { - eventWeeklyViewToCheck.slotMax = nextSlot - eventWeeklyView.slotMax = nextSlot - for ((_, collisionEventWeeklyView) in collisionEventWeeklyViews) { - collisionEventWeeklyView.slotMax = - max(collisionEventWeeklyView.slotMax, nextSlot) - } - } else { - eventWeeklyViewToCheck.slotMax = currentSlotMax - } - } - eventWeeklyView.collisions.add(toCheckId) - eventWeeklyViewToCheck.collisions.add(eventId) - } - } - } + val isRtl = requireContext().isRTLLayout + val eventBorderColor = if (isPrintVersion) { + Color.WHITE + } else { + requireContext().getProperBackgroundColor() } dayevents@ for (event in events) { @@ -765,7 +720,7 @@ class WeekFragment : Fragment(), WeeklyCalendar { event.color } var textColor = backgroundColor.getContrastColor() - val currentEventWeeklyView = eventTimeRanges[currentDayCode]!![event.id] + val currentEventWeeklyView = eventTimeRanges[currentDayCode]!![event.id]!! val adjustAlpha = if (event.isTask()) { dimCompletedTasks && event.isTaskCompleted() @@ -774,13 +729,26 @@ class WeekFragment : Fragment(), WeeklyCalendar { } if (adjustAlpha) { - backgroundColor = backgroundColor.adjustAlpha(MEDIUM_ALPHA) + // keep the color opaque, so that overlapping events don't show through + backgroundColor = ColorUtils.compositeColors( + backgroundColor.adjustAlpha(MEDIUM_ALPHA), + eventBorderColor + ) textColor = textColor.adjustAlpha(HIGHER_ALPHA) } - root.background = ColorDrawable(backgroundColor) + root.background = if (currentEventWeeklyView.indent > 0f) { + GradientDrawable().apply { + setColor(backgroundColor) + setStroke(density, eventBorderColor) + } + } else { + ColorDrawable(backgroundColor) + } dayColumn.addView(root) - root.y = currentEventWeeklyView!!.range.lower * minuteHeight + root.outlineProvider = null + root.translationZ = currentEventWeeklyView.layer.toFloat() + root.y = currentEventWeeklyView.range.lower * minuteHeight // compensate grid offset root.y -= (currentEventWeeklyView.range.lower / 60) / 2 @@ -810,12 +778,12 @@ class WeekFragment : Fragment(), WeeklyCalendar { } (root.layoutParams as RelativeLayout.LayoutParams).apply { - width = (dayColumn.width - 1) / currentEventWeeklyView.slotMax - root.x = (width * (currentEventWeeklyView.slot - 1)).toFloat() - if (currentEventWeeklyView.slot > 1) { - root.x += density - width -= density - } + addRule(RelativeLayout.ALIGN_PARENT_LEFT) + val columnWidth = dayColumn.width - 1 + val left = columnWidth * currentEventWeeklyView.indent + width = (columnWidth - left).toInt() + // in RTL the title is on the right, so nested events are indented from the right side + root.x = if (isRtl) 0f else left } root.setOnClickListener { diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/WeeklyEventLayout.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/WeeklyEventLayout.kt new file mode 100644 index 000000000..3c3e65a36 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/WeeklyEventLayout.kt @@ -0,0 +1,50 @@ +package org.fossify.calendar.helpers + +import org.fossify.calendar.models.EventWeeklyView +import kotlin.math.min + +/** + * Lays out overlapping events of a single day the way desktop calendars do: the earliest event (or the longest one, + * if they start at the same time) keeps the full width and every later overlapping event is drawn on top of it, + * indented to the right. + */ +object WeeklyEventLayout { + const val CLOSE_START_THRESHOLD_MINUTES = 30 + + // a nested event covers this part of the width of the event below it + private const val NESTED_INDENT_RATIO = 0.25f + + // an event starting at about the same time would hide the title of the event below it, so it covers only half of it + private const val CLOSE_START_INDENT_RATIO = 0.5f + + // keep deeply nested events wide enough to be visible and tappable + private const val MAX_INDENT = 0.8f + + /** + * @param closeStartThreshold events starting less than this many minutes after the event below them + * are indented more + */ + fun arrange(events: Collection, closeStartThreshold: Int) { + val sorted = events.sortedWith( + compareBy { it.range.lower }.thenByDescending { it.range.upper } + ) + + val placed = ArrayList() + for ((layer, event) in sorted.withIndex()) { + // nest over the topmost event still visible when this one starts, events that already ended don't matter + val parent = placed + .filter { it.range.upper > event.range.lower } + .maxByOrNull { it.indent } + + event.indent = if (parent == null) { + 0f + } else { + val startsClose = event.range.lower - parent.range.lower < closeStartThreshold + val ratio = if (startsClose) CLOSE_START_INDENT_RATIO else NESTED_INDENT_RATIO + min(parent.indent + (1 - parent.indent) * ratio, MAX_INDENT) + } + event.layer = layer + placed.add(event) + } + } +} diff --git a/app/src/main/kotlin/org/fossify/calendar/models/EventWeeklyView.kt b/app/src/main/kotlin/org/fossify/calendar/models/EventWeeklyView.kt index 68163e258..c3606a523 100644 --- a/app/src/main/kotlin/org/fossify/calendar/models/EventWeeklyView.kt +++ b/app/src/main/kotlin/org/fossify/calendar/models/EventWeeklyView.kt @@ -4,7 +4,8 @@ import android.util.Range data class EventWeeklyView( val range: Range, - var slot: Int = 0, - var slotMax: Int = 0, - var collisions: ArrayList = ArrayList() + // the left offset as a fraction of the day column width, events nested over earlier ones are indented + var indent: Float = 0f, + // the drawing order within the day, events nested over earlier ones are drawn on top of them + var layer: Int = 0, ) diff --git a/app/src/main/res/layout/week_event_marker.xml b/app/src/main/res/layout/week_event_marker.xml index aa34e2648..4e76f856d 100644 --- a/app/src/main/res/layout/week_event_marker.xml +++ b/app/src/main/res/layout/week_event_marker.xml @@ -29,6 +29,7 @@ android:includeFontPadding="false" android:maxLines="3" android:paddingStart="@dimen/tiny_margin" + android:textAlignment="viewStart" android:textColor="@android:color/white" android:textSize="@dimen/small_text_size" app:layout_constraintEnd_toEndOf="parent"