Browse Source

[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)
pull/5006/head
Konstantin 5 months ago committed by GitHub
parent
commit
5c141b5213
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 51
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceEnvironment.kt
  2. 38
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt

51
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() } .also { if (it.size == 1) return it.first() }
.filterBy(environment.theme) .filterBy(environment.theme)
.also { if (it.size == 1) return it.first() } .also { if (it.size == 1) return it.first() }
.filterBy(environment.density) .filterByDensity(environment.density)
.also { if (it.size == 1) return it.first() } .also { if (it.size == 1) return it.first() }
.let { items -> .let { items ->
if (items.isEmpty()) { if (items.isEmpty()) {
@ -125,12 +125,59 @@ private fun List<ResourceItem>.filterBy(qualifier: Qualifier): List<ResourceItem
} }
} }
// https://developer.android.com/guide/topics/resources/providing-resources#BestMatch
// In general, Android prefers scaling down a larger original image to scaling up a smaller original image.
private fun List<ResourceItem>.filterByDensity(density: DensityQualifier): List<ResourceItem> {
val items = this
var withQualifier = emptyList<ResourceItem>()
// 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: // 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 // 1) if there is the exact match language+region then use it
// 2) if there is the language WITHOUT region match 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 // 3) in other cases use items WITHOUT language and region qualifiers at all
// issue: https://github.com/JetBrains/compose-multiplatform/issues/4571 // issue: https://github.com/JetBrains/compose-multiplatform/issues/4571
private fun List<ResourceItem>.filterByLocale(language: LanguageQualifier, region: RegionQualifier): List<ResourceItem> { private fun List<ResourceItem>.filterByLocale(
language: LanguageQualifier,
region: RegionQualifier
): List<ResourceItem> {
val withLanguage = filter { item -> val withLanguage = filter { item ->
item.qualifiers.any { it == language } item.qualifiers.any { it == language }
} }

38
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 @Test
fun testStringResourceCache() = runComposeUiTest { fun testStringResourceCache() = runComposeUiTest {
val testResourceReader = TestResourceReader() val testResourceReader = TestResourceReader()

Loading…
Cancel
Save