@@ -2,146 +2,142 @@ package com.google.maps.android.utils.demo
22
33import android.graphics.Canvas
44import android.graphics.Color
5- import android.os.Bundle
6- import android.widget.Button
5+ import android.view.ViewGroup
76import android.widget.SeekBar
8- import android.widget.TextView
9- import androidx.appcompat.app.AppCompatActivity
107import androidx.core.content.ContextCompat
8+ import androidx.core.graphics.createBitmap
9+ import androidx.core.graphics.toColorInt
1110import androidx.lifecycle.MutableLiveData
11+ import androidx.lifecycle.lifecycleScope
1212import com.google.android.gms.maps.CameraUpdateFactory
13- import com.google.android.gms.maps.GoogleMap
14- import com.google.android.gms.maps.OnMapReadyCallback
15- import com.google.android.gms.maps.SupportMapFragment
1613import com.google.android.gms.maps.model.BitmapDescriptor
1714import com.google.android.gms.maps.model.BitmapDescriptorFactory
1815import com.google.android.gms.maps.model.LatLng
16+ import com.google.android.gms.maps.model.LatLngBounds
1917import com.google.android.gms.maps.model.Marker
2018import com.google.android.gms.maps.model.MarkerOptions
2119import com.google.android.gms.maps.model.Polyline
2220import com.google.android.gms.maps.model.PolylineOptions
2321import com.google.maps.android.SphericalUtil
24- import kotlinx.coroutines.CoroutineScope
25- import kotlinx.coroutines.Dispatchers
22+ import com.google.maps.android.utils.demo.databinding.ActivityPolylineProgressDemoBinding
2623import kotlinx.coroutines.Job
2724import kotlinx.coroutines.delay
2825import kotlinx.coroutines.launch
29- import androidx.core.graphics.createBitmap
30- import androidx.core.graphics.toColorInt
3126
32- class PolylineProgressDemoActivity : AppCompatActivity (), OnMapReadyCallback, SeekBar.OnSeekBarChangeListener {
27+ /* *
28+ * This demo showcases how to animate a marker along a geodesic polyline, illustrating
29+ * key features of the Android Maps Utils library and modern Android development practices.
30+ */
31+ class PolylineProgressDemoActivity : BaseDemoActivity (), SeekBar.OnSeekBarChangeListener {
32+
33+ companion object {
34+ private const val POLYLINE_WIDTH = 15f
35+ private const val PROGRESS_POLYLINE_WIDTH = 7f
36+ private const val ANIMATION_STEP_SIZE = 1
37+ private const val ANIMATION_DELAY_MS = 75L
38+ }
3339
34- private var planeIcon: BitmapDescriptor ? = null
35- private lateinit var map: GoogleMap
40+ private lateinit var binding: ActivityPolylineProgressDemoBinding
3641 private lateinit var originalPolyline: Polyline
3742 private var progressPolyline: Polyline ? = null
3843 private var progressMarker: Marker ? = null
3944
40- private lateinit var seekBar: SeekBar
41- private lateinit var percentageTextView: TextView
45+ private val planeIcon: BitmapDescriptor by lazy {
46+ bitmapDescriptorFromVector(R .drawable.baseline_airplanemode_active_24, " #FFD700" .toColorInt())
47+ }
4248
43- private var isReady = false
44- private var animationJob: Job ? = null
49+ private data class AnimationState (val progress : Int , val direction : Int )
4550
46- private val progress = MutableLiveData <Int >()
47- private val direction = MutableLiveData <Int >()
48- private val stepSize = 1
51+ private val animationState = MutableLiveData <AnimationState >()
52+ private var animationJob: Job ? = null
4953
5054 private val polylinePoints = listOf (
5155 LatLng (40.7128 , - 74.0060 ), // New York
56+ LatLng (47.6062 , - 122.3321 ), // Seattle
57+ LatLng (39.7392 , - 104.9903 ), // Denver
58+ LatLng (37.7749 , - 122.4194 ), // San Francisco
5259 LatLng (34.0522 , - 118.2437 ), // Los Angeles
5360 LatLng (41.8781 , - 87.6298 ), // Chicago
5461 LatLng (29.7604 , - 95.3698 ), // Houston
5562 LatLng (39.9526 , - 75.1652 ) // Philadelphia
5663 )
5764
58- override fun onCreate (savedInstanceState : Bundle ? ) {
59- super .onCreate(savedInstanceState)
60- isReady = false
61- setContentView(R .layout.activity_polyline_progress_demo)
65+ override fun getLayoutId (): Int = R .layout.activity_polyline_progress_demo
6266
63- val mapFragment = supportFragmentManager.findFragmentById(R .id.map) as SupportMapFragment
64- mapFragment.getMapAsync(this )
67+ /* *
68+ * This is where the demo begins. It is called from the base activity's `onMapReady` callback.
69+ */
70+ override fun startDemo (isRestore : Boolean ) {
71+ // The layout is already inflated by the base class. We can now bind to it.
72+ val rootView = (findViewById<ViewGroup >(android.R .id.content)).getChildAt(0 )
73+ binding = ActivityPolylineProgressDemoBinding .bind(rootView)
6574
66- progress.value = 0
67- direction.value = 1
75+ setupMap()
76+ setupUI()
77+ // Set the initial state. The observer in setupUI will handle the first UI update.
78+ animationState.value = AnimationState (progress = 0 , direction = 1 )
79+ startAnimation()
6880 }
6981
70- override fun onMapReady (googleMap : GoogleMap ) {
71- map = googleMap
72- isReady = true
73-
74- planeIcon = bitmapDescriptorFromVector(R .drawable.baseline_airplanemode_active_24, " #FFD700" .toColorInt())
75-
82+ private fun setupMap () {
7683 originalPolyline = map.addPolyline(
7784 PolylineOptions ()
7885 .addAll(polylinePoints)
7986 .color(Color .GRAY )
80- .width(15f ) // Set the width of the base polyline
81- .geodesic(true ) // Be sure to set the geodesic flag!
87+ .width(POLYLINE_WIDTH )
88+ .geodesic(true ) // A geodesic polyline follows the curvature of the Earth.
8289 )
8390
84- val bounds = with (com.google.android.gms.maps.model.LatLngBounds .Builder ()) {
85- for (point in polylinePoints) {
86- include(point)
87- }
88- build()
89- }
90-
91+ val bounds = LatLngBounds .builder().apply {
92+ polylinePoints.forEach { include(it) }
93+ }.build()
9194 map.moveCamera(CameraUpdateFactory .newLatLngBounds(bounds, 100 ))
95+ }
9296
93- percentageTextView = findViewById(R .id.percentageTextView)
94- seekBar = findViewById<SeekBar >(R .id.seekBar).also {
95- it.setOnSeekBarChangeListener(this )
96- it.progress = 0
97- }
98-
99- findViewById<Button >(R .id.resetButton).setOnClickListener {
100- if (animationJob != null ) {
101- animationJob?.cancel()
102- animationJob = null
103- }
104- direction.value = 1
105- progress.value = 0
97+ private fun setupUI () {
98+ binding.seekBar.setOnSeekBarChangeListener(this )
99+ binding.resetButton.setOnClickListener {
100+ stopAnimation()
101+ animationState.value = AnimationState (progress = 0 , direction = 1 )
106102 startAnimation()
107103 }
108-
109- findViewById<Button >(R .id.pauseButton).setOnClickListener {
110- if (animationJob != null ) {
111- animationJob?.cancel()
112- animationJob = null
104+ binding.pauseButton.setOnClickListener {
105+ if (animationJob?.isActive == true ) {
106+ stopAnimation()
113107 } else {
114108 startAnimation()
115109 }
116110 }
117111
118- progress .observe(this ) {
119- seekBar.progress = it
120- percentageTextView.text = " $it % "
121- updateProgress(it / 100.0 )
112+ animationState .observe(this ) { state ->
113+ binding. seekBar.progress = state.progress
114+ binding. percentageTextView.text = getString( R .string.percentage_format, state.progress)
115+ updateProgressOnMap(state.progress / 100.0 , state.direction )
122116 }
123-
124- startAnimation()
125117 }
126118
127119 private fun startAnimation () {
128- stopAnimation() // Stop any existing animation first
120+ stopAnimation()
121+ val currentState = animationState.value ? : return
129122
130- // Start a coroutine to animate the polyline progress
131- animationJob = CoroutineScope (Dispatchers .Main ).launch {
123+ animationJob = lifecycleScope.launch {
124+ var progress = currentState.progress
125+ var direction = currentState.direction
132126 while (true ) {
133- val currentProgress = progress.value ? : 0
134- val currentDirection = direction.value ? : 1
135- var nextProgress = currentProgress + currentDirection * stepSize
136- if (nextProgress >= 100 ) {
137- nextProgress = 100
138- direction.value = - 1
139- } else if (nextProgress <= 0 ) {
140- nextProgress = 0
141- direction.value = 1
127+ progress = when {
128+ progress > 100 -> {
129+ direction = - 1
130+ 100
131+ }
132+ progress < 0 -> {
133+ direction = 1
134+ 0
135+ }
136+ else -> progress + direction * ANIMATION_STEP_SIZE
142137 }
143- progress.value = nextProgress
144- delay(50 )
138+
139+ animationState.postValue(AnimationState (progress, direction))
140+ delay(ANIMATION_DELAY_MS )
145141 }
146142 }
147143 }
@@ -151,30 +147,18 @@ class PolylineProgressDemoActivity : AppCompatActivity(), OnMapReadyCallback, Se
151147 animationJob = null
152148 }
153149
154- override fun onPause () {
155- super .onPause()
156- animationJob?.cancel()
157- }
158-
159- override fun onResume () {
160- super .onResume()
161- startAnimation()
162- }
163-
164150 override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
165151 if (fromUser) {
166152 stopAnimation()
167- this .progress. value = progress
153+ animationState. value = AnimationState ( progress, animationState.value?.direction ? : 1 )
168154 }
169155 }
170156
171- override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
157+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) { /* No-op */ }
172158
173- override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
174-
175- private fun updateProgress (percentage : Double ) {
176- if (! isReady) return
159+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) { /* No-op */ }
177160
161+ private fun updateProgressOnMap (percentage : Double , direction : Int ) {
178162 progressPolyline?.remove()
179163
180164 val prefix = SphericalUtil .getPolylinePrefix(polylinePoints, percentage)
@@ -183,53 +167,49 @@ class PolylineProgressDemoActivity : AppCompatActivity(), OnMapReadyCallback, Se
183167 PolylineOptions ()
184168 .addAll(prefix)
185169 .color(Color .BLUE )
186- .width(7f ) // Set the width of the progress polyline
187- .zIndex(1f ) // Set the z-index to draw it on top
188- .geodesic(true ) // Be sure to set the geodesic flag!
170+ .width(PROGRESS_POLYLINE_WIDTH )
171+ .zIndex(1f )
172+ .geodesic(true )
189173 )
190174 }
191175
192- val point = SphericalUtil .getPointOnPolyline(polylinePoints, percentage)
193- if (point != null ) {
194- // Get the next point to calculate the heading
195- val nextPoint = SphericalUtil .getPointOnPolyline(polylinePoints, percentage + 0.0001 )
196- var heading = nextPoint?.let { SphericalUtil .computeHeading(point, it) }
197- if (direction.value == - 1 ) {
198- heading = heading?.plus(180 )
199- }
176+ SphericalUtil .getPointOnPolyline(polylinePoints, percentage)?.let { point ->
177+ updateMarker(point, percentage, direction)
178+ }
179+ }
200180
201- if (progressMarker == null ) {
202- progressMarker = map.addMarker(
203- MarkerOptions ()
204- .position(point)
205- .flat(true )
206- .draggable(false )
207- .also {
208- if (heading != null ) {
209- it.rotation(heading.toFloat())
210- }
211- if (planeIcon != null ) {
212- it.icon(planeIcon)
213- }
214- }
215- )
216- } else {
217- progressMarker?.position = point
218- if (heading != null ) {
219- progressMarker?.rotation = heading.toFloat()
220- }
181+ private fun updateMarker (point : LatLng , percentage : Double , direction : Int ) {
182+ val heading = SphericalUtil .getPointOnPolyline(polylinePoints, percentage + 0.0001 )
183+ ?.let { SphericalUtil .computeHeading(point, it) }
184+ ?.let { if (direction == - 1 ) it + 180 else it } // Adjust for reverse direction.
185+
186+ if (progressMarker == null ) {
187+ progressMarker = map.addMarker(
188+ MarkerOptions ()
189+ .position(point)
190+ .flat(true )
191+ .draggable(false )
192+ .icon(planeIcon)
193+ .apply { heading?.let { rotation(it.toFloat()) } }
194+ )
195+ } else {
196+ progressMarker?.also {
197+ it.position = point
198+ heading?.let { newHeading -> it.rotation = newHeading.toFloat() }
221199 }
222200 }
223201 }
224202
225- private fun bitmapDescriptorFromVector (vectorResId : Int , color : Int ): BitmapDescriptor ? {
226- return ContextCompat .getDrawable(this , vectorResId)?.run {
227- setTint(color)
228- setBounds(0 , 0 , intrinsicWidth, intrinsicHeight)
229- val bitmap = createBitmap(intrinsicWidth, intrinsicHeight)
230- val canvas = Canvas (bitmap)
231- draw(canvas)
232- BitmapDescriptorFactory .fromBitmap(bitmap)
233- }
203+ private fun bitmapDescriptorFromVector (vectorResId : Int , color : Int ): BitmapDescriptor {
204+ val vectorDrawable = ContextCompat .getDrawable(this , vectorResId)!!
205+ vectorDrawable.setTint(color)
206+ val bitmap = createBitmap(
207+ vectorDrawable.intrinsicWidth,
208+ vectorDrawable.intrinsicHeight
209+ )
210+ val canvas = Canvas (bitmap)
211+ vectorDrawable.setBounds(0 , 0 , canvas.width, canvas.height)
212+ vectorDrawable.draw(canvas)
213+ return BitmapDescriptorFactory .fromBitmap(bitmap)
234214 }
235215}
0 commit comments