diff --git a/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt b/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt index 054fc6789..1434665b5 100644 --- a/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt +++ b/core/src/main/java/com/alamkanak/weekview/CalendarRenderer.kt @@ -5,12 +5,13 @@ import android.graphics.Paint import android.graphics.RectF import android.text.StaticLayout import androidx.collection.ArrayMap +import com.alamkanak.weekview.WeekView.DrawBase import java.util.Calendar import kotlin.math.max import kotlin.math.min internal class CalendarRenderer( - viewState: ViewState, + private val viewState: ViewState, eventChipsCacheProvider: EventChipsCacheProvider ) : Renderer { @@ -29,9 +30,21 @@ internal class CalendarRenderer( override fun render(canvas: Canvas) { eventsUpdater.update() + drawAdditional(canvas, DrawBase.CANVAS) for (drawer in drawers) { - drawer.draw(canvas) + drawer.draw(canvas, viewState) + drawAdditional(canvas, drawer.base) } + drawAdditional(canvas, DrawBase.TOP) + } + + private fun drawAdditional(canvas: Canvas, base: DrawBase) { + val drawers = viewState.additionalDrawers[base] + if (drawers.isNullOrEmpty()) return + + val saveCount = canvas.save() + drawers.forEach { it.draw(canvas, viewState) } + canvas.restoreToCount(saveCount) } } @@ -107,9 +120,10 @@ private class SingleEventsUpdater( private class DayBackgroundDrawer( private val viewState: ViewState -) : Drawer { +) : WeekView.Drawer { + override val base = DrawBase.BACKGROUND - override fun draw(canvas: Canvas) { + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { canvas.drawInBounds(viewState.calendarGridBounds) { viewState.dateRangeWithStartPixels.forEach { (date, startPixel) -> drawDayBackground(date, startPixel, canvas) @@ -167,9 +181,10 @@ private class DayBackgroundDrawer( private class BackgroundGridDrawer( private val viewState: ViewState -) : Drawer { +) : WeekView.Drawer { + override val base = DrawBase.GRID - override fun draw(canvas: Canvas) { + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { canvas.drawInBounds(viewState.calendarGridBounds) { if (viewState.showHourSeparators) { drawHourLines() @@ -201,7 +216,7 @@ private class BackgroundGridDrawer( private fun Canvas.drawHourLine(hour: Int) { val heightOfHour = (viewState.hourHeight * (hour - viewState.minHour)) val verticalOffset = viewState.headerHeight + viewState.currentOrigin.y + heightOfHour - val horizontalOffset = if (viewState.isLtr) viewState.timeColumnWidth else 0f + val horizontalOffset = viewState.calendarGridBounds.left drawHorizontalLine( verticalOffset = verticalOffset, @@ -216,11 +231,12 @@ private class SingleEventsDrawer( private val viewState: ViewState, private val chipsCacheProvider: EventChipsCacheProvider, private val eventLabels: ArrayMap -) : Drawer { +) : WeekView.Drawer { + override val base = DrawBase.EVENTS private val eventChipDrawer = EventChipDrawer(viewState) - override fun draw(canvas: Canvas) { + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { canvas.drawInBounds(viewState.calendarGridBounds) { for (date in viewState.dateRange) { drawEventsForDate(date) @@ -241,9 +257,10 @@ private class SingleEventsDrawer( private class NowLineDrawer( private val viewState: ViewState -) : Drawer { +) : WeekView.Drawer { + override val base = DrawBase.NOWLINE - override fun draw(canvas: Canvas) { + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { if (viewState.showNowLine.not()) { return } diff --git a/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt b/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt index 791bc03eb..9208acf7b 100644 --- a/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt +++ b/core/src/main/java/com/alamkanak/weekview/HeaderRenderer.kt @@ -136,7 +136,7 @@ private class HeaderUpdater( private class DateLabelsDrawer( private val viewState: ViewState, private val dateLabelLayouts: SparseArray -) : Drawer { +) : DrawerInternal { override fun draw(canvas: Canvas) { canvas.drawInBounds(viewState.headerBounds) { @@ -240,7 +240,7 @@ private class AllDayEventsUpdater( internal class AllDayEventsDrawer( private val viewState: ViewState, private val allDayEventLayouts: ArrayMap -) : Drawer { +) : DrawerInternal { private val eventChipDrawer = EventChipDrawer(viewState) @@ -319,7 +319,7 @@ internal class AllDayEventsDrawer( private class HeaderDrawer( context: Context, private val viewState: ViewState -) : Drawer { +) : DrawerInternal { private val upArrow: Drawable by lazy { checkNotNull(ContextCompat.getDrawable(context, R.drawable.ic_arrow_up)) diff --git a/core/src/main/java/com/alamkanak/weekview/Renderers.kt b/core/src/main/java/com/alamkanak/weekview/Renderers.kt index f0180e907..98f606f35 100644 --- a/core/src/main/java/com/alamkanak/weekview/Renderers.kt +++ b/core/src/main/java/com/alamkanak/weekview/Renderers.kt @@ -6,7 +6,7 @@ internal interface Updater { fun update() } -internal interface Drawer { +internal interface DrawerInternal { fun draw(canvas: Canvas) } diff --git a/core/src/main/java/com/alamkanak/weekview/ViewState.kt b/core/src/main/java/com/alamkanak/weekview/ViewState.kt index 44e8ab6eb..aa5093840 100644 --- a/core/src/main/java/com/alamkanak/weekview/ViewState.kt +++ b/core/src/main/java/com/alamkanak/weekview/ViewState.kt @@ -8,7 +8,7 @@ import android.graphics.Typeface import android.os.Build import android.text.TextPaint import android.view.View -import java.util.Calendar +import java.util.* import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -16,13 +16,13 @@ import kotlin.math.min typealias DateFormatter = (Calendar) -> String typealias TimeFormatter = (Int) -> String -internal class ViewState { +internal class ViewState : WeekView.DrawBounds { // View - var viewWidth: Int = 0 - var viewHeight: Int = 0 + override var viewWidth: Int = 0 + override var viewHeight: Int = 0 - var isLtr: Boolean = true + override var isLtr: Boolean = true // Calendar state var firstVisibleDate: Calendar = today() @@ -64,7 +64,7 @@ internal class ViewState { var eventMarginVertical: Int = 0 var singleDayHorizontalPadding: Int = 0 - var hourHeight: Float = 0f + override var hourHeight: Float = 0f var minHourHeight: Float = 0f var maxHourHeight: Float = 0f var effectiveMinHourHeight: Float = 0f @@ -95,6 +95,8 @@ internal class ViewState { var typeface: Typeface = Typeface.DEFAULT + var additionalDrawers: Map> = emptyMap() + var timeColumnWidth: Float = 0f var timeColumnTextHeight: Float = 0f @@ -134,13 +136,13 @@ internal class ViewState { // In LTR: Dates in the past have origin.x > 0, dates in the future have origin.x < 0 // In RTL: Dates in the past have origin.x < 0, dates in the future have origin.x > 0 - var currentOrigin = PointF(0f, 0f) + override var currentOrigin = PointF(0f, 0f) val headerBackgroundPaint = Paint() val headerBackgroundWithShadowPaint = Paint() - val dayWidth: Float + override val dayWidth: Float get() = (viewWidth - timeColumnWidth) / numberOfVisibleDays val drawableDayWidth: Float @@ -237,7 +239,7 @@ internal class ViewState { private val _calendarGridBounds: RectF = RectF() - val calendarGridBounds: RectF + override val calendarGridBounds: RectF get() = _calendarGridBounds.apply { left = if (isLtr) timeColumnWidth else 0f top = headerHeight diff --git a/core/src/main/java/com/alamkanak/weekview/WeekView.kt b/core/src/main/java/com/alamkanak/weekview/WeekView.kt index e8211e7c5..a9b542b72 100644 --- a/core/src/main/java/com/alamkanak/weekview/WeekView.kt +++ b/core/src/main/java/com/alamkanak/weekview/WeekView.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration import android.graphics.Canvas +import android.graphics.PointF import android.graphics.RectF import android.graphics.Typeface import android.os.Parcelable @@ -1354,6 +1355,53 @@ class WeekView @JvmOverloads constructor( invalidate() } + /* + *********************************************************************************************** + * + * Custom drawers + * + *********************************************************************************************** + */ + + /** + * Bounds available to [Drawer] to draw on the [Canvas] during rendering. + */ + @PublicApi + interface DrawBounds { + val calendarGridBounds: RectF + val currentOrigin: PointF + val viewWidth: Int + val viewHeight: Int + val dayWidth: Float + val hourHeight: Float + val isLtr: Boolean + } + + @PublicApi + enum class DrawBase { + CANVAS, BACKGROUND, GRID, EVENTS, NOWLINE, TOP; + } + + @PublicApi + interface Drawer { + fun draw(canvas: Canvas, bounds: DrawBounds) + val base: DrawBase + } + + /** + * Additional drawers that will be draw on the view during rendering. + * + * Each drawer will draw on top of the layer specified by [Drawer.base]. Drawers with the same + * base will draw in the order specified in the list. + */ + @PublicApi + var additionalDrawers: List + get() = viewState.additionalDrawers.values.flatten() + set(value) { + viewState.additionalDrawers = value.groupBy { it.base } + invalidate() + } + /* *********************************************************************************************** * diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index b65af9d17..4ebb8ff18 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -78,6 +78,14 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".ui.main.MainActivity" /> + + + diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomDrawersActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomDrawersActivity.kt new file mode 100644 index 000000000..5ec39b050 --- /dev/null +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomDrawersActivity.kt @@ -0,0 +1,115 @@ +package com.alamkanak.weekview.sample.ui + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.alamkanak.weekview.WeekView +import com.alamkanak.weekview.emoji.enableEmojiProcessing +import com.alamkanak.weekview.sample.data.EventsDatabase +import com.alamkanak.weekview.sample.databinding.ActivityBasicBinding +import com.alamkanak.weekview.sample.ui.basic.BasicActivityWeekViewAdapter +import com.alamkanak.weekview.sample.ui.basic.BasicViewModel +import com.alamkanak.weekview.sample.util.setupWithWeekView +import com.alamkanak.weekview.sample.util.showWithRetryAction +import com.google.android.material.snackbar.Snackbar +import kotlin.math.min + +class CustomDrawersActivity : AppCompatActivity() { + + private val binding: ActivityBasicBinding by lazy { + ActivityBasicBinding.inflate(layoutInflater) + } + + private val viewModel: BasicViewModel by lazy { + BasicViewModel(database = EventsDatabase(this)) + } + + private val snackbar: Snackbar by lazy { + Snackbar.make(binding.weekView, "Something went wrong", Snackbar.LENGTH_INDEFINITE) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + val basicAdapter = BasicActivityWeekViewAdapter( + loadMoreHandler = { params -> viewModel.fetchEvents(params) }) + + binding.weekView.apply { + enableEmojiProcessing() + binding.toolbarContainer.toolbar.setupWithWeekView(this) + adapter = basicAdapter + additionalDrawers = listOf( + HalfHourLineDrawer(binding.weekView), + SampleTextDrawer() + ) + } + + viewModel.viewState.observe(this) { viewState -> + if (viewState.error != null) { + val params = viewState.error.loadParams + snackbar.showWithRetryAction { viewModel.retry(params) } + } else { + snackbar.dismiss() + } + + basicAdapter.submitList(viewState.entities) + } + } +} + +class HalfHourLineDrawer(private val view: WeekView) : WeekView.Drawer { + override val base: WeekView.DrawBase = WeekView.DrawBase.GRID + private val paint = Paint().apply { + color = Color.argb(60, 75, 75, 200) + strokeWidth = 2f + pathEffect = DashPathEffect(floatArrayOf(20f, 10f), 0f) + style = Paint.Style.STROKE + } + + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { + val gridBounds = bounds.calendarGridBounds + canvas.clipRect(gridBounds) + for (i in view.minHour..view.maxHour) { + // Draw a line at the 30 min mark between each hour + val y = gridBounds.top + bounds.currentOrigin.y + + ((i - view.minHour + 0.5) * bounds.hourHeight).toInt() + + canvas.drawLine(gridBounds.left, y, gridBounds.right, y, paint) + } + } +} + +class SampleTextDrawer : WeekView.Drawer { + val text = "Sample" + + override val base: WeekView.DrawBase = WeekView.DrawBase.TOP + private val basePaint = Paint().apply { + color = Color.argb(125, 150, 75, 75) + isAntiAlias = true + textSize = 1f + textAlign = Paint.Align.CENTER + } + + override fun draw(canvas: Canvas, bounds: WeekView.DrawBounds) { + val maxSize = min(bounds.viewWidth, bounds.viewHeight) + val paint = Paint(basePaint) + + // Use the largest text size that will fit + while (true) { + val measuredSize = paint.measureText(text) + if (measuredSize > maxSize) { + paint.textSize-- + break + } + paint.textSize++ + } + val centerX = bounds.viewWidth / 2f + val centerY = bounds.viewHeight / 2f + canvas.rotate(45f, centerX, centerY) + canvas.drawText(text, centerX, centerY, paint) + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/alamkanak/weekview/sample/ui/main/MainActivity.kt b/sample/src/main/java/com/alamkanak/weekview/sample/ui/main/MainActivity.kt index e4582e8cc..e7a860d96 100644 --- a/sample/src/main/java/com/alamkanak/weekview/sample/ui/main/MainActivity.kt +++ b/sample/src/main/java/com/alamkanak/weekview/sample/ui/main/MainActivity.kt @@ -6,11 +6,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import com.alamkanak.weekview.sample.R import com.alamkanak.weekview.sample.databinding.ActivityMainBinding -import com.alamkanak.weekview.sample.ui.CustomFontActivity -import com.alamkanak.weekview.sample.ui.LegacyActivity -import com.alamkanak.weekview.sample.ui.LimitedActivity -import com.alamkanak.weekview.sample.ui.StaticActivity -import com.alamkanak.weekview.sample.ui.WithFragmentActivity +import com.alamkanak.weekview.sample.ui.* import com.alamkanak.weekview.sample.ui.async.AsyncActivity import com.alamkanak.weekview.sample.ui.basic.BasicActivity import com.alamkanak.weekview.sample.util.EqualSpacingItemDecoration @@ -47,6 +43,7 @@ class MainActivity : AppCompatActivity() { Sample(R.string.title_activity_custom_font, "Custom font in WeekView\nAll-day events arranged horizontally", CustomFontActivity::class.java), Sample(R.string.title_activity_asynchronous, "Asynchronous events fetching from an API", AsyncActivity::class.java), Sample(R.string.title_activity_with_fragment, "Displays WeekView within a Fragment", WithFragmentActivity::class.java), + Sample(R.string.title_activity_custom_drawers, "Custom drawers on the view", CustomDrawersActivity::class.java), Sample(R.string.title_activity_legacy, "Implemented in Java", LegacyActivity::class.java) ) } diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 2beca81a5..317d871f1 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -15,4 +15,5 @@ Could not download events from the internet From %1$s to %2$s With Fragment + With custom drawers