From 5c141b52133d6b57d803122906e815bec8f4f898 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 20 Jun 2024 13:57:10 +0200 Subject: [PATCH] [resources] Update resource density-based lookup to be equal with the android logic (#4969) In general, Android prefers scaling down a larger original image to scaling up a smaller original image: https://developer.android.com/guide/topics/resources/providing-resources#BestMatch Fixes https://github.com/JetBrains/compose-multiplatform/issues/4368 ## Release Notes ### Highlights - Resources - If there is no resource with suitable density, use resource with the most suitable density, otherwise use default (similar to the Android logic) --- .../compose/resources/ResourceEnvironment.kt | 51 ++++++++++++++++++- .../compose/resources/ComposeResourceTest.kt | 38 ++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt index 20ac17ca09..e658ad3e11 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt @@ -97,7 +97,7 @@ internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironm .also { if (it.size == 1) return it.first() } .filterBy(environment.theme) .also { if (it.size == 1) return it.first() } - .filterBy(environment.density) + .filterByDensity(environment.density) .also { if (it.size == 1) return it.first() } .let { items -> if (items.isEmpty()) { @@ -125,12 +125,59 @@ private fun List.filterBy(qualifier: Qualifier): List.filterByDensity(density: DensityQualifier): List { + val items = this + var withQualifier = emptyList() + + // filter with the same or better density + val exactAndHigherQualifiers = DensityQualifier.entries + .filter { it.dpi >= density.dpi } + .sortedBy { it.dpi } + + for (qualifier in exactAndHigherQualifiers) { + withQualifier = items.filter { item -> item.qualifiers.any { it == qualifier } } + if (withQualifier.isNotEmpty()) break + } + if (withQualifier.isNotEmpty()) return withQualifier + + // filter with low density + val lowQualifiers = DensityQualifier.entries + .minus(DensityQualifier.LDPI) + .filter { it.dpi < density.dpi } + .sortedByDescending { it.dpi } + for (qualifier in lowQualifiers) { + withQualifier = items.filter { item -> item.qualifiers.any { it == qualifier } } + if (withQualifier.isNotEmpty()) break + } + if (withQualifier.isNotEmpty()) return withQualifier + + //items with no DensityQualifier (default) + // The system assumes that default resources (those from a directory without configuration qualifiers) + // are designed for the baseline pixel density (mdpi) and resizes those bitmaps + // to the appropriate size for the current pixel density. + // https://developer.android.com/training/multiscreen/screendensities#DensityConsiderations + val withNoDensity = items.filter { item -> + item.qualifiers.none { it is DensityQualifier } + } + if (withNoDensity.isNotEmpty()) return withNoDensity + + //items with LDPI density + return items.filter { item -> + item.qualifiers.any { it == DensityQualifier.LDPI } + } +} + // we need to filter by language and region together because there is slightly different logic: // 1) if there is the exact match language+region then use it // 2) if there is the language WITHOUT region match then use it // 3) in other cases use items WITHOUT language and region qualifiers at all // issue: https://github.com/JetBrains/compose-multiplatform/issues/4571 -private fun List.filterByLocale(language: LanguageQualifier, region: RegionQualifier): List { +private fun List.filterByLocale( + language: LanguageQualifier, + region: RegionQualifier +): List { val withLanguage = filter { item -> item.qualifiers.any { it == language } } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index 712e8a8cbf..cdd1287ea6 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -59,6 +59,44 @@ class ComposeResourceTest { ) } + @Test + fun testImageResourceDensity() = runComposeUiTest { + val testResourceReader = TestResourceReader() + val imgRes = DrawableResource( + "test_id", setOf( + ResourceItem(setOf(DensityQualifier.XXXHDPI), "2.png", -1, -1), + ResourceItem(setOf(DensityQualifier.MDPI), "1.png", -1, -1), + ) + ) + val mdpiEnvironment = object : ComposeEnvironment { + @Composable + override fun rememberEnvironment() = ResourceEnvironment( + language = LanguageQualifier("en"), + region = RegionQualifier("US"), + theme = ThemeQualifier.LIGHT, + density = DensityQualifier.MDPI + ) + } + + var environment by mutableStateOf(TestComposeEnvironment) + setContent { + CompositionLocalProvider( + LocalResourceReader provides testResourceReader, + LocalComposeEnvironment provides environment + ) { + Image(painterResource(imgRes), null) + } + } + waitForIdle() + environment = mdpiEnvironment + waitForIdle() + + assertEquals( + expected = listOf("2.png", "1.png"), //XXXHDPI - fist, MDPI - next + actual = testResourceReader.readPaths + ) + } + @Test fun testStringResourceCache() = runComposeUiTest { val testResourceReader = TestResourceReader()