From 8724cf71ee79f6f6aae4b4b74e86658d73891a8c Mon Sep 17 00:00:00 2001 From: Patrick Michalik <120058021+patrickmichalik@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:40:23 +0200 Subject: [PATCH] Add `LineFill` and `AreaFill` to `LineCartesianLayer`, and make `DynamicShader`-related updates (WIP) Co-authored-by: Patryk Goworowski --- .../vico/sample/showcase/charts/Chart1.kt | 5 +- .../vico/sample/showcase/charts/Chart3.kt | 7 +- .../vico/sample/showcase/charts/Chart4.kt | 5 +- .../vico/sample/showcase/charts/Chart7.kt | 8 +- .../vico/sample/showcase/charts/Chart8.kt | 7 +- .../vico/sample/showcase/charts/Chart9.kt | 141 +++++---- .../cartesian/layer/LineCartesianLayer.kt | 79 +---- .../vico/compose/common/Fill.kt | 28 ++ .../compose/common/shader/DynamicShader.kt | 4 - .../core/cartesian/CartesianDrawContext.kt | 4 +- .../data/LineCartesianLayerDrawingModel.kt | 25 +- .../vico/core/cartesian/layer/AreaFills.kt | 203 +++++++++++++ .../{ => layer}/CubicPointConnector.kt | 4 +- .../cartesian/layer/LineCartesianLayer.kt | 279 +++++++++--------- .../vico/core/cartesian/layer/LineFills.kt | 102 +++++++ .../vico/core/common/DrawContext.kt | 8 +- .../patrykandpatrick/vico/core/common/Fill.kt | 48 +++ .../vico/core/common/data/CacheStore.kt | 29 +- .../core/common/shader/BaseDynamicShader.kt | 51 ---- .../common/shader/CacheableDynamicShader.kt | 2 +- .../vico/core/common/shader/ColorShader.kt | 52 ---- .../vico/core/common/shader/DynamicShader.kt | 26 +- .../vico/core/common/shader/StaticShader.kt | 40 --- .../core/common/shader/TopBottomShader.kt | 98 ------ .../vico/views/common/theme/ComponentStyle.kt | 17 +- 25 files changed, 695 insertions(+), 577 deletions(-) create mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/Fill.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/AreaFills.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/{ => layer}/CubicPointConnector.kt (91%) create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineFills.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Fill.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/BaseDynamicShader.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/ColorShader.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/StaticShader.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/TopBottomShader.kt diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart1.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart1.kt index 62f777043..cd47ec5e7 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart1.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart1.kt @@ -30,12 +30,11 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.data.rememberExtraLambda -import com.patrykandpatrick.vico.compose.common.shader.color +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.databinding.Chart1Binding import com.patrykandpatrick.vico.sample.showcase.UIFramework import com.patrykandpatrick.vico.sample.showcase.rememberMarker @@ -69,7 +68,7 @@ private fun ComposeChart1(modelProducer: CartesianChartModelProducer, modifier: rememberCartesianChart( rememberLineCartesianLayer( LineCartesianLayer.LineProvider.series( - rememberLine(DynamicShader.color(Color(0xffa485e0))) + rememberLine(remember { LineCartesianLayer.LineFill.single(fill(Color(0xffa485e0))) }) ) ), startAxis = rememberStartAxis(), diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt index b5a662aa6..91b9e778a 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt @@ -36,8 +36,8 @@ import com.patrykandpatrick.vico.compose.cartesian.rememberFadingEdges import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.of -import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider @@ -46,7 +46,6 @@ import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.common.Dimensions -import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.databinding.Chart3Binding import com.patrykandpatrick.vico.sample.showcase.Defaults @@ -86,7 +85,9 @@ private fun ComposeChart3(modelProducer: CartesianChartModelProducer, modifier: rememberCartesianChart( rememberLineCartesianLayer( lineProvider = - LineCartesianLayer.LineProvider.series(rememberLine(DynamicShader.color(lineColor))), + LineCartesianLayer.LineProvider.series( + rememberLine(remember { LineCartesianLayer.LineFill.single(fill(lineColor)) }) + ), axisValueOverrider = axisValueOverrider, ), startAxis = diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt index 89f1ecf24..fdc04ddb5 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart4.kt @@ -32,14 +32,13 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent -import com.patrykandpatrick.vico.compose.common.shader.color +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.shape.rounded import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.databinding.Chart4Binding import com.patrykandpatrick.vico.sample.showcase.Defaults @@ -100,7 +99,7 @@ private fun ComposeChart4(modelProducer: CartesianChartModelProducer, modifier: rememberLineCartesianLayer( LineCartesianLayer.LineProvider.series( rememberLine( - shader = DynamicShader.color(lineColor), + fill = remember { LineCartesianLayer.LineFill.single(fill(lineColor)) }, pointConnector = remember { LineCartesianLayer.PointConnector.cubic(curvature = 0f) }, ) ) diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart7.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart7.kt index 84c564a8b..cc9b75c85 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart7.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart7.kt @@ -35,10 +35,10 @@ import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.of import com.patrykandpatrick.vico.compose.common.rememberLegendItem import com.patrykandpatrick.vico.compose.common.rememberVerticalLegend -import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.compose.common.shape.rounded import com.patrykandpatrick.vico.compose.common.vicoTheme import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext @@ -48,7 +48,6 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.common.Dimensions -import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.databinding.Chart7Binding import com.patrykandpatrick.vico.sample.showcase.Defaults @@ -99,7 +98,10 @@ private fun ComposeChart7(modelProducer: CartesianChartModelProducer, modifier: rememberLineCartesianLayer( LineCartesianLayer.LineProvider.series( chartColors.map { color -> - rememberLine(shader = DynamicShader.color(color), backgroundShader = null) + rememberLine( + fill = remember { LineCartesianLayer.LineFill.single(fill(color)) }, + areaFill = null, + ) } ) ), diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt index b7712b5c4..342f1a68e 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart8.kt @@ -33,7 +33,7 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent -import com.patrykandpatrick.vico.compose.common.shader.color +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer @@ -41,7 +41,6 @@ import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.databinding.Chart8Binding import com.patrykandpatrick.vico.sample.showcase.Defaults @@ -104,7 +103,9 @@ private fun ComposeChart8(modelProducer: CartesianChartModelProducer, modifier: ), rememberLineCartesianLayer( lineProvider = - LineCartesianLayer.LineProvider.series(rememberLine(DynamicShader.color(color4))), + LineCartesianLayer.LineProvider.series( + rememberLine(remember { LineCartesianLayer.LineFill.single(fill(color4)) }) + ), verticalAxisPosition = AxisPosition.Vertical.End, ), startAxis = rememberStartAxis(guideline = null), diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart9.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart9.kt index ac8a0869a..eedf76c06 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart9.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart9.kt @@ -39,8 +39,9 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent +import com.patrykandpatrick.vico.compose.common.component.shapeComponent +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.of -import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.compose.common.shader.component import com.patrykandpatrick.vico.compose.common.shader.verticalGradient import com.patrykandpatrick.vico.compose.common.shape.dashed @@ -52,9 +53,10 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.Fill import com.patrykandpatrick.vico.core.common.component.ShapeComponent +import com.patrykandpatrick.vico.core.common.shader.ComponentShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.patrykandpatrick.vico.core.common.shader.TopBottomShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.databinding.Chart9Binding import com.patrykandpatrick.vico.sample.showcase.Defaults @@ -99,38 +101,46 @@ private fun ComposeChart9(modelProducer: CartesianChartModelProducer, modifier: lineProvider = LineCartesianLayer.LineProvider.series( rememberLine( - shader = - TopBottomShader(DynamicShader.color(colors[0]), DynamicShader.color(colors[1])), - backgroundShader = - TopBottomShader( - DynamicShader.compose( - DynamicShader.component( - componentSize = 6.dp, - component = - rememberShapeComponent( - color = colors[0], - shape = Shape.Pill, - margins = Dimensions.of(1.dp), + fill = + remember(colors) { + LineCartesianLayer.LineFill.double(fill(colors[0]), fill(colors[1])) + }, + areaFill = + remember(colors) { + LineCartesianLayer.AreaFill.double( + fill( + DynamicShader.compose( + DynamicShader.component( + component = + shapeComponent( + color = colors[0], + shape = Shape.Pill, + margins = Dimensions.of(1.dp), + ), + componentSize = 6.dp, ), + DynamicShader.verticalGradient(arrayOf(Color.Black, Color.Transparent)), + PorterDuff.Mode.DST_IN, + ) ), - DynamicShader.verticalGradient(arrayOf(Color.Black, Color.Transparent)), - PorterDuff.Mode.DST_IN, - ), - DynamicShader.compose( - DynamicShader.component( - componentSize = 5.dp, - component = - rememberShapeComponent( - color = colors[1], - shape = Shape.Rectangle, - margins = Dimensions.of(horizontal = 2.dp), + fill( + DynamicShader.compose( + DynamicShader.component( + component = + shapeComponent( + color = colors[1], + shape = Shape.Rectangle, + margins = Dimensions.of(horizontal = 2.dp), + ), + componentSize = 5.dp, + checkeredArrangement = false, ), - checkeredArrangement = false, + DynamicShader.verticalGradient(arrayOf(Color.Transparent, Color.Black)), + PorterDuff.Mode.DST_IN, + ) ), - DynamicShader.verticalGradient(arrayOf(Color.Transparent, Color.Black)), - PorterDuff.Mode.DST_IN, - ), - ), + ) + }, ) ) ), @@ -190,36 +200,49 @@ private fun ViewChart9(modelProducer: CartesianChartModelProducer, modifier: Mod lineProvider = LineCartesianLayer.LineProvider.series( LineCartesianLayer.Line( - shader = - TopBottomShader(DynamicShader.color(colors[0]), DynamicShader.color(colors[1])), - backgroundShader = - TopBottomShader( - DynamicShader.compose( - DynamicShader.component( - componentSize = 6.dp, - component = - ShapeComponent( - color = colors[0].toArgb(), - shape = Shape.Pill, - margins = Dimensions.of(1.dp), - ), - ), - DynamicShader.verticalGradient(arrayOf(Color.Black, Color.Transparent)), - PorterDuff.Mode.DST_IN, + fill = + LineCartesianLayer.LineFill.double( + Fill(colors[0].toArgb()), + Fill(colors[1].toArgb()), + ), + areaFill = + LineCartesianLayer.AreaFill.double( + Fill( + DynamicShader.compose( + ComponentShader( + component = + ShapeComponent( + color = colors[0].toArgb(), + shape = Shape.Pill, + margins = Dimensions(allDp = 1f), + ), + componentSizeDp = 6f, + ), + DynamicShader.verticalGradient( + android.graphics.Color.BLACK, + android.graphics.Color.TRANSPARENT, + ), + PorterDuff.Mode.DST_IN, + ) ), - DynamicShader.compose( - DynamicShader.component( - componentSize = 5.dp, - component = - ShapeComponent( - color = colors[1].toArgb(), - shape = Shape.Rectangle, - margins = Dimensions.of(horizontal = 2.dp), - ), - checkeredArrangement = false, - ), - DynamicShader.verticalGradient(arrayOf(Color.Transparent, Color.Black)), - PorterDuff.Mode.DST_IN, + Fill( + DynamicShader.compose( + ComponentShader( + component = + ShapeComponent( + color = colors[1].toArgb(), + shape = Shape.Rectangle, + margins = Dimensions(horizontalDp = 2f, verticalDp = 0f), + ), + componentSizeDp = 5f, + checkeredArrangement = false, + ), + DynamicShader.verticalGradient( + android.graphics.Color.TRANSPARENT, + android.graphics.Color.BLACK, + ), + PorterDuff.Mode.DST_IN, + ) ), ), ) diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/layer/LineCartesianLayer.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/layer/LineCartesianLayer.kt index d55aa8d85..eebf4c9a3 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/layer/LineCartesianLayer.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/layer/LineCartesianLayer.kt @@ -19,36 +19,32 @@ package com.patrykandpatrick.vico.compose.cartesian.layer import android.graphics.Paint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.common.shader.color -import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader +import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.vicoTheme import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerDrawingModel import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.DefaultAlpha +import com.patrykandpatrick.vico.core.cartesian.layer.getDefaultAreaFill import com.patrykandpatrick.vico.core.common.Defaults import com.patrykandpatrick.vico.core.common.VerticalPosition import com.patrykandpatrick.vico.core.common.component.Component import com.patrykandpatrick.vico.core.common.component.TextComponent import com.patrykandpatrick.vico.core.common.data.DefaultDrawingModelInterpolator import com.patrykandpatrick.vico.core.common.data.DrawingModelInterpolator -import com.patrykandpatrick.vico.core.common.shader.ColorShader -import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.patrykandpatrick.vico.core.common.shader.TopBottomShader /** Creates and remembers a [LineCartesianLayer]. */ @Composable public fun rememberLineCartesianLayer( lineProvider: LineCartesianLayer.LineProvider = LineCartesianLayer.LineProvider.series( - vicoTheme.lineCartesianLayerColors.map { rememberLine(DynamicShader.color(it)) } + vicoTheme.lineCartesianLayerColors.map { color -> + rememberLine(LineCartesianLayer.LineFill.single(fill(color))) + } ), pointSpacing: Dp = Defaults.POINT_SPACING.dp, axisValueOverrider: AxisValueOverrider = remember { AxisValueOverrider.auto() }, @@ -74,9 +70,12 @@ public fun rememberLineCartesianLayer( /** Creates and remembers a [LineCartesianLayer.Line]. */ @Composable public fun rememberLine( - shader: DynamicShader = DynamicShader.color(vicoTheme.lineCartesianLayerColors.first()), + fill: LineCartesianLayer.LineFill = + vicoTheme.lineCartesianLayerColors.first().let { color -> + remember(color) { LineCartesianLayer.LineFill.single(fill(color)) } + }, thickness: Dp = Defaults.LINE_SPEC_THICKNESS_DP.dp, - backgroundShader: DynamicShader? = shader.getDefaultBackgroundShader(), + areaFill: LineCartesianLayer.AreaFill? = remember(fill) { fill.getDefaultAreaFill() }, cap: StrokeCap = StrokeCap.Round, pointProvider: LineCartesianLayer.PointProvider? = null, pointConnector: LineCartesianLayer.PointConnector = remember { @@ -88,9 +87,9 @@ public fun rememberLine( dataLabelRotationDegrees: Float = 0f, ): LineCartesianLayer.Line = remember( - shader, + fill, thickness, - backgroundShader, + areaFill, cap, pointProvider, pointConnector, @@ -100,9 +99,9 @@ public fun rememberLine( dataLabelRotationDegrees, ) { LineCartesianLayer.Line( - shader, + fill, thickness.value, - backgroundShader, + areaFill, cap.paintCap, pointProvider, pointConnector, @@ -121,56 +120,6 @@ public fun rememberPoint( ): LineCartesianLayer.Point = remember(component, size) { LineCartesianLayer.Point(component, size.value) } -private fun DynamicShader.getDefaultBackgroundShader(): DynamicShader? = - when (this) { - is ColorShader -> - TopBottomShader( - topShader = - Brush.verticalGradient( - listOf( - Color(color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - Color(color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - ) - ) - .toDynamicShader(), - bottomShader = - Brush.verticalGradient( - listOf( - Color(color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - Color(color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - ) - ) - .toDynamicShader(), - ) - is TopBottomShader -> { - val topShader = topShader - val bottomShader = bottomShader - if (topShader is ColorShader && bottomShader is ColorShader) { - TopBottomShader( - topShader = - Brush.verticalGradient( - listOf( - Color(topShader.color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - Color(topShader.color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - ) - ) - .toDynamicShader(), - bottomShader = - Brush.verticalGradient( - listOf( - Color(bottomShader.color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END), - Color(bottomShader.color).copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - ) - ) - .toDynamicShader(), - ) - } else { - null - } - } - else -> null - } - private val StrokeCap.paintCap: Paint.Cap get() = when (this) { diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/Fill.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/Fill.kt new file mode 100644 index 000000000..9ef516965 --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/Fill.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.compose.common + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.patrykandpatrick.vico.core.common.Fill +import com.patrykandpatrick.vico.core.common.shader.DynamicShader + +/** Creates a [Color][Fill]. */ +public fun fill(color: Color): Fill = Fill(color.toArgb()) + +/** Creates a [DynamicShader][Fill]. */ +public fun fill(shader: DynamicShader): Fill = Fill(shader) diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/shader/DynamicShader.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/shader/DynamicShader.kt index c0ccc0477..fb61b8754 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/shader/DynamicShader.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/shader/DynamicShader.kt @@ -29,7 +29,6 @@ import androidx.core.graphics.translationMatrix import com.patrykandpatrick.vico.core.common.DrawContext import com.patrykandpatrick.vico.core.common.component.Component import com.patrykandpatrick.vico.core.common.shader.CacheableDynamicShader -import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.patrykandpatrick.vico.core.common.shader.ComponentShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shader.LinearGradientShader @@ -58,9 +57,6 @@ public fun DynamicShader.Companion.component( tileYMode = tileYMode, ) -/** Creates a [ColorShader]. */ -public fun DynamicShader.Companion.color(color: Color): ColorShader = ColorShader(color.toArgb()) - /** * Creates a [DynamicShader] with a horizontal gradient. [colors] houses the gradient colors, and * [positions] specifies the color offsets (between 0 and 1), with `null` producing an even diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianDrawContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianDrawContext.kt index 308311f4d..131d22eea 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianDrawContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianDrawContext.kt @@ -86,10 +86,10 @@ public fun CartesianDrawContext( override val zoom: Float = zoom - override fun withOtherCanvas(canvas: Canvas, block: (DrawContext) -> Unit) { + override fun withOtherCanvas(canvas: Canvas, block: () -> Unit) { val originalCanvas = this.canvas this.canvas = canvas - block(this) + block() this.canvas = originalCanvas } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/data/LineCartesianLayerDrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/data/LineCartesianLayerDrawingModel.kt index 2a86727b7..bcdc0b59b 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/data/LineCartesianLayerDrawingModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/data/LineCartesianLayerDrawingModel.kt @@ -21,43 +21,28 @@ import com.patrykandpatrick.vico.core.common.data.DrawingModel import com.patrykandpatrick.vico.core.common.lerp import com.patrykandpatrick.vico.core.common.orZero -/** - * Houses drawing information for a [LineCartesianLayer]. [opacity] is the lines’ opacity. [zeroY], - * restricted to the interval [[0, 1]], specifies the position of the zero line (_y_ = 0) from the - * top of the [LineCartesianLayer] as a fraction of the [LineCartesianLayer]’s height. - */ +/** Houses [LineCartesianLayer] drawing information. [opacity] is the lines’ opacity. */ public class LineCartesianLayerDrawingModel( private val pointInfo: List>, - public val zeroY: Float, public val opacity: Float = 1f, ) : DrawingModel(pointInfo) { override fun transform( drawingInfo: List>, from: DrawingModel?, fraction: Float, - ): DrawingModel { - val oldOpacity = (from as LineCartesianLayerDrawingModel?)?.opacity.orZero - val oldZeroY = from?.zeroY ?: zeroY - return LineCartesianLayerDrawingModel( + ): DrawingModel = + LineCartesianLayerDrawingModel( drawingInfo, - oldZeroY.lerp(zeroY, fraction), - oldOpacity.lerp(opacity, fraction), + (from as LineCartesianLayerDrawingModel?)?.opacity.orZero.lerp(opacity, fraction), ) - } override fun equals(other: Any?): Boolean = this === other || other is LineCartesianLayerDrawingModel && pointInfo == other.pointInfo && - zeroY == other.zeroY && opacity == other.opacity - override fun hashCode(): Int { - var result = pointInfo.hashCode() - result = 31 * result + zeroY.hashCode() - result = 31 * result + opacity.hashCode() - return result - } + override fun hashCode(): Int = 31 * pointInfo.hashCode() + opacity.hashCode() /** * Houses positional information for a [LineCartesianLayer]’s point. [y] expresses the distance of diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/AreaFills.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/AreaFills.kt new file mode 100644 index 000000000..bec7742f7 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/AreaFills.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2024 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.cartesian.layer + +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import androidx.annotation.RestrictTo +import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext +import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition +import com.patrykandpatrick.vico.core.common.DefaultAlpha +import com.patrykandpatrick.vico.core.common.Fill +import com.patrykandpatrick.vico.core.common.copyColor +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import com.patrykandpatrick.vico.core.common.getEnd +import com.patrykandpatrick.vico.core.common.getStart +import com.patrykandpatrick.vico.core.common.shader.DynamicShader +import com.patrykandpatrick.vico.core.common.withOpacity + +internal abstract class BaseAreaFill(open val splitY: (ExtraStore) -> Double) : + LineCartesianLayer.AreaFill { + private val areaBounds = RectF() + private val areaPath = Path() + private val clipPath = Path() + private val fillBounds = RectF() + + open fun reset() {} + + abstract fun onTopAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) + + abstract fun onBottomAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) + + open fun onAreasCreated(context: CartesianDrawContext, opacity: Float) {} + + override fun draw( + context: CartesianDrawContext, + linePath: Path, + halfLineThickness: Float, + opacity: Float, + verticalAxisPosition: AxisPosition.Vertical?, + ) { + reset() + linePath.computeBounds(areaBounds, false) + with(context) { + val canvasSplitY = getCanvasSplitY(splitY, halfLineThickness, verticalAxisPosition) + if (canvasSplitY > layerBounds.top) { + clipPath.rewind() + fillBounds.set(layerBounds.left, layerBounds.top, layerBounds.right, canvasSplitY) + clipPath.addRect(fillBounds, Path.Direction.CW) + with(areaPath) { + set(linePath) + lineTo(areaBounds.getEnd(isLtr), layerBounds.bottom) + lineTo(areaBounds.getStart(isLtr), layerBounds.bottom) + close() + op(clipPath, Path.Op.INTERSECT) + } + onTopAreasCreated(this, areaPath, fillBounds, opacity) + } + if (canvasSplitY < layerBounds.bottom) { + clipPath.rewind() + fillBounds.set(layerBounds.left, canvasSplitY, layerBounds.right, layerBounds.bottom) + clipPath.addRect(fillBounds, Path.Direction.CW) + with(areaPath) { + set(linePath) + lineTo(areaBounds.getEnd(isLtr), layerBounds.top) + lineTo(areaBounds.getStart(isLtr), layerBounds.top) + close() + op(clipPath, Path.Op.INTERSECT) + } + onBottomAreasCreated(this, areaPath, fillBounds, opacity) + } + onAreasCreated(this, opacity) + } + } +} + +internal data class SingleAreaFill( + private val fill: Fill, + override val splitY: (ExtraStore) -> Double, +) : BaseAreaFill(splitY) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val areaPath = Path() + + override fun reset() { + areaPath.rewind() + } + + override fun onTopAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) { + areaPath.addPath(path) + } + + override fun onBottomAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) { + areaPath.addPath(path) + } + + override fun onAreasCreated(context: CartesianDrawContext, opacity: Float) { + with(context) { + paint.color = fill.color + paint.shader = fill.shader?.provideShader(this, layerBounds) + paint.withOpacity(opacity) { canvas.drawPath(areaPath, it) } + } + } +} + +internal data class DoubleAreaFill( + private val topFill: Fill, + private val bottomFill: Fill, + override val splitY: (ExtraStore) -> Double, +) : BaseAreaFill(splitY) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + override fun onTopAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) { + with(context) { + paint.color = topFill.color + paint.shader = topFill.shader?.provideShader(this, fillBounds) + paint.withOpacity(opacity) { canvas.drawPath(path, it) } + } + } + + override fun onBottomAreasCreated( + context: CartesianDrawContext, + path: Path, + fillBounds: RectF, + opacity: Float, + ) { + with(context) { + paint.color = bottomFill.color + paint.shader = bottomFill.shader?.provideShader(this, fillBounds) + paint.withOpacity(opacity) { canvas.drawPath(path, it) } + } + } +} + +private fun LineCartesianLayer.AreaFill.Companion.default( + topColor: Int, + bottomColor: Int, + splitY: (ExtraStore) -> Double = { 0.0 }, +) = + double( + Fill( + DynamicShader.verticalGradient( + topColor.copyColor(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + topColor.copyColor(DefaultAlpha.LINE_BACKGROUND_SHADER_END), + ) + ), + Fill( + DynamicShader.verticalGradient( + bottomColor.copyColor(DefaultAlpha.LINE_BACKGROUND_SHADER_END), + bottomColor.copyColor(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + ) + ), + splitY, + ) + +/** @suppress */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public fun LineCartesianLayer.LineFill.getDefaultAreaFill(): LineCartesianLayer.AreaFill? = + when { + this is SingleLineFill && fill.shader == null -> + LineCartesianLayer.AreaFill.default(fill.color, fill.color) + this is DoubleLineFill && topFill.shader == null && bottomFill.shader == null -> + LineCartesianLayer.AreaFill.default(topFill.color, bottomFill.color, splitY) + else -> null + } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CubicPointConnector.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CubicPointConnector.kt similarity index 91% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CubicPointConnector.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CubicPointConnector.kt index 6d56dd674..a16ebda34 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CubicPointConnector.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CubicPointConnector.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.patrykandpatrick.vico.core.cartesian +package com.patrykandpatrick.vico.core.cartesian.layer import android.graphics.Path -import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext import kotlin.math.abs internal data class CubicPointConnector(private val curvature: Float) : diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt index 986463223..f79a66563 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt @@ -16,12 +16,14 @@ package com.patrykandpatrick.vico.core.cartesian.layer +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color import android.graphics.Paint import android.graphics.Path -import android.graphics.RectF +import android.graphics.PorterDuff import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext -import com.patrykandpatrick.vico.core.cartesian.CubicPointConnector import com.patrykandpatrick.vico.core.cartesian.HorizontalDimensions import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Insets @@ -40,36 +42,36 @@ import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerT import com.patrykandpatrick.vico.core.cartesian.marker.MutableLineCartesianLayerMarkerTarget import com.patrykandpatrick.vico.core.common.Defaults import com.patrykandpatrick.vico.core.common.DrawContext +import com.patrykandpatrick.vico.core.common.Fill import com.patrykandpatrick.vico.core.common.Point import com.patrykandpatrick.vico.core.common.VerticalPosition import com.patrykandpatrick.vico.core.common.component.Component import com.patrykandpatrick.vico.core.common.component.TextComponent +import com.patrykandpatrick.vico.core.common.data.CacheStore import com.patrykandpatrick.vico.core.common.data.DefaultDrawingModelInterpolator import com.patrykandpatrick.vico.core.common.data.DrawingModelInterpolator import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.MutableExtraStore import com.patrykandpatrick.vico.core.common.doubled -import com.patrykandpatrick.vico.core.common.getEnd import com.patrykandpatrick.vico.core.common.getRepeating import com.patrykandpatrick.vico.core.common.getStart import com.patrykandpatrick.vico.core.common.half import com.patrykandpatrick.vico.core.common.inBounds import com.patrykandpatrick.vico.core.common.orZero -import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.patrykandpatrick.vico.core.common.shader.TopBottomShader import com.patrykandpatrick.vico.core.common.withOpacity +import kotlin.math.ceil import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt /** - * [LineCartesianLayer] displays data as a continuous line. + * Draws the content of line charts. * - * @param lineProvider provides the [Line]s. - * @param pointSpacingDp the point spacing (in dp). - * @param verticalAxisPosition the position of the [VerticalAxis] with which the + * @property lineProvider provides the [Line]s. + * @property pointSpacingDp the point spacing (in dp). + * @property verticalAxisPosition the position of the [VerticalAxis] with which the * [LineCartesianLayer] should be associated. Use this for independent [CartesianLayer] scaling. - * @param drawingModelInterpolator interpolates the [LineCartesianLayer]’s - * [LineCartesianLayerDrawingModel]s. + * @property drawingModelInterpolator interpolates the [LineCartesianLayerDrawingModel]s. */ public open class LineCartesianLayer( public var lineProvider: LineProvider, @@ -85,116 +87,115 @@ public open class LineCartesianLayer( /** * Defines the appearance of a line in a line chart. * - * @param shader the [DynamicShader] for the line. - * @param thicknessDp the thickness of the line (in dp). - * @param backgroundShader an optional [DynamicShader] to use for the areas bounded by the - * [LineCartesianLayer] line and the zero line (_y_ = 0). - * @param cap the stroke cap for the line. - * @param pointProvider provides the [Point]s. - * @param dataLabel an optional [TextComponent] to use for data labels. - * @param dataLabelVerticalPosition the vertical position of data labels relative to the line. - * @param dataLabelValueFormatter the [CartesianValueFormatter] to use for data labels. - * @param dataLabelRotationDegrees the rotation of data labels (in degrees). - * @param pointConnector the [PointConnector] for the line. + * @param cap the stroke cap. + * @property fill draws the line fill. + * @property thicknessDp the line thickness (in dp). + * @property areaFill draws the area fill. + * @property pointProvider provides the [Point]s. + * @property dataLabel used for the data labels. + * @property dataLabelVerticalPosition the vertical position of the data labels relative to the + * points. + * @property dataLabelValueFormatter formats the data-label values. + * @property dataLabelRotationDegrees the data-label rotation (in degrees). + * @property pointConnector connects the line’s points, thus defining its shape. */ public open class Line( - public var shader: DynamicShader, - public var thicknessDp: Float = Defaults.LINE_SPEC_THICKNESS_DP, - public var backgroundShader: DynamicShader? = null, + protected val fill: LineFill, + public val thicknessDp: Float = Defaults.LINE_SPEC_THICKNESS_DP, + protected val areaFill: AreaFill? = fill.getDefaultAreaFill(), cap: Paint.Cap = Paint.Cap.ROUND, - public var pointProvider: PointProvider? = null, - public var pointConnector: PointConnector = PointConnector.cubic(), - public var dataLabel: TextComponent? = null, - public var dataLabelVerticalPosition: VerticalPosition = VerticalPosition.Top, - public var dataLabelValueFormatter: CartesianValueFormatter = CartesianValueFormatter.decimal(), - public var dataLabelRotationDegrees: Float = 0f, + public val pointProvider: PointProvider? = null, + public val pointConnector: PointConnector = PointConnector.cubic(), + public val dataLabel: TextComponent? = null, + public val dataLabelVerticalPosition: VerticalPosition = VerticalPosition.Top, + public val dataLabelValueFormatter: CartesianValueFormatter = CartesianValueFormatter.decimal(), + public val dataLabelRotationDegrees: Float = 0f, ) { - /** Returns `true` if the [backgroundShader] is not null, and `false` otherwise. */ - public val hasBackgroundShader: Boolean - get() = backgroundShader != null - protected val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeCap = cap } - protected val lineBackgroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) - - protected val lineBackgroundPath: Path = Path() - - protected val clipPath: Path = Path() - - protected val pathBounds: RectF = RectF() - - /** The stroke cap for the line. */ + /** The stroke cap. */ public var cap: Paint.Cap by linePaint::strokeCap /** Draws the line. */ - public fun drawLine( - context: DrawContext, - bounds: RectF, - zeroLineYFraction: Float, + public fun draw( + context: CartesianDrawContext, path: Path, - opacity: Float = 1f, + fillCanvas: Canvas, + opacity: Float, + verticalAxisPosition: AxisPosition.Vertical?, ) { with(context) { - linePaint.strokeWidth = thicknessDp.pixels - setSplitY(zeroLineYFraction) - linePaint.shader = shader.provideShader(context, bounds) - linePaint.withOpacity(opacity) { canvas.drawPath(path, it) } + val thickness = thicknessDp.pixels + linePaint.strokeWidth = thickness + val halfThickness = thickness.half + areaFill?.draw(context, path, halfThickness, opacity, verticalAxisPosition) + linePaint.withOpacity(opacity) { fillCanvas.drawPath(path, it) } + withOtherCanvas(fillCanvas) { fill.draw(context, halfThickness, verticalAxisPosition) } } } + } - /** Draws the line background. */ - public fun drawBackground( - context: DrawContext, - bounds: RectF, - zeroLineYFraction: Float, - path: Path, - opacity: Float = 1f, - ) { - val fill = backgroundShader ?: return - with(lineBackgroundPaint) { - if (zeroLineYFraction > 0) { - val zeroLineY = bounds.top + (zeroLineYFraction * bounds.height()) - setSplitY(1f) - shader = fill.provideShader(context, bounds.left, bounds.top, bounds.right, zeroLineY) - lineBackgroundPath.set(path) - lineBackgroundPath.computeBounds(pathBounds, false) - lineBackgroundPath.lineTo(pathBounds.getEnd(context.isLtr), bounds.bottom) - lineBackgroundPath.lineTo(pathBounds.getStart(context.isLtr), bounds.bottom) - lineBackgroundPath.close() - clipPath.rewind() - clipPath.addRect(bounds.left, bounds.top, bounds.right, zeroLineY, Path.Direction.CW) - lineBackgroundPath.op(clipPath, Path.Op.INTERSECT) - withOpacity(opacity) { context.canvas.drawPath(lineBackgroundPath, it) } - } + /** Draws a [LineCartesianLayer] line’s fill. */ + public interface LineFill { + /** Draws the fill. [PorterDuff.Mode.SRC_IN] should be used. */ + public fun draw( + context: CartesianDrawContext, + halfLineThickness: Float, + verticalAxisPosition: AxisPosition.Vertical?, + ) - if (zeroLineYFraction < 1f) { - val zeroLineY = bounds.top + (zeroLineYFraction * bounds.height()) - setSplitY(0f) - shader = fill.provideShader(context, bounds.left, zeroLineY, bounds.right, bounds.bottom) - lineBackgroundPath.set(path) - lineBackgroundPath.computeBounds(pathBounds, false) - lineBackgroundPath.lineTo(pathBounds.getEnd(context.isLtr), bounds.top) - lineBackgroundPath.lineTo(pathBounds.getStart(context.isLtr), bounds.top) - lineBackgroundPath.close() - clipPath.rewind() - clipPath.addRect(bounds.left, zeroLineY, bounds.right, bounds.bottom, Path.Direction.CW) - lineBackgroundPath.op(clipPath, Path.Op.INTERSECT) - withOpacity(opacity) { context.canvas.drawPath(lineBackgroundPath, it) } - } - } + /** Houses [LineFill] factory functions. */ + public companion object { + /** Uses a single [Fill]. */ + public fun single(fill: Fill): LineFill = SingleLineFill(fill) + + /** + * Uses [topFill] for the portions of the line that are above the _y_ value returned by + * [splitY], and analogously for [bottomFill]. + */ + public fun double( + topFill: Fill, + bottomFill: Fill, + splitY: (ExtraStore) -> Double = { 0.0 }, + ): LineFill = DoubleLineFill(topFill, bottomFill, splitY) } + } + + /** Draws a [LineCartesianLayer] line’s area fill. */ + public interface AreaFill { + /** Draws the area fill. */ + public fun draw( + context: CartesianDrawContext, + linePath: Path, + halfLineThickness: Float, + opacity: Float, + verticalAxisPosition: AxisPosition.Vertical?, + ) - /** - * For [shader] and [backgroundShader], if the [DynamicShader] is a [TopBottomShader], updates - * [TopBottomShader.splitY] to match the position of the zero line (_y_ = 0). - */ - public fun setSplitY(splitY: Float) { - (shader as? TopBottomShader)?.splitY = splitY - (backgroundShader as? TopBottomShader)?.splitY = splitY + /** Houses [AreaFill] factory functions. */ + public companion object { + /** + * Uses [fill] for the areas bounded by the [LineCartesianLayer] line and the [splitY] line. + * (The [splitY] line is an imaginary horizontal line positioned at the _y_ value returned by + * [splitY].) + */ + public fun single(fill: Fill, splitY: (ExtraStore) -> Double = { 0.0 }): AreaFill = + SingleAreaFill(fill, splitY) + + /** + * Uses [topFill] for those areas bounded by the [LineCartesianLayer] line and the [splitY] + * line that are above the [splitY] line, and analogously for [bottomFill]. (The [splitY] line + * is an imaginary horizontal line positioned at the _y_ value returned by [splitY].) + */ + public fun double( + topFill: Fill, + bottomFill: Fill, + splitY: (ExtraStore) -> Double = { 0.0 }, + ): AreaFill = DoubleAreaFill(topFill, bottomFill, splitY) } } @@ -303,10 +304,12 @@ public open class LineCartesianLayer( protected val linePath: Path = Path() - protected val lineBackgroundPath: Path = Path() + protected var lineFillCanvas: Canvas = Canvas() protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() + protected val cacheKeyNamespace: CacheStore.KeyNamespace = CacheStore.KeyNamespace() + override val markerTargets: Map> = _markerTargets override fun drawInternal(context: CartesianDrawContext, model: LineCartesianLayerModel): Unit = @@ -314,19 +317,12 @@ public open class LineCartesianLayer( resetTempData() val drawingModel = model.extraStore.getOrNull(drawingModelKey) - val yRange = chartValues.getYRange(verticalAxisPosition) - val zeroLineYFraction = - drawingModel?.zeroY ?: (yRange.maxY / yRange.length).coerceIn(0.0..1.0).toFloat() model.series.forEachIndexed { seriesIndex, series -> val pointInfoMap = drawingModel?.getOrNull(seriesIndex) linePath.rewind() - lineBackgroundPath.rewind() - val line = - lineProvider.getLine(seriesIndex, chartValues.model.extraStore).apply { - setSplitY(zeroLineYFraction) - } + val line = lineProvider.getLine(seriesIndex, chartValues.model.extraStore) var prevX = layerBounds.getStart(isLtr = isLtr) var prevY = layerBounds.bottom @@ -337,7 +333,7 @@ public open class LineCartesianLayer( val drawingStart = layerBounds.getStart(isLtr = isLtr) + drawingStartAlignmentCorrection - scroll - forEachPointInBounds(series, drawingStart, pointInfoMap) { entry, x, y, _, _ -> + forEachPointInBounds(series, drawingStart, pointInfoMap) { _, x, y, _, _ -> if (linePath.isEmpty) { linePath.moveTo(x, y) } else { @@ -345,39 +341,43 @@ public open class LineCartesianLayer( } prevX = x prevY = y - - updateMarkerTargets(entry, x, y, line) } - if (line.hasBackgroundShader) { - lineBackgroundPath.addPath(linePath) - lineBackgroundPath.lineTo(prevX, layerBounds.bottom) - line.drawBackground( - context, - layerBounds, - zeroLineYFraction, - lineBackgroundPath, - drawingModel?.opacity ?: 1f, - ) - } + val lineFillBitmap = getLineFillBitmap(seriesIndex) + lineFillCanvas.setBitmap(lineFillBitmap) - line.drawLine( + line.draw( context, - layerBounds, - zeroLineYFraction, linePath, + lineFillCanvas, drawingModel?.opacity ?: 1f, + verticalAxisPosition, ) + canvas.drawBitmap(lineFillBitmap, 0f, 0f, null) + + forEachPointInBounds(series, drawingStart, pointInfoMap) { entry, x, y, _, _ -> + updateMarkerTargets(entry, x, y, lineFillBitmap) + } + drawPointsAndDataLabels(line, series, seriesIndex, drawingStart, pointInfoMap) } } + private fun DrawContext.getLineFillBitmap(seriesIndex: Int) = + cacheStore + .getOrNull(cacheKeyNamespace, seriesIndex) + ?.takeIf { it.width == canvas.width && it.height == canvas.height } + ?.apply { eraseColor(Color.TRANSPARENT) } + ?: Bitmap.createBitmap(canvas.width, canvas.height, Bitmap.Config.ARGB_8888).also { + cacheStore[cacheKeyNamespace, seriesIndex] = it + } + protected open fun CartesianDrawContext.updateMarkerTargets( entry: LineCartesianLayerModel.Entry, canvasX: Float, canvasY: Float, - line: Line, + lineFillBitmap: Bitmap, ) { if (canvasX <= layerBounds.left - 1 || canvasX >= layerBounds.right + 1) return val limitedCanvasY = canvasY.coerceIn(layerBounds.top, layerBounds.bottom) @@ -388,7 +388,7 @@ public open class LineCartesianLayer( LineCartesianLayerMarkerTarget.Point( entry, limitedCanvasY, - line.shader.getColorAt(Point(canvasX, limitedCanvasY), this, layerBounds), + lineFillBitmap.getPixel(canvasX.roundToInt(), limitedCanvasY.roundToInt()), ) } @@ -497,7 +497,6 @@ public open class LineCartesianLayer( protected fun resetTempData() { _markerTargets.clear() linePath.rewind() - lineBackgroundPath.rewind() } protected open fun CartesianDrawContext.forEachPointInBounds( @@ -635,8 +634,8 @@ public open class LineCartesianLayer( chartValues: ChartValues ): LineCartesianLayerDrawingModel { val yRange = chartValues.getYRange(verticalAxisPosition) - return series - .map { series -> + return LineCartesianLayerDrawingModel( + series.map { series -> series.associate { entry -> entry.x to LineCartesianLayerDrawingModel.PointInfo( @@ -644,11 +643,19 @@ public open class LineCartesianLayer( ) } } - .let { pointInfo -> - LineCartesianLayerDrawingModel( - pointInfo, - (yRange.maxY / yRange.length).coerceIn(0.0..1.0).toFloat(), - ) - } + ) } } + +internal fun CartesianDrawContext.getCanvasSplitY( + splitY: (ExtraStore) -> Double, + halfLineThickness: Float, + verticalAxisPosition: AxisPosition.Vertical?, +): Float { + val yRange = chartValues.getYRange(verticalAxisPosition) + val base = + layerBounds.bottom - + ((splitY(chartValues.model.extraStore) - yRange.minY) / yRange.length).toFloat() * + layerBounds.height() + halfLineThickness + return ceil(base).coerceIn(layerBounds.top..layerBounds.bottom) +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineFills.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineFills.kt new file mode 100644 index 000000000..0a8da7d9b --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineFills.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2024 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.cartesian.layer + +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext +import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition +import com.patrykandpatrick.vico.core.common.Fill +import com.patrykandpatrick.vico.core.common.data.ExtraStore + +internal data class SingleLineFill(val fill: Fill) : LineCartesianLayer.LineFill { + private val paint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = fill.color + xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + } + + override fun draw( + context: CartesianDrawContext, + halfLineThickness: Float, + verticalAxisPosition: AxisPosition.Vertical?, + ) { + with(context) { + paint.shader = + fill.shader?.provideShader( + this, + layerBounds.left, + layerBounds.top, + layerBounds.right, + layerBounds.bottom, + ) + canvas.drawPaint(paint) + } + } +} + +internal data class DoubleLineFill( + val topFill: Fill, + val bottomFill: Fill, + val splitY: (ExtraStore) -> Double, +) : LineCartesianLayer.LineFill { + private val paint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) } + + override fun draw( + context: CartesianDrawContext, + halfLineThickness: Float, + verticalAxisPosition: AxisPosition.Vertical?, + ) { + with(context) { + val canvasSplitY = getCanvasSplitY(splitY, halfLineThickness, verticalAxisPosition) + paint.color = topFill.color + paint.shader = + topFill.shader?.provideShader( + this, + layerBounds.left, + layerBounds.top - halfLineThickness, + layerBounds.right, + canvasSplitY, + ) + canvas.drawRect( + layerBounds.left, + layerBounds.top - halfLineThickness, + layerBounds.right, + canvasSplitY, + paint, + ) + paint.color = bottomFill.color + paint.shader = + bottomFill.shader?.provideShader( + this, + layerBounds.left, + canvasSplitY, + layerBounds.right, + layerBounds.bottom + halfLineThickness, + ) + canvas.drawRect( + layerBounds.left, + canvasSplitY, + layerBounds.right, + layerBounds.bottom + halfLineThickness, + paint, + ) + } + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/DrawContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/DrawContext.kt index 4570a560a..696496e2e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/DrawContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/DrawContext.kt @@ -37,8 +37,8 @@ public interface DrawContext : MeasureContext { */ public fun saveCanvas(): Int = canvas.save() - /** Temporarily swaps the [Canvas] and yields [DrawContext] as the [block]’s receiver. */ - public fun withOtherCanvas(canvas: Canvas, block: (DrawContext) -> Unit) + /** Updates [DrawContext.canvas] to [canvas], runs [block], and resets [DrawContext.canvas]. */ + public fun withOtherCanvas(canvas: Canvas, block: () -> Unit) /** * Clips the [Canvas] to the specified rectangle. @@ -115,10 +115,10 @@ public fun drawContext( override val cacheStore: CacheStore = CacheStore() - override fun withOtherCanvas(canvas: Canvas, block: (DrawContext) -> Unit) { + override fun withOtherCanvas(canvas: Canvas, block: () -> Unit) { val originalCanvas = this.canvas this.canvas = canvas - block(this) + block() this.canvas = originalCanvas } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Fill.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Fill.kt new file mode 100644 index 000000000..502b8b638 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Fill.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.core.common + +import android.graphics.Color +import com.patrykandpatrick.vico.core.common.shader.DynamicShader + +/** + * Stores fill properties. + * + * @property color the color. If [shader] is not `null`, this is [Color.BLACK]. + * @property shader the [DynamicShader]. + */ +public class Fill private constructor(public val color: Int, public val shader: DynamicShader?) { + /** Creates a color [Fill]. */ + public constructor(color: Int) : this(color = color, shader = null) + + /** Creates a [DynamicShader][Fill]. */ + public constructor(shader: DynamicShader) : this(Color.BLACK, shader) + + override fun equals(other: Any?): Boolean = + this === other || other is Fill && color == other.color && shader == other.shader + + override fun hashCode(): Int = 31 * color + shader?.hashCode().orZero + + /** Houses [Fill] singletons. */ + public companion object { + /** A black [Fill]. */ + public val Black: Fill = Fill(Color.BLACK) + + /** A transparent [Fill]. */ + public val Transparent: Fill = Fill(Color.TRANSPARENT) + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/data/CacheStore.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/data/CacheStore.kt index 19e39ed90..75168c9af 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/data/CacheStore.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/data/CacheStore.kt @@ -21,6 +21,24 @@ public class CacheStore { private var map = mutableMapOf() private var purgedMap = mutableMapOf() + /** + * Retrieves the value associated with the key belonging to the specified namespace and matching + * the given components. If there’s no such value, `null` is returned. + */ + public fun getOrNull(keyNamespace: KeyNamespace, vararg keyComponents: Any?): T? { + val key = keyNamespace.getKey(*keyComponents) + val value = map[key] + if (value != null) purgedMap[key] = value + @Suppress("UNCHECKED_CAST") return value as T? + } + + /** Caches [value]. */ + public operator fun set(keyNamespace: KeyNamespace, vararg keyComponents: Any?, value: Any) { + val key = keyNamespace.getKey(*keyComponents) + map[key] = value + purgedMap[key] = value + } + /** * Retrieves the value associated with the key belonging to the specified namespace and matching * the given components. If there’s no such value, [value] is called, and its result is cached and @@ -30,14 +48,9 @@ public class CacheStore { keyNamespace: KeyNamespace, vararg keyComponents: Any?, value: () -> T, - ): T { - val key = keyNamespace.getKey(*keyComponents) - return (@Suppress("UNCHECKED_CAST") (map[key]?.also { purgedMap[key] = it } as T?)) - ?: value().also { newValue -> - map[key] = newValue - purgedMap[key] = newValue - } - } + ): T = + getOrNull(keyNamespace, keyComponents) + ?: value().also { this[keyNamespace, keyComponents] = it } /** * Removes all values that were added before the last call to this function and haven’t been read diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/BaseDynamicShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/BaseDynamicShader.kt deleted file mode 100644 index 961cf88d0..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/BaseDynamicShader.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2024 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.common.shader - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF -import com.patrykandpatrick.vico.core.common.DrawContext -import com.patrykandpatrick.vico.core.common.Point -import com.patrykandpatrick.vico.core.common.data.CacheStore -import kotlin.math.roundToInt - -/** A base [DynamicShader] implementation. This overrides [getColorAt]. */ -public abstract class BaseDynamicShader : DynamicShader { - private val cacheKeyNamespace = CacheStore.KeyNamespace() - - override fun getColorAt(point: Point, context: DrawContext, bounds: RectF): Int = - context.cacheStore - .getOrSet(cacheKeyNamespace) { toBitmap(context, bounds) } - .getPixel( - (point.x - bounds.left).toInt().coerceIn(0, bounds.width().toInt() - 1), - (point.y - bounds.top).toInt().coerceIn(0, bounds.height().toInt() - 1), - ) -} - -private fun DynamicShader.toBitmap(context: DrawContext, bounds: RectF): Bitmap { - val width = bounds.width().roundToInt() - val height = bounds.height().roundToInt() - val paint = - Paint(Paint.ANTI_ALIAS_FLAG).apply { - shader = provideShader(context, 0f, 0f, width.toFloat(), height.toFloat()) - } - return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { - Canvas(it).drawPaint(paint) - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/CacheableDynamicShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/CacheableDynamicShader.kt index 5ad8f9a56..fd132b175 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/CacheableDynamicShader.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/CacheableDynamicShader.kt @@ -23,7 +23,7 @@ import com.patrykandpatrick.vico.core.common.DrawContext * [CacheableDynamicShader] can cache created [Shader] instances for reuse between identical sets of * bounds. */ -public abstract class CacheableDynamicShader : BaseDynamicShader() { +public abstract class CacheableDynamicShader : DynamicShader { private val cache = HashMap(1) override fun provideShader( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/ColorShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/ColorShader.kt deleted file mode 100644 index 580024a58..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/ColorShader.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2024 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.common.shader - -import android.graphics.Bitmap -import android.graphics.BitmapShader -import android.graphics.RectF -import android.graphics.Shader -import com.patrykandpatrick.vico.core.common.DrawContext -import com.patrykandpatrick.vico.core.common.Point - -/** Applies the given color ([color]) to the shaded area. */ -public class ColorShader(public val color: Int) : DynamicShader { - private val shader = - BitmapShader( - Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888).apply { setPixel(0, 0, color) }, - Shader.TileMode.CLAMP, - Shader.TileMode.CLAMP, - ) - - override fun provideShader( - context: DrawContext, - left: Float, - top: Float, - right: Float, - bottom: Float, - ): Shader = shader - - override fun getColorAt(point: Point, context: DrawContext, bounds: RectF): Int = color - - override fun equals(other: Any?): Boolean = - this === other || (other is ColorShader && color == other.color) - - override fun hashCode(): Int = color.hashCode() - - @OptIn(ExperimentalStdlibApi::class) - override fun toString(): String = "ColorShader(color=${color.toHexString(HexFormat.UpperCase)})" -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/DynamicShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/DynamicShader.kt index a60559168..339646b49 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/DynamicShader.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/DynamicShader.kt @@ -26,13 +26,8 @@ import android.graphics.Shader import android.os.Build import androidx.annotation.RequiresApi import com.patrykandpatrick.vico.core.common.DrawContext -import com.patrykandpatrick.vico.core.common.Point -/** - * [DynamicShader] creates [Shader] instances on demand. - * - * @see Shader - */ +/** Creates [Shader]s on demand. */ public interface DynamicShader { /** Creates a [Shader] by using the provided [bounds]. */ public fun provideShader(context: DrawContext, bounds: RectF): Shader = @@ -53,9 +48,6 @@ public interface DynamicShader { bottom: Float, ): Shader - /** Gets the color of the pixel at the given point. [bounds] specifies the shaded area. */ - public fun getColorAt(point: Point, context: DrawContext, bounds: RectF): Int - public companion object { /** Creates a [DynamicShader] out of the given [bitmap]. */ public fun bitmap( @@ -83,7 +75,7 @@ public interface DynamicShader { second: DynamicShader, mode: BlendMode, ): DynamicShader = - object : BaseDynamicShader() { + object : DynamicShader { override fun provideShader( context: DrawContext, left: Float, @@ -107,7 +99,7 @@ public interface DynamicShader { second: DynamicShader, mode: PorterDuff.Mode, ): DynamicShader = - object : BaseDynamicShader() { + object : DynamicShader { override fun provideShader( context: DrawContext, left: Float, @@ -161,3 +153,15 @@ public interface DynamicShader { LinearGradientShader(colors, positions, false) } } + +/** Converts this [Shader] to a [DynamicShader]. */ +public fun Shader.toDynamicShader(): DynamicShader = + object : DynamicShader { + override fun provideShader( + context: DrawContext, + left: Float, + top: Float, + right: Float, + bottom: Float, + ): Shader = this@toDynamicShader + } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/StaticShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/StaticShader.kt deleted file mode 100644 index 4ee45c656..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/StaticShader.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.common.shader - -import android.graphics.Shader -import com.patrykandpatrick.vico.core.common.DrawContext - -/** - * Creates a [DynamicShader], which always provides the same [Shader] instance. - * - * @property shader the [Shader] that will always be provided, regardless of the [provideShader] - * function’s arguments. - */ -public class StaticShader(private val shader: Shader) : BaseDynamicShader() { - override fun provideShader( - context: DrawContext, - left: Float, - top: Float, - right: Float, - bottom: Float, - ): Shader = shader -} - -/** Converts this [Shader] to a [StaticShader] and returns it as a [DynamicShader]. */ -public val Shader.dynamic: DynamicShader - get() = StaticShader(this) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/TopBottomShader.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/TopBottomShader.kt deleted file mode 100644 index 2968d739e..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/shader/TopBottomShader.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2024 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.core.common.shader - -import android.graphics.Bitmap -import android.graphics.BitmapShader -import android.graphics.Canvas -import android.graphics.Matrix -import android.graphics.Paint -import android.graphics.Shader -import com.patrykandpatrick.vico.core.common.DrawContext - -/** - * Splits the shaded area into two parts and applies two other [DynamicShader]s, [topShader] and - * [bottomShader]. [splitY] expresses the distance of the split from the top of the shaded area as a - * fraction of the area’s height. - */ -public class TopBottomShader( - public var topShader: DynamicShader, - public var bottomShader: DynamicShader, - public var splitY: Float = 0f, -) : CacheableDynamicShader() { - private val paint = Paint() - - override fun createShader( - context: DrawContext, - left: Float, - top: Float, - right: Float, - bottom: Float, - ): Shader { - val width = (right - left).toInt() - val height = (bottom - top).toInt() - if (width == 0 || height == 0) return EmptyBitmapShader - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - if (splitY > 0f) { - paint.shader = - topShader.provideShader( - context = context, - left = left, - top = 0f, - right = right, - bottom = height * splitY, - ) - canvas.drawRect(0f, 0f, width.toFloat(), height * splitY, paint) - } - if (splitY < 1f) { - paint.shader = - bottomShader.provideShader( - context = context, - left = left, - top = height * splitY, - right = right, - bottom = height.toFloat(), - ) - canvas.drawRect(0f, height * splitY, width.toFloat(), height.toFloat(), paint) - } - return BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP).apply { - setLocalMatrix(Matrix().apply { postTranslate(left, top) }) - } - } - - override fun createKey(left: Float, top: Float, right: Float, bottom: Float): String = - "${super.createKey(left, top, right, bottom)},${topShader.hashCode()},${bottomShader.hashCode()}" - - override fun equals(other: Any?): Boolean = - this === other || - other is TopBottomShader && - topShader == other.topShader && - bottomShader == other.bottomShader && - splitY == other.splitY - - override fun hashCode(): Int = 31 * topShader.hashCode() + bottomShader.hashCode() - - private companion object { - val EmptyBitmapShader = - BitmapShader( - Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8), - Shader.TileMode.CLAMP, - Shader.TileMode.CLAMP, - ) - } -} diff --git a/vico/views/src/main/java/com/patrykandpatrick/vico/views/common/theme/ComponentStyle.kt b/vico/views/src/main/java/com/patrykandpatrick/vico/views/common/theme/ComponentStyle.kt index e43bd83b0..20aab4c70 100644 --- a/vico/views/src/main/java/com/patrykandpatrick/vico/views/common/theme/ComponentStyle.kt +++ b/vico/views/src/main/java/com/patrykandpatrick/vico/views/common/theme/ComponentStyle.kt @@ -23,15 +23,14 @@ import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.common.DefaultAlpha import com.patrykandpatrick.vico.core.common.Defaults import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.Fill import com.patrykandpatrick.vico.core.common.LayeredComponent import com.patrykandpatrick.vico.core.common.VerticalPosition import com.patrykandpatrick.vico.core.common.component.Component import com.patrykandpatrick.vico.core.common.component.LineComponent import com.patrykandpatrick.vico.core.common.component.ShapeComponent import com.patrykandpatrick.vico.core.common.copyColor -import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.patrykandpatrick.vico.core.common.shader.TopBottomShader import com.patrykandpatrick.vico.core.common.shape.Shape import com.patrykandpatrick.vico.views.R import com.patrykandpatrick.vico.views.common.defaultColors @@ -174,18 +173,18 @@ internal fun TypedArray.getLine(context: Context, defaultColor: Int): LineCartes ) return LineCartesianLayer.Line( - shader = + fill = if (positiveLineColor != negativeLineColor) { - TopBottomShader(ColorShader(positiveLineColor), ColorShader(negativeLineColor)) + LineCartesianLayer.LineFill.double(Fill(positiveLineColor), Fill(negativeLineColor)) } else { - ColorShader(positiveLineColor) + LineCartesianLayer.LineFill.single(Fill(positiveLineColor)) }, thicknessDp = getRawDimension(context, R.styleable.LineStyle_thickness, Defaults.LINE_SPEC_THICKNESS_DP), - backgroundShader = - TopBottomShader( - DynamicShader.verticalGradient(positiveGradientTopColor, positiveGradientBottomColor), - DynamicShader.verticalGradient(negativeGradientTopColor, negativeGradientBottomColor), + areaFill = + LineCartesianLayer.AreaFill.double( + Fill(DynamicShader.verticalGradient(positiveGradientTopColor, positiveGradientBottomColor)), + Fill(DynamicShader.verticalGradient(negativeGradientTopColor, negativeGradientBottomColor)), ), pointProvider = getNestedTypedArray(context, R.styleable.LineStyle_pointStyle, R.styleable.ComponentStyle)