Skip to content

Commit f83372b

Browse files
authored
Improve Example02Zoom with reusable toolbar components (#11)
* Improve over zoom effect and the example 2 * Draw a red quadrilateral * Calculate the average point * Draw a smaller quadrilateral * Use average in example 1 too
1 parent eaad9a2 commit f83372b

9 files changed

Lines changed: 335 additions & 64 deletions

File tree

-1.4 MB
Loading

examples/Example01Pan/src/main/kotlin/de/afarber/openmapview/example01pan/MainActivity.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,23 @@ class MainActivity : ComponentActivity() {
6868
fun MapViewScreen() {
6969
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
7070

71-
// Preset locations around Bochum city, Germany
72-
val initialLocation = LatLng(51.4661, 7.2491)
73-
val location1 = LatLng(51.4700, 7.2400) // North-West
74-
val location2 = LatLng(51.4620, 7.2600) // East
75-
val location3 = LatLng(51.4550, 7.2350) // South-West
76-
7771
// Bochum area bounds for constraint demo
7872
val bochumBounds = LatLngBounds(
7973
southwest = LatLng(51.4400, 7.1800),
8074
northeast = LatLng(51.5000, 7.3200),
8175
)
8276

77+
// Initial location: center of the bounds
78+
val initialLocation = LatLng(
79+
(bochumBounds.southwest.latitude + bochumBounds.northeast.latitude) / 2,
80+
(bochumBounds.southwest.longitude + bochumBounds.northeast.longitude) / 2,
81+
)
82+
83+
// Preset locations around Bochum city, Germany
84+
val location1 = LatLng(51.4700, 7.2400) // North-West
85+
val location2 = LatLng(51.4620, 7.2600) // East
86+
val location3 = LatLng(51.4550, 7.2350) // South-West
87+
8388
// State variables
8489
var mapView: OpenMapView? by remember { mutableStateOf(null) }
8590
var cameraState by remember { mutableStateOf("Idle") }

examples/Example02Zoom/README.md

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
[Back to README](../../README.md)
44

5-
This example demonstrates zoom functionality in OpenMapView, including both programmatic zoom controls and pinch-to-zoom gestures.
5+
This example demonstrates zoom functionality in OpenMapView, including custom zoom controls and pinch-to-zoom gestures with real-time status display.
66

77
## Features Demonstrated
88

99
- Map tile rendering with zoom support
10-
- Floating Action Button (FAB) zoom controls (+/-)
10+
- Custom zoom toolbar with +/- buttons
1111
- Pinch-to-zoom gesture detection
1212
- Real-time zoom level display
13-
- Smooth zoom animations
13+
- Camera state tracking (Idle/Moving)
1414
- Zoom limits (min: 2.0, max: 19.0)
1515

1616
## Screenshot
@@ -36,53 +36,70 @@ This example demonstrates zoom functionality in OpenMapView, including both prog
3636
adb shell am start -n de.afarber.openmapview.example02zoom/.MainActivity
3737
```
3838

39+
## Project Structure
40+
41+
```
42+
example02zoom/
43+
├── MainActivity.kt # Main activity and MapViewScreen composable
44+
├── ZoomToolbar.kt # Vertical toolbar with +/- zoom buttons
45+
├── StatusToolbar.kt # Status overlay showing zoom level and camera state
46+
└── Colors.kt # OSM-inspired colors and shared dimensions
47+
```
48+
3949
## Code Highlights
4050

41-
### MainActivity.kt - FAB Zoom Controls
51+
### MainActivity.kt
4252

4353
```kotlin
44-
// Zoom In FAB
45-
FloatingActionButton(
46-
onClick = {
47-
mapView?.apply {
48-
val newZoom = (getZoom() + 1.0).coerceAtMost(19.0)
49-
setZoom(newZoom)
50-
zoomLevel = getZoom()
51-
}
54+
@Composable
55+
fun MapViewScreen() {
56+
val lifecycleOwner = LocalLifecycleOwner.current
57+
var mapView: OpenMapView? by remember { mutableStateOf(null) }
58+
var zoomLevel by remember { mutableStateOf("14.0") }
59+
60+
Box(modifier = Modifier.fillMaxSize()) {
61+
AndroidView(
62+
factory = { ctx ->
63+
OpenMapView(ctx).apply {
64+
lifecycleOwner.lifecycle.addObserver(this)
65+
setCenter(LatLng(51.4661, 7.2491)) // Bochum, Germany
66+
setZoom(14.0f)
67+
mapView = this
68+
}
69+
},
70+
modifier = Modifier.fillMaxSize(),
71+
)
72+
73+
StatusToolbar(zoomLevel = zoomLevel, cameraState = cameraState, ...)
74+
ZoomToolbar(onZoomInClick = { ... }, onZoomOutClick = { ... }, ...)
5275
}
53-
) {
54-
Icon(Icons.Default.Add, "Zoom In")
5576
}
5677
```
5778

58-
### Zoom Level Display
79+
### OSM-Inspired Colors (Colors.kt)
5980

6081
```kotlin
61-
// Real-time zoom level indicator
62-
Text(
63-
text = "Zoom: %.1f".format(zoomLevel),
64-
modifier = Modifier
65-
.align(Alignment.BottomStart)
66-
.padding(16.dp)
67-
.background(Color.White.copy(alpha = 0.8f))
68-
)
82+
val OsmParkGreen = Color(0xFFAAD3A2) // Parks and forests
83+
val OsmHighwayPink = Color(0xFFE892A2) // Highways and roads
84+
val OsmWaterBlue = Color(0xFFAAD3DF) // Water areas
6985
```
7086

7187
### Key Concepts
7288

73-
- **setZoom()**: Programmatically set zoom level (2.0-19.0)
74-
- **getZoom()**: Read current zoom level
89+
- **setZoom()**: Sets zoom level (2.0 = world view, 19.0 = street level)
90+
- **getZoom()**: Returns current zoom level
7591
- **Pinch gesture**: Built-in ScaleGestureDetector in OpenMapView
7692
- **Zoom limits**: Automatically enforced to prevent over-zoom
93+
- **OnCameraMoveListener**: Callback for camera changes during zoom
7794

7895
## What to Test
7996

80-
1. **Click the + button** - map should zoom in, counter updates
81-
2. **Click the - button** - map should zoom out, counter updates
82-
3. **Pinch to zoom** - use two fingers to zoom in/out
83-
4. **Check zoom focus** - pinch-to-zoom should zoom toward pinch center
84-
5. **Test limits** - zooming beyond min/max should stop gracefully
85-
6. **Observe zoom level** - bottom-left display updates in real-time
97+
1. **Tap + button** to zoom in, observe zoom level update
98+
2. **Tap - button** to zoom out, observe zoom level update
99+
3. **Pinch to zoom** using two fingers
100+
4. **Check zoom focus** - pinch-to-zoom zooms toward pinch center
101+
5. **Test limits** - zooming beyond min/max stops at the limit
102+
6. **Observe status** - zoom level and camera state update in real-time
86103

87104
## Technical Details
88105

@@ -99,9 +116,9 @@ Text(
99116
OpenMapView uses:
100117

101118
- `ScaleGestureDetector` for pinch-to-zoom
102-
- Fractional zoom (Double) for smooth transitions
119+
- Fractional zoom (Float) for smooth transitions
103120
- Web Mercator projection for tile calculation
104121

105122
## Map Location
106123

107-
**Default Center:** Berlin, Germany (52.52°N, 13.405°E) at zoom 14.0
124+
**Default Center:** Bochum, Germany (51.4661N, 7.2491E) at zoom 14.0
9.53 MB
Loading
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview.example02zoom
9+
10+
import androidx.compose.ui.graphics.Color
11+
import androidx.compose.ui.unit.dp
12+
13+
/**
14+
* OSM-inspired colors and shared dimensions for the Example02Zoom app.
15+
*/
16+
17+
/** Green color used by OpenStreetMap for parks and forests. */
18+
val OsmParkGreen = Color(0xFFAAD3A2)
19+
20+
/** Pink color used by OpenStreetMap for highways and major roads. */
21+
val OsmHighwayPink = Color(0xFFE892A2)
22+
23+
/** Blue color used by OpenStreetMap for water areas (lakes, rivers). */
24+
val OsmWaterBlue = Color(0xFFAAD3DF)
25+
26+
/** Shared corner radius for all toolbar components (matches Material3 FAB). */
27+
val ToolbarCornerRadius = 16.dp

examples/Example02Zoom/src/main/kotlin/de/afarber/openmapview/example02zoom/MainActivity.kt

Lines changed: 117 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,38 @@ import androidx.activity.compose.setContent
1313
import androidx.compose.foundation.layout.Box
1414
import androidx.compose.foundation.layout.fillMaxSize
1515
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.material.icons.Icons
17+
import androidx.compose.material.icons.filled.LocationSearching
18+
import androidx.compose.material3.FloatingActionButton
19+
import androidx.compose.material3.Icon
1620
import androidx.compose.material3.MaterialTheme
1721
import androidx.compose.material3.Surface
18-
import androidx.compose.material3.Text
1922
import androidx.compose.runtime.Composable
2023
import androidx.compose.runtime.getValue
2124
import androidx.compose.runtime.mutableStateOf
2225
import androidx.compose.runtime.remember
2326
import androidx.compose.runtime.setValue
2427
import androidx.compose.ui.Alignment
2528
import androidx.compose.ui.Modifier
26-
import androidx.compose.ui.platform.LocalContext
29+
import androidx.compose.ui.graphics.Color
2730
import androidx.compose.ui.unit.dp
2831
import androidx.compose.ui.viewinterop.AndroidView
32+
import androidx.lifecycle.compose.LocalLifecycleOwner
33+
import de.afarber.openmapview.CameraUpdateFactory
2934
import de.afarber.openmapview.LatLng
35+
import de.afarber.openmapview.OnCameraMoveStartedListener
3036
import de.afarber.openmapview.OpenMapView
31-
import kotlin.math.roundToInt
37+
import de.afarber.openmapview.Polyline
3238

39+
/**
40+
* Main activity demonstrating OpenMapView zoom controls.
41+
*
42+
* This example showcases:
43+
* - Custom zoom toolbar with +/- buttons
44+
* - Pinch-to-zoom gesture support
45+
* - Real-time zoom level display
46+
* - Camera state tracking
47+
*/
3348
class MainActivity : ComponentActivity() {
3449
override fun onCreate(savedInstanceState: Bundle?) {
3550
super.onCreate(savedInstanceState)
@@ -46,48 +61,127 @@ class MainActivity : ComponentActivity() {
4661
}
4762
}
4863

64+
/**
65+
* Main composable screen containing the map and zoom controls.
66+
*
67+
* Displays an OpenMapView with a status toolbar showing zoom level and camera state,
68+
* and a zoom toolbar for programmatic zoom control.
69+
* The map is centered on Bochum, Germany.
70+
*/
4971
@Composable
5072
fun MapViewScreen() {
51-
val context = LocalContext.current
52-
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
53-
var zoomLevel by remember { mutableStateOf(14.0f) }
73+
val lifecycleOwner = LocalLifecycleOwner.current
74+
75+
// Irregular quadrilateral around Bochum (not a rectangle)
76+
val bochumCorners = listOf(
77+
LatLng(51.4873, 7.2050), // North-West
78+
LatLng(51.4764, 7.2159), // South-West
79+
LatLng(51.4824, 7.2290), // South-East
80+
LatLng(51.4890, 7.2166), // North-East
81+
)
82+
83+
// Initial location: center of the quadrilateral
84+
val initialLocation = LatLng(
85+
bochumCorners.map { it.latitude }.average(),
86+
bochumCorners.map { it.longitude }.average(),
87+
)
88+
val initialZoom = 14.0f
89+
90+
val bochumOutline = Polyline(
91+
points = bochumCorners + bochumCorners.first(), // Close the shape
92+
strokeColor = Color.Red,
93+
strokeWidth = 4f,
94+
)
95+
96+
// State variables
5497
var mapView: OpenMapView? by remember { mutableStateOf(null) }
98+
var zoomLevel by remember { mutableStateOf("%.1f".format(initialZoom)) }
99+
var cameraState by remember { mutableStateOf("Idle") }
55100

56101
Box(modifier = Modifier.fillMaxSize()) {
102+
// Map view
57103
AndroidView(
58104
factory = { ctx ->
59105
OpenMapView(ctx).apply {
60-
// Register lifecycle observer for proper cleanup
61106
lifecycleOwner.lifecycle.addObserver(this)
62107

63-
setCenter(LatLng(51.4661, 7.2491)) // Bochum, Germany
64-
setZoom(14.0f)
65-
mapView = this
108+
setCenter(initialLocation)
109+
setZoom(initialZoom)
110+
111+
// Add the red polyline outline
112+
addPolyline(bochumOutline)
66113

67-
// Enable built-in zoom controls
68-
getUiSettings().isZoomControlsEnabled = true
114+
// Camera move started listener
115+
setOnCameraMoveStartedListener { reason ->
116+
cameraState = when (reason) {
117+
OnCameraMoveStartedListener.REASON_GESTURE -> "Moving (gesture)"
118+
OnCameraMoveStartedListener.REASON_API_ANIMATION -> "Moving (animation)"
119+
OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION -> "Moving (programmatic)"
120+
else -> "Moving"
121+
}
122+
}
69123

70-
// Add camera move listener to update zoom label
124+
// Camera move listener - updates zoom level during movement
71125
setOnCameraMoveListener {
72-
zoomLevel = getZoom()
126+
zoomLevel = "%.1f".format(getZoom())
127+
}
128+
129+
// Camera idle listener
130+
setOnCameraIdleListener {
131+
cameraState = "Idle"
132+
zoomLevel = "%.1f".format(getZoom())
73133
}
134+
135+
mapView = this
74136
}
75137
},
76138
modifier = Modifier.fillMaxSize(),
77139
)
78140

79-
// Zoom level title at the top
80-
Surface(
141+
// Status overlay at top
142+
StatusToolbar(
143+
zoomLevel = zoomLevel,
144+
cameraState = cameraState,
81145
modifier = Modifier
82146
.align(Alignment.TopCenter)
83-
.padding(top = 16.dp),
84-
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
85-
shape = MaterialTheme.shapes.small,
147+
.padding(16.dp),
148+
)
149+
150+
// Zoom toolbar at bottom
151+
ZoomToolbar(
152+
onZoomInClick = {
153+
mapView?.apply {
154+
setZoom(getZoom() + 1.0f)
155+
zoomLevel = "%.1f".format(getZoom())
156+
}
157+
},
158+
onZoomOutClick = {
159+
mapView?.apply {
160+
setZoom(getZoom() - 1.0f)
161+
zoomLevel = "%.1f".format(getZoom())
162+
}
163+
},
164+
modifier = Modifier
165+
.align(Alignment.BottomCenter)
166+
.padding(bottom = 16.dp),
167+
)
168+
169+
FloatingActionButton(
170+
onClick = {
171+
mapView?.animateCamera(
172+
CameraUpdateFactory.newLatLngZoom(initialLocation, initialZoom),
173+
500,
174+
)
175+
},
176+
containerColor = OsmHighwayPink,
177+
contentColor = Color.Black,
178+
modifier = Modifier
179+
.align(Alignment.BottomEnd)
180+
.padding(16.dp),
86181
) {
87-
Text(
88-
text = "Zoom: ${zoomLevel.roundToInt()}",
89-
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
90-
style = MaterialTheme.typography.titleMedium,
182+
Icon(
183+
imageVector = Icons.Default.LocationSearching,
184+
contentDescription = "Reset",
91185
)
92186
}
93187
}

0 commit comments

Comments
 (0)