@@ -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,22 +75,45 @@ public fun ScaleBar(
7275 lineColor : Color = DarkGray ,
7376 shadowColor : Color = Color .White ,
7477) {
75- var horizontalLineWidthMeters by remember {
76- mutableIntStateOf(0 )
77- }
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
7895
79- LaunchedEffect (key1 = cameraPositionState.position) {
80- val upperLeftLatLng = cameraPositionState.projection?.fromScreenLocation( Point ( 0 , 0 )) ? : LatLng ( 0.0 , 0.0 )
81- val upperRightLatLng = cameraPositionState. projection? .fromScreenLocation(Point (0 , width.value.toInt())) ? : LatLng ( 0.0 , 0.0 )
82- val canvasWidthMeters = upperLeftLatLng.sphericalDistance( upperRightLatLng)
83- val eightNinthsCanvasMeters = (canvasWidthMeters * 8 / 9 ). toInt()
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()) )
84101
85- horizontalLineWidthMeters = eightNinthsCanvasMeters
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()
109+ }
86110 }
87111
88112 Box (
89113 modifier = modifier.size(width = width, height = height)
90114 ) {
115+ // The Canvas composable is used for custom drawing. Here, we are drawing the
116+ // lines of the scale bar.
91117 Canvas (
92118 modifier = Modifier .fillMaxSize(),
93119 onDraw = {
@@ -98,23 +124,27 @@ public fun ScaleBar(
98124 val strokeWidth = 4f
99125 val shadowStrokeWidth = strokeWidth + 3
100126
101- // 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
102132 drawLine(
103133 color = shadowColor,
104134 start = Offset (oneNinthWidth, midHeight),
105135 end = Offset (size.width, midHeight),
106136 strokeWidth = shadowStrokeWidth,
107137 cap = StrokeCap .Round
108138 )
109- // Top vertical line shadow (drawn under main lines)
139+ // Top vertical line shadow
110140 drawLine(
111141 color = shadowColor,
112142 start = Offset (oneNinthWidth, oneThirdHeight),
113143 end = Offset (oneNinthWidth, midHeight),
114144 strokeWidth = shadowStrokeWidth,
115145 cap = StrokeCap .Round
116146 )
117- // Bottom vertical line shadow (drawn under main lines)
147+ // Bottom vertical line shadow
118148 drawLine(
119149 color = shadowColor,
120150 start = Offset (oneNinthWidth, midHeight),
@@ -123,6 +153,8 @@ public fun ScaleBar(
123153 cap = StrokeCap .Round
124154 )
125155
156+ // These are the main lines of the scale bar.
157+
126158 // Middle horizontal line
127159 drawLine(
128160 color = lineColor,
@@ -153,6 +185,9 @@ public fun ScaleBar(
153185 modifier = Modifier .fillMaxSize(),
154186 verticalArrangement = Arrangement .SpaceAround
155187 ) {
188+ // Here, we determine the appropriate units (meters/kilometers and feet/miles)
189+ // based on the calculated distance in meters.
190+
156191 var metricUnits = " m"
157192 var metricDistance = horizontalLineWidthMeters
158193 if (horizontalLineWidthMeters > METERS_IN_KILOMETER ) {
@@ -169,6 +204,8 @@ public fun ScaleBar(
169204 imperialDistance = imperialDistance.toMiles()
170205 }
171206
207+ // We display the calculated distances in two Text composables, one for imperial
208+ // and one for metric units.
172209 ScaleText (
173210 modifier = Modifier .align(End ),
174211 textColor = textColor,
@@ -189,8 +226,16 @@ public fun ScaleBar(
189226 * An animated scale bar that appears when the zoom level of the map changes, and then disappears
190227 * after [visibilityDurationMillis]. This composable wraps [ScaleBar] with visibility animations.
191228 *
192- * Implement your own observer on camera move events using [CameraPositionState] and pass it in
193- * 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.
194239 */
195240@Composable
196241public fun DisappearingScaleBar (
@@ -208,13 +253,24 @@ public fun DisappearingScaleBar(
208253 val visible = remember {
209254 MutableTransitionState (true )
210255 }
211- // This effect controls visibility, not data updates
256+
257+ // `LaunchedEffect` is a coroutine-based effect that is launched when the composable
258+ // enters the composition. The `key1` parameter is used to re-launch the effect
259+ // whenever the value of the key changes. In this case, we are using
260+ // `cameraPositionState.position` as the key, so the effect will be re-launched
261+ // every time the camera position changes.
262+ //
263+ // The effect itself makes the scale bar visible, waits for the specified duration,
264+ // and then makes it invisible again. This creates the "disappearing" effect.
212265 LaunchedEffect (key1 = cameraPositionState.position) {
213266 visible.targetState = true
214267 delay(visibilityDurationMillis.toLong())
215268 visible.targetState = false
216269 }
217270
271+ // `AnimatedVisibility` is a composable that animates the appearance and disappearance
272+ // of its content. We are using it here to wrap the `ScaleBar` and provide the
273+ // fade-in and fade-out animations.
218274 AnimatedVisibility (
219275 visibleState = visible,
220276 modifier = modifier,
@@ -257,15 +313,17 @@ private fun ScaleText(
257313}
258314
259315/* *
260- * Converts [this] value in meters to the corresponding value in feet
316+ * Converts [this] value in meters to the corresponding value in feet.
317+ * This is a utility function used for unit conversion.
261318 * @return [this] meters value converted to feet
262319 */
263320internal fun Double.toFeet (): Double {
264321 return this * CENTIMETERS_IN_METER / CENTIMETERS_IN_INCH / INCHES_IN_FOOT
265322}
266323
267324/* *
268- * Converts [this] value in feet to the corresponding value in miles
325+ * Converts [this] value in feet to the corresponding value in miles.
326+ * This is a utility function used for unit conversion.
269327 * @return [this] feet value converted to miles
270328 */
271329internal fun Double.toMiles (): Double {
0 commit comments