Skip to content

Commit

Permalink
Merge pull request #624 from lammertw/images-dark-mode
Browse files Browse the repository at this point in the history
Add support for dark mode for images
  • Loading branch information
Alex009 authored Apr 19, 2024
2 parents 9eb3be1 + 532a2f6 commit 5595047
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 61 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ implement all you UI in Kotlin with Jetpack Compose and MOKO resources.
- **Strings, Plurals** to access the corresponding resources from common code;
- **Colors** with light/dark mode support;
- **Compose Multiplatform** support;
- **Images** support (`svg`, `png`, `jpg`);
- **Images** support (`svg`, `png`, `jpg`) with light/dark mode support;
- **Fonts** support (`ttf`, `otf`);
- **Files** support (as `raw` or `assets` for android);
- **StringDesc** for lifecycle-aware access to resources and unified localization on both platforms;
Expand Down Expand Up @@ -547,6 +547,13 @@ Then we get an autogenerated `MR.images.home_black_18` `ImageResource` in code.
- Android: `imageView.setImageResource(image.drawableResId)`
- iOS: `imageView.image = image.toUIImage()`

#### dark mode

To support Dark Mode images, you can add -dark and optionally -light to the name of an image. Make sure the rest of the name matches the corresponding light mode image:

- `car.svg`
- `car-dark.svg`

#### svg

The Image generator also supports `svg` files.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package dev.icerock.moko.resources.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
Expand All @@ -22,18 +25,25 @@ import org.jetbrains.skia.Data
import org.jetbrains.skia.Image
import org.jetbrains.skia.svg.SVGDOM

@OptIn(InternalComposeApi::class)
@Composable
actual fun painterResource(imageResource: ImageResource): Painter {
val bytes: ByteArray? by produceByteArray(url = imageResource.fileUrl)
val fileUrl: String = if (LocalSystemTheme.current == SystemTheme.Dark) {
imageResource.darkFileUrl ?: imageResource.fileUrl
} else {
imageResource.fileUrl
}

val bytes: ByteArray? by produceByteArray(url = fileUrl)
val localBytes: ByteArray? = bytes
val density: Density = LocalDensity.current
return remember(localBytes) {

return remember(localBytes, density, fileUrl) {
if (localBytes == null) {
return@remember ColorPainter(color = Color.Transparent)
}

if (imageResource.fileUrl.endsWith(".svg", ignoreCase = true)) {
if (fileUrl.endsWith(".svg", ignoreCase = true)) {
SVGPainter(SVGDOM(Data.makeFromBytes(localBytes)), density)
} else {
val skiaImage: Image = Image.makeFromEncoded(bytes = localBytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@
package dev.icerock.moko.resources.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import dev.icerock.moko.resources.ImageResource

@OptIn(InternalComposeApi::class)
@Composable
actual fun painterResource(imageResource: ImageResource): Painter =
painterResource(resourcePath = imageResource.filePath)
actual fun painterResource(imageResource: ImageResource): Painter {
val filePath: String = if (LocalSystemTheme.current == SystemTheme.Dark) {
imageResource.darkFilePath ?: imageResource.filePath
} else {
imageResource.filePath
}

return painterResource(resourcePath = filePath)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ internal class AndroidImageResourceGenerator(
override fun generateResourceFiles(data: List<ImageMetadata>) {
data.flatMap { imageMetadata ->
imageMetadata.values.map { imageMetadata.key to it }
}.forEach { (key: String, item: ImageMetadata.ImageQualityItem) ->
val drawableDirName: String = "drawable" + when (item.quality) {
}.forEach { (key: String, item: ImageMetadata.ImageItem) ->
val densityRes = when (item.quality) {
"0.75" -> "-ldpi"
"1" -> "-mdpi"
"1.5" -> "-hdpi"
Expand All @@ -72,6 +72,8 @@ internal class AndroidImageResourceGenerator(
return@forEach
}
}
val themeSuffix = item.appearance.resourceSuffix
val drawableDirName = "drawable$themeSuffix$densityRes"

val drawableDir = File(resourcesGenerationDir, drawableDirName)
val processedKey: String = processKey(key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal class AppleImageResourceGenerator(
)
}

@Suppress("LongMethod")
override fun generateResourceFiles(data: List<ImageMetadata>) {
val assetsDirectory = File(assetsGenerationDir, Constants.Apple.assetsDirectoryName)

Expand All @@ -41,7 +42,7 @@ internal class AppleImageResourceGenerator(
assetDir.mkdirs()
val contentsFile = File(assetDir, "Contents.json")

val validItems: List<ImageMetadata.ImageQualityItem> =
val validItems: List<ImageMetadata.ImageItem> =
imageMetadata.values.filter { item ->
item.quality == null || VALID_SIZES.any { item.quality == it.toString() }
}
Expand All @@ -64,30 +65,59 @@ internal class AppleImageResourceGenerator(
val imagesContent: JsonArray = buildJsonArray {
validItems.map { item ->
buildJsonObject {
put("idiom", JsonPrimitive("universal"))
put("filename", JsonPrimitive(item.filePath.name))
put(
key = "idiom",
element = JsonPrimitive("universal")
)
put(
key = "filename",
element = JsonPrimitive(item.filePath.name)
)
item.quality?.let { quality ->
put("scale", JsonPrimitive(quality + "x"))
put(
key = "scale",
element = JsonPrimitive(quality + "x")
)
}
put(
key = "appearances",
element = buildJsonArray {
add(
buildJsonObject {
put(
key = "appearance",
element = JsonPrimitive("luminosity")
)
put(
key = "value",
element = JsonPrimitive(item.appearance.name.lowercase())
)
}
)
}
)
}
}.forEach { add(it) }
}

val content: String = buildJsonObject {
put("images", imagesContent)
put(key = "images", element = imagesContent)
put(
"info",
buildJsonObject {
put("version", JsonPrimitive(1))
put("author", JsonPrimitive("xcode"))
key = "info",
element = buildJsonObject {
put(key = "version", element = JsonPrimitive(1))
put(key = "author", element = JsonPrimitive("xcode"))
}
)

if (validItems.any { it.quality == null }) {
put(
"properties",
buildJsonObject {
put("preserves-vector-representation", JsonPrimitive(true))
key = "properties",
element = buildJsonObject {
put(
key = "preserves-vector-representation",
element = JsonPrimitive(true)
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import dev.icerock.gradle.generator.Constants
import dev.icerock.gradle.generator.ResourceGenerator
import dev.icerock.gradle.generator.generateKey
import dev.icerock.gradle.metadata.resource.ImageMetadata
import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance
import dev.icerock.gradle.utils.nameWithoutScale
import dev.icerock.gradle.utils.scale
import dev.icerock.gradle.utils.svg
import dev.icerock.gradle.utils.withoutAppearance
import java.io.File

internal class ImageResourceGenerator : ResourceGenerator<ImageMetadata> {
Expand All @@ -21,8 +23,9 @@ internal class ImageResourceGenerator : ResourceGenerator<ImageMetadata> {
ImageMetadata(
key = generateKey(key),
values = files.map { file ->
ImageMetadata.ImageQualityItem(
ImageMetadata.ImageItem(
quality = if (file.svg) null else file.scale,
appearance = Appearance.getFromFile(file),
filePath = file
)
}
Expand All @@ -39,6 +42,6 @@ internal class ImageResourceGenerator : ResourceGenerator<ImageMetadata> {
file.nameWithoutExtension
} else {
file.nameWithoutScale
}
}.withoutAppearance
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.icerock.gradle.generator.Constants
import dev.icerock.gradle.generator.PlatformResourceGenerator
import dev.icerock.gradle.generator.addEmptyPlatformResourceProperty
import dev.icerock.gradle.metadata.resource.ImageMetadata
import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance
import java.io.File

internal class JsImageResourceGenerator(
Expand All @@ -23,14 +24,36 @@ internal class JsImageResourceGenerator(
override fun imports(): List<ClassName> = emptyList()

override fun generateInitializer(metadata: ImageMetadata): CodeBlock {
val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem()
val fileName = "${metadata.key}.${item.filePath.extension}"
val requireDeclaration = """require("$IMAGES_DIR/$fileName")"""
return CodeBlock.of(
"ImageResource(fileUrl = js(%S) as String, fileName = %S)",
requireDeclaration,
fileName
)
var fileName: String = ""
var darkFileName: String? = null

metadata.values.groupBy { it.appearance }.forEach { (theme, resources) ->
val item: ImageMetadata.ImageItem = resources.getHighestQualityItem(theme)

if (theme == Appearance.DARK) {
darkFileName = "${metadata.key}${theme.themeSuffix}.${item.filePath.extension}"
} else {
fileName = "${metadata.key}.${item.filePath.extension}"
}
}

val requireDeclaration: String = """require("$IMAGES_DIR/$fileName")"""
val darkRequireDeclaration: String = """require("$IMAGES_DIR/$darkFileName")"""

return if (darkFileName != null) {
CodeBlock.of(
"ImageResource(fileUrl = js(%S) as String, darkFileUrl = js(%S) as String, fileName = %S)",
requireDeclaration,
darkRequireDeclaration,
fileName
)
} else {
CodeBlock.of(
"ImageResource(fileUrl = js(%S) as String, fileName = %S)",
requireDeclaration,
fileName
)
}
}

override fun generateBeforeProperties(
Expand All @@ -52,7 +75,7 @@ internal class JsImageResourceGenerator(
override fun generateAfterProperties(
builder: Builder,
metadata: List<ImageMetadata>,
modifier: KModifier?
modifier: KModifier?,
) {
val languageKeysList: String = metadata.joinToString { it.key }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.icerock.gradle.generator.PlatformResourceGenerator
import dev.icerock.gradle.generator.addJvmPlatformResourceClassLoaderProperty
import dev.icerock.gradle.generator.addValuesFunction
import dev.icerock.gradle.metadata.resource.ImageMetadata
import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance
import java.io.File

internal class JvmImageResourceGenerator(
Expand All @@ -23,17 +24,38 @@ internal class JvmImageResourceGenerator(
override fun imports(): List<ClassName> = emptyList()

override fun generateInitializer(metadata: ImageMetadata): CodeBlock {
val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem()
val fileName = "${metadata.key}.${item.filePath.extension}"
var fileName: String = ""
var darkFileName: String? = null

metadata.values.groupBy { it.appearance }.forEach { (theme, resources) ->
val item: ImageMetadata.ImageItem = resources.getHighestQualityItem(theme)

if (theme == Appearance.DARK) {
darkFileName = "${metadata.key}${theme.themeSuffix}.${item.filePath.extension}"
} else {
fileName = "${metadata.key}.${item.filePath.extension}"
}
}

val darkFilePath: String = if (darkFileName != null) {
"\"$IMAGES_DIR/$darkFileName\""
} else {
"null"
}

return CodeBlock.of(
"ImageResource(resourcesClassLoader = %L, filePath = %S)",
"ImageResource(resourcesClassLoader = %L, filePath = %S, darkFilePath = $darkFilePath)",
"${PlatformDetails.platformDetailsPropertyName}.${Jvm.resourcesClassLoaderPropertyName}",
"$IMAGES_DIR/$fileName"
)
}

override fun generateResourceFiles(data: List<ImageMetadata>) {
generateHighestQualityImageResources(resourcesGenerationDir, data, IMAGES_DIR)
generateHighestQualityImageResources(
resourcesGenerationDir = resourcesGenerationDir,
data = data,
imagesDirName = IMAGES_DIR
)
}

override fun generateBeforeProperties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,42 @@
package dev.icerock.gradle.generator.resources.image

import dev.icerock.gradle.metadata.resource.ImageMetadata
import dev.icerock.gradle.metadata.resource.ImageMetadata.Appearance.DARK
import dev.icerock.gradle.metadata.resource.ImageMetadata.ImageItem
import java.io.File

internal fun generateHighestQualityImageResources(
resourcesGenerationDir: File,
data: List<ImageMetadata>,
imagesDirName: String
imagesDirName: String,
) {
val imagesDir = File(resourcesGenerationDir, imagesDirName)
imagesDir.mkdirs()

data.forEach { metadata ->
val item: ImageMetadata.ImageQualityItem = metadata.getHighestQualityItem()
val file: File = item.filePath
val key: String = metadata.key
metadata.values
.groupBy { it.appearance }
.forEach { (theme, list: List<ImageItem>) ->
val item: ImageMetadata.ImageItem = list.getHighestQualityItem(theme)
val file: File = item.filePath
val key: String = metadata.key

file.copyTo(File(imagesDir, "$key.${file.extension}"))
val fileName: String = if (theme == DARK) {
"$key${theme.themeSuffix}.${file.extension}"
} else {
"$key.${file.extension}"
}

file.copyTo(File(imagesDir, fileName))
}
}
}

internal fun ImageMetadata.getHighestQualityItem(): ImageMetadata.ImageQualityItem {
return values.singleOrNull { it.quality == null }
?: values.maxBy { it.quality!!.toDouble() }
internal fun List<ImageItem>.getHighestQualityItem(
appearance: ImageMetadata.Appearance,
): ImageMetadata.ImageItem {
val filteredList = filter { it.appearance == appearance }

return filteredList.singleOrNull { it.quality == null }
?: filteredList.maxBy { it.quality!!.toDouble() }
}
Loading

0 comments on commit 5595047

Please sign in to comment.