Skip to content

Commit 09541e6

Browse files
authored
Merge pull request #465 from icerockdev/develop
Release 0.22.0
2 parents 4ed22eb + c8241b7 commit 09541e6

29 files changed

Lines changed: 547 additions & 80 deletions

File tree

README.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ implement all you UI in Kotlin with Jetpack Compose and MOKO resources.
3737
- **Compose Multiplatform** support;
3838
- **StringDesc** for lifecycle-aware access to resources and unified localization on both platforms;
3939
- **Static** iOS frameworks support;
40-
- **Fat and XC** frameworks support.
40+
- **Fat and XC** frameworks support;
41+
- **SVG** images support.
4142

4243
## Requirements
4344

@@ -58,7 +59,7 @@ buildscript {
5859
}
5960
6061
dependencies {
61-
classpath "dev.icerock.moko:resources-generator:0.21.2"
62+
classpath "dev.icerock.moko:resources-generator:0.22.0"
6263
}
6364
}
6465
@@ -76,10 +77,10 @@ project build.gradle
7677
apply plugin: "dev.icerock.mobile.multiplatform-resources"
7778
7879
dependencies {
79-
commonMainApi("dev.icerock.moko:resources:0.21.2")
80-
commonMainApi("dev.icerock.moko:resources-compose:0.21.2") // for compose multiplatform
80+
commonMainApi("dev.icerock.moko:resources:0.22.0")
81+
commonMainApi("dev.icerock.moko:resources-compose:0.22.0") // for compose multiplatform
8182
82-
commonTestImplementation("dev.icerock.moko:resources-test:0.21.2")
83+
commonTestImplementation("dev.icerock.moko:resources-test:0.22.0")
8384
}
8485
8586
multiplatformResources {
@@ -98,7 +99,7 @@ should [add `export` declarations](https://kotlinlang.org/docs/multiplatform-bui
9899

99100
```
100101
framework {
101-
export("dev.icerock.moko:resources:0.21.2")
102+
export("dev.icerock.moko:resources:0.22.0")
102103
export("dev.icerock.moko:graphics:0.9.0") // toUIColor here
103104
}
104105
```
@@ -520,6 +521,7 @@ Image name should be end with one of:
520521
- `@4x` - android xxxhdpi.
521522
Supported `png` and `jpg` resources for now.
522523

524+
523525
If we add to `commonMain/resources/MR/images` files:
524526

525527
- `home_black_18@1x.png`
@@ -532,7 +534,20 @@ We got autogenerated `MR.images.home_black_18` `ImageResource` in code, that we
532534

533535
You can get images by their name too
534536

535-
in `commonMain` create a `Resources.kt` file with the content below
537+
in `commonMain` create a `Resources.kt` file with the content below.
538+
539+
The Image generator also supports `svg` files.
540+
541+
If we add to `commonMain/resources/MR/images` files:
542+
- `car_black.svg`
543+
544+
We got autogenerated `MR.images.car_black` `ImageResource` in code, that we can use:
545+
546+
- Android: `imageView.setImageResource(image.drawableResId)`
547+
- iOS: `imageView.image = image.toUIImage()`
548+
549+
On Android it is a VectorDrawable,
550+
on iOS 13 or later it is UIImage in the Assets catalog with `preserves-vector-representation` set to `true`.
536551

537552
```kotlin
538553
fun getImageByFileName(name: String): ImageResource {

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[versions]
22
kotlinVersion = "1.8.10"
33
androidGradleVersion = "7.4.2"
4+
androidSdkCommonVersion = "30.4.2"
45

56
# kotlinx
67
kotlinxSerializationVersion = "1.5.0"
@@ -23,6 +24,7 @@ kotlinPoetVersion = "1.12.0"
2324
detektVersion = "1.15.0"
2425
icu4jVersion = "70.1"
2526
commonsCodecVersion = "1.15"
27+
batikVersion = "1.16"
2628

2729
# tests
2830
espressoCoreVersion = "3.5.1"
@@ -68,6 +70,8 @@ composeGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin",
6870
# jvm
6971
kotlinPoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoetVersion" }
7072
apacheCommonsText = { module = "org.apache.commons:commons-text", version.ref = "apacheCommonsTextVersion" }
73+
batikRasterizer = { module = "org.apache.xmlgraphics:batik-rasterizer", version.ref = "batikVersion" }
74+
batikTranscoder = { module = "org.apache.xmlgraphics:batik-transcoder", version.ref = "batikVersion" }
7175
commonsCodec = { module = "commons-codec:commons-codec", version.ref = "commonsCodecVersion" }
7276
icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4jVersion" }
7377

@@ -85,6 +89,7 @@ kotlinTestAnnotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations
8589
# gradle
8690
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
8791
androidGradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "androidGradleVersion" }
92+
androidSdkCommon = { module = "com.android.tools:sdk-common", version.ref = "androidSdkCommonVersion" }
8893
kotlinCompilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlinVersion" }
8994
detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detektVersion" }
9095
mokoMultiplatformPlugin = { module = "dev.icerock:mobile-multiplatform", version.ref = "mokoMultiplatformPluginVersion" }

gradle/moko.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
resourcesVersion = "0.21.2"
2+
resourcesVersion = "0.22.0"
33

44
[libraries]
55
resources = { module = "dev.icerock.moko:resources", version.ref = "resourcesVersion" }

resources-build-logic/src/main/kotlin/publication-convention.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ publishing {
5454
name.set("Nagy Robert")
5555
email.set("nagyrobi144@gmail.com")
5656
}
57+
developer {
58+
id.set("warnyul")
59+
name.set("Balázs Varga")
60+
email.set("balazs.varga@apter.tech")
61+
}
5762
}
5863

5964
scm {

resources-compose/src/jsMain/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,34 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
1313
import androidx.compose.ui.graphics.painter.ColorPainter
1414
import androidx.compose.ui.graphics.painter.Painter
1515
import androidx.compose.ui.graphics.toComposeImageBitmap
16+
import androidx.compose.ui.platform.LocalDensity
17+
import androidx.compose.ui.unit.Density
1618
import dev.icerock.moko.resources.ImageResource
19+
import dev.icerock.moko.resources.compose.internal.SVGPainter
1720
import dev.icerock.moko.resources.compose.internal.produceByteArray
21+
import org.jetbrains.skia.Data
1822
import org.jetbrains.skia.Image
23+
import org.jetbrains.skia.svg.SVGDOM
1924

2025
@Composable
2126
actual fun painterResource(imageResource: ImageResource): Painter {
27+
2228
val bytes: ByteArray? by produceByteArray(url = imageResource.fileUrl)
2329

2430
val localBytes: ByteArray? = bytes
25-
31+
val density: Density = LocalDensity.current
2632
return remember(localBytes) {
2733
if (localBytes == null) {
2834
return@remember ColorPainter(color = Color.Transparent)
2935
}
3036

31-
val skiaImage: Image = Image.makeFromEncoded(bytes = localBytes)
32-
val imageBitmap: ImageBitmap = skiaImage.toComposeImageBitmap()
37+
if (imageResource.fileUrl.endsWith(".svg", ignoreCase = true)) {
38+
SVGPainter(SVGDOM(Data.makeFromBytes(localBytes)), density)
39+
} else {
40+
val skiaImage: Image = Image.makeFromEncoded(bytes = localBytes)
41+
val imageBitmap: ImageBitmap = skiaImage.toComposeImageBitmap()
3342

34-
BitmapPainter(image = imageBitmap)
43+
BitmapPainter(image = imageBitmap)
44+
}
3545
}
3646
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2020 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// From
18+
// org.jetbrains.compose.ui:ui-js:klib:1.3.1
19+
// DrawCache.kt
20+
// package androidx.compose.ui.graphics.vector
21+
package dev.icerock.moko.resources.compose.internal
22+
23+
import androidx.compose.ui.graphics.BlendMode
24+
import androidx.compose.ui.graphics.Canvas
25+
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.graphics.ColorFilter
27+
import androidx.compose.ui.graphics.ImageBitmap
28+
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
29+
import androidx.compose.ui.graphics.drawscope.DrawScope
30+
import androidx.compose.ui.unit.Density
31+
import androidx.compose.ui.unit.IntSize
32+
import androidx.compose.ui.unit.LayoutDirection
33+
import androidx.compose.ui.unit.toSize
34+
35+
/**
36+
* Creates a drawing environment that directs its drawing commands to an [ImageBitmap]
37+
* which can be drawn directly in another [DrawScope] instance. This is useful to cache
38+
* complicated drawing commands across frames especially if the content has not changed.
39+
* Additionally some drawing operations such as rendering paths are done purely in
40+
* software so it is beneficial to cache the result and render the contents
41+
* directly through a texture as done by [DrawScope.drawImage]
42+
*/
43+
internal class DrawCache {
44+
45+
@PublishedApi
46+
internal var mCachedImage: ImageBitmap? = null
47+
private var cachedCanvas: Canvas? = null
48+
private var scopeDensity: Density? = null
49+
private var layoutDirection: LayoutDirection = LayoutDirection.Ltr
50+
private var size: IntSize = IntSize.Zero
51+
52+
private val cacheScope = CanvasDrawScope()
53+
54+
/**
55+
* Draw the contents of the lambda with receiver scope into an [ImageBitmap] with the provided
56+
* size. If the same size is provided across calls, the same [ImageBitmap] instance is
57+
* re-used and the contents are cleared out before drawing content in it again
58+
*/
59+
fun drawCachedImage(
60+
size: IntSize,
61+
density: Density,
62+
layoutDirection: LayoutDirection,
63+
block: DrawScope.() -> Unit
64+
) {
65+
this.scopeDensity = density
66+
this.layoutDirection = layoutDirection
67+
var targetImage = mCachedImage
68+
var targetCanvas = cachedCanvas
69+
@Suppress("ComplexCondition")
70+
if (targetImage == null ||
71+
targetCanvas == null ||
72+
size.width > targetImage.width ||
73+
size.height > targetImage.height
74+
) {
75+
targetImage = ImageBitmap(size.width, size.height)
76+
targetCanvas = Canvas(targetImage)
77+
78+
mCachedImage = targetImage
79+
cachedCanvas = targetCanvas
80+
}
81+
this.size = size
82+
cacheScope.draw(density, layoutDirection, targetCanvas, size.toSize()) {
83+
clear()
84+
block()
85+
}
86+
targetImage.prepareToDraw()
87+
}
88+
89+
/**
90+
* Draw the cached content into the provided [DrawScope] instance
91+
*/
92+
fun drawInto(
93+
target: DrawScope,
94+
alpha: Float = 1.0f,
95+
colorFilter: ColorFilter? = null
96+
) {
97+
val targetImage = mCachedImage
98+
check(targetImage != null) {
99+
"drawCachedImage must be invoked first before attempting to draw the result " +
100+
"into another destination"
101+
}
102+
target.drawImage(targetImage, srcSize = size, alpha = alpha, colorFilter = colorFilter)
103+
}
104+
105+
/**
106+
* Helper method to clear contents of the draw environment from the given bounds of the
107+
* DrawScope
108+
*/
109+
private fun DrawScope.clear() {
110+
drawRect(color = Color.Black, blendMode = BlendMode.Clear)
111+
}
112+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// From:
18+
// org.jetbrains.compose.ui:ui-desktop:1.3.1
19+
// DesktopSvgResources.desktop.kt
20+
// package androidx.compose.ui.res
21+
package dev.icerock.moko.resources.compose.internal
22+
23+
import androidx.compose.ui.geometry.Size
24+
import androidx.compose.ui.geometry.isSpecified
25+
import androidx.compose.ui.graphics.ColorFilter
26+
import androidx.compose.ui.graphics.drawscope.DrawScope
27+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
28+
import androidx.compose.ui.graphics.nativeCanvas
29+
import androidx.compose.ui.graphics.painter.Painter
30+
import androidx.compose.ui.unit.Density
31+
import androidx.compose.ui.unit.IntSize
32+
import org.jetbrains.skia.Rect
33+
import org.jetbrains.skia.svg.SVGDOM
34+
import org.jetbrains.skia.svg.SVGLength
35+
import org.jetbrains.skia.svg.SVGLengthUnit
36+
import org.jetbrains.skia.svg.SVGPreserveAspectRatio
37+
import org.jetbrains.skia.svg.SVGPreserveAspectRatioAlign
38+
import kotlin.math.ceil
39+
40+
internal class SVGPainter(
41+
private val dom: SVGDOM,
42+
private val density: Density
43+
) : Painter() {
44+
private val root = dom.root
45+
46+
private val defaultSizePx: Size = run {
47+
val width = root?.width?.withUnit(SVGLengthUnit.PX)?.value ?: 0f
48+
val height = root?.height?.withUnit(SVGLengthUnit.PX)?.value ?: 0f
49+
if (width == 0f && height == 0f) {
50+
Size.Unspecified
51+
} else {
52+
Size(width, height)
53+
}
54+
}
55+
56+
init {
57+
if (root?.viewBox == null && defaultSizePx.isSpecified) {
58+
root?.viewBox = Rect.makeXYWH(0f, 0f, defaultSizePx.width, defaultSizePx.height)
59+
}
60+
}
61+
62+
override val intrinsicSize: Size get() {
63+
return if (defaultSizePx.isSpecified) {
64+
defaultSizePx * density.density
65+
} else {
66+
Size.Unspecified
67+
}
68+
}
69+
70+
private var previousDrawSize: Size = Size.Unspecified
71+
private var alpha: Float = 1.0f
72+
private var colorFilter: ColorFilter? = null
73+
74+
// with caching into bitmap FPS is 3x-4x higher (tested with idea-logo.svg with 30x30 icons)
75+
private val drawCache = DrawCache()
76+
77+
override fun applyAlpha(alpha: Float): Boolean {
78+
this.alpha = alpha
79+
return true
80+
}
81+
82+
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
83+
this.colorFilter = colorFilter
84+
return true
85+
}
86+
87+
override fun DrawScope.onDraw() {
88+
if (previousDrawSize != size) {
89+
drawCache.drawCachedImage(
90+
IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()),
91+
density = this,
92+
layoutDirection,
93+
) {
94+
drawSvg(size)
95+
}
96+
}
97+
98+
drawCache.drawInto(this, alpha, colorFilter)
99+
}
100+
101+
private fun DrawScope.drawSvg(size: Size) {
102+
drawIntoCanvas { canvas ->
103+
root?.width = SVGLength(size.width, SVGLengthUnit.PX)
104+
root?.height = SVGLength(size.height, SVGLengthUnit.PX)
105+
root?.preserveAspectRatio = SVGPreserveAspectRatio(SVGPreserveAspectRatioAlign.NONE)
106+
dom.render(canvas.nativeCanvas)
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)