Skip to content

Commit c4106df

Browse files
authored
fix: fixes ScaleBar to properly handle dp and px (#779)
* fix: fix ScaleBar to properly handle dp and px * fix: fix ScaleBar to properly handle dp and px * chore: trigger CI * fix: scale bar TOM
1 parent 7c6ee5a commit c4106df

File tree

4 files changed

+79
-30
lines changed

4 files changed

+79
-30
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ android-application = { id = "com.android.application", version.ref = "agp" }
7171
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
7272
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
7373
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
74-
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
74+
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}

maps-compose-widgets/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ android {
1111
sarifOutput = layout.buildDirectory.file("reports/lint-results.sarif").get().asFile
1212
}
1313

14+
packaging {
15+
resources {
16+
excludes += "META-INF/LICENSE.md"
17+
excludes += "META-INF/LICENSE-notice.md"
18+
}
19+
}
20+
1421
namespace = "com.google.maps.android.compose.widgets"
1522
compileSdk = 36
1623

1724
defaultConfig {
1825
minSdk = 21
26+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1927
}
2028

2129
compileOptions {
@@ -55,4 +63,6 @@ dependencies {
5563
androidTestImplementation(platform(libs.androidx.compose.bom))
5664
androidTestImplementation(libs.androidx.test.espresso)
5765
androidTestImplementation(libs.androidx.test.junit.ktx)
66+
androidTestImplementation(libs.mockk)
67+
androidTestImplementation(libs.truth)
5868
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.google.maps.android.compose.widgets
2+
3+
4+
import android.graphics.Point
5+
import io.mockk.every
6+
import io.mockk.mockk
7+
import org.junit.Test
8+
import com.google.common.truth.Truth.assertThat
9+
import androidx.compose.ui.unit.Density
10+
import androidx.compose.ui.unit.dp
11+
import androidx.test.ext.junit.runners.AndroidJUnit4
12+
import com.google.android.gms.maps.Projection
13+
import com.google.android.gms.maps.model.LatLng
14+
import com.google.maps.android.ktx.utils.sphericalDistance
15+
import org.junit.runner.RunWith
16+
17+
18+
@RunWith(AndroidJUnit4::class)
19+
public class ScaleBarUnitTest {
20+
21+
@Test
22+
public fun testScaleBarCalculation() {
23+
val projection = mockk<Projection>(relaxed = true)
24+
val density = Density(1f, 1f)
25+
val width = 100.dp
26+
27+
val startPoint = Point(0, 0)
28+
val endPoint = Point(width.value.toInt(), 0)
29+
30+
val startLatLng = LatLng(0.0, 0.0)
31+
val endLatLng = LatLng(0.0, 0.001)
32+
33+
every { projection.fromScreenLocation(startPoint) } returns startLatLng
34+
every { projection.fromScreenLocation(endPoint) } returns endLatLng
35+
36+
val expectedDistance = startLatLng.sphericalDistance(endLatLng)
37+
val expectedResult = (expectedDistance * 8 / 9).toInt()
38+
39+
val result = calculateDistance(projection, width, density)
40+
41+
assertThat(result).isEqualTo(expectedResult)
42+
}
43+
}

maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,36 @@ import androidx.compose.ui.geometry.Offset
4040
import androidx.compose.ui.graphics.Color
4141
import androidx.compose.ui.graphics.Shadow
4242
import androidx.compose.ui.graphics.StrokeCap
43+
import androidx.compose.ui.platform.LocalDensity
4344
import androidx.compose.ui.text.style.TextAlign
45+
import androidx.compose.ui.unit.Density
4446
import androidx.compose.ui.unit.Dp
4547
import androidx.compose.ui.unit.dp
4648
import androidx.compose.ui.unit.em
4749
import androidx.compose.ui.unit.sp
50+
import com.google.android.gms.maps.Projection
4851
import com.google.maps.android.compose.CameraPositionState
4952
import com.google.maps.android.ktx.utils.sphericalDistance
5053
import kotlinx.coroutines.delay
5154

55+
internal fun calculateDistance(
56+
projection: Projection,
57+
width: Dp,
58+
density: Density
59+
): Int {
60+
val widthInPixels = with(density) {
61+
width.toPx().toInt()
62+
}
63+
64+
val upperLeftLatLng = projection.fromScreenLocation(Point(0, 0))
65+
val upperRightLatLng =
66+
projection.fromScreenLocation(Point(widthInPixels, 0))
67+
68+
val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng)
69+
70+
return (canvasWidthMeters * 8 / 9).toInt()
71+
}
72+
5273
public val DarkGray: Color = Color(0xFF3a3c3b)
5374
private val defaultWidth: Dp = 65.dp
5475
private val defaultHeight: Dp = 50.dp
@@ -75,37 +96,12 @@ public fun ScaleBar(
7596
lineColor: Color = DarkGray,
7697
shadowColor: Color = Color.White,
7798
) {
78-
// This is the core logic for calculating the scale of the map.
79-
//
80-
// `remember` with a key (`cameraPositionState.position.zoom`) is used for performance.
81-
// It ensures that the calculation inside is only re-executed when the zoom level changes.
82-
// This is important because we don't need to recalculate the scale every time the map pans,
83-
// only when the zoom level changes.
84-
//
85-
// `derivedStateOf` is a Compose state function that creates a new state object that is
86-
// derived from other state objects. The calculation inside `derivedStateOf` is only
87-
// re-executed when one of the state objects it reads from changes. In this case, it's
88-
// `cameraPositionState.projection`. This is another performance optimization that
89-
// prevents unnecessary recalculations.
99+
val density = LocalDensity.current
90100
val horizontalLineWidthMeters by remember(cameraPositionState.position.zoom) {
91101
derivedStateOf {
92-
// The projection is used to convert between screen coordinates (pixels) and
93-
// geographical coordinates (LatLng). It can be null if the map is not ready yet.
94-
val projection = cameraPositionState.projection ?: return@derivedStateOf 0
95-
96-
// We get the geographical coordinates of two points on the screen: the top-left
97-
// corner (0, 0) and a point to the right of it, at the width of the scale bar.
98-
val upperLeftLatLng = projection.fromScreenLocation(Point(0, 0))
99-
val upperRightLatLng =
100-
projection.fromScreenLocation(Point(0, width.value.toInt()))
101-
102-
// We then calculate the spherical distance between these two points in meters.
103-
// This gives us the distance that the scale bar represents on the map.
104-
val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng)
105-
106-
// We take 8/9th of the canvas width to provide some padding on the right side
107-
// of the scale bar.
108-
(canvasWidthMeters * 8 / 9).toInt()
102+
cameraPositionState.projection?.let {
103+
calculateDistance(it, width, density)
104+
} ?: 0
109105
}
110106
}
111107

0 commit comments

Comments
 (0)