Skip to content

Commit 45c3440

Browse files
kediarovgithub-actions[bot]
authored andcommitted
[compose] Add incremental camera animation methods to MapViewportState (#14735)
Fix [MAPSAND-2800](https://mapbox.atlassian.net/browse/MAPSAND-2800) ### Summary - Add `rotateBy`, `moveBy`, `pitchBy`, and `scaleBy` methods to `MapViewportState` in the Compose extension - Each method integrates with the viewport system via a new `RelativeAnimationViewportTransition` - Add demo activity `MapViewportStatePredefinedAnimatorsActivity` showcasing all four methods ### Context `MapViewportState` lacked parity with the View-based `CameraAnimationsPlugin` for incremental/relative camera animations. Users needed these methods to perform gesture-like programmatic camera movements in Compose. ### Discussion - All methods are exclusive, so 2 animations can't run at the same time, so they do not interfere with other viewport transitions, but substitute them. To add more granular control with `playAnimatorsTogether` or `playAnimatorsSequentially`, we would need some other API. [MAPSAND-2800]: https://mapbox.atlassian.net/browse/MAPSAND-2800?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ cc @mapbox/maps-android GitOrigin-RevId: d02269c8d262b201d2bc0bc696457957fa46e1e5
1 parent 01b58ac commit 45c3440

9 files changed

Lines changed: 533 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone.
88
## Features ✨ and improvements 🏁
99
* [compose] Introduce experimental `IndoorSelector` composable function available inside `MapboxMap`, displaying a scrollable floor-selection widget that appears automatically when an indoor building is in view. Exposes `IndoorSelectorState` for programmatic access to the current floor list and selected floor, and an `onFloorClicked` callback for reacting to user selections.
1010
* [compose] Add `IndoorSelectorControl` headless composable inside `MapIndoorSelectorScope`: attaches the indoor plugin to an `IndoorSelectorState` without rendering any UI, enabling custom floor-selector implementations.
11+
* [compose] Add `rotateBy`, `moveBy`, `pitchBy`, and `scaleBy` methods to `MapViewportState` for incremental camera animations.
1112
* [compose] Add `rememberStyleImage(imageId, image9Patch: NinePatchImage)` and `remember9PatchStyleImage(imageId, bitmap: Bitmap)` overloads for nine-patch images.
1213
* Introduce new experimental `ViewAnnotationOptions.enableSymbolLayerCollision` option which allows view annotations to hide underlying map symbols to avoid visual clutter.
1314
By default, the full bounding box of the view annotation is used for collision detection. If your annotation has a non-rectangular shape, it is highly recommended to mark the specific subviews that should participate via the new experimental `View.mbxCollisionBox` flag.

compose-app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@
163163
android:name="@string/category"
164164
android:value="@string/category_camera" />
165165
</activity>
166+
<activity
167+
android:name=".examples.animation.MapViewportStatePredefinedAnimatorsActivity"
168+
android:configChanges="orientation|screenSize|screenLayout"
169+
android:description="@string/description_map_viewport_state_predefined_animators"
170+
android:exported="true"
171+
android:label="@string/activity_map_viewport_state_predefined_animators"
172+
android:parentActivityName=".ExampleOverviewActivity">
173+
<meta-data
174+
android:name="@string/category"
175+
android:value="@string/category_camera" />
176+
</activity>
166177
<activity
167178
android:name=".examples.basic.MultiDisplayActivity"
168179
android:configChanges="orientation|screenSize|screenLayout"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.mapbox.maps.compose.testapp.examples.animation
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.WindowInsets
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.statusBars
11+
import androidx.compose.foundation.layout.windowInsetsPadding
12+
import androidx.compose.material.DropdownMenu
13+
import androidx.compose.material.DropdownMenuItem
14+
import androidx.compose.material.Icon
15+
import androidx.compose.material.IconButton
16+
import androidx.compose.material.MaterialTheme
17+
import androidx.compose.material.Scaffold
18+
import androidx.compose.material.Surface
19+
import androidx.compose.material.Text
20+
import androidx.compose.material.TopAppBar
21+
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.filled.ArrowBack
23+
import androidx.compose.material.icons.filled.MoreVert
24+
import androidx.compose.material.primarySurface
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
27+
import androidx.compose.runtime.remember
28+
import androidx.compose.runtime.setValue
29+
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.unit.dp
31+
import com.mapbox.geojson.Point
32+
import com.mapbox.maps.ScreenCoordinate
33+
import com.mapbox.maps.compose.testapp.ui.theme.MapboxMapComposeTheme
34+
import com.mapbox.maps.dsl.cameraOptions
35+
import com.mapbox.maps.extension.compose.MapboxMap
36+
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
37+
import com.mapbox.maps.plugin.animation.MapAnimationOptions
38+
39+
/**
40+
* Showcase incremental camera animations (rotateBy, moveBy, pitchBy, scaleBy) using [com.mapbox.maps.extension.compose.animation.viewport.MapViewportState].
41+
*/
42+
public class MapViewportStatePredefinedAnimatorsActivity : ComponentActivity() {
43+
override fun onCreate(savedInstanceState: Bundle?) {
44+
super.onCreate(savedInstanceState)
45+
setContent {
46+
val mapViewportState = rememberMapViewportState {
47+
setCameraOptions(START_CAMERA)
48+
}
49+
MapboxMapComposeTheme {
50+
var menuExpanded by remember { mutableStateOf(false) }
51+
52+
fun jumpToStart() {
53+
mapViewportState.setCameraOptions(START_CAMERA)
54+
}
55+
56+
Scaffold(
57+
topBar = {
58+
Surface(
59+
color = MaterialTheme.colors.primarySurface,
60+
elevation = 4.dp
61+
) {
62+
Column(Modifier.windowInsetsPadding(WindowInsets.statusBars)) {
63+
TopAppBar(
64+
title = { Text(text = title.toString()) },
65+
navigationIcon = {
66+
IconButton(onClick = { finish() }) {
67+
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
68+
}
69+
},
70+
actions = {
71+
IconButton(onClick = { menuExpanded = true }) {
72+
Icon(Icons.Filled.MoreVert, contentDescription = "More options")
73+
}
74+
DropdownMenu(
75+
expanded = menuExpanded,
76+
onDismissRequest = { menuExpanded = false }
77+
) {
78+
DropdownMenuItem(onClick = {
79+
menuExpanded = false
80+
jumpToStart()
81+
}) { Text("Jump to Start") }
82+
DropdownMenuItem(onClick = {
83+
menuExpanded = false
84+
jumpToStart()
85+
mapViewportState.rotateBy(
86+
first = ScreenCoordinate(0.0, 0.0),
87+
second = ScreenCoordinate(500.0, 500.0),
88+
animationOptions = ANIMATION_OPTIONS
89+
)
90+
}) { Text("Rotate By") }
91+
DropdownMenuItem(onClick = {
92+
menuExpanded = false
93+
jumpToStart()
94+
mapViewportState.moveBy(
95+
screenCoordinate = ScreenCoordinate(500.0, 500.0),
96+
animationOptions = ANIMATION_OPTIONS
97+
)
98+
}) { Text("Move By") }
99+
DropdownMenuItem(onClick = {
100+
menuExpanded = false
101+
jumpToStart()
102+
mapViewportState.pitchBy(
103+
pitch = 70.0,
104+
animationOptions = ANIMATION_OPTIONS
105+
)
106+
}) { Text("Pitch By") }
107+
DropdownMenuItem(onClick = {
108+
menuExpanded = false
109+
jumpToStart()
110+
mapViewportState.scaleBy(
111+
amount = 15.0,
112+
screenCoordinate = ScreenCoordinate(10.0, 10.0),
113+
animationOptions = ANIMATION_OPTIONS
114+
)
115+
}) { Text("Scale By") }
116+
}
117+
},
118+
elevation = 0.dp
119+
)
120+
}
121+
}
122+
}
123+
) {
124+
Box(modifier = Modifier.fillMaxSize()) {
125+
MapboxMap(
126+
modifier = Modifier.fillMaxSize(),
127+
mapViewportState = mapViewportState,
128+
)
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
private companion object {
136+
val START_CAMERA = cameraOptions {
137+
center(Point.fromLngLat(-0.11968, 51.50325))
138+
zoom(15.0)
139+
bearing(0.0)
140+
pitch(0.0)
141+
}
142+
val ANIMATION_OPTIONS: MapAnimationOptions =
143+
MapAnimationOptions.mapAnimationOptions { duration(2000) }
144+
}
145+
}

compose-app/src/main/res/values/example_descriptions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<string name="description_view_annotation">Add ViewAnnotation on the Map</string>
1212
<string name="description_dynamic_view_annotation">Showcase using dynamic ViewAnnotation</string>
1313
<string name="description_map_viewport_animation">Use map viewport animations</string>
14+
<string name="description_map_viewport_state_predefined_animators">Use incremental camera animations (rotateBy, moveBy, pitchBy, scaleBy) via MapViewportState</string>
1415
<string name="description_multiple_display">Display the map on a secondary display</string>
1516
<string name="description_ornaments_customisation">Customise ornaments of the Map</string>
1617
<string name="description_custom_attribution">Customise attribution of the Map</string>

compose-app/src/main/res/values/example_titles.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<string name="activity_view_annotation">View annotation</string>
1212
<string name="activity_dynamic_view_annotation">Dynamic view annotation</string>
1313
<string name="activity_map_viewport_animation">Map Viewport animation</string>
14+
<string name="activity_map_viewport_state_predefined_animators">Map Viewport State Predefined Animators</string>
1415
<string name="activity_multiple_display">Multi display</string>
1516
<string name="activity_ornaments_customisation">Ornament customisation</string>
1617
<string name="activity_custom_attribution">Custom attribution</string>

extension-compose/api/Release/metalava.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ package com.mapbox.maps.extension.compose.animation.viewport {
144144
method public com.mapbox.maps.plugin.viewport.data.ViewportStatusChangeReason? getMapViewportStatusChangedReason();
145145
method public com.mapbox.maps.CameraOptions? getStyleDefaultCameraOptions();
146146
method public void idle();
147+
method public void moveBy(com.mapbox.maps.ScreenCoordinate screenCoordinate, com.mapbox.maps.plugin.animation.MapAnimationOptions? animationOptions = null, com.mapbox.maps.plugin.viewport.CompletionListener? completionListener = null);
148+
method public void pitchBy(double pitch, com.mapbox.maps.plugin.animation.MapAnimationOptions? animationOptions = null, com.mapbox.maps.plugin.viewport.CompletionListener? completionListener = null);
149+
method public void rotateBy(com.mapbox.maps.ScreenCoordinate first, com.mapbox.maps.ScreenCoordinate second, com.mapbox.maps.plugin.animation.MapAnimationOptions? animationOptions = null, com.mapbox.maps.plugin.viewport.CompletionListener? completionListener = null);
150+
method public void scaleBy(double amount, com.mapbox.maps.ScreenCoordinate? screenCoordinate = null, com.mapbox.maps.plugin.animation.MapAnimationOptions? animationOptions = null, com.mapbox.maps.plugin.viewport.CompletionListener? completionListener = null);
147151
method @UiThread public void setCameraOptions(com.mapbox.maps.CameraOptions cameraOptions);
148152
method @UiThread public void setCameraOptions(kotlin.jvm.functions.Function1<? super com.mapbox.maps.CameraOptions.Builder,kotlin.Unit> block);
149153
method public void transitionToFollowPuckState(com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions followPuckViewportStateOptions = FollowPuckViewportStateOptions.<init>().build(), com.mapbox.maps.plugin.viewport.data.DefaultViewportTransitionOptions defaultTransitionOptions = DefaultViewportTransitionOptions.<init>().build(), com.mapbox.maps.plugin.viewport.CompletionListener? completionListener = null);

extension-compose/api/extension-compose.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ public final class com/mapbox/maps/extension/compose/animation/viewport/MapViewp
171171
public final fun getMapViewportStatusChangedReason ()Lcom/mapbox/maps/plugin/viewport/data/ViewportStatusChangeReason;
172172
public final fun getStyleDefaultCameraOptions ()Lcom/mapbox/maps/CameraOptions;
173173
public final fun idle ()V
174+
public final fun moveBy (Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;)V
175+
public static synthetic fun moveBy$default (Lcom/mapbox/maps/extension/compose/animation/viewport/MapViewportState;Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;ILjava/lang/Object;)V
176+
public final fun pitchBy (DLcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;)V
177+
public static synthetic fun pitchBy$default (Lcom/mapbox/maps/extension/compose/animation/viewport/MapViewportState;DLcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;ILjava/lang/Object;)V
178+
public final fun rotateBy (Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;)V
179+
public static synthetic fun rotateBy$default (Lcom/mapbox/maps/extension/compose/animation/viewport/MapViewportState;Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;ILjava/lang/Object;)V
180+
public final fun scaleBy (DLcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;)V
181+
public static synthetic fun scaleBy$default (Lcom/mapbox/maps/extension/compose/animation/viewport/MapViewportState;DLcom/mapbox/maps/ScreenCoordinate;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;ILjava/lang/Object;)V
174182
public final fun setCameraOptions (Lcom/mapbox/maps/CameraOptions;)V
175183
public final fun setCameraOptions (Lkotlin/jvm/functions/Function1;)V
176184
public final fun transitionToFollowPuckState (Lcom/mapbox/maps/plugin/viewport/data/FollowPuckViewportStateOptions;Lcom/mapbox/maps/plugin/viewport/data/DefaultViewportTransitionOptions;Lcom/mapbox/maps/plugin/viewport/CompletionListener;)V

0 commit comments

Comments
 (0)