Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package androidx.compose.ui.graphics

import androidx.compose.runtime.InternalComposeApi
import androidx.compose.ui.InternalComposeUiApi
import kotlin.jvm.JvmName
import androidx.compose.ui.geometry.Offset
Expand All @@ -35,12 +36,13 @@ import org.jetbrains.skia.Image
import org.jetbrains.skia.Matrix44
import org.jetbrains.skia.MipmapMode
import org.jetbrains.skia.SamplingMode
import org.jetbrains.skia.Surface
import org.jetbrains.skia.impl.use

/**
* Convert the [org.jetbrains.skia.Canvas] instance into a Compose-compatible Canvas
*/
fun SkCanvas.asComposeCanvas(): Canvas = SkiaBackedCanvas(this)
fun SkCanvas.asComposeCanvas(): Canvas = SkiaBackedCanvas().apply { internalSkiaCanvas = this@asComposeCanvas }

/**
* Provides access to the underlying [org.jetbrains.skia.Canvas] instance.
Expand All @@ -62,9 +64,31 @@ val Canvas.skiaCanvas: SkCanvas
val Canvas.nativeCanvas: SkCanvas
get() = skiaCanvas

internal class SkiaBackedCanvas(
internal val internalSkiaCanvas: SkCanvas,
) : Canvas {
// 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 = Surface.makeNull(1,1).canvas

/**
* Holder class that is used to issue scoped calls to a [Canvas]
* without having to allocate a SkiaBackedCanvas on each draw call.
*/
@InternalComposeApi
class SkiaCanvasHolder(skiaCanvas : SkCanvas? = null) {
internal val skiaBackedCanvas = SkiaBackedCanvas().apply { internalSkiaCanvas = skiaCanvas ?: EmptyCanvas }

internal inline fun drawInto(targetCanvas: SkCanvas, crossinline block: Canvas.() -> Unit) {
val previousCanvas = skiaBackedCanvas.internalSkiaCanvas
skiaBackedCanvas.internalSkiaCanvas = targetCanvas
skiaBackedCanvas.block()
skiaBackedCanvas.internalSkiaCanvas = previousCanvas
}
}

internal class SkiaBackedCanvas : Canvas {

// Keep the internal canvas as a var prevent having to allocate an AndroidCanvas
// instance on each draw call
internal var internalSkiaCanvas: SkCanvas = EmptyCanvas
override fun save() {
internalSkiaCanvas.save()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import org.jetbrains.skia.Matrix33 as SkMatrix3
import org.jetbrains.skia.Path as SkPath
import org.jetbrains.skia.PathDirection
import org.jetbrains.skia.PathBuilder
Expand Down Expand Up @@ -93,6 +94,11 @@ internal class SkiaBackedPath(
*/
internal var isSkiaPathObserved = false

// Temporary value holders to reuse an object (not part of a state):
private var radii: FloatArray? = null

private var mMatrix: SkMatrix3? = null

private inline fun mutatePath(block: PathBuilder.() -> Unit) {
synchronizeBuilderIfNeeded()
pathBuilder.apply(block)
Expand Down Expand Up @@ -301,42 +307,29 @@ internal class SkiaBackedPath(
replaceWith = ReplaceWith("addRoundRect(roundRect)"),
level = DeprecationLevel.HIDDEN
)
override fun addRoundRect(roundRect: RoundRect) = mutatePath {
addRRect(
roundRect.left,
roundRect.top,
roundRect.right,
roundRect.bottom,
floatArrayOf(
roundRect.topLeftCornerRadius.x,
roundRect.topLeftCornerRadius.y,
roundRect.topRightCornerRadius.x,
roundRect.topRightCornerRadius.y,
roundRect.bottomRightCornerRadius.x,
roundRect.bottomRightCornerRadius.y,
roundRect.bottomLeftCornerRadius.x,
roundRect.bottomLeftCornerRadius.y
),
PathDirection.COUNTER_CLOCKWISE
)
}
override fun addRoundRect(roundRect: RoundRect) = addRoundRect(roundRect)

override fun addRoundRect(roundRect: RoundRect, direction: Path.Direction) = mutatePath {
if (radii == null) radii = FloatArray(8)
with(radii!!) {
this[0] = roundRect.topLeftCornerRadius.x
this[1] = roundRect.topLeftCornerRadius.y

this[2] = roundRect.topRightCornerRadius.x
this[3] = roundRect.topRightCornerRadius.y

this[4] = roundRect.bottomRightCornerRadius.x
this[5] = roundRect.bottomRightCornerRadius.y

this[6] = roundRect.bottomLeftCornerRadius.x
this[7] = roundRect.bottomLeftCornerRadius.y
}
addRRect(
roundRect.left,
roundRect.top,
roundRect.right,
roundRect.bottom,
floatArrayOf(
roundRect.topLeftCornerRadius.x,
roundRect.topLeftCornerRadius.y,
roundRect.topRightCornerRadius.x,
roundRect.topRightCornerRadius.y,
roundRect.bottomRightCornerRadius.x,
roundRect.bottomRightCornerRadius.y,
roundRect.bottomLeftCornerRadius.x,
roundRect.bottomLeftCornerRadius.y
),
radii!!,
direction.toSkiaPathDirection()
)
}
Expand Down Expand Up @@ -376,7 +369,9 @@ internal class SkiaBackedPath(
}

override fun transform(matrix: Matrix) = mutatePath {
transform(identityMatrix33().apply { setFrom(matrix) })
if (mMatrix == null) mMatrix = identityMatrix33()
mMatrix!!.setFrom(matrix)
transform(mMatrix!!)
}

override fun getBounds(): Rect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ private fun List<Color>.toSkiaGradient(
),
)

/**
* This overload avoids iterator allocation in comparison with its stdlib counterpart
*/
@Suppress("NOTHING_TO_INLINE")
internal inline fun List<Float>.toFloatArray(): FloatArray = FloatArray(size) { i -> this[i] }

private fun List<Color>.toColor4fArray(): Array<Color4f> =
Array(size) { i ->
val color = this[i]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.graphics.layer

import androidx.compose.runtime.InternalComposeApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isUnspecified
Expand All @@ -26,8 +27,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.SkiaBackedCanvas
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.SkiaCanvasHolder
import androidx.compose.ui.graphics.asSkiaColorFilter
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
Expand Down Expand Up @@ -68,6 +68,12 @@ internal class SkikoGraphicsLayer(
private var cachedOutline: Outline? = null
private var cachedClip: Boolean = false

// Temporary value holders to reuse an object (not part of a state):
private var radii: FloatArray? = null

@OptIn(InternalComposeApi::class)
private val canvasHolder : SkiaCanvasHolder = SkiaCanvasHolder()

override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
set(value) {
field = value
Expand Down Expand Up @@ -204,23 +210,30 @@ internal class SkikoGraphicsLayer(
outline.rect.bottom + dy,
antiAlias = true
)
is Outline.Rounded -> renderNode.setClipRRect(
outline.roundRect.left + dx,
outline.roundRect.top + dy,
outline.roundRect.right + dx,
outline.roundRect.bottom + dy,
floatArrayOf(
outline.roundRect.topLeftCornerRadius.x,
outline.roundRect.topLeftCornerRadius.y,
outline.roundRect.topRightCornerRadius.x,
outline.roundRect.topRightCornerRadius.y,
outline.roundRect.bottomRightCornerRadius.x,
outline.roundRect.bottomRightCornerRadius.y,
outline.roundRect.bottomLeftCornerRadius.x,
outline.roundRect.bottomLeftCornerRadius.y
),
antiAlias = true
)
is Outline.Rounded -> {
if (radii == null) radii = FloatArray(8)
with(radii!!) {
this[0] = outline.roundRect.topLeftCornerRadius.x
this[1] = outline.roundRect.topLeftCornerRadius.y

this[2] = outline.roundRect.topRightCornerRadius.x
this[3] = outline.roundRect.topRightCornerRadius.y

this[4] = outline.roundRect.bottomRightCornerRadius.x
this[5] = outline.roundRect.bottomRightCornerRadius.y

this[6] = outline.roundRect.bottomLeftCornerRadius.x
this[7] = outline.roundRect.bottomLeftCornerRadius.y
}
renderNode.setClipRRect(
outline.roundRect.left,
outline.roundRect.top,
outline.roundRect.right,
outline.roundRect.bottom,
radii!!,
antiAlias = true
)
}
is Outline.Generic -> renderNode.setClipPath(
updatePathOutline(outline.path),
antiAlias = true
Expand All @@ -236,6 +249,7 @@ internal class SkikoGraphicsLayer(
path
}.materializeSkiaPath()

@OptIn(InternalComposeApi::class)
override fun record(
density: Density,
layoutDirection: LayoutDirection,
Expand All @@ -245,21 +259,23 @@ internal class SkikoGraphicsLayer(
val renderNode = renderNode ?: return
val recordingCanvas = renderNode.beginRecording()
try {
val composeCanvas = recordingCanvas.asComposeCanvas() as SkiaBackedCanvas
if (outsetLeft > 0 || outsetTop > 0) {
composeCanvas.save()
composeCanvas.translate(outsetLeft.toFloat(), outsetTop.toFloat())
}
pictureDrawScope.draw(
density = density,
layoutDirection = layoutDirection,
canvas = composeCanvas,
size = layer.size.toSize(),
graphicsLayer = layer,
block = block,
)
if (outsetLeft > 0 || outsetTop > 0) {
composeCanvas.restore()
@Suppress("INVISIBLE_REFERENCE")
canvasHolder.drawInto(recordingCanvas) {
if (outsetLeft > 0 || outsetTop > 0) {
save()
translate(outsetLeft.toFloat(), outsetTop.toFloat())
}
pictureDrawScope.draw(
density = density,
layoutDirection = layoutDirection,
canvas = this,
size = layer.size.toSize(),
graphicsLayer = layer,
block = block,
)
if (outsetLeft > 0 || outsetTop > 0) {
restore()
}
}
} finally {
renderNode.endRecording()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
package androidx.compose.ui.test

import androidx.compose.runtime.Composable
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.SkiaCanvasHolder
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.node.RootForTest
Expand Down Expand Up @@ -227,6 +228,9 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
)

private val surface = Surface.makeRasterN32Premul(width, height)

@OptIn(InternalComposeApi::class)
private val canvasHolder : SkiaCanvasHolder = SkiaCanvasHolder(surface.canvas)
private val size = IntSize(width, height)

@InternalComposeUiApi
Expand Down Expand Up @@ -325,11 +329,13 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
* so a capture reflects the latest state. Draw is decoupled from idle, so producing
* an up-to-date image is the capture's responsibility rather than the idle loop's.
*/
@OptIn(InternalComposeApi::class)
private fun redraw() = runOnUiThread {
scene.measureAndLayout()
with(surface.canvas) {
clear(Color.TRANSPARENT)
scene.draw(asComposeCanvas())
@Suppress("INVISIBLE_REFERENCE")
canvasHolder.drawInto(this) { scene.draw(this) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package androidx.compose.ui.scene

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.ComposeUiFlags
Expand All @@ -32,7 +33,8 @@ import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.SkiaCanvasHolder
import androidx.compose.ui.graphics.toAwtImage
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.input.InputModeManager
Expand Down Expand Up @@ -193,6 +195,8 @@ internal class ComposeSceneMediator(
val renderApi by skiaLayerComponent::renderApi
val semanticsOwners: Collection<SemanticsOwner> by semanticsOwnerManager::semanticsOwners

@OptIn(InternalComposeApi::class)
private val canvasHolder: SkiaCanvasHolder = SkiaCanvasHolder()
/**
* @see ComposeFeatureFlags.useInteropBlending
*/
Expand Down Expand Up @@ -739,13 +743,14 @@ internal class ComposeSceneMediator(
interopContainer.postponingExecutingScheduledUpdates {
canvas.withSceneOffset {
with(sceneRenderingScope) {
scene.render(frameRecomposer, asComposeCanvas(), nanoTime)
scene.render(frameRecomposer, this@withSceneOffset, nanoTime)
}
}
}
}

private inline fun SkCanvas.withSceneOffset(crossinline block: SkCanvas.() -> Unit) {
@OptIn(InternalComposeApi::class)
private inline fun SkCanvas.withSceneOffset(crossinline block: Canvas.() -> Unit) {
// Offset of scene relative to [container]
val sceneBoundsOffset = sceneBoundsInPx?.topLeft ?: Offset.Zero
// Offset of canvas relative to [container]
Expand All @@ -756,7 +761,10 @@ internal class ComposeSceneMediator(
val sceneOffset = sceneBoundsOffset - contentOffset
save()
translate(sceneOffset.x, sceneOffset.y)
block()
@Suppress("INVISIBLE_REFERENCE")
canvasHolder.drawInto(this){
block(this@drawInto)
}
restore()
}

Expand Down Expand Up @@ -960,7 +968,7 @@ internal class ComposeSceneMediator(
target.drawScene(offsetX, offsetY, size, contentComponent.density) {
fillBackground(contentComponent.background)
if (!shouldPlaceInteropAbove) drawInterop(interopContainer.root)
drawCompose { canvas -> canvas.withSceneOffset { scene.draw(asComposeCanvas()) } }
drawCompose { canvas -> canvas.withSceneOffset { scene.draw(this) } }
if (shouldPlaceInteropAbove) drawInterop(interopContainer.root)
}
}
Expand Down
Loading