Skip to content

Commit 182641f

Browse files
committed
Make various improvement throughout the codebase
- Resolve issue where scrolling didn't work after rotating the device - Simplify how text in event chips is calculated and laid out - Default to semi-bold text instead of bold text - Use theme colors as smarter default values for WeekView colors
1 parent bb35dec commit 182641f

35 files changed

+496
-693
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ buildscript {
1010
}
1111
}
1212
dependencies {
13-
classpath 'com.android.tools.build:gradle:4.0.0'
13+
classpath 'com.android.tools.build:gradle:4.0.1'
1414
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
1515
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
1616
classpath 'org.jlleitschuh.gradle:ktlint-gradle:9.2.1'

core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt

Lines changed: 104 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,25 @@ package com.alamkanak.weekview
33
import android.graphics.Canvas
44
import android.graphics.Paint
55
import android.graphics.RectF
6+
import android.text.StaticLayout
7+
import androidx.collection.ArrayMap
68
import java.util.Calendar
79
import kotlin.math.max
8-
import kotlin.math.roundToInt
910

1011
internal class CalendarRenderer(
1112
viewState: ViewState,
1213
eventChipsCache: EventChipsCache
1314
) : Renderer {
1415

15-
private val eventsUpdater = SingleEventsUpdater(viewState, eventChipsCache)
16+
private val singleEventLabels = ArrayMap<String, StaticLayout>()
17+
private val eventsUpdater = SingleEventsUpdater(viewState, eventChipsCache, singleEventLabels)
1618

1719
// Be careful when changing the order of the drawers, as that might cause
1820
// views to incorrectly draw over each other
1921
private val drawers = listOf(
2022
DayBackgroundDrawer(viewState),
2123
BackgroundGridDrawer(viewState),
22-
SingleEventsDrawer(viewState, eventChipsCache),
24+
SingleEventsDrawer(viewState, eventChipsCache, singleEventLabels),
2325
NowLineDrawer(viewState)
2426
)
2527

@@ -36,45 +38,73 @@ internal class CalendarRenderer(
3638

3739
private class SingleEventsUpdater(
3840
private val viewState: ViewState,
39-
private val chipsCache: EventChipsCache
41+
private val chipsCache: EventChipsCache,
42+
private val eventLabels: ArrayMap<String, StaticLayout>
4043
) : Updater {
4144

4245
private val boundsCalculator = EventChipBoundsCalculator(viewState)
46+
private val textFitter = TextFitter(viewState)
4347

4448
override fun isRequired() = true
4549

4650
override fun update() {
4751
chipsCache.clearSingleEventsCache()
4852

49-
viewState
50-
.dateRangeWithStartPixels
51-
.forEach { (date, startPixel) ->
52-
// If we use a horizontal margin in the day view, we need to offset the start pixel.
53-
val modifiedStartPixel = when {
54-
viewState.isSingleDay -> startPixel + viewState.eventMarginHorizontal.toFloat()
55-
else -> startPixel
56-
}
57-
calculateRectsForEventsOnDate(date, modifiedStartPixel)
53+
for ((date, startPixel) in viewState.dateRangeWithStartPixels) {
54+
// If we use a horizontal margin in the day view, we need to offset the start pixel.
55+
val modifiedStartPixel = when {
56+
viewState.isSingleDay -> startPixel + viewState.eventMarginHorizontal.toFloat()
57+
else -> startPixel
58+
}
59+
60+
val eventChips = chipsCache.normalEventChipsByDate(date).filter {
61+
it.event.isWithin(viewState.minHour, viewState.maxHour)
5862
}
63+
64+
eventChips.calculateBounds(startPixel = modifiedStartPixel)
65+
eventChips.calculateTextLayouts()
66+
}
5967
}
6068

61-
private fun calculateRectsForEventsOnDate(
62-
date: Calendar,
63-
startPixel: Float
64-
) {
65-
chipsCache.normalEventChipsByDate(date)
66-
.filter { it.event.isNotAllDay && it.event.isWithin(viewState.minHour, viewState.maxHour) }
67-
.forEach {
68-
val chipRect = boundsCalculator.calculateSingleEvent(it, startPixel)
69-
if (chipRect.isValidSingleEventRect) {
70-
it.bounds = chipRect
71-
} else {
72-
it.bounds = null
73-
}
69+
private fun List<EventChip>.calculateBounds(startPixel: Float) {
70+
for (eventChip in this) {
71+
val chipRect = boundsCalculator.calculateSingleEvent(eventChip, startPixel)
72+
if (chipRect.isValid) {
73+
eventChip.bounds.set(chipRect)
74+
} else {
75+
eventChip.bounds.setEmpty()
7476
}
77+
}
7578
}
7679

77-
private val RectF.isValidSingleEventRect: Boolean
80+
private fun List<EventChip>.calculateTextLayouts() {
81+
for (eventChip in this) {
82+
val bounds = eventChip.bounds
83+
val horizontalPadding = viewState.eventPaddingHorizontal * 2
84+
val verticalPadding = viewState.eventPaddingVertical * 2
85+
86+
val chipHeight = (bounds.height() - verticalPadding).toInt()
87+
val chipWidth = (bounds.width() - horizontalPadding).toInt()
88+
if (chipHeight <= 0 || chipWidth <= 0) {
89+
eventChip.bounds.setEmpty()
90+
continue
91+
}
92+
93+
val didAvailableAreaChange = eventChip.didAvailableAreaChange(
94+
area = bounds,
95+
horizontalPadding = horizontalPadding,
96+
verticalPadding = verticalPadding
97+
)
98+
val isCached = eventChip.id in eventLabels
99+
100+
if (didAvailableAreaChange || !isCached) {
101+
eventChip.updateAvailableArea(chipWidth, chipHeight)
102+
eventLabels[eventChip.id] = textFitter.fit(eventChip = eventChip)
103+
}
104+
}
105+
}
106+
107+
private val RectF.isValid: Boolean
78108
get() {
79109
val hasCorrectWidth = left < right && left < viewState.viewWidth
80110
val hasCorrectHeight = top < viewState.viewHeight
@@ -88,8 +118,10 @@ private class DayBackgroundDrawer(
88118
) : Drawer {
89119

90120
override fun draw(canvas: Canvas) {
91-
viewState.dateRangeWithStartPixels.forEach { (date, startPixel) ->
92-
drawDayBackground(date, startPixel, canvas)
121+
canvas.drawInBounds(viewState.calendarGridBounds) {
122+
viewState.dateRangeWithStartPixels.forEach { (date, startPixel) ->
123+
drawDayBackground(date, startPixel, canvas)
124+
}
93125
}
94126
}
95127

@@ -157,100 +189,76 @@ private class BackgroundGridDrawer(
157189
private val viewState: ViewState
158190
) : Drawer {
159191

160-
private lateinit var hourLines: FloatArray
161-
162192
override fun draw(canvas: Canvas) {
163-
viewState.startPixels.forEach { startPixel ->
164-
val startX = max(startPixel, viewState.timeColumnWidth)
165-
drawGrid(startX, startPixel, canvas)
166-
}
167-
}
168-
169-
private fun createHourLines(): FloatArray {
170-
val gridHeight = viewState.viewHeight - viewState.headerHeight
171-
val linesPerDay = (gridHeight / viewState.hourHeight) + 1
172-
val overallLines = linesPerDay.roundToInt() * (viewState.numberOfVisibleDays + 1)
173-
return FloatArray(overallLines * 4) // 4 lines make a cube in the grid
174-
}
175-
176-
private fun drawGrid(startX: Float, startPixel: Float, canvas: Canvas) {
177-
if (viewState.showHourSeparators) {
178-
hourLines = createHourLines()
179-
drawHourLines(startX, startPixel, canvas)
180-
}
193+
canvas.drawInBounds(viewState.calendarGridBounds) {
194+
if (viewState.showHourSeparators) {
195+
drawHourLines()
196+
}
181197

182-
if (viewState.showDaySeparators) {
183-
drawDaySeparators(startPixel, canvas)
198+
if (viewState.showDaySeparators) {
199+
drawDaySeparators()
200+
}
184201
}
185202
}
186203

187-
private fun drawDaySeparators(startPixel: Float, canvas: Canvas) {
188-
val days = viewState.numberOfVisibleDays
189-
val widthPerDay = viewState.totalDayWidth
190-
val top = viewState.headerHeight
204+
private fun Canvas.drawDaySeparators() {
205+
for (startPixel in viewState.startPixels) {
206+
if (viewState.showTimeColumnSeparator && startPixel == viewState.timeColumnWidth) {
207+
continue
208+
}
191209

192-
for (i in 0 until days) {
193-
val start = startPixel + widthPerDay * (i + 1)
194-
canvas.drawLine(start, top, start, top + viewState.viewHeight, viewState.daySeparatorPaint)
210+
drawVerticalLine(
211+
horizontalOffset = startPixel,
212+
startY = viewState.headerHeight,
213+
endY = viewState.headerHeight + viewState.viewHeight,
214+
paint = viewState.daySeparatorPaint
215+
)
195216
}
196217
}
197218

198-
private fun drawHourLines(startX: Float, startPixel: Float, canvas: Canvas) {
199-
val hourStep = viewState.timeColumnHoursInterval
200-
var lineIndex = 0
201-
202-
for (hour in hourStep until viewState.hoursPerDay step hourStep) {
203-
val heightOfHour = (viewState.hourHeight * hour)
204-
val top = viewState.headerHeight + viewState.currentOrigin.y + heightOfHour
205-
206-
val widthPerDay = viewState.totalDayWidth
207-
val separatorWidth = viewState.hourSeparatorPaint.strokeWidth
208-
209-
val isNotHiddenByHeader = top > viewState.headerHeight - separatorWidth
210-
val isWithinVisibleRange = top < viewState.viewHeight
211-
val isVisibleHorizontally = startPixel + widthPerDay - startX > 0
212-
213-
if (isNotHiddenByHeader && isWithinVisibleRange && isVisibleHorizontally) {
214-
hourLines[lineIndex * 4] = startX
215-
hourLines[lineIndex * 4 + 1] = top
216-
hourLines[lineIndex * 4 + 2] = startPixel + widthPerDay
217-
hourLines[lineIndex * 4 + 3] = top
218-
lineIndex++
219+
private fun Canvas.drawHourLines() {
220+
for (hour in viewState.displayedHours) {
221+
for (startPixel in viewState.startPixels) {
222+
drawHourLine(hour, startPixel)
219223
}
220224
}
225+
}
221226

222-
canvas.drawLines(hourLines, viewState.hourSeparatorPaint)
227+
private fun Canvas.drawHourLine(hour: Int, startPixel: Float) {
228+
val heightOfHour = (viewState.hourHeight * hour)
229+
val verticalOffset = viewState.headerHeight + viewState.currentOrigin.y + heightOfHour
230+
231+
drawHorizontalLine(
232+
verticalOffset = verticalOffset,
233+
startX = startPixel,
234+
endX = startPixel + viewState.totalDayWidth,
235+
paint = viewState.hourSeparatorPaint
236+
)
223237
}
224238
}
225239

226240
private class SingleEventsDrawer(
227241
private val viewState: ViewState,
228-
private val chipsCache: EventChipsCache
242+
private val chipsCache: EventChipsCache,
243+
private val eventLabels: ArrayMap<String, StaticLayout>
229244
) : Drawer {
230245

231246
private val eventChipDrawer = EventChipDrawer(viewState)
232247

233248
override fun draw(canvas: Canvas) {
234-
canvas.drawInBounds(
235-
left = viewState.timeColumnWidth,
236-
top = viewState.headerHeight,
237-
right = viewState.viewWidth.toFloat(),
238-
bottom = viewState.viewHeight.toFloat()
239-
) {
249+
canvas.drawInBounds(viewState.calendarGridBounds) {
240250
for (date in viewState.dateRange) {
241-
drawEventsForDate(date, canvas)
251+
drawEventsForDate(date)
242252
}
243253
}
244254
}
245255

246-
private fun drawEventsForDate(
247-
date: Calendar,
248-
canvas: Canvas
249-
) {
250-
chipsCache
251-
.normalEventChipsByDate(date)
252-
.filter { it.bounds != null }
253-
.forEach { eventChipDrawer.draw(it, canvas) }
256+
private fun Canvas.drawEventsForDate(date: Calendar) {
257+
val eventChips = chipsCache.normalEventChipsByDate(date).filterNot { it.bounds.isEmpty }
258+
for (eventChip in eventChips) {
259+
val textLayout = eventLabels[eventChip.id] ?: continue
260+
eventChipDrawer.draw(eventChip, canvas = this, textLayout)
261+
}
254262
}
255263
}
256264

core/src/main/java/com/alamkanak/weekview/CanvasExtensions.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@ package com.alamkanak.weekview
33
import android.graphics.Canvas
44
import android.graphics.Paint
55
import android.graphics.RectF
6+
import android.text.StaticLayout
67

7-
fun Canvas.withTranslation(x: Float, y: Float, block: Canvas.() -> Unit) {
8+
internal fun Canvas.withTranslation(x: Float, y: Float, block: Canvas.() -> Unit) {
89
save()
910
translate(x, y)
1011
block()
1112
restore()
1213
}
1314

14-
fun Canvas.drawVerticalLine(
15+
internal fun Canvas.draw(staticLayout: StaticLayout) {
16+
staticLayout.draw(this)
17+
}
18+
19+
internal fun Canvas.drawVerticalLine(
1520
horizontalOffset: Float,
1621
startY: Float,
1722
endY: Float,
@@ -20,21 +25,30 @@ fun Canvas.drawVerticalLine(
2025
drawLine(horizontalOffset, startY, horizontalOffset, endY, paint)
2126
}
2227

23-
fun Canvas.drawInBounds(
24-
left: Float,
25-
top: Float,
26-
right: Float,
27-
bottom: Float,
28+
internal fun Canvas.drawHorizontalLine(
29+
verticalOffset: Float,
30+
startX: Float,
31+
endX: Float,
32+
paint: Paint
33+
) {
34+
drawLine(startX, verticalOffset, endX, verticalOffset, paint)
35+
}
36+
37+
internal fun Canvas.drawInBounds(
38+
bounds: RectF,
2839
block: Canvas.() -> Unit
2940
) {
3041
save()
31-
clipRect(left, top, right, bottom)
42+
clipRect(bounds)
3243
block()
3344
restore()
3445
}
3546

36-
fun RectF.insetBy(inset: Float): RectF {
47+
internal fun RectF.insetBy(inset: Float): RectF {
3748
return RectF(this).apply {
3849
inset(inset, inset)
3950
}
4051
}
52+
53+
internal val RectF.isNotEmpty: Boolean
54+
get() = !isEmpty

0 commit comments

Comments
 (0)