Skip to content

Commit

Permalink
[resources] Update resource density-based lookup to be equal with the…
Browse files Browse the repository at this point in the history
… 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 #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)
  • Loading branch information
terrakok committed Jun 20, 2024
1 parent 53bf4df commit 5c141b5
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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:
// 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<ResourceItem>.filterByLocale(language: LanguageQualifier, region: RegionQualifier): List<ResourceItem> {
private fun List<ResourceItem>.filterByLocale(
language: LanguageQualifier,
region: RegionQualifier
): List<ResourceItem> {
val withLanguage = filter { item ->
item.qualifiers.any { it == language }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 5c141b5

Please sign in to comment.