Skip to content

Commit 5a9c517

Browse files
authored
Show Callout Sample (#137)
1 parent a32b86e commit 5a9c517

26 files changed

+663
-0
lines changed

show-callout/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

show-callout/README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Show callout
2+
3+
Show a callout with the latitude and longitude of user-tapped points.
4+
5+
![Show Callout App](show-callout.png)
6+
7+
## Use case
8+
9+
Callouts are used to display temporary detail content on a map. You can display text and arbitrary UI controls in callouts.
10+
11+
## How to use the sample
12+
13+
Tap anywhere on the map. A callout showing the WGS84 coordinates for the tapped point will appear.
14+
15+
## How it works
16+
17+
1. When the user taps, get the tapped location(map point) from the `SingleTapConfirmedEvent`.
18+
2. Project the point's geometry to WGS84 using `GeometryEngine.projectOrNull(mapPoint, SpatialReference.wgs84())`.
19+
3. Create a new Android TextView object and set its text to the coordinate string from the point.
20+
4. Show the `Callout` on the map view using `mapView.callout.show()` which takes the above created View and WGS84 point as parameters.
21+
5. Center the map on the tapped location using `mapView.setViewpointCenter`.
22+
23+
## Relevant API
24+
25+
* Callout
26+
* GeometryEngine
27+
* MapView
28+
* Point
29+
30+
## Tags
31+
32+
balloon, bubble, callout, flyout, flyover, info window, popup, tap

show-callout/README.metadata.json

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"category": "Maps",
3+
"description": "Show a callout with the latitude and longitude of user-tapped points.",
4+
"formal_name": "ShowCallout",
5+
"ignore": false,
6+
"images": [
7+
"show-callout.png"
8+
],
9+
"keywords": [
10+
"balloon",
11+
"bubble",
12+
"callout",
13+
"flyout",
14+
"flyover",
15+
"info window",
16+
"popup",
17+
"tap",
18+
"Callout",
19+
"GeometryEngine",
20+
"MapView",
21+
"Point"
22+
],
23+
"language": "kotlin",
24+
"redirect_from": "",
25+
"relevant_apis": [
26+
"Callout",
27+
"GeometryEngine",
28+
"MapView",
29+
"Point"
30+
],
31+
"snippets": [
32+
"src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt",
33+
"src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt",
34+
"src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt",
35+
"src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt"
36+
],
37+
"title": "Show callout"
38+
}

show-callout/build.gradle

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
apply plugin: 'com.android.application'
2+
apply plugin: 'org.jetbrains.kotlin.android'
3+
4+
android {
5+
compileSdkVersion rootProject.ext.compileSdkVersion
6+
7+
defaultConfig {
8+
applicationId "com.esri.arcgismaps.sample.showcallout"
9+
minSdkVersion rootProject.ext.minSdkVersion
10+
targetSdkVersion rootProject.ext.targetSdkVersion
11+
versionCode rootProject.ext.versionCode
12+
versionName rootProject.ext.versionName
13+
buildConfigField("String", "API_KEY", API_KEY)
14+
}
15+
16+
buildTypes {
17+
release {
18+
minifyEnabled false
19+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20+
}
21+
}
22+
23+
buildFeatures {
24+
compose = true
25+
buildConfig = true
26+
}
27+
composeOptions {
28+
kotlinCompilerExtensionVersion = "$kotlinCompilerExt"
29+
}
30+
31+
namespace 'com.esri.arcgismaps.sample.showcallout'
32+
}
33+
34+
dependencies {
35+
// lib dependencies from rootProject build.gradle
36+
implementation "androidx.core:core-ktx:$ktxAndroidCore"
37+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle"
38+
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle"
39+
implementation "androidx.activity:activity-compose:$composeActivityVersion"
40+
// Jetpack Compose Bill of Materials
41+
implementation platform("androidx.compose:compose-bom:$composeBOM")
42+
// Jetpack Compose dependencies
43+
implementation "androidx.compose.ui:ui"
44+
implementation "androidx.compose.material3:material3"
45+
implementation "androidx.compose.ui:ui-tooling"
46+
implementation "androidx.compose.ui:ui-tooling-preview"
47+
implementation project(path: ':samples-lib')
48+
}

show-callout/proguard-rules.pro

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile

show-callout/show-callout.png

182 KB
Loading
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
6+
<application
7+
android:allowBackup="true"
8+
android:icon="@mipmap/ic_launcher"
9+
android:label="@string/app_name"
10+
android:roundIcon="@mipmap/ic_launcher_round"
11+
android:supportsRtl="true"
12+
android:theme="@style/AppTheme">
13+
<activity
14+
android:exported="true"
15+
android:name=".MainActivity">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
19+
<category android:name="android.intent.category.LAUNCHER" />
20+
</intent-filter>
21+
</activity>
22+
</application>
23+
24+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* Copyright 2023 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.showcallout
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Surface
24+
import androidx.compose.runtime.Composable
25+
import com.arcgismaps.ApiKey
26+
import com.arcgismaps.ArcGISEnvironment
27+
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
28+
import com.esri.arcgismaps.sample.showcallout.screens.MainScreen
29+
30+
class MainActivity : ComponentActivity() {
31+
32+
override fun onCreate(savedInstanceState: Bundle?) {
33+
super.onCreate(savedInstanceState)
34+
// authentication with an API key or named user is
35+
// required to access basemaps and other location services
36+
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
37+
38+
setContent {
39+
SampleAppTheme {
40+
ShowCalloutApp()
41+
}
42+
}
43+
}
44+
45+
@Composable
46+
private fun ShowCalloutApp() {
47+
Surface(
48+
color = MaterialTheme.colorScheme.background
49+
) {
50+
MainScreen(
51+
sampleName = getString(R.string.app_name),
52+
application = application
53+
)
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* Copyright 2023 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.showcallout.components
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.DisposableEffect
21+
import androidx.compose.runtime.LaunchedEffect
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.platform.LocalContext
26+
import androidx.compose.ui.platform.LocalLifecycleOwner
27+
import androidx.compose.ui.viewinterop.AndroidView
28+
import androidx.lifecycle.LifecycleOwner
29+
import androidx.lifecycle.lifecycleScope
30+
import com.arcgismaps.mapping.view.MapView
31+
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
32+
import kotlinx.coroutines.launch
33+
34+
/**
35+
* Wraps the MapView in a Composable function.
36+
*/
37+
@Composable
38+
fun ComposeMapView(
39+
modifier: Modifier = Modifier,
40+
mapViewModel: MapViewModel,
41+
onSingleTap: (SingleTapConfirmedEvent) -> Unit = {}
42+
) {
43+
// get an instance of the current lifecycle owner
44+
val lifecycleOwner = LocalLifecycleOwner.current
45+
// collect the latest state of the MapViewState
46+
val mapViewState by mapViewModel.mapViewState.collectAsState()
47+
// create and add MapView to the activity lifecycle
48+
val mapView = createMapViewInstance(lifecycleOwner).apply {
49+
map = mapViewState.arcGISMap
50+
setViewpoint(mapViewState.viewpoint)
51+
// enable animated callout
52+
callout.isAnimationEnabled = true
53+
}
54+
55+
// wrap the MapView as an AndroidView
56+
AndroidView(
57+
modifier = modifier,
58+
factory = { mapView },
59+
// recomposes the MapView on changes in the MapViewState
60+
update = { mapView ->
61+
mapView.apply {
62+
val latlonPoint = mapViewModel.latLonPoint
63+
latlonPoint?.let {
64+
// show callout at the tapped location using the set View
65+
callout.show(
66+
mapViewModel.calloutContent,
67+
latlonPoint
68+
)
69+
lifecycleOwner.lifecycleScope.launch {
70+
// center the map on the tapped location
71+
setViewpointCenter(latlonPoint)
72+
}
73+
}
74+
}
75+
}
76+
)
77+
78+
// launch coroutine functions in the composition's CoroutineContext
79+
LaunchedEffect(Unit) {
80+
mapView.onSingleTapConfirmed.collect {
81+
onSingleTap(it)
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Create the MapView instance and add it to the Activity lifecycle
88+
*/
89+
@Composable
90+
fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView {
91+
// create the MapView
92+
val mapView = MapView(LocalContext.current)
93+
// add the side effects for MapView composition
94+
DisposableEffect(lifecycleOwner) {
95+
lifecycleOwner.lifecycle.addObserver(mapView)
96+
onDispose {
97+
lifecycleOwner.lifecycle.removeObserver(mapView)
98+
}
99+
}
100+
return mapView
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* Copyright 2023 Esri
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
package com.esri.arcgismaps.sample.showcallout.components
18+
19+
import android.app.Application
20+
import android.widget.TextView
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.setValue
24+
import androidx.lifecycle.AndroidViewModel
25+
import com.arcgismaps.geometry.GeometryEngine
26+
import com.arcgismaps.geometry.Point
27+
import com.arcgismaps.geometry.SpatialReference
28+
import com.arcgismaps.mapping.ArcGISMap
29+
import com.arcgismaps.mapping.BasemapStyle
30+
import com.arcgismaps.mapping.Viewpoint
31+
import com.esri.arcgismaps.sample.showcallout.R
32+
import kotlinx.coroutines.flow.MutableStateFlow
33+
34+
class MapViewModel(private val application: Application) : AndroidViewModel(application) {
35+
// set the MapView mutable stateflow
36+
val mapViewState = MutableStateFlow(MapViewState(application))
37+
// View to show callout
38+
var calloutContent: TextView by mutableStateOf(TextView(application))
39+
// initialize lat long point
40+
var latLonPoint: Point? by mutableStateOf(null)
41+
42+
fun onMapTapped(mapPoint: Point?) {
43+
// get map point from the Single tap event
44+
mapPoint?.let { point ->
45+
// convert the point to WGS84 for obtaining lat/lon format
46+
latLonPoint = GeometryEngine.projectOrNull(
47+
point,
48+
SpatialReference.wgs84()
49+
) as Point
50+
// set the callout text to display point coordinates
51+
calloutContent.text = application.getString(
52+
R.string.callout_text,
53+
latLonPoint?.y,
54+
latLonPoint?.x
55+
)
56+
}
57+
}
58+
}
59+
60+
/**
61+
* Data class that represents the MapView state
62+
*/
63+
data class MapViewState(val application: Application) {
64+
var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight)
65+
var viewpoint: Viewpoint = Viewpoint(34.056295, -117.195800, 1000000.0)
66+
}

0 commit comments

Comments
 (0)