Month widget: one lane shows bars and per-day counts (#491)
This commit is contained in:
1 parent
726dbbbaf3
commit
dec2d1cb05
3 files changed
+60
-14
No files matched your search
@@ -36,16 +36,24 @@ internal fun <T : Any> mergeGaps(columns: Int, cellAt: (Int) -> GridCell<T>?): L
|
||||
return cells
|
||||
}
|
||||
|
||||
/** What a cell of the single mixed lane shows: an event's bar, or a day's event count. */
|
||||
internal sealed interface LaneItem {
|
||||
data class Bar(val event: EventInstance) : LaneItem
|
||||
data class Count(val count: Int) : LaneItem
|
||||
}
|
||||
|
||||
/**
|
||||
* One week of the grid seated with [laneCap] lanes, drawn [compact] when the
|
||||
* row is too short for a full one. [showsOverflow] is off when the week has no
|
||||
* room for the "+N" row.
|
||||
* room for the "+N" row; a [mixed] single lane then shows each day's count in
|
||||
* place of its bar where the day has more than fits (#491).
|
||||
*/
|
||||
internal class WeekLayout(
|
||||
val week: MonthWeek,
|
||||
val laneCap: Int,
|
||||
val showsOverflow: Boolean = true,
|
||||
val compact: Boolean = false,
|
||||
val mixed: Boolean = false,
|
||||
) {
|
||||
private val seating: List<DaySeating> = week.days.indices.map { week.seating(it, laneCap) }
|
||||
|
||||
@@ -61,6 +69,16 @@ internal class WeekLayout(
|
||||
}
|
||||
}
|
||||
|
||||
/** The single lane of a [mixed] week, bars cut short where a day shows its count instead; null otherwise. */
|
||||
val mixedLane: List<GridCell<LaneItem>>? = if (!mixed) null else mergeGaps(week.days.size) { col ->
|
||||
val day = seating[col]
|
||||
if (day.hidden.isNotEmpty()) return@mergeGaps GridCell(LaneItem.Count(day.seated.size + day.hidden.size), 1)
|
||||
val seat = day.seated.firstOrNull() ?: return@mergeGaps null
|
||||
val spanEnd = if (seat.bar) week.spanAt(col, seat.lane)?.endCol ?: col else col
|
||||
val end = (col + 1..spanEnd).firstOrNull { seating[it].hidden.isNotEmpty() }?.minus(1) ?: spanEnd
|
||||
GridCell(LaneItem.Bar(seat.event), end - col + 1)
|
||||
}
|
||||
|
||||
/** The "+N" row as counts and merged gaps; empty when everything fits. */
|
||||
val overflow: List<GridCell<Int>> = seating.map { it.hidden.size }.let { hidden ->
|
||||
if (hidden.none { it > 0 }) emptyList()
|
||||
@@ -85,12 +103,14 @@ internal fun lanesFitting(weekHeight: Dp, fontScale: Float): LaneFit {
|
||||
|
||||
/**
|
||||
* Seats each week in [fit]'s lanes. A week whose events need more gives its
|
||||
* last lane to the "+N" row instead. A compact lane has no room for that row,
|
||||
* so it keeps its bars (#491).
|
||||
* last lane to the "+N" row instead. A single lane has no room for that row,
|
||||
* so it mixes bars and per-day counts (#491).
|
||||
*/
|
||||
internal fun fitWeeks(weeks: List<MonthWeek>, fit: LaneFit): List<WeekLayout> = weeks.map { week ->
|
||||
val lanes = fit.lanes
|
||||
if (lanes == 0 || fit.compact) return@map WeekLayout(week, laneCap = lanes, showsOverflow = false, compact = fit.compact)
|
||||
if (lanes <= 1) {
|
||||
return@map WeekLayout(week, laneCap = lanes, showsOverflow = false, compact = fit.compact, mixed = lanes == 1)
|
||||
}
|
||||
val needed = WeekLayout(week, UNCAPPED_LANES).usedLanes
|
||||
WeekLayout(week, if (needed <= lanes) lanes else lanes - 1)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,21 @@ private fun weekViews(
|
||||
week.setViewVisibility(TODAY_IDS[col], if (isToday) View.VISIBLE else View.GONE)
|
||||
if (isToday) week.setImageTint(context, TODAY_IDS[col], colors.primary)
|
||||
}
|
||||
layout.mixedLane?.let { cells ->
|
||||
val lane = laneViews(context, layout.compact)
|
||||
cells.forEach { cell ->
|
||||
lane.addView(
|
||||
R.id.lane,
|
||||
when (val item = cell.item) {
|
||||
null -> gap(context, cell.cols)
|
||||
is LaneItem.Bar -> bar(context, item.event, cell.cols, soften, mode)
|
||||
is LaneItem.Count -> more(context, item.count, colors)
|
||||
},
|
||||
)
|
||||
}
|
||||
week.addView(R.id.week_lanes, lane)
|
||||
return week
|
||||
}
|
||||
layout.lanes.forEach { cells ->
|
||||
val lane = laneViews(context, layout.compact)
|
||||
cells.forEach { cell ->
|
||||
@@ -106,10 +121,7 @@ private fun weekViews(
|
||||
if (count == null) {
|
||||
lane.addView(R.id.lane, gap(context, cell.cols))
|
||||
} else {
|
||||
val more = RemoteViews(context.packageName, R.layout.widget_month_more)
|
||||
more.setTextViewText(R.id.more, "+$count")
|
||||
more.setTextColor(context, R.id.more, colors.onSurfaceVariant)
|
||||
lane.addView(R.id.lane, more)
|
||||
lane.addView(R.id.lane, more(context, count, colors))
|
||||
}
|
||||
}
|
||||
week.addView(R.id.week_lanes, lane)
|
||||
@@ -117,6 +129,12 @@ private fun weekViews(
|
||||
return week
|
||||
}
|
||||
|
||||
private fun more(context: Context, count: Int, colors: GridColors) =
|
||||
RemoteViews(context.packageName, R.layout.widget_month_more).apply {
|
||||
setTextViewText(R.id.more, "+$count")
|
||||
setTextColor(context, R.id.more, colors.onSurfaceVariant)
|
||||
}
|
||||
|
||||
private fun laneViews(context: Context, compact: Boolean = false) =
|
||||
RemoteViews(context.packageName, if (compact) R.layout.widget_month_lane_compact else R.layout.widget_month_lane)
|
||||
.apply { removeAllViews(R.id.lane) }
|
||||
|
||||
@@ -75,16 +75,24 @@ class MonthGridTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a single full lane still folds a busy week into the count`() {
|
||||
fun `a single lane shows counts on busy days`() {
|
||||
val layouts = fitWeeks(busyWeeks(perDay = 5), LaneFit(1))
|
||||
assertThat(layouts.all { it.laneCap == 0 && it.showsOverflow && it.overflow.isNotEmpty() }).isTrue()
|
||||
assertThat(layouts.all { it.mixedLane == List(7) { GridCell<LaneItem>(LaneItem.Count(5), 1) } }).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a compact lane keeps its bars in a busy week`() {
|
||||
val layouts = fitWeeks(busyWeeks(perDay = 5), LaneFit(1, compact = true))
|
||||
assertThat(layouts.all { it.laneCap == 1 && it.lanes.size == 1 && it.compact }).isTrue()
|
||||
assertThat(layouts.none { it.showsOverflow }).isTrue()
|
||||
fun `a single lane keeps bars on days that fit`() {
|
||||
val lone = timedEvent(monday.plus(1, DateTimeUnit.DAY), 9, zone = zone, id = 1, title = "Lone")
|
||||
val busy = (2L..4L).map { timedEvent(monday.plus(3, DateTimeUnit.DAY), it.toInt() + 8, zone = zone, id = it, title = "B$it") }
|
||||
val week = fitWeeks(weeks(listOf(lone) + busy), LaneFit(1, compact = true))[1]
|
||||
assertThat(week.compact).isTrue()
|
||||
assertThat(week.mixedLane).containsExactly(
|
||||
GridCell<LaneItem>(null, 1),
|
||||
GridCell<LaneItem>(LaneItem.Bar(lone), 1),
|
||||
GridCell<LaneItem>(null, 1),
|
||||
GridCell<LaneItem>(LaneItem.Count(3), 1),
|
||||
GridCell<LaneItem>(null, 3),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user