Konstantin
7 months ago
committed by
GitHub
11 changed files with 307 additions and 9 deletions
@ -0,0 +1,8 @@ |
|||||||
|
package org.jetbrains.compose.resources.demo.shared |
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
|
||||||
|
@Composable |
||||||
|
actual fun SvgShowcase() { |
||||||
|
//Android platform doesn't support SVG resources |
||||||
|
} |
After Width: | Height: | Size: 817 B |
@ -0,0 +1,39 @@ |
|||||||
|
package org.jetbrains.compose.resources.demo.shared |
||||||
|
|
||||||
|
import androidx.compose.foundation.Image |
||||||
|
import androidx.compose.foundation.layout.Column |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.foundation.layout.padding |
||||||
|
import androidx.compose.foundation.layout.size |
||||||
|
import androidx.compose.material3.OutlinedCard |
||||||
|
import androidx.compose.material3.Text |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import components.resources.demo.shared.generated.resources.Res |
||||||
|
import components.resources.demo.shared.generated.resources.sailing |
||||||
|
import org.jetbrains.compose.resources.painterResource |
||||||
|
|
||||||
|
@Composable |
||||||
|
actual fun SvgShowcase() { |
||||||
|
OutlinedCard(modifier = Modifier.padding(8.dp)) { |
||||||
|
Column( |
||||||
|
modifier = Modifier.padding(16.dp).fillMaxWidth(), |
||||||
|
horizontalAlignment = Alignment.CenterHorizontally |
||||||
|
) { |
||||||
|
Image( |
||||||
|
modifier = Modifier.size(100.dp), |
||||||
|
painter = painterResource(Res.drawable.sailing), |
||||||
|
contentDescription = null |
||||||
|
) |
||||||
|
Text( |
||||||
|
""" |
||||||
|
Image( |
||||||
|
painter = painterResource(Res.drawable.sailing) |
||||||
|
) |
||||||
|
""".trimIndent() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
package org.jetbrains.compose.resources |
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.BlendMode |
||||||
|
import androidx.compose.ui.graphics.Canvas |
||||||
|
import androidx.compose.ui.graphics.Color |
||||||
|
import androidx.compose.ui.graphics.ColorFilter |
||||||
|
import androidx.compose.ui.graphics.ImageBitmap |
||||||
|
import androidx.compose.ui.graphics.ImageBitmapConfig |
||||||
|
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope |
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope |
||||||
|
import androidx.compose.ui.unit.Density |
||||||
|
import androidx.compose.ui.unit.IntSize |
||||||
|
import androidx.compose.ui.unit.LayoutDirection |
||||||
|
import androidx.compose.ui.unit.toSize |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a drawing environment that directs its drawing commands to an [ImageBitmap] |
||||||
|
* which can be drawn directly in another [DrawScope] instance. This is useful to cache |
||||||
|
* complicated drawing commands across frames especially if the content has not changed. |
||||||
|
* Additionally some drawing operations such as rendering paths are done purely in |
||||||
|
* software so it is beneficial to cache the result and render the contents |
||||||
|
* directly through a texture as done by [DrawScope.drawImage] |
||||||
|
*/ |
||||||
|
internal class DrawCache { |
||||||
|
|
||||||
|
@PublishedApi internal var mCachedImage: ImageBitmap? = null |
||||||
|
private var cachedCanvas: Canvas? = null |
||||||
|
private var scopeDensity: Density? = null |
||||||
|
private var layoutDirection: LayoutDirection = LayoutDirection.Ltr |
||||||
|
private var size: IntSize = IntSize.Zero |
||||||
|
private var config: ImageBitmapConfig = ImageBitmapConfig.Argb8888 |
||||||
|
|
||||||
|
private val cacheScope = CanvasDrawScope() |
||||||
|
|
||||||
|
/** |
||||||
|
* Draw the contents of the lambda with receiver scope into an [ImageBitmap] with the provided |
||||||
|
* size. If the same size is provided across calls, the same [ImageBitmap] instance is |
||||||
|
* re-used and the contents are cleared out before drawing content in it again |
||||||
|
*/ |
||||||
|
fun drawCachedImage( |
||||||
|
config: ImageBitmapConfig, |
||||||
|
size: IntSize, |
||||||
|
density: Density, |
||||||
|
layoutDirection: LayoutDirection, |
||||||
|
block: DrawScope.() -> Unit |
||||||
|
) { |
||||||
|
this.scopeDensity = density |
||||||
|
this.layoutDirection = layoutDirection |
||||||
|
var targetImage = mCachedImage |
||||||
|
var targetCanvas = cachedCanvas |
||||||
|
if (targetImage == null || |
||||||
|
targetCanvas == null || |
||||||
|
size.width > targetImage.width || |
||||||
|
size.height > targetImage.height || |
||||||
|
this.config != config |
||||||
|
) { |
||||||
|
targetImage = ImageBitmap(size.width, size.height, config = config) |
||||||
|
targetCanvas = Canvas(targetImage) |
||||||
|
|
||||||
|
mCachedImage = targetImage |
||||||
|
cachedCanvas = targetCanvas |
||||||
|
this.config = config |
||||||
|
} |
||||||
|
this.size = size |
||||||
|
cacheScope.draw(density, layoutDirection, targetCanvas, size.toSize()) { |
||||||
|
clear() |
||||||
|
block() |
||||||
|
} |
||||||
|
targetImage.prepareToDraw() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Draw the cached content into the provided [DrawScope] instance |
||||||
|
*/ |
||||||
|
fun drawInto( |
||||||
|
target: DrawScope, |
||||||
|
alpha: Float = 1.0f, |
||||||
|
colorFilter: ColorFilter? = null |
||||||
|
) { |
||||||
|
val targetImage = mCachedImage |
||||||
|
check(targetImage != null) { |
||||||
|
"drawCachedImage must be invoked first before attempting to draw the result " + |
||||||
|
"into another destination" |
||||||
|
} |
||||||
|
target.drawImage(targetImage, srcSize = size, alpha = alpha, colorFilter = colorFilter) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper method to clear contents of the draw environment from the given bounds of the |
||||||
|
* DrawScope |
||||||
|
*/ |
||||||
|
private fun DrawScope.clear() { |
||||||
|
drawRect(color = Color.Black, blendMode = BlendMode.Clear) |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,20 @@ |
|||||||
package org.jetbrains.compose.resources |
package org.jetbrains.compose.resources |
||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap |
import androidx.compose.ui.graphics.ImageBitmap |
||||||
|
import androidx.compose.ui.graphics.painter.Painter |
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap |
import androidx.compose.ui.graphics.toComposeImageBitmap |
||||||
|
import androidx.compose.ui.unit.Density |
||||||
|
import org.jetbrains.skia.Data |
||||||
import org.jetbrains.skia.Image |
import org.jetbrains.skia.Image |
||||||
|
import org.jetbrains.skia.svg.SVGDOM |
||||||
|
|
||||||
internal actual fun ByteArray.toImageBitmap(): ImageBitmap = |
internal actual fun ByteArray.toImageBitmap(): ImageBitmap = |
||||||
Image.makeFromEncoded(this).toComposeImageBitmap() |
Image.makeFromEncoded(this).toComposeImageBitmap() |
||||||
|
|
||||||
|
internal actual class SvgElement(val svgdom: SVGDOM) |
||||||
|
|
||||||
|
internal actual fun ByteArray.toSvgElement(): SvgElement = |
||||||
|
SvgElement(SVGDOM(Data.makeFromBytes(this))) |
||||||
|
|
||||||
|
internal actual fun SvgElement.toSvgPainter(density: Density): Painter = |
||||||
|
SvgPainter(svgdom, density) |
@ -0,0 +1,91 @@ |
|||||||
|
package org.jetbrains.compose.resources |
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Size |
||||||
|
import androidx.compose.ui.geometry.isSpecified |
||||||
|
import androidx.compose.ui.graphics.ColorFilter |
||||||
|
import androidx.compose.ui.graphics.ImageBitmapConfig |
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope |
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas |
||||||
|
import androidx.compose.ui.graphics.nativeCanvas |
||||||
|
import androidx.compose.ui.graphics.painter.Painter |
||||||
|
import androidx.compose.ui.unit.Density |
||||||
|
import androidx.compose.ui.unit.IntSize |
||||||
|
import org.jetbrains.skia.Rect |
||||||
|
import org.jetbrains.skia.svg.SVGDOM |
||||||
|
import org.jetbrains.skia.svg.SVGLength |
||||||
|
import org.jetbrains.skia.svg.SVGLengthUnit |
||||||
|
import org.jetbrains.skia.svg.SVGPreserveAspectRatio |
||||||
|
import org.jetbrains.skia.svg.SVGPreserveAspectRatioAlign |
||||||
|
import kotlin.math.ceil |
||||||
|
|
||||||
|
internal class SvgPainter( |
||||||
|
private val dom: SVGDOM, |
||||||
|
private val density: Density |
||||||
|
) : Painter() { |
||||||
|
private val root = dom.root |
||||||
|
|
||||||
|
private val defaultSizePx: Size = run { |
||||||
|
val width = root?.width?.withUnit(SVGLengthUnit.PX)?.value ?: 0f |
||||||
|
val height = root?.height?.withUnit(SVGLengthUnit.PX)?.value ?: 0f |
||||||
|
if (width == 0f && height == 0f) { |
||||||
|
Size.Unspecified |
||||||
|
} else { |
||||||
|
Size(width, height) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
init { |
||||||
|
if (root?.viewBox == null && defaultSizePx.isSpecified) { |
||||||
|
root?.viewBox = Rect.makeXYWH(0f, 0f, defaultSizePx.width, defaultSizePx.height) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override val intrinsicSize: Size get() { |
||||||
|
return if (defaultSizePx.isSpecified) { |
||||||
|
defaultSizePx * density.density |
||||||
|
} else { |
||||||
|
Size.Unspecified |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private var previousDrawSize: Size = Size.Unspecified |
||||||
|
private var alpha: Float = 1.0f |
||||||
|
private var colorFilter: ColorFilter? = null |
||||||
|
|
||||||
|
// with caching into bitmap FPS is 3x-4x higher (tested with idea-logo.svg with 30x30 icons) |
||||||
|
private val drawCache = DrawCache() |
||||||
|
|
||||||
|
override fun applyAlpha(alpha: Float): Boolean { |
||||||
|
this.alpha = alpha |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { |
||||||
|
this.colorFilter = colorFilter |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
override fun DrawScope.onDraw() { |
||||||
|
if (previousDrawSize != size) { |
||||||
|
drawCache.drawCachedImage( |
||||||
|
ImageBitmapConfig.Argb8888, |
||||||
|
IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()), |
||||||
|
density = this, |
||||||
|
layoutDirection, |
||||||
|
) { |
||||||
|
drawSvg(size) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
drawCache.drawInto(this, alpha, colorFilter) |
||||||
|
} |
||||||
|
|
||||||
|
private fun DrawScope.drawSvg(size: Size) { |
||||||
|
drawIntoCanvas { canvas -> |
||||||
|
root?.width = SVGLength(size.width, SVGLengthUnit.PX) |
||||||
|
root?.height = SVGLength(size.height, SVGLengthUnit.PX) |
||||||
|
root?.preserveAspectRatio = SVGPreserveAspectRatio(SVGPreserveAspectRatioAlign.NONE) |
||||||
|
dom.render(canvas.nativeCanvas) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue