-
Notifications
You must be signed in to change notification settings - Fork 39
Show Callout Sample #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Show Callout Sample #137
Changes from all commits
1e67d7c
b1cfe8f
eae6aa9
62203e8
aa66653
58f9972
625c73e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Show callout | ||
|
||
Show a callout with the latitude and longitude of user-tapped points. | ||
|
||
 | ||
|
||
## Use case | ||
|
||
Callouts are used to display temporary detail content on a map. You can display text and arbitrary UI controls in callouts. | ||
|
||
## How to use the sample | ||
|
||
Tap anywhere on the map. A callout showing the WGS84 coordinates for the tapped point will appear. | ||
|
||
## How it works | ||
|
||
1. When the user taps, get the tapped location(map point) from the `SingleTapConfirmedEvent`. | ||
2. Project the point's geometry to WGS84 using `GeometryEngine.projectOrNull(mapPoint, SpatialReference.wgs84())`. | ||
3. Create a new Android TextView object and set its text to the coordinate string from the point. | ||
4. Show the `Callout` on the map view using `mapView.callout.show()` which takes the above created View and WGS84 point as parameters. | ||
5. Center the map on the tapped location using `mapView.setViewpointCenter`. | ||
|
||
## Relevant API | ||
|
||
* Callout | ||
* GeometryEngine | ||
* MapView | ||
* Point | ||
|
||
## Tags | ||
|
||
balloon, bubble, callout, flyout, flyover, info window, popup, tap |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"category": "Maps", | ||
"description": "Show a callout with the latitude and longitude of user-tapped points.", | ||
"formal_name": "ShowCallout", | ||
"ignore": false, | ||
"images": [ | ||
"show-callout.png" | ||
], | ||
"keywords": [ | ||
"balloon", | ||
"bubble", | ||
"callout", | ||
"flyout", | ||
"flyover", | ||
"info window", | ||
"popup", | ||
"tap", | ||
"Callout", | ||
"GeometryEngine", | ||
"MapView", | ||
"Point" | ||
], | ||
"language": "kotlin", | ||
"redirect_from": "", | ||
"relevant_apis": [ | ||
"Callout", | ||
"GeometryEngine", | ||
"MapView", | ||
"Point" | ||
], | ||
"snippets": [ | ||
"src/main/java/com/esri/arcgismaps/sample/showcallout/MainActivity.kt", | ||
"src/main/java/com/esri/arcgismaps/sample/showcallout/components/ComposeMapView.kt", | ||
"src/main/java/com/esri/arcgismaps/sample/showcallout/components/MapViewModel.kt", | ||
"src/main/java/com/esri/arcgismaps/sample/showcallout/screens/MainScreen.kt" | ||
], | ||
"title": "Show callout" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
apply plugin: 'com.android.application' | ||
apply plugin: 'org.jetbrains.kotlin.android' | ||
|
||
android { | ||
compileSdkVersion rootProject.ext.compileSdkVersion | ||
|
||
defaultConfig { | ||
applicationId "com.esri.arcgismaps.sample.showcallout" | ||
minSdkVersion rootProject.ext.minSdkVersion | ||
targetSdkVersion rootProject.ext.targetSdkVersion | ||
versionCode rootProject.ext.versionCode | ||
versionName rootProject.ext.versionName | ||
buildConfigField("String", "API_KEY", API_KEY) | ||
} | ||
|
||
buildTypes { | ||
release { | ||
minifyEnabled false | ||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||
} | ||
} | ||
|
||
buildFeatures { | ||
compose = true | ||
buildConfig = true | ||
} | ||
composeOptions { | ||
kotlinCompilerExtensionVersion = "$kotlinCompilerExt" | ||
} | ||
|
||
namespace 'com.esri.arcgismaps.sample.showcallout' | ||
} | ||
|
||
dependencies { | ||
// lib dependencies from rootProject build.gradle | ||
implementation "androidx.core:core-ktx:$ktxAndroidCore" | ||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ktxLifecycle" | ||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$ktxLifecycle" | ||
implementation "androidx.activity:activity-compose:$composeActivityVersion" | ||
// Jetpack Compose Bill of Materials | ||
implementation platform("androidx.compose:compose-bom:$composeBOM") | ||
// Jetpack Compose dependencies | ||
implementation "androidx.compose.ui:ui" | ||
implementation "androidx.compose.material3:material3" | ||
implementation "androidx.compose.ui:ui-tooling" | ||
implementation "androidx.compose.ui:ui-tooling-preview" | ||
implementation project(path: ':samples-lib') | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
<uses-permission android:name="android.permission.INTERNET" /> | ||
|
||
<application | ||
android:allowBackup="true" | ||
android:icon="@mipmap/ic_launcher" | ||
android:label="@string/app_name" | ||
android:roundIcon="@mipmap/ic_launcher_round" | ||
android:supportsRtl="true" | ||
android:theme="@style/AppTheme"> | ||
<activity | ||
android:exported="true" | ||
android:name=".MainActivity"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
|
||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
|
||
</manifest> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* Copyright 2023 Esri | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.esri.arcgismaps.sample.showcallout | ||
|
||
import android.os.Bundle | ||
import androidx.activity.ComponentActivity | ||
import androidx.activity.compose.setContent | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.Surface | ||
import androidx.compose.runtime.Composable | ||
import com.arcgismaps.ApiKey | ||
import com.arcgismaps.ArcGISEnvironment | ||
import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme | ||
import com.esri.arcgismaps.sample.showcallout.screens.MainScreen | ||
|
||
class MainActivity : ComponentActivity() { | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
// authentication with an API key or named user is | ||
// required to access basemaps and other location services | ||
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY) | ||
|
||
setContent { | ||
SampleAppTheme { | ||
ShowCalloutApp() | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ShowCalloutApp() { | ||
Surface( | ||
color = MaterialTheme.colorScheme.background | ||
) { | ||
MainScreen( | ||
sampleName = getString(R.string.app_name), | ||
application = application | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* Copyright 2023 Esri | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.esri.arcgismaps.sample.showcallout.components | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.platform.LocalLifecycleOwner | ||
import androidx.compose.ui.viewinterop.AndroidView | ||
import androidx.lifecycle.LifecycleOwner | ||
import androidx.lifecycle.lifecycleScope | ||
import com.arcgismaps.mapping.view.MapView | ||
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Wraps the MapView in a Composable function. | ||
*/ | ||
@Composable | ||
fun ComposeMapView( | ||
modifier: Modifier = Modifier, | ||
mapViewModel: MapViewModel, | ||
onSingleTap: (SingleTapConfirmedEvent) -> Unit = {} | ||
) { | ||
// get an instance of the current lifecycle owner | ||
val lifecycleOwner = LocalLifecycleOwner.current | ||
// collect the latest state of the MapViewState | ||
val mapViewState by mapViewModel.mapViewState.collectAsState() | ||
// create and add MapView to the activity lifecycle | ||
val mapView = createMapViewInstance(lifecycleOwner).apply { | ||
map = mapViewState.arcGISMap | ||
setViewpoint(mapViewState.viewpoint) | ||
// enable animated callout | ||
callout.isAnimationEnabled = true | ||
} | ||
|
||
// wrap the MapView as an AndroidView | ||
AndroidView( | ||
modifier = modifier, | ||
factory = { mapView }, | ||
// recomposes the MapView on changes in the MapViewState | ||
update = { mapView -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, The argument There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if I understand this completely. By To me If at all, we decide to make a change, I would vote for making change to the mapView var created using createMapViewInstance() to something like CC: @shubham7109 since you spearheaded these changes in the initial sample design phase There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean the line 60, the |
||
mapView.apply { | ||
val latlonPoint = mapViewModel.latLonPoint | ||
latlonPoint?.let { | ||
// show callout at the tapped location using the set View | ||
callout.show( | ||
mapViewModel.calloutContent, | ||
latlonPoint | ||
) | ||
lifecycleOwner.lifecycleScope.launch { | ||
// center the map on the tapped location | ||
setViewpointCenter(latlonPoint) | ||
} | ||
} | ||
} | ||
} | ||
) | ||
|
||
// launch coroutine functions in the composition's CoroutineContext | ||
LaunchedEffect(Unit) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a little better to change this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it may be not necessary. |
||
mapView.onSingleTapConfirmed.collect { | ||
onSingleTap(it) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After this Friday's coffee meeting(9/29), I think you need to change this line to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is OK for this case, |
||
} | ||
} | ||
} | ||
|
||
/** | ||
* Create the MapView instance and add it to the Activity lifecycle | ||
*/ | ||
@Composable | ||
fun createMapViewInstance(lifecycleOwner: LifecycleOwner): MapView { | ||
// create the MapView | ||
val mapView = MapView(LocalContext.current) | ||
// add the side effects for MapView composition | ||
DisposableEffect(lifecycleOwner) { | ||
lifecycleOwner.lifecycle.addObserver(mapView) | ||
onDispose { | ||
lifecycleOwner.lifecycle.removeObserver(mapView) | ||
} | ||
} | ||
return mapView | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* Copyright 2023 Esri | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
package com.esri.arcgismaps.sample.showcallout.components | ||
|
||
import android.app.Application | ||
import android.widget.TextView | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.setValue | ||
import androidx.lifecycle.AndroidViewModel | ||
import com.arcgismaps.geometry.GeometryEngine | ||
import com.arcgismaps.geometry.Point | ||
import com.arcgismaps.geometry.SpatialReference | ||
import com.arcgismaps.mapping.ArcGISMap | ||
import com.arcgismaps.mapping.BasemapStyle | ||
import com.arcgismaps.mapping.Viewpoint | ||
import com.esri.arcgismaps.sample.showcallout.R | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
|
||
class MapViewModel(private val application: Application) : AndroidViewModel(application) { | ||
// set the MapView mutable stateflow | ||
val mapViewState = MutableStateFlow(MapViewState(application)) | ||
// View to show callout | ||
var calloutContent: TextView by mutableStateOf(TextView(application)) | ||
// initialize lat long point | ||
var latLonPoint: Point? by mutableStateOf(null) | ||
|
||
fun onMapTapped(mapPoint: Point?) { | ||
// get map point from the Single tap event | ||
mapPoint?.let { point -> | ||
// convert the point to WGS84 for obtaining lat/lon format | ||
latLonPoint = GeometryEngine.projectOrNull( | ||
point, | ||
SpatialReference.wgs84() | ||
) as Point | ||
// set the callout text to display point coordinates | ||
calloutContent.text = application.getString( | ||
R.string.callout_text, | ||
latLonPoint?.y, | ||
latLonPoint?.x | ||
) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Data class that represents the MapView state | ||
*/ | ||
data class MapViewState(val application: Application) { | ||
var arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight) | ||
var viewpoint: Viewpoint = Viewpoint(34.056295, -117.195800, 1000000.0) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could it be better for the callback be a
suspend
function,onSingleTap: suspend (SingleTapConfirmedEvent) -> Unit = {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@changanxian I don't think I understand, what value will it bring in current samples, making the onSingleTap callback a suspend function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may not be necessary in this case, and keep the current implementation. But it will be an asynchronous call. Imagine if the
onSingleTap
is called in a loop, it will be different.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay!