From 0990d4855a39a548fadfdc6998774d3040637b46 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Sat, 28 Nov 2020 12:53:39 +0900 Subject: [PATCH 1/2] Add support for additional drawers Additional drawers allow users to customize the appearance of the calendar by drawing additional items on any layer (background, on top of the grid, on top of everything, etc). This allows supporting customization use-cases that are too uncommon to implement and support in the base library. --- .../alamkanak/weekview/CalendarRenderer.kt | 39 ++++++++++----- .../com/alamkanak/weekview/HeaderRenderer.kt | 6 +-- .../java/com/alamkanak/weekview/Renderers.kt | 2 +- .../java/com/alamkanak/weekview/ViewState.kt | 20 ++++---- .../java/com/alamkanak/weekview/WeekView.kt | 48 +++++++++++++++++++ 5 files changed, 91 insertions(+), 24 deletions(-) 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() + } + /* *********************************************************************************************** * From 343619e9f637d29fe0c74267cd689c7e49787373 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Sat, 9 Jan 2021 23:00:28 +0900 Subject: [PATCH 2/2] Add sample activity for custom drawers The sample activity demonstrates a drawer on top of the grid that draws dashed lines at the 30 minute marks for each hour, and a drawer on top of the whole view that draws a big "Sample" text. --- sample/src/main/AndroidManifest.xml | 8 ++ .../sample/ui/CustomDrawersActivity.kt | 115 ++++++++++++++++++ .../weekview/sample/ui/main/MainActivity.kt | 7 +- sample/src/main/res/values/strings.xml | 1 + 4 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 sample/src/main/java/com/alamkanak/weekview/sample/ui/CustomDrawersActivity.kt 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