@@ -31,10 +31,9 @@ import androidx.compose.material.MaterialTheme
3131import androidx.compose.material.Text
3232import androidx.compose.runtime.Composable
3333import androidx.compose.runtime.LaunchedEffect
34+ import androidx.compose.runtime.derivedStateOf
3435import androidx.compose.runtime.getValue
35- import androidx.compose.runtime.mutableIntStateOf
3636import androidx.compose.runtime.remember
37- import androidx.compose.runtime.setValue
3837import androidx.compose.ui.Alignment.Companion.End
3938import androidx.compose.ui.Modifier
4039import androidx.compose.ui.geometry.Offset
@@ -46,7 +45,6 @@ import androidx.compose.ui.unit.Dp
4645import androidx.compose.ui.unit.dp
4746import androidx.compose.ui.unit.em
4847import androidx.compose.ui.unit.sp
49- import com.google.android.gms.maps.model.LatLng
5048import com.google.maps.android.compose.CameraPositionState
5149import com.google.maps.android.ktx.utils.sphericalDistance
5250import kotlinx.coroutines.delay
@@ -59,8 +57,13 @@ private val defaultHeight: Dp = 50.dp
5957 * A scale bar composable that shows the current scale of the map in feet and meters when zoomed in
6058 * to the map, changing to miles and kilometers, respectively, when zooming out.
6159 *
62- * Implement your own observer on camera move events using [CameraPositionState] and pass it in
63- * as [cameraPositionState].
60+ * @param modifier Modifier to be applied to the composable.
61+ * @param width The width of the composable.
62+ * @param height The height of the composable.
63+ * @param cameraPositionState The state of the camera position, used to calculate the scale.
64+ * @param textColor The color of the text on the scale bar.
65+ * @param lineColor The color of the lines on the scale bar.
66+ * @param shadowColor The color of the shadow behind the text and lines.
6467 */
6568@Composable
6669public fun ScaleBar (
@@ -72,53 +75,76 @@ public fun ScaleBar(
7275 lineColor : Color = DarkGray ,
7376 shadowColor : Color = Color .White ,
7477) {
75- Box (
76- modifier = modifier
77- .size(width = width, height = height)
78- ) {
79- var horizontalLineWidthMeters by remember {
80- mutableIntStateOf(0 )
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.
90+ val horizontalLineWidthMeters by remember(cameraPositionState.position.zoom) {
91+ 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()
81109 }
110+ }
82111
112+ Box (
113+ modifier = modifier.size(width = width, height = height)
114+ ) {
115+ // The Canvas composable is used for custom drawing. Here, we are drawing the
116+ // lines of the scale bar.
83117 Canvas (
84118 modifier = Modifier .fillMaxSize(),
85119 onDraw = {
86- // Get width of canvas in meters
87- val upperLeftLatLng =
88- cameraPositionState.projection?.fromScreenLocation(Point (0 , 0 ))
89- ? : LatLng (0.0 , 0.0 )
90- val upperRightLatLng =
91- cameraPositionState.projection?.fromScreenLocation(Point (0 , size.width.toInt()))
92- ? : LatLng (0.0 , 0.0 )
93- val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng)
94- val eightNinthsCanvasMeters = (canvasWidthMeters * 8 / 9 ).toInt()
95-
96- horizontalLineWidthMeters = eightNinthsCanvasMeters
97-
98120 val oneNinthWidth = size.width / 9
99121 val midHeight = size.height / 2
100122 val oneThirdHeight = size.height / 3
101123 val twoThirdsHeight = size.height * 2 / 3
102124 val strokeWidth = 4f
103125 val shadowStrokeWidth = strokeWidth + 3
104126
105- // Middle horizontal line shadow (drawn under main lines)
127+ // The shadows are drawn first, slightly offset from the main lines, to create
128+ // a "drop shadow" effect. This makes the scale bar more readable on different
129+ // map backgrounds.
130+
131+ // Middle horizontal line shadow
106132 drawLine(
107133 color = shadowColor,
108134 start = Offset (oneNinthWidth, midHeight),
109135 end = Offset (size.width, midHeight),
110136 strokeWidth = shadowStrokeWidth,
111137 cap = StrokeCap .Round
112138 )
113- // Top vertical line shadow (drawn under main lines)
139+ // Top vertical line shadow
114140 drawLine(
115141 color = shadowColor,
116142 start = Offset (oneNinthWidth, oneThirdHeight),
117143 end = Offset (oneNinthWidth, midHeight),
118144 strokeWidth = shadowStrokeWidth,
119145 cap = StrokeCap .Round
120146 )
121- // Bottom vertical line shadow (drawn under main lines)
147+ // Bottom vertical line shadow
122148 drawLine(
123149 color = shadowColor,
124150 start = Offset (oneNinthWidth, midHeight),
@@ -127,6 +153,8 @@ public fun ScaleBar(
127153 cap = StrokeCap .Round
128154 )
129155
156+ // These are the main lines of the scale bar.
157+
130158 // Middle horizontal line
131159 drawLine(
132160 color = lineColor,
@@ -157,6 +185,9 @@ public fun ScaleBar(
157185 modifier = Modifier .fillMaxSize(),
158186 verticalArrangement = Arrangement .SpaceAround
159187 ) {
188+ // Here, we determine the appropriate units (meters/kilometers and feet/miles)
189+ // based on the calculated distance in meters.
190+
160191 var metricUnits = " m"
161192 var metricDistance = horizontalLineWidthMeters
162193 if (horizontalLineWidthMeters > METERS_IN_KILOMETER ) {
@@ -173,6 +204,8 @@ public fun ScaleBar(
173204 imperialDistance = imperialDistance.toMiles()
174205 }
175206
207+ // We display the calculated distances in two Text composables, one for imperial
208+ // and one for metric units.
176209 ScaleText (
177210 modifier = Modifier .align(End ),
178211 textColor = textColor,
@@ -193,8 +226,16 @@ public fun ScaleBar(
193226 * An animated scale bar that appears when the zoom level of the map changes, and then disappears
194227 * after [visibilityDurationMillis]. This composable wraps [ScaleBar] with visibility animations.
195228 *
196- * Implement your own observer on camera move events using [CameraPositionState] and pass it in
197- * as [cameraPositionState].
229+ * @param modifier Modifier to be applied to the composable.
230+ * @param width The width of the composable.
231+ * @param height The height of the composable.
232+ * @param cameraPositionState The state of the camera position, used to calculate the scale.
233+ * @param textColor The color of the text on the scale bar.
234+ * @param lineColor The color of the lines on the scale bar.
235+ * @param shadowColor The color of the shadow behind the text and lines.
236+ * @param visibilityDurationMillis The duration in milliseconds that the scale bar will be visible.
237+ * @param enterTransition The animation to use when the scale bar appears.
238+ * @param exitTransition The animation to use when the scale bar disappears.
198239 */
199240@Composable
200241public fun DisappearingScaleBar (
@@ -213,14 +254,19 @@ public fun DisappearingScaleBar(
213254 MutableTransitionState (true )
214255 }
215256
216- LaunchedEffect (key1 = cameraPositionState.position.zoom) {
217- // Show ScaleBar
257+ // This effect is re-launched every time the camera position changes.
258+ //
259+ // The effect itself makes the scale bar visible, waits for the specified duration,
260+ // and then makes it invisible again. This creates the "disappearing" effect.
261+ LaunchedEffect (key1 = cameraPositionState.position) {
218262 visible.targetState = true
219263 delay(visibilityDurationMillis.toLong())
220- // Hide ScaleBar after timeout period
221264 visible.targetState = false
222265 }
223266
267+ // `AnimatedVisibility` is a composable that animates the appearance and disappearance
268+ // of its content. We are using it here to wrap the `ScaleBar` and provide the
269+ // fade-in and fade-out animations.
224270 AnimatedVisibility (
225271 visibleState = visible,
226272 modifier = modifier,
@@ -263,15 +309,17 @@ private fun ScaleText(
263309}
264310
265311/* *
266- * Converts [this] value in meters to the corresponding value in feet
312+ * Converts [this] value in meters to the corresponding value in feet.
313+ * This is a utility function used for unit conversion.
267314 * @return [this] meters value converted to feet
268315 */
269316internal fun Double.toFeet (): Double {
270317 return this * CENTIMETERS_IN_METER / CENTIMETERS_IN_INCH / INCHES_IN_FOOT
271318}
272319
273320/* *
274- * Converts [this] value in feet to the corresponding value in miles
321+ * Converts [this] value in feet to the corresponding value in miles.
322+ * This is a utility function used for unit conversion.
275323 * @return [this] feet value converted to miles
276324 */
277325internal fun Double.toMiles (): Double {
0 commit comments