Skip to content

Commit b858c08

Browse files
authored
Merge branch 'main' into chore/better_map_id_handling
2 parents 3b6036a + af704c5 commit b858c08

File tree

15 files changed

+488
-6
lines changed

15 files changed

+488
-6
lines changed

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
java-version: '17'
4141
distribution: 'temurin'
4242
- name: Install Go
43-
uses: actions/setup-go@v5
43+
uses: actions/setup-go@v6
4444
with:
4545
go-version: '1.20'
4646

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
SONATYPE_TOKEN_PASSWORD: ${{ secrets.SONATYPE_TOKEN_PASSWORD }}
5252
SONATYPE_TOKEN_USERNAME: ${{ secrets.SONATYPE_TOKEN }}
5353

54-
- uses: actions/setup-node@v4
54+
- uses: actions/setup-node@v5
5555
with:
5656
node-version: '14'
5757

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ dependencies {
4545
// Utilities for Maps SDK for Android (requires Google Play Services)
4646
// You do not need to add a separate dependency for the Maps SDK for Android
4747
// since this library builds in the compatible version of the Maps SDK.
48-
implementation 'com.google.maps.android:android-maps-utils:3.17.0'
48+
implementation 'com.google.maps.android:android-maps-utils:3.18.0'
4949
5050
// Optionally add the Kotlin Extensions (KTX) for full Kotlin language support
5151
// See latest version at https://github.com/googlemaps/android-maps-ktx

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ tasks.register<Delete>("clean") {
3737

3838
allprojects {
3939
group = "com.google.maps.android"
40-
version = "3.17.0"
40+
version = "3.18.0"
4141
}

demo/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ android {
2828
defaultConfig {
2929
compileSdk = libs.versions.compileSdk.get().toInt()
3030
applicationId = "com.google.maps.android.utils.demo"
31-
minSdk = 21
31+
minSdk = libs.versions.minimumSdk.get().toInt()
3232
targetSdk = libs.versions.targetSdk.get().toInt()
3333
versionCode = 1
3434
versionName = "1.0"
@@ -44,6 +44,10 @@ android {
4444
}
4545
}
4646

47+
buildFeatures {
48+
viewBinding = true
49+
}
50+
4751
kotlinOptions {
4852
jvmTarget = "17"
4953
}

demo/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
<activity
7272
android:name=".PolySimplifyDemoActivity"
7373
android:exported="true" />
74+
<activity
75+
android:name=".PolylineProgressDemoActivity"
76+
android:exported="true" />
7477
<activity
7578
android:name=".IconGeneratorDemoActivity"
7679
android:exported="true" />

demo/src/main/java/com/google/maps/android/utils/demo/MainActivity.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ protected void onCreate(Bundle savedInstanceState) {
7777
addDemo("Clustering: Force on Zoom", ZoomClusteringDemoActivity.class);
7878
addDemo("PolyUtil.decode", PolyDecodeDemoActivity.class);
7979
addDemo("PolyUtil.simplify", PolySimplifyDemoActivity.class);
80+
addDemo("Polyline Progress", PolylineProgressDemoActivity.class);
8081
addDemo("IconGenerator", IconGeneratorDemoActivity.class);
8182
addDemo("SphericalUtil.computeDistanceBetween", DistanceDemoActivity.class);
8283
addDemo("Generating tiles", TileProviderAndProjectionDemo.class);

demo/src/main/java/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Copyright 2025 Google LLC
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.
114
/*
215
* Copyright 2015 Sean J. Barbeau
316
*
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.maps.android.utils.demo
18+
19+
import android.graphics.Canvas
20+
import android.graphics.Color
21+
import android.view.ViewGroup
22+
import android.widget.SeekBar
23+
import androidx.core.content.ContextCompat
24+
import androidx.core.graphics.createBitmap
25+
import androidx.core.graphics.toColorInt
26+
import androidx.lifecycle.MutableLiveData
27+
import androidx.lifecycle.lifecycleScope
28+
import com.google.android.gms.maps.CameraUpdateFactory
29+
import com.google.android.gms.maps.model.BitmapDescriptor
30+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
31+
import com.google.android.gms.maps.model.LatLng
32+
import com.google.android.gms.maps.model.LatLngBounds
33+
import com.google.android.gms.maps.model.Marker
34+
import com.google.android.gms.maps.model.MarkerOptions
35+
import com.google.android.gms.maps.model.Polyline
36+
import com.google.android.gms.maps.model.PolylineOptions
37+
import com.google.maps.android.SphericalUtil
38+
import com.google.maps.android.utils.demo.databinding.ActivityPolylineProgressDemoBinding
39+
import kotlinx.coroutines.Job
40+
import kotlinx.coroutines.delay
41+
import kotlinx.coroutines.launch
42+
43+
/**
44+
* This demo showcases how to animate a marker along a geodesic polyline, illustrating
45+
* key features of the Android Maps Utils library and modern Android development practices.
46+
*/
47+
class PolylineProgressDemoActivity : BaseDemoActivity(), SeekBar.OnSeekBarChangeListener {
48+
49+
companion object {
50+
private const val POLYLINE_WIDTH = 15f
51+
private const val PROGRESS_POLYLINE_WIDTH = 7f
52+
private const val ANIMATION_STEP_SIZE = 1
53+
private const val ANIMATION_DELAY_MS = 75L
54+
}
55+
56+
private lateinit var binding: ActivityPolylineProgressDemoBinding
57+
private lateinit var originalPolyline: Polyline
58+
private var progressPolyline: Polyline? = null
59+
private var progressMarker: Marker? = null
60+
61+
private val planeIcon: BitmapDescriptor by lazy {
62+
bitmapDescriptorFromVector(R.drawable.baseline_airplanemode_active_24, "#FFD700".toColorInt())
63+
}
64+
65+
private data class AnimationState(val progress: Int, val direction: Int)
66+
67+
private val animationState = MutableLiveData<AnimationState>()
68+
private var animationJob: Job? = null
69+
70+
private val polylinePoints = listOf(
71+
LatLng(40.7128, -74.0060), // New York
72+
LatLng(47.6062, -122.3321), // Seattle
73+
LatLng(39.7392, -104.9903), // Denver
74+
LatLng(37.7749, -122.4194), // San Francisco
75+
LatLng(34.0522, -118.2437), // Los Angeles
76+
LatLng(41.8781, -87.6298), // Chicago
77+
LatLng(29.7604, -95.3698), // Houston
78+
LatLng(39.9526, -75.1652) // Philadelphia
79+
)
80+
81+
override fun getLayoutId(): Int = R.layout.activity_polyline_progress_demo
82+
83+
/**
84+
* This is where the demo begins. It is called from the base activity's `onMapReady` callback.
85+
*/
86+
override fun startDemo(isRestore: Boolean) {
87+
// The layout is already inflated by the base class. We can now bind to it.
88+
val rootView = (findViewById<ViewGroup>(android.R.id.content)).getChildAt(0)
89+
binding = ActivityPolylineProgressDemoBinding.bind(rootView)
90+
91+
setupMap()
92+
setupUI()
93+
// Set the initial state. The observer in setupUI will handle the first UI update.
94+
animationState.value = AnimationState(progress = 0, direction = 1)
95+
startAnimation()
96+
}
97+
98+
private fun setupMap() {
99+
originalPolyline = map.addPolyline(
100+
PolylineOptions()
101+
.addAll(polylinePoints)
102+
.color(Color.GRAY)
103+
.width(POLYLINE_WIDTH)
104+
.geodesic(true) // A geodesic polyline follows the curvature of the Earth.
105+
)
106+
107+
val bounds = LatLngBounds.builder().apply {
108+
polylinePoints.forEach { include(it) }
109+
}.build()
110+
map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))
111+
}
112+
113+
private fun setupUI() {
114+
binding.seekBar.setOnSeekBarChangeListener(this)
115+
binding.resetButton.setOnClickListener {
116+
stopAnimation()
117+
animationState.value = AnimationState(progress = 0, direction = 1)
118+
startAnimation()
119+
}
120+
binding.pauseButton.setOnClickListener {
121+
if (animationJob?.isActive == true) {
122+
stopAnimation()
123+
} else {
124+
startAnimation()
125+
}
126+
}
127+
128+
animationState.observe(this) { state ->
129+
binding.seekBar.progress = state.progress
130+
binding.percentageTextView.text = getString(R.string.percentage_format, state.progress)
131+
updateProgressOnMap(state.progress / 100.0, state.direction)
132+
}
133+
}
134+
135+
private fun startAnimation() {
136+
stopAnimation()
137+
val currentState = animationState.value ?: return
138+
139+
animationJob = lifecycleScope.launch {
140+
var progress = currentState.progress
141+
var direction = currentState.direction
142+
while (true) {
143+
progress = when {
144+
progress > 100 -> {
145+
direction = -1
146+
100
147+
}
148+
progress < 0 -> {
149+
direction = 1
150+
0
151+
}
152+
else -> progress + direction * ANIMATION_STEP_SIZE
153+
}
154+
155+
animationState.postValue(AnimationState(progress, direction))
156+
delay(ANIMATION_DELAY_MS)
157+
}
158+
}
159+
}
160+
161+
private fun stopAnimation() {
162+
animationJob?.cancel()
163+
animationJob = null
164+
}
165+
166+
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
167+
if (fromUser) {
168+
stopAnimation()
169+
animationState.value = AnimationState(progress, animationState.value?.direction ?: 1)
170+
}
171+
}
172+
173+
override fun onStartTrackingTouch(seekBar: SeekBar?) { /* No-op */ }
174+
175+
override fun onStopTrackingTouch(seekBar: SeekBar?) { /* No-op */ }
176+
177+
private fun updateProgressOnMap(percentage: Double, direction: Int) {
178+
progressPolyline?.remove()
179+
180+
val prefix = SphericalUtil.getPolylinePrefix(polylinePoints, percentage)
181+
if (prefix.isNotEmpty()) {
182+
progressPolyline = map.addPolyline(
183+
PolylineOptions()
184+
.addAll(prefix)
185+
.color(Color.BLUE)
186+
.width(PROGRESS_POLYLINE_WIDTH)
187+
.zIndex(1f)
188+
.geodesic(true)
189+
)
190+
}
191+
192+
SphericalUtil.getPointOnPolyline(polylinePoints, percentage)?.let { point ->
193+
updateMarker(point, percentage, direction)
194+
}
195+
}
196+
197+
private fun updateMarker(point: LatLng, percentage: Double, direction: Int) {
198+
val heading = SphericalUtil.getPointOnPolyline(polylinePoints, percentage + 0.0001)
199+
?.let { SphericalUtil.computeHeading(point, it) }
200+
?.let { if (direction == -1) it + 180 else it } // Adjust for reverse direction.
201+
202+
if (progressMarker == null) {
203+
progressMarker = map.addMarker(
204+
MarkerOptions()
205+
.position(point)
206+
.flat(true)
207+
.draggable(false)
208+
.icon(planeIcon)
209+
.apply { heading?.let { rotation(it.toFloat()) } }
210+
)
211+
} else {
212+
progressMarker?.also {
213+
it.position = point
214+
heading?.let { newHeading -> it.rotation = newHeading.toFloat() }
215+
}
216+
}
217+
}
218+
219+
private fun bitmapDescriptorFromVector(vectorResId: Int, color: Int): BitmapDescriptor {
220+
val vectorDrawable = ContextCompat.getDrawable(this, vectorResId)!!
221+
vectorDrawable.setTint(color)
222+
val bitmap = createBitmap(
223+
vectorDrawable.intrinsicWidth,
224+
vectorDrawable.intrinsicHeight
225+
)
226+
val canvas = Canvas(bitmap)
227+
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
228+
vectorDrawable.draw(canvas)
229+
return BitmapDescriptorFactory.fromBitmap(bitmap)
230+
}
231+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2025 Google LLC
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
19+
20+
<path android:fillColor="@android:color/white" android:pathData="M22,16v-2l-8.5,-5V3.5C13.5,2.67 12.83,2 12,2s-1.5,0.67 -1.5,1.5V9L2,14v2l8.5,-2.5V19L8,20.5L8,22l4,-1l4,1l0,-1.5L13.5,19v-5.5L22,16z"/>
21+
22+
</vector>

0 commit comments

Comments
 (0)