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 |
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap |
||||
import androidx.compose.ui.graphics.painter.Painter |
||||
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.svg.SVGDOM |
||||
|
||||
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