Skip to content

Commit a09b137

Browse files
authored
Improve Example01Pan with reusable toolbar components and camera controls (#9)
* Add status overlay, control buttons * Move arrow buttons into a toolbar * Move 2 toolbars into separate files * Add control toolbar on the left * Draw bounds and locations
1 parent 715fe75 commit a09b137

8 files changed

Lines changed: 607 additions & 28 deletions

File tree

examples/Example01Pan/README.md

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

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

5-
This example demonstrates the core functionality of OpenMapView: displaying OpenStreetMap tiles and responding to touch pan gestures.
5+
This example demonstrates the core functionality of OpenMapView: displaying OpenStreetMap tiles, responding to touch pan gestures, and using camera controls with status display.
66

77
## Features Demonstrated
88

99
- Map tile rendering from OpenStreetMap
1010
- Touch pan/drag gestures
11-
- Smooth real-time map updates
12-
- Basic OpenMapView setup
11+
- Arrow button toolbar for programmatic panning
12+
- Preset location buttons with animated camera moves
13+
- Camera bounds constraints with visual polyline indicator
14+
- Real-time camera state and position display
15+
- Colored markers at preset locations
1316

1417
## Screenshot
1518

@@ -34,38 +37,83 @@ This example demonstrates the core functionality of OpenMapView: displaying Open
3437
adb shell am start -n de.afarber.openmapview.example01pan/.MainActivity
3538
```
3639

40+
## Project Structure
41+
42+
```
43+
example01pan/
44+
├── MainActivity.kt # Main activity and MapViewScreen composable
45+
├── ArrowToolbar.kt # Horizontal toolbar with pan arrow buttons
46+
├── LocationToolbar.kt # Vertical toolbar with preset location buttons
47+
├── ControlToolbar.kt # Vertical toolbar with bounds/reset controls
48+
├── StatusToolbar.kt # Status overlay showing camera state
49+
└── Colors.kt # OSM-inspired colors and shared dimensions
50+
```
51+
3752
## Code Highlights
3853

3954
### MainActivity.kt
4055

4156
```kotlin
4257
@Composable
4358
fun MapViewScreen() {
44-
AndroidView(
45-
factory = { context ->
46-
OpenMapView(context).apply {
47-
setCenter(LatLng(51.4661, 7.2491)) // Bochum, Germany
48-
setZoom(14.0)
49-
}
50-
},
51-
modifier = Modifier.fillMaxSize(),
52-
)
59+
val lifecycleOwner = LocalLifecycleOwner.current
60+
var mapView: OpenMapView? by remember { mutableStateOf(null) }
61+
62+
Box(modifier = Modifier.fillMaxSize()) {
63+
AndroidView(
64+
factory = { ctx ->
65+
OpenMapView(ctx).apply {
66+
lifecycleOwner.lifecycle.addObserver(this)
67+
setCenter(LatLng(51.4661, 7.2491)) // Bochum, Germany
68+
setZoom(14.0f)
69+
mapView = this
70+
}
71+
},
72+
modifier = Modifier.fillMaxSize(),
73+
)
74+
75+
// Toolbars overlay the map
76+
StatusToolbar(...)
77+
ArrowToolbar(...)
78+
LocationToolbar(...)
79+
ControlToolbar(...)
80+
}
5381
}
5482
```
5583

84+
### OSM-Inspired Colors (Colors.kt)
85+
86+
```kotlin
87+
val OsmParkGreen = Color(0xFFAAD3A2) // Parks and forests
88+
val OsmHighwayPink = Color(0xFFE892A2) // Highways and roads
89+
val OsmWaterBlue = Color(0xFFAAD3DF) // Water areas
90+
```
91+
5692
### Key Concepts
5793

5894
- **LatLng**: Represents geographic coordinates (latitude, longitude)
5995
- **setCenter()**: Sets the initial map center position
6096
- **setZoom()**: Sets zoom level (2.0 = world view, 19.0 = street level)
61-
- **Touch handling**: Built-in via OpenMapView's onTouchEvent()
97+
- **moveCamera()**: Instantly moves the camera (no animation)
98+
- **animateCamera()**: Smoothly animates camera to new position
99+
- **setLatLngBoundsForCameraTarget()**: Constrains camera movement to bounds
100+
- **OnCameraMoveListener**: Callback for camera position changes
62101

63102
## What to Test
64103

65104
1. **Pan the map** by dragging with your finger/mouse
66-
2. **Observe tiles loading** as you pan to new areas
67-
3. **Check smooth rendering** - map should update in real-time without lag
105+
2. **Use arrow buttons** to pan the map programmatically
106+
3. **Tap location buttons** (Loc 1, 2, 3) to animate to preset positions
107+
4. **Toggle bounds** to constrain camera movement (blue polyline shows bounds)
108+
5. **Tap Reset** to return to initial position
109+
6. **Observe status overlay** showing camera state and coordinates
68110

69111
## Map Location
70112

71-
**Default Center:** Bochum, Germany (51.4661°N, 7.2491°E)
113+
**Default Center:** Bochum, Germany (51.4661N, 7.2491E)
114+
115+
**Preset Locations:**
116+
- Location 1 (Red marker): North-West of center
117+
- Location 2 (Green marker): East of center
118+
- Location 3 (Magenta marker): South-West of center
119+
- Initial (Cyan marker): Center position
18.2 MB
Loading
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.example01pan
9+
10+
import androidx.compose.foundation.layout.Row
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.foundation.shape.RoundedCornerShape
13+
import androidx.compose.material.icons.Icons
14+
import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
15+
import androidx.compose.material.icons.filled.KeyboardDoubleArrowLeft
16+
import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight
17+
import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
18+
import androidx.compose.material3.FilledIconButton
19+
import androidx.compose.material3.Icon
20+
import androidx.compose.material3.IconButtonDefaults
21+
import androidx.compose.material3.MaterialTheme
22+
import androidx.compose.material3.Surface
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.graphics.RectangleShape
27+
import androidx.compose.ui.unit.dp
28+
29+
/**
30+
* A horizontal toolbar with four arrow buttons for panning the map.
31+
*
32+
* Displays left, up, down, and right arrow buttons in a row with OSM park green background.
33+
*
34+
* @param onLeftClick Callback invoked when the left arrow button is clicked.
35+
* @param onUpClick Callback invoked when the up arrow button is clicked.
36+
* @param onDownClick Callback invoked when the down arrow button is clicked.
37+
* @param onRightClick Callback invoked when the right arrow button is clicked.
38+
* @param modifier Modifier to be applied to the toolbar.
39+
*/
40+
@Composable
41+
fun ArrowToolbar(
42+
onLeftClick: () -> Unit,
43+
onUpClick: () -> Unit,
44+
onDownClick: () -> Unit,
45+
onRightClick: () -> Unit,
46+
modifier: Modifier = Modifier,
47+
) {
48+
Surface(
49+
modifier = modifier,
50+
shape = RoundedCornerShape(ToolbarCornerRadius),
51+
shadowElevation = 6.dp,
52+
color = MaterialTheme.colorScheme.surface,
53+
) {
54+
Row {
55+
FilledIconButton(
56+
onClick = onLeftClick,
57+
modifier = Modifier.size(56.dp),
58+
shape = RoundedCornerShape(topStart = ToolbarCornerRadius, bottomStart = ToolbarCornerRadius),
59+
colors = IconButtonDefaults.filledIconButtonColors(
60+
containerColor = OsmParkGreen,
61+
contentColor = Color.Black,
62+
),
63+
) {
64+
Icon(Icons.Default.KeyboardDoubleArrowLeft, contentDescription = "Left")
65+
}
66+
FilledIconButton(
67+
onClick = onUpClick,
68+
modifier = Modifier.size(56.dp),
69+
shape = RectangleShape,
70+
colors = IconButtonDefaults.filledIconButtonColors(
71+
containerColor = OsmParkGreen,
72+
contentColor = Color.Black,
73+
),
74+
) {
75+
Icon(Icons.Default.KeyboardDoubleArrowUp, contentDescription = "Up")
76+
}
77+
FilledIconButton(
78+
onClick = onDownClick,
79+
modifier = Modifier.size(56.dp),
80+
shape = RectangleShape,
81+
colors = IconButtonDefaults.filledIconButtonColors(
82+
containerColor = OsmParkGreen,
83+
contentColor = Color.Black,
84+
),
85+
) {
86+
Icon(Icons.Default.KeyboardDoubleArrowDown, contentDescription = "Down")
87+
}
88+
FilledIconButton(
89+
onClick = onRightClick,
90+
modifier = Modifier.size(56.dp),
91+
shape = RoundedCornerShape(topEnd = ToolbarCornerRadius, bottomEnd = ToolbarCornerRadius),
92+
colors = IconButtonDefaults.filledIconButtonColors(
93+
containerColor = OsmParkGreen,
94+
contentColor = Color.Black,
95+
),
96+
) {
97+
Icon(Icons.Default.KeyboardDoubleArrowRight, contentDescription = "Right")
98+
}
99+
}
100+
}
101+
}
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.example01pan
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 Example01Pan 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. */
27+
val ToolbarCornerRadius = 8.dp
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.example01pan
9+
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.PaddingValues
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material3.ButtonDefaults
15+
import androidx.compose.material3.FilledTonalButton
16+
import androidx.compose.material3.MaterialTheme
17+
import androidx.compose.material3.Surface
18+
import androidx.compose.material3.Text
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.unit.dp
23+
24+
/**
25+
* A vertical toolbar with map control buttons.
26+
*
27+
* Contains a bounds toggle button and a reset button, using OSM water blue background.
28+
*
29+
* @param boundsEnabled Whether camera bounds constraint is currently enabled.
30+
* @param onBoundsClick Callback invoked when the bounds toggle button is clicked.
31+
* @param onResetClick Callback invoked when the reset button is clicked.
32+
* @param modifier Modifier to be applied to the toolbar.
33+
*/
34+
@Composable
35+
fun ControlToolbar(
36+
boundsEnabled: Boolean,
37+
onBoundsClick: () -> Unit,
38+
onResetClick: () -> Unit,
39+
modifier: Modifier = Modifier,
40+
) {
41+
Surface(
42+
modifier = modifier,
43+
shape = RoundedCornerShape(ToolbarCornerRadius),
44+
shadowElevation = 6.dp,
45+
color = MaterialTheme.colorScheme.surface,
46+
) {
47+
Column {
48+
FilledTonalButton(
49+
onClick = onBoundsClick,
50+
modifier = Modifier.size(width = 80.dp, height = 48.dp),
51+
shape = RoundedCornerShape(topStart = ToolbarCornerRadius, topEnd = ToolbarCornerRadius),
52+
contentPadding = PaddingValues(4.dp),
53+
colors = ButtonDefaults.filledTonalButtonColors(
54+
containerColor = OsmWaterBlue,
55+
contentColor = Color.Black,
56+
),
57+
) {
58+
Text(
59+
text = if (boundsEnabled) "Bounds On" else "Bounds Off",
60+
style = MaterialTheme.typography.bodySmall,
61+
)
62+
}
63+
FilledTonalButton(
64+
onClick = onResetClick,
65+
modifier = Modifier.size(width = 80.dp, height = 48.dp),
66+
shape = RoundedCornerShape(bottomStart = ToolbarCornerRadius, bottomEnd = ToolbarCornerRadius),
67+
contentPadding = PaddingValues(4.dp),
68+
colors = ButtonDefaults.filledTonalButtonColors(
69+
containerColor = OsmWaterBlue,
70+
contentColor = Color.Black,
71+
),
72+
) {
73+
Text("Reset", style = MaterialTheme.typography.bodySmall)
74+
}
75+
}
76+
}
77+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.example01pan
9+
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.PaddingValues
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material3.ButtonDefaults
15+
import androidx.compose.material3.FilledTonalButton
16+
import androidx.compose.material3.MaterialTheme
17+
import androidx.compose.material3.Surface
18+
import androidx.compose.material3.Text
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.graphics.RectangleShape
23+
import androidx.compose.ui.unit.dp
24+
import de.afarber.openmapview.LatLng
25+
26+
/**
27+
* A vertical toolbar displaying buttons for navigating to preset locations.
28+
*
29+
* Each button is labeled "Loc 1", "Loc 2", etc. and uses OSM highway pink background.
30+
*
31+
* @param locations List of [LatLng] positions to display as buttons.
32+
* @param onLocationClick Callback invoked when a location button is clicked, receiving the [LatLng].
33+
* @param modifier Modifier to be applied to the toolbar.
34+
*/
35+
@Composable
36+
fun LocationToolbar(
37+
locations: List<LatLng>,
38+
onLocationClick: (LatLng) -> Unit,
39+
modifier: Modifier = Modifier,
40+
) {
41+
Surface(
42+
modifier = modifier,
43+
shape = RoundedCornerShape(ToolbarCornerRadius),
44+
shadowElevation = 6.dp,
45+
color = MaterialTheme.colorScheme.surface,
46+
) {
47+
Column {
48+
locations.forEachIndexed { index, location ->
49+
val shape = when {
50+
locations.size == 1 -> RoundedCornerShape(ToolbarCornerRadius)
51+
index == 0 -> RoundedCornerShape(topStart = ToolbarCornerRadius, topEnd = ToolbarCornerRadius)
52+
index == locations.lastIndex -> RoundedCornerShape(bottomStart = ToolbarCornerRadius, bottomEnd = ToolbarCornerRadius)
53+
else -> RectangleShape
54+
}
55+
FilledTonalButton(
56+
onClick = { onLocationClick(location) },
57+
modifier = Modifier.size(width = 72.dp, height = 48.dp),
58+
shape = shape,
59+
contentPadding = PaddingValues(0.dp),
60+
colors = ButtonDefaults.filledTonalButtonColors(
61+
containerColor = OsmHighwayPink,
62+
contentColor = Color.Black,
63+
),
64+
) {
65+
Text("Loc ${index + 1}", style = MaterialTheme.typography.bodySmall)
66+
}
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)