Skip to content

Instantly share code, notes, and snippets.

@tadfisher
Created July 22, 2021 00:05
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tadfisher/dc908504a7189be82c8df2151ede590b to your computer and use it in GitHub Desktop.
Save tadfisher/dc908504a7189be82c8df2151ede590b to your computer and use it in GitHub Desktop.
Blur effect in Compose
package com.mercury.ui.design.graphics
import android.graphics.RenderEffect
import android.graphics.RenderNode
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toAndroidTileMode
import kotlin.math.roundToInt
internal actual fun DrawScope.blurInternal(
radiusX: Float,
radiusY: Float,
tileMode: TileMode,
fallback: DrawScope.() -> Unit,
block: DrawScope.() -> Unit
) {
if (Build.VERSION.SDK_INT >= 31 &&
drawContext.canvas.nativeCanvas.isHardwareAccelerated
) {
renderEffect(
RenderEffect.createBlurEffect(
radiusX,
radiusY,
tileMode.toAndroidTileMode(),
),
block
)
} else {
fallback()
}
}
@RequiresApi(31)
internal fun DrawScope.renderEffect(
effect: RenderEffect,
block: DrawScope.() -> Unit
) {
val canvasHolder = CanvasHolder()
val renderNode = RenderNode("ComposeRenderEffect")
renderNode.setPosition(0, 0, size.width.roundToInt(), size.height.roundToInt())
val recording = renderNode.beginRecording()
val drawScope = RecordingCanvasDrawScope()
try {
canvasHolder.drawInto(recording) {
drawScope.drawParams.density = this@renderEffect
drawScope.drawParams.canvas = this
drawScope.drawParams.layoutDirection = layoutDirection
drawScope.drawParams.size = size
drawScope.block()
}
} finally {
renderNode.endRecording()
}
renderNode.setRenderEffect(effect)
drawContext.canvas.nativeCanvas.drawRenderNode(renderNode)
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.mercury.ui.design.graphics
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.NativeCanvas
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.Vertices
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.isIdentity
import androidx.compose.ui.graphics.setFrom
import androidx.compose.ui.graphics.toAndroidVertexMode
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
/**
* Holder class that is used to issue scoped calls to a [Canvas] from the framework
* equivalent canvas without having to allocate an object on each draw call
*/
class CanvasHolder {
@PublishedApi internal val androidCanvas = AndroidCanvas()
inline fun drawInto(targetCanvas: android.graphics.Canvas, block: Canvas.() -> Unit) {
val previousCanvas = androidCanvas.internalCanvas
androidCanvas.internalCanvas = targetCanvas
androidCanvas.block()
androidCanvas.internalCanvas = previousCanvas
}
}
// Stub canvas instance used to keep the internal canvas parameter non-null during its
// scoped usage and prevent unnecessary byte code null checks from being generated
private val EmptyCanvas = android.graphics.Canvas()
@PublishedApi internal class AndroidCanvas() : Canvas {
// Keep the internal canvas as a var prevent having to allocate an AndroidCanvas
// instance on each draw call
@PublishedApi internal var internalCanvas: NativeCanvas = EmptyCanvas
private val srcRect: android.graphics.Rect by lazy(LazyThreadSafetyMode.NONE) {
android.graphics.Rect()
}
private val dstRect: android.graphics.Rect by lazy(LazyThreadSafetyMode.NONE) {
android.graphics.Rect()
}
/**
* @see Canvas.save
*/
override fun save() {
internalCanvas.save()
}
/**
* @see Canvas.restore
*/
override fun restore() {
internalCanvas.restore()
}
/**
* @see Canvas.saveLayer
*/
@SuppressWarnings("deprecation")
override fun saveLayer(bounds: Rect, paint: Paint) {
@Suppress("DEPRECATION")
internalCanvas.saveLayer(
bounds.left,
bounds.top,
bounds.right,
bounds.bottom,
paint.asFrameworkPaint(),
android.graphics.Canvas.ALL_SAVE_FLAG
)
}
/**
* @see Canvas.translate
*/
override fun translate(dx: Float, dy: Float) {
internalCanvas.translate(dx, dy)
}
/**
* @see Canvas.scale
*/
override fun scale(sx: Float, sy: Float) {
internalCanvas.scale(sx, sy)
}
/**
* @see Canvas.rotate
*/
override fun rotate(degrees: Float) {
internalCanvas.rotate(degrees)
}
/**
* @see Canvas.skew
*/
override fun skew(sx: Float, sy: Float) {
internalCanvas.skew(sx, sy)
}
/**
* @throws IllegalStateException if an arbitrary transform is provided
*/
override fun concat(matrix: Matrix) {
if (!matrix.isIdentity()) {
val frameworkMatrix = android.graphics.Matrix()
frameworkMatrix.setFrom(matrix)
internalCanvas.concat(frameworkMatrix)
}
}
@SuppressWarnings("deprecation")
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
@Suppress("DEPRECATION")
internalCanvas.clipRect(left, top, right, bottom, clipOp.toRegionOp())
}
/**
* @see Canvas.clipPath
*/
override fun clipPath(path: Path, clipOp: ClipOp) {
@Suppress("DEPRECATION")
internalCanvas.clipPath(path.asAndroidPath(), clipOp.toRegionOp())
}
fun ClipOp.toRegionOp(): android.graphics.Region.Op =
when (this) {
ClipOp.Difference -> android.graphics.Region.Op.DIFFERENCE
else -> android.graphics.Region.Op.INTERSECT
}
/**
* @see Canvas.drawLine
*/
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
internalCanvas.drawLine(
p1.x,
p1.y,
p2.x,
p2.y,
paint.asFrameworkPaint()
)
}
override fun drawRect(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
internalCanvas.drawRect(left, top, right, bottom, paint.asFrameworkPaint())
}
override fun drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
radiusX: Float,
radiusY: Float,
paint: Paint
) {
internalCanvas.drawRoundRect(
left,
top,
right,
bottom,
radiusX,
radiusY,
paint.asFrameworkPaint()
)
}
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
internalCanvas.drawOval(left, top, right, bottom, paint.asFrameworkPaint())
}
/**
* @see Canvas.drawCircle
*/
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
internalCanvas.drawCircle(
center.x,
center.y,
radius,
paint.asFrameworkPaint()
)
}
override fun drawArc(
left: Float,
top: Float,
right: Float,
bottom: Float,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
paint: Paint
) {
internalCanvas.drawArc(
left,
top,
right,
bottom,
startAngle,
sweepAngle,
useCenter,
paint.asFrameworkPaint()
)
}
/**
* @see Canvas.drawPath
*/
override fun drawPath(path: Path, paint: Paint) {
internalCanvas.drawPath(path.asAndroidPath(), paint.asFrameworkPaint())
}
/**
* @see Canvas.drawImage
*/
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
internalCanvas.drawBitmap(
image.asAndroidBitmap(),
topLeftOffset.x,
topLeftOffset.y,
paint.asFrameworkPaint()
)
}
/**
* @See Canvas.drawImageRect
*/
override fun drawImageRect(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
paint: Paint
) {
// There is no framework API to draw a subset of a target bitmap
// that consumes only primitives so lazily allocate a src and dst
// rect to populate the dimensions and re-use across calls
internalCanvas.drawBitmap(
image.asAndroidBitmap(),
srcRect.apply {
left = srcOffset.x
top = srcOffset.y
right = srcOffset.x + srcSize.width
bottom = srcOffset.y + srcSize.height
},
dstRect.apply {
left = dstOffset.x
top = dstOffset.y
right = dstOffset.x + dstSize.width
bottom = dstOffset.y + dstSize.height
},
paint.asFrameworkPaint()
)
}
/**
* @see Canvas.drawPoints
*/
override fun drawPoints(pointMode: PointMode, points: List<Offset>, paint: Paint) {
when (pointMode) {
// Draw a line between each pair of points, each point has at most one line
// If the number of points is odd, then the last point is ignored.
PointMode.Lines -> drawLines(points, paint, 2)
// Connect each adjacent point with a line
PointMode.Polygon -> drawLines(points, paint, 1)
// Draw a point at each provided coordinate
PointMode.Points -> drawPoints(points, paint)
}
}
override fun enableZ() {
CanvasUtils.enableZ(internalCanvas, true)
}
override fun disableZ() {
CanvasUtils.enableZ(internalCanvas, false)
}
private fun drawPoints(points: List<Offset>, paint: Paint) {
points.fastForEach { point ->
internalCanvas.drawPoint(
point.x,
point.y,
paint.asFrameworkPaint()
)
}
}
/**
* Draw lines connecting points based on the corresponding step.
*
* ex. 3 points with a step of 1 would draw 2 lines between the first and second points
* and another between the second and third
*
* ex. 4 points with a step of 2 would draw 2 lines between the first and second and another
* between the third and fourth. If there is an odd number of points, the last point is
* ignored
*
* @see drawRawLines
*/
private fun drawLines(points: List<Offset>, paint: Paint, stepBy: Int) {
if (points.size >= 2) {
for (i in 0 until points.size - 1 step stepBy) {
val p1 = points[i]
val p2 = points[i + 1]
internalCanvas.drawLine(
p1.x,
p1.y,
p2.x,
p2.y,
paint.asFrameworkPaint()
)
}
}
}
/**
* @throws IllegalArgumentException if a non even number of points is provided
*/
override fun drawRawPoints(pointMode: PointMode, points: FloatArray, paint: Paint) {
if (points.size % 2 != 0) {
throw IllegalArgumentException("points must have an even number of values")
}
when (pointMode) {
PointMode.Lines -> drawRawLines(points, paint, 2)
PointMode.Polygon -> drawRawLines(points, paint, 1)
PointMode.Points -> drawRawPoints(points, paint, 2)
}
}
private fun drawRawPoints(points: FloatArray, paint: Paint, stepBy: Int) {
if (points.size % 2 == 0) {
for (i in 0 until points.size - 1 step stepBy) {
val x = points[i]
val y = points[i + 1]
internalCanvas.drawPoint(x, y, paint.asFrameworkPaint())
}
}
}
/**
* Draw lines connecting points based on the corresponding step. The points are interpreted
* as x, y coordinate pairs in alternating index positions
*
* ex. 3 points with a step of 1 would draw 2 lines between the first and second points
* and another between the second and third
*
* ex. 4 points with a step of 2 would draw 2 lines between the first and second and another
* between the third and fourth. If there is an odd number of points, the last point is
* ignored
*
* @see drawLines
*/
private fun drawRawLines(points: FloatArray, paint: Paint, stepBy: Int) {
// Float array is treated as alternative set of x and y coordinates
// x1, y1, x2, y2, x3, y3, ... etc.
if (points.size >= 4 && points.size % 2 == 0) {
for (i in 0 until points.size - 3 step stepBy * 2) {
val x1 = points[i]
val y1 = points[i + 1]
val x2 = points[i + 2]
val y2 = points[i + 3]
internalCanvas.drawLine(
x1,
y1,
x2,
y2,
paint.asFrameworkPaint()
)
}
}
}
override fun drawVertices(vertices: Vertices, blendMode: BlendMode, paint: Paint) {
// TODO(njawad) align drawVertices blendMode parameter usage with framework
// android.graphics.Canvas#drawVertices does not consume a blendmode argument
internalCanvas.drawVertices(
vertices.vertexMode.toAndroidVertexMode(),
vertices.positions.size,
vertices.positions,
0, // TODO(njawad) figure out proper vertOffset)
vertices.textureCoordinates,
0, // TODO(njawad) figure out proper texOffset)
vertices.colors,
0, // TODO(njawad) figure out proper colorOffset)
vertices.indices,
0, // TODO(njawad) figure out proper indexOffset)
vertices.indices.size,
paint.asFrameworkPaint()
)
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.mercury.ui.design.graphics
import android.annotation.SuppressLint
import android.graphics.Canvas
import android.os.Build
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
internal object CanvasUtils {
private var reorderBarrierMethod: Method? = null
private var inorderBarrierMethod: Method? = null
private var orderMethodsFetched = false
/**
* Enables Z support for the Canvas.
*
* This is only supported on Lollipop and later.
*/
@SuppressLint("SoonBlockedPrivateApi")
fun enableZ(canvas: Canvas, enable: Boolean) {
if (Build.VERSION.SDK_INT >= 29) {
CanvasZHelper.enableZ(canvas, enable)
} else {
if (!orderMethodsFetched) {
try {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// use double reflection to avoid grey list on P
val getDeclaredMethod = Class::class.java.getDeclaredMethod(
"getDeclaredMethod",
String::class.java,
arrayOf<Class<*>>()::class.java
)
reorderBarrierMethod = getDeclaredMethod.invoke(
Canvas::class.java,
"insertReorderBarrier",
emptyArray<Class<*>>()
) as Method?
inorderBarrierMethod = getDeclaredMethod.invoke(
Canvas::class.java,
"insertInorderBarrier",
emptyArray<Class<*>>()
) as Method?
} else {
reorderBarrierMethod = Canvas::class.java.getDeclaredMethod(
"insertReorderBarrier"
)
inorderBarrierMethod = Canvas::class.java.getDeclaredMethod(
"insertInorderBarrier"
)
}
reorderBarrierMethod?.isAccessible = true
inorderBarrierMethod?.isAccessible = true
} catch (ignore: IllegalAccessException) { // Do nothing
} catch (ignore: InvocationTargetException) { // Do nothing
} catch (ignore: NoSuchMethodException) { // Do nothing
}
orderMethodsFetched = true
}
try {
if (enable && reorderBarrierMethod != null) {
reorderBarrierMethod!!.invoke(canvas)
}
if (!enable && inorderBarrierMethod != null) {
inorderBarrierMethod!!.invoke(canvas)
}
} catch (ignore: IllegalAccessException) { // Do nothing
} catch (ignore: InvocationTargetException) { // Do nothing
}
}
}
}
@RequiresApi(29)
private object CanvasZHelper {
@DoNotInline
fun enableZ(canvas: Canvas, enable: Boolean) {
if (enable) {
canvas.enableZ()
} else {
canvas.disableZ()
}
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.mercury.ui.design.graphics
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.Vertices
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
/**
* Stub implementation of [Canvas] to be used to ensure
* the internal canvas object within [DrawScope] is never
* null. All methods here are no-ops to ensure no
* null pointer exceptions are thrown at runtime. During
* normal use, the canvas used within [DrawScope] is
* consuming a valid Canvas that draws content
* into a valid destination
*/
internal class EmptyCanvas : Canvas {
override fun save() {
throw UnsupportedOperationException()
}
override fun restore() {
throw UnsupportedOperationException()
}
override fun saveLayer(bounds: Rect, paint: Paint) {
throw UnsupportedOperationException()
}
override fun translate(dx: Float, dy: Float) {
throw UnsupportedOperationException()
}
override fun scale(sx: Float, sy: Float) {
throw UnsupportedOperationException()
}
override fun rotate(degrees: Float) {
throw UnsupportedOperationException()
}
override fun skew(sx: Float, sy: Float) {
throw UnsupportedOperationException()
}
override fun concat(matrix: Matrix) {
throw UnsupportedOperationException()
}
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
throw UnsupportedOperationException()
}
override fun clipPath(path: Path, clipOp: ClipOp) {
throw UnsupportedOperationException()
}
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawRect(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
radiusX: Float,
radiusY: Float,
paint: Paint
) {
throw UnsupportedOperationException()
}
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawArc(
left: Float,
top: Float,
right: Float,
bottom: Float,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
paint: Paint
) {
throw UnsupportedOperationException()
}
override fun drawPath(path: Path, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawImageRect(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
paint: Paint
) {
throw UnsupportedOperationException()
}
override fun drawPoints(pointMode: PointMode, points: List<Offset>, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawRawPoints(pointMode: PointMode, points: FloatArray, paint: Paint) {
throw UnsupportedOperationException()
}
override fun drawVertices(vertices: Vertices, blendMode: BlendMode, paint: Paint) {
throw UnsupportedOperationException()
}
override fun enableZ() {
throw UnsupportedOperationException()
}
override fun disableZ() {
throw UnsupportedOperationException()
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* 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.mercury.ui.design.graphics
import android.graphics.RecordingCanvas
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.DrawContext
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.DrawTransform
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
/**
* Default density value that is used as a stub to provide a non-null
* density parameter within CanvasDrawScope.
* Density is provided as a parameter as part of the draw call to
* issue drawing commands into a target canvas so this Density value is never consumed
*/
private val DefaultDensity = Density(1.0f, 1.0f)
/**
* Implementation of [DrawScope] that issues drawing commands
* into the specified canvas and bounds via [CanvasDrawScope.draw]
*/
@RequiresApi(29)
internal class RecordingCanvasDrawScope : DrawScope {
@PublishedApi internal val drawParams = DrawParams()
override val layoutDirection: LayoutDirection
get() = drawParams.layoutDirection
override val density: Float
get() = drawParams.density.density
override val fontScale: Float
get() = drawParams.density.fontScale
override val drawContext = object : DrawContext {
override val canvas: Canvas
get() = drawParams.canvas
override var size: Size
get() = drawParams.size
set(value) {
drawParams.size = value
}
override val transform: DrawTransform = asDrawTransform()
}
/**
* Internal [Paint] used only for drawing filled in shapes with a color or gradient
* This is lazily allocated on the first drawing command that uses the [Fill] [DrawStyle]
* and re-used across subsequent calls
*/
private var fillPaint: Paint? = null
/**
* Internal [Paint] used only for drawing stroked shapes with a color or gradient
* This is lazily allocated on the first drawing command that uses the [Stroke] [DrawStyle]
* and re-used across subsequent calls
*/
private var strokePaint: Paint? = null
/**
* @see [DrawScope.drawLine]
*/
override fun drawLine(
brush: Brush,
start: Offset,
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawLine(
start,
end,
configureStrokePaint(
brush,
strokeWidth,
Stroke.DefaultMiter,
cap,
StrokeJoin.Miter,
pathEffect,
alpha,
colorFilter,
blendMode
)
)
/**
* @see [DrawScope.drawLine]
*/
override fun drawLine(
color: Color,
start: Offset,
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawLine(
start,
end,
configureStrokePaint(
color,
strokeWidth,
Stroke.DefaultMiter,
cap,
StrokeJoin.Miter,
pathEffect,
alpha,
colorFilter,
blendMode
)
)
/**
* @see [DrawScope.drawRect]
*/
override fun drawRect(
brush: Brush,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawRect(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
paint = configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawRect]
*/
override fun drawRect(
color: Color,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawRect(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
paint = configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawImage]
*/
override fun drawImage(
image: ImageBitmap,
topLeft: Offset,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawImage(
image,
topLeft,
configurePaint(null, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawImage]
*/
override fun drawImage(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawImageRect(
image,
srcOffset,
srcSize,
dstOffset,
dstSize,
configurePaint(null, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawRoundRect]
*/
override fun drawRoundRect(
brush: Brush,
topLeft: Offset,
size: Size,
cornerRadius: CornerRadius,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawRoundRect(
topLeft.x,
topLeft.y,
topLeft.x + size.width,
topLeft.y + size.height,
cornerRadius.x,
cornerRadius.y,
configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawRoundRect]
*/
override fun drawRoundRect(
color: Color,
topLeft: Offset,
size: Size,
cornerRadius: CornerRadius,
style: DrawStyle,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawRoundRect(
topLeft.x,
topLeft.y,
topLeft.x + size.width,
topLeft.y + size.height,
cornerRadius.x,
cornerRadius.y,
configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawCircle]
*/
override fun drawCircle(
brush: Brush,
radius: Float,
center: Offset,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawCircle(
center,
radius,
configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawCircle]
*/
override fun drawCircle(
color: Color,
radius: Float,
center: Offset,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawCircle(
center,
radius,
configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawOval]
*/
override fun drawOval(
brush: Brush,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawOval(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
paint = configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawOval]
*/
override fun drawOval(
color: Color,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawOval(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
paint = configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawArc]
*/
override fun drawArc(
brush: Brush,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawArc(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = useCenter,
paint = configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawArc]
*/
override fun drawArc(
color: Color,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
topLeft: Offset,
size: Size,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawArc(
left = topLeft.x,
top = topLeft.y,
right = topLeft.x + size.width,
bottom = topLeft.y + size.height,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = useCenter,
paint = configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawPath]
*/
override fun drawPath(
path: Path,
color: Color,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawPath(
path,
configurePaint(color, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawPath]
*/
override fun drawPath(
path: Path,
brush: Brush,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawPath(
path,
configurePaint(brush, style, alpha, colorFilter, blendMode)
)
/**
* @see [DrawScope.drawPoints]
*/
override fun drawPoints(
points: List<Offset>,
pointMode: PointMode,
color: Color,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawPoints(
pointMode,
points,
configureStrokePaint(
color,
strokeWidth,
Stroke.DefaultMiter,
cap,
StrokeJoin.Miter,
pathEffect,
alpha,
colorFilter,
blendMode
)
)
/**
* @see [DrawScope.drawPoints]
*/
override fun drawPoints(
points: List<Offset>,
pointMode: PointMode,
brush: Brush,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = drawParams.canvas.drawPoints(
pointMode,
points,
configureStrokePaint(
brush,
strokeWidth,
Stroke.DefaultMiter,
cap,
StrokeJoin.Miter,
pathEffect,
alpha,
colorFilter,
blendMode
)
)
/**
* Draws into the provided [Canvas] with the commands specified in the lambda with this
* [DrawScope] as a receiver
*
* @param canvas target canvas to render into
* @param size bounds relative to the current canvas translation in which the [DrawScope]
* should draw within
* @param block lambda that is called to issue drawing commands on this [DrawScope]
*/
inline fun draw(
density: Density,
layoutDirection: LayoutDirection,
canvas: Canvas,
size: Size,
block: DrawScope.() -> Unit
) {
// Remember the previous drawing parameters in case we are temporarily re-directing our
// drawing to a separate Layer/RenderNode only to draw that content back into the original
// Canvas. If there is no previous canvas that was being drawing into, this ends up
// resetting these parameters back to defaults defensively
val (prevDensity, prevLayoutDirection, prevCanvas, prevSize) = drawParams
drawParams.apply {
this.density = density
this.layoutDirection = layoutDirection
this.canvas = canvas
this.size = size
}
canvas.save()
this.block()
canvas.restore()
drawParams.apply {
this.density = prevDensity
this.layoutDirection = prevLayoutDirection
this.canvas = prevCanvas
this.size = prevSize
}
}
/**
* Internal published APIs used to support inline scoped extension methods
* on DrawScope directly, without exposing the underlying stateful APIs
* to conduct the transformations themselves as inline methods require
* all methods called within them to be public
*/
/**
* Helper method to instantiate the paint object on first usage otherwise
* return the previously allocated Paint used for drawing filled regions
*/
private fun obtainFillPaint(): Paint =
fillPaint ?: Paint().apply { style = PaintingStyle.Fill }.also {
fillPaint = it
}
/**
* Helper method to instantiate the paint object on first usage otherwise
* return the previously allocated Paint used for drawing strokes
*/
private fun obtainStrokePaint(): Paint =
strokePaint ?: Paint().apply { style = PaintingStyle.Stroke }.also {
strokePaint = it
}
/**
* Selects the appropriate [Paint] object based on the style
* and applies the underlying [DrawStyle] parameters
*/
private fun selectPaint(drawStyle: DrawStyle): Paint =
when (drawStyle) {
Fill -> obtainFillPaint()
is Stroke ->
obtainStrokePaint()
.apply {
if (strokeWidth != drawStyle.width) strokeWidth = drawStyle.width
if (strokeCap != drawStyle.cap) strokeCap = drawStyle.cap
if (strokeMiterLimit != drawStyle.miter) strokeMiterLimit = drawStyle.miter
if (strokeJoin != drawStyle.join) strokeJoin = drawStyle.join
if (pathEffect != drawStyle.pathEffect) pathEffect = drawStyle.pathEffect
}
}
/**
* Helper method to configure the corresponding [Brush] along with other properties
* on the corresponding paint specified by [DrawStyle]
*/
private fun configurePaint(
brush: Brush?,
style: DrawStyle,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
): Paint = selectPaint(style).apply {
if (brush != null) {
brush.applyTo(size, this, alpha)
} else if (this.alpha != alpha) {
this.alpha = alpha
}
if (this.colorFilter != colorFilter) this.colorFilter = colorFilter
if (this.blendMode != blendMode) this.blendMode = blendMode
}
/**
* Helper method to configure the corresponding [Color] along with other properties
* on the corresponding paint specified by [DrawStyle]
*/
private fun configurePaint(
color: Color,
style: DrawStyle,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
): Paint = selectPaint(style).apply {
// Modulate the color alpha directly
// instead of configuring a separate alpha parameter
val targetColor = color.modulate(alpha)
if (this.color != targetColor) this.color = targetColor
if (this.shader != null) this.shader = null
if (this.colorFilter != colorFilter) this.colorFilter = colorFilter
if (this.blendMode != blendMode) this.blendMode = blendMode
}
private fun configureStrokePaint(
color: Color,
strokeWidth: Float,
miter: Float,
cap: StrokeCap,
join: StrokeJoin,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) =
obtainStrokePaint().apply {
// Modulate the color alpha directly
// instead of configuring a separate alpha parameter
val targetColor = color.modulate(alpha)
if (this.color != targetColor) this.color = targetColor
if (this.shader != null) this.shader = null
if (this.colorFilter != colorFilter) this.colorFilter = colorFilter
if (this.blendMode != blendMode) this.blendMode = blendMode
if (this.strokeWidth != strokeWidth) this.strokeWidth = strokeWidth
if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
if (this.strokeCap != cap) this.strokeCap = cap
if (this.strokeJoin != join) this.strokeJoin = join
if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
}
private fun configureStrokePaint(
brush: Brush?,
strokeWidth: Float,
miter: Float,
cap: StrokeCap,
join: StrokeJoin,
pathEffect: PathEffect?,
/*FloatRange(from = 0.0, to = 1.0)*/
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
) = obtainStrokePaint().apply {
if (brush != null) {
brush.applyTo(size, this, alpha)
} else if (this.alpha != alpha) {
this.alpha = alpha
}
if (this.colorFilter != colorFilter) this.colorFilter = colorFilter
if (this.blendMode != blendMode) this.blendMode = blendMode
if (this.strokeWidth != strokeWidth) this.strokeWidth = strokeWidth
if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
if (this.strokeCap != cap) this.strokeCap = cap
if (this.strokeJoin != join) this.strokeJoin = join
if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
}
/**
* Returns a [Color] modulated with the given alpha value
*/
private fun Color.modulate(alpha: Float): Color =
if (alpha != 1.0f) {
copy(alpha = this.alpha * alpha)
} else {
this
}
/**
* Internal parameters to represent the current CanvasDrawScope
* used to reduce the size of the inline draw call to avoid
* bloat of additional assignment calls for each parameter
* individually
*/
@PublishedApi internal data class DrawParams(
var density: Density = DefaultDensity,
var layoutDirection: LayoutDirection = LayoutDirection.Ltr,
var canvas: Canvas = EmptyCanvas(),
var size: Size = Size.Zero
)
}
/**
* Convenience method for creating a [DrawTransform] from the current [DrawContext]
*/
private fun DrawContext.asDrawTransform(): DrawTransform = object : DrawTransform {
override val size: Size
get() = this@asDrawTransform.size
override val center: Offset
get() = size.center
override fun inset(left: Float, top: Float, right: Float, bottom: Float) {
this@asDrawTransform.canvas.let {
val updatedSize = Size(size.width - (left + right), size.height - (top + bottom))
require(updatedSize.width >= 0 && updatedSize.height >= 0) {
"Width and height must be greater than or equal to zero"
}
this@asDrawTransform.size = updatedSize
it.translate(left, top)
}
}
override fun clipRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
clipOp: ClipOp
) {
this@asDrawTransform.canvas.clipRect(left, top, right, bottom, clipOp)
}
override fun clipPath(path: Path, clipOp: ClipOp) {
this@asDrawTransform.canvas.clipPath(path, clipOp)
}
override fun translate(left: Float, top: Float) {
this@asDrawTransform.canvas.translate(left, top)
}
override fun rotate(degrees: Float, pivot: Offset) {
this@asDrawTransform.canvas.apply {
translate(pivot.x, pivot.y)
rotate(degrees)
translate(-pivot.x, -pivot.y)
}
}
override fun scale(scaleX: Float, scaleY: Float, pivot: Offset) {
this@asDrawTransform.canvas.apply {
translate(pivot.x, pivot.y)
scale(scaleX, scaleY)
translate(-pivot.x, -pivot.y)
}
}
override fun transform(matrix: Matrix) {
this@asDrawTransform.canvas.concat(matrix)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment