@@ -3,23 +3,25 @@ package com.alamkanak.weekview
3
3
import android.graphics.Canvas
4
4
import android.graphics.Paint
5
5
import android.graphics.RectF
6
+ import android.text.StaticLayout
7
+ import androidx.collection.ArrayMap
6
8
import java.util.Calendar
7
9
import kotlin.math.max
8
- import kotlin.math.roundToInt
9
10
10
11
internal class CalendarRenderer (
11
12
viewState : ViewState ,
12
13
eventChipsCache : EventChipsCache
13
14
) : Renderer {
14
15
15
- private val eventsUpdater = SingleEventsUpdater (viewState, eventChipsCache)
16
+ private val singleEventLabels = ArrayMap <String , StaticLayout >()
17
+ private val eventsUpdater = SingleEventsUpdater (viewState, eventChipsCache, singleEventLabels)
16
18
17
19
// Be careful when changing the order of the drawers, as that might cause
18
20
// views to incorrectly draw over each other
19
21
private val drawers = listOf (
20
22
DayBackgroundDrawer (viewState),
21
23
BackgroundGridDrawer (viewState),
22
- SingleEventsDrawer (viewState, eventChipsCache),
24
+ SingleEventsDrawer (viewState, eventChipsCache, singleEventLabels ),
23
25
NowLineDrawer (viewState)
24
26
)
25
27
@@ -36,45 +38,73 @@ internal class CalendarRenderer(
36
38
37
39
private class SingleEventsUpdater (
38
40
private val viewState : ViewState ,
39
- private val chipsCache : EventChipsCache
41
+ private val chipsCache : EventChipsCache ,
42
+ private val eventLabels : ArrayMap <String , StaticLayout >
40
43
) : Updater {
41
44
42
45
private val boundsCalculator = EventChipBoundsCalculator (viewState)
46
+ private val textFitter = TextFitter (viewState)
43
47
44
48
override fun isRequired () = true
45
49
46
50
override fun update () {
47
51
chipsCache.clearSingleEventsCache()
48
52
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 )
58
62
}
63
+
64
+ eventChips.calculateBounds(startPixel = modifiedStartPixel)
65
+ eventChips.calculateTextLayouts()
66
+ }
59
67
}
60
68
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()
74
76
}
77
+ }
75
78
}
76
79
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
78
108
get() {
79
109
val hasCorrectWidth = left < right && left < viewState.viewWidth
80
110
val hasCorrectHeight = top < viewState.viewHeight
@@ -88,8 +118,10 @@ private class DayBackgroundDrawer(
88
118
) : Drawer {
89
119
90
120
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
+ }
93
125
}
94
126
}
95
127
@@ -157,100 +189,76 @@ private class BackgroundGridDrawer(
157
189
private val viewState : ViewState
158
190
) : Drawer {
159
191
160
- private lateinit var hourLines: FloatArray
161
-
162
192
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
+ }
181
197
182
- if (viewState.showDaySeparators) {
183
- drawDaySeparators(startPixel, canvas)
198
+ if (viewState.showDaySeparators) {
199
+ drawDaySeparators()
200
+ }
184
201
}
185
202
}
186
203
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
+ }
191
209
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
+ )
195
216
}
196
217
}
197
218
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)
219
223
}
220
224
}
225
+ }
221
226
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
+ )
223
237
}
224
238
}
225
239
226
240
private class SingleEventsDrawer (
227
241
private val viewState : ViewState ,
228
- private val chipsCache : EventChipsCache
242
+ private val chipsCache : EventChipsCache ,
243
+ private val eventLabels : ArrayMap <String , StaticLayout >
229
244
) : Drawer {
230
245
231
246
private val eventChipDrawer = EventChipDrawer (viewState)
232
247
233
248
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) {
240
250
for (date in viewState.dateRange) {
241
- drawEventsForDate(date, canvas )
251
+ drawEventsForDate(date)
242
252
}
243
253
}
244
254
}
245
255
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
+ }
254
262
}
255
263
}
256
264
0 commit comments