Skip to content

Commit 4d17bc2

Browse files
kediarovgithub-actions[bot]
authored andcommitted
Incorrect flyTo behavior when CameraOptions.zoom is null or is equal to the current zoom level (#6016)
https://mapbox.atlassian.net/browse/MAPSAND-2289 CameraZoomAnimator evaluator has a dependency on traveled path (CameraAnimatorsFactory.kt:441). Skipping this animator means no onAnimationUpdateInternal is called and no camera zoom updated cc @mapbox/maps-android GitOrigin-RevId: 100d18cbde583c1a85e70d2ccbb9b1d167b44782
1 parent e24ac55 commit 4d17bc2

File tree

13 files changed

+221
-121
lines changed

13 files changed

+221
-121
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ Mapbox welcomes participation and contributions from everyone.
1212
## Bug fixes 🐞
1313
* MapboxTracing was deprecated and moved to package com.mapbox.common.
1414

15+
# 11.15.0
16+
17+
## Bug fixes 🐞
18+
* Fix flyTo animation when zoom property is null or not changed
19+
1520
# 11.14.4 September 03, 2025
1621

1722
## Features ✨ and improvements 🏁

extension-compose/src/androidTest/java/com/mapbox/maps/extension/compose/style/LayerPositionAwareNodeTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import com.mapbox.maps.plugin.annotation.ClusterOptions
4343
import com.mapbox.maps.plugin.annotation.generated.CircleAnnotationOptions
4444
import kotlinx.coroutines.flow.first
4545
import org.junit.Assert
46+
import org.junit.Ignore
4647
import org.junit.Rule
4748
import org.junit.Test
4849
import java.util.concurrent.CountDownLatch
@@ -405,6 +406,7 @@ public class LayerPositionAwareNodeTest {
405406
}
406407

407408
@Test
409+
@Ignore("Flaky test, circle-layer-1 is missing ")
408410
public fun testAnnotationsInStyleSlot() {
409411
val mapboxMap = setMapContent(
410412
slotContent = { btn1State, btn2State, btn3State ->

plugin-animation/api/Release/metalava.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ package com.mapbox.maps.plugin.animation.animator {
6868
method public final void removeAllUpdateListeners();
6969
method public final void removeListener(android.animation.Animator.AnimatorListener? listener);
7070
method public final void removeUpdateListener(android.animation.ValueAnimator.AnimatorUpdateListener? listener);
71+
method public Object![] resolveAnimationObjectValues(Object startValue);
7172
method public final void setEvaluator(android.animation.TypeEvaluator<T!>? value);
7273
method public final void setObjectValues(java.lang.Object?... values);
7374
method public final void start();
75+
method public final void updateObjectValues(kotlin.jvm.functions.Function0<com.mapbox.maps.CameraState> getStartCameraState);
7476
property public final String? owner;
7577
property public final T? startValue;
7678
property public final T![] targets;
@@ -81,6 +83,10 @@ package com.mapbox.maps.plugin.animation.animator {
8183
public static final class CameraAnimator.Companion {
8284
}
8385

86+
public fun interface CameraTypeEvaluator<T> extends android.animation.TypeEvaluator<T> {
87+
method public default boolean canSkip(Object cameraCurrentValue, Object startValue, Object![] values);
88+
}
89+
8490
public final class Evaluators {
8591
method public android.animation.TypeEvaluator<java.lang.Double> getDOUBLE();
8692
method public android.animation.TypeEvaluator<com.mapbox.maps.EdgeInsets> getEDGE_INSET();

plugin-animation/api/plugin-animation.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,24 @@ public abstract class com/mapbox/maps/plugin/animation/animator/CameraAnimator :
7979
public final fun removeAllUpdateListeners ()V
8080
public final fun removeListener (Landroid/animation/Animator$AnimatorListener;)V
8181
public final fun removeUpdateListener (Landroid/animation/ValueAnimator$AnimatorUpdateListener;)V
82+
public fun resolveAnimationObjectValues (Ljava/lang/Object;)[Ljava/lang/Object;
8283
public final fun setEvaluator (Landroid/animation/TypeEvaluator;)V
8384
public final fun setObjectValues ([Ljava/lang/Object;)V
8485
public final fun start ()V
86+
public final fun updateObjectValues (Lkotlin/jvm/functions/Function0;)V
8587
}
8688

8789
public final class com/mapbox/maps/plugin/animation/animator/CameraAnimator$Companion {
8890
}
8991

92+
public abstract interface class com/mapbox/maps/plugin/animation/animator/CameraTypeEvaluator : android/animation/TypeEvaluator {
93+
public abstract fun canSkip (Ljava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Z
94+
}
95+
96+
public final class com/mapbox/maps/plugin/animation/animator/CameraTypeEvaluator$DefaultImpls {
97+
public static fun canSkip (Lcom/mapbox/maps/plugin/animation/animator/CameraTypeEvaluator;Ljava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Z
98+
}
99+
90100
public final class com/mapbox/maps/plugin/animation/animator/Evaluators {
91101
public static final field INSTANCE Lcom/mapbox/maps/plugin/animation/animator/Evaluators;
92102
public final fun getDOUBLE ()Landroid/animation/TypeEvaluator;

plugin-animation/src/main/java/com/mapbox/maps/plugin/animation/CameraAnimationsPluginImpl.kt

Lines changed: 5 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import com.mapbox.maps.logE
1818
import com.mapbox.maps.logI
1919
import com.mapbox.maps.logW
2020
import com.mapbox.maps.plugin.MapCameraPlugin
21-
import com.mapbox.maps.plugin.animation.CameraTransform.wrapCoordinate
2221
import com.mapbox.maps.plugin.animation.animator.CameraAnchorAnimator
2322
import com.mapbox.maps.plugin.animation.animator.CameraAnimator
2423
import com.mapbox.maps.plugin.animation.animator.CameraBearingAnimator
@@ -33,9 +32,7 @@ import com.mapbox.maps.plugin.delegates.MapTransformDelegate
3332
import com.mapbox.maps.threading.AnimationThreadController.postOnAnimatorThread
3433
import com.mapbox.maps.threading.AnimationThreadController.postOnMainThread
3534
import com.mapbox.maps.threading.AnimationThreadController.usingBackgroundThread
36-
import com.mapbox.maps.util.MathUtils
3735
import com.mapbox.maps.util.isEmpty
38-
import java.util.Objects
3936
import java.util.concurrent.CopyOnWriteArraySet
4037
import kotlin.properties.Delegates
4138

@@ -154,6 +151,9 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
154151
private lateinit var mapCameraManagerDelegate: MapCameraManagerDelegate
155152
private lateinit var mapTransformDelegate: MapTransformDelegate
156153
private lateinit var mapProjectionDelegate: MapProjectionDelegate
154+
private val getCurrentCameraState = {
155+
mapCameraManagerDelegate.cameraState
156+
}
157157

158158
/**
159159
* Factory to provide animators for the default animations like easeTo, scaleBy, moveBy, rotateBy, pitchBy
@@ -270,95 +270,6 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
270270
}
271271
}
272272

273-
// Returns true if values were applied to animator, false if animation was skipped.
274-
private fun updateAnimatorValues(cameraAnimator: CameraAnimator<*>): Boolean {
275-
if (cameraAnimator.targets.isEmpty()) {
276-
logE(
277-
TAG,
278-
"Skipped animation ${cameraAnimator.type.name} with no targets!"
279-
)
280-
return false
281-
}
282-
val startValue = cameraAnimator.startValue ?: when (cameraAnimator.type) {
283-
CameraAnimatorType.CENTER -> mapCameraManagerDelegate.cameraState.center
284-
CameraAnimatorType.ZOOM -> mapCameraManagerDelegate.cameraState.zoom
285-
CameraAnimatorType.ANCHOR -> ScreenCoordinate(0.0, 0.0)
286-
CameraAnimatorType.PADDING -> mapCameraManagerDelegate.cameraState.padding
287-
CameraAnimatorType.BEARING -> mapCameraManagerDelegate.cameraState.bearing
288-
CameraAnimatorType.PITCH -> mapCameraManagerDelegate.cameraState.pitch
289-
}.also {
290-
if (debugMode) {
291-
logI(
292-
TAG,
293-
"Animation ${cameraAnimator.type.name}(${cameraAnimator.hashCode()}): automatically setting start value $it."
294-
)
295-
}
296-
}
297-
val animationObjectValues =
298-
if (cameraAnimator is CameraBearingAnimator && cameraAnimator.useShortestPath) {
299-
MathUtils.prepareOptimalBearingPath(
300-
DoubleArray(cameraAnimator.targets.size + 1) { index ->
301-
if (index == 0) {
302-
startValue as Double
303-
} else {
304-
cameraAnimator.targets[index - 1]
305-
}
306-
}
307-
).toTypedArray()
308-
} else if (cameraAnimator is CameraCenterAnimator && cameraAnimator.useShortestPath) {
309-
// assemble the original targets by inserting the start point
310-
val originalTargets: List<Point> = listOf(startValue as Point) + cameraAnimator.targets
311-
// Build the reversed target list with wrapped coordinates
312-
val mutableTargetReversedList = mutableListOf<Point>()
313-
originalTargets.map { it.wrapCoordinate() }.reversed().forEach {
314-
if (mutableTargetReversedList.isEmpty()) {
315-
// insert the raw end point
316-
mutableTargetReversedList.add(it)
317-
} else {
318-
// calculate the previous point
319-
mutableTargetReversedList.add(
320-
CameraTransform.unwrapForShortestPath(
321-
start = it,
322-
end = mutableTargetReversedList.last()
323-
)
324-
)
325-
}
326-
}
327-
mutableTargetReversedList.reversed().toTypedArray()
328-
} else {
329-
Array(cameraAnimator.targets.size + 1) { index ->
330-
if (index == 0) {
331-
startValue
332-
} else {
333-
cameraAnimator.targets[index - 1]
334-
}
335-
}
336-
}
337-
if (skipRedundantAnimator(animationObjectValues, cameraAnimator.type)) {
338-
if (debugMode) {
339-
logI(
340-
TAG,
341-
"Animation ${cameraAnimator.type.name}(${cameraAnimator.hashCode()}) was skipped."
342-
)
343-
}
344-
return false
345-
}
346-
cameraAnimator.setObjectValues(*animationObjectValues)
347-
return true
348-
}
349-
350-
private fun skipRedundantAnimator(
351-
animationObjectValues: Array<out Any?>,
352-
type: CameraAnimatorType
353-
) = when (type) {
354-
CameraAnimatorType.ANCHOR -> false // anchor animations are never skipped
355-
CameraAnimatorType.CENTER -> animationObjectValues.all { Objects.equals(center, it) }
356-
CameraAnimatorType.ZOOM -> animationObjectValues.all { Objects.equals(zoom, it) }
357-
CameraAnimatorType.PADDING -> animationObjectValues.all { Objects.equals(padding, it) }
358-
CameraAnimatorType.BEARING -> animationObjectValues.all { Objects.equals(bearing, it) }
359-
CameraAnimatorType.PITCH -> animationObjectValues.all { Objects.equals(pitch, it) }
360-
}
361-
362273
private fun registerInternalListener(animator: CameraAnimator<*>) {
363274
postOnAnimatorThread {
364275
animator.addInternalListener(object : Animator.AnimatorListener {
@@ -383,9 +294,9 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
383294
if (startingAnimator.canceled) {
384295
return
385296
}
386-
if (!updateAnimatorValues(startingAnimator)) {
297+
startingAnimator.updateObjectValues(getCurrentCameraState)
298+
if (startingAnimator.skipped) {
387299
// animation was skipped - camera values are already applied
388-
startingAnimator.skipped = true
389300
return
390301
}
391302
lifecycleListeners.forEach {

plugin-animation/src/main/java/com/mapbox/maps/plugin/animation/CameraAnimatorsFactory.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,12 +433,18 @@ class CameraAnimatorsFactory internal constructor(mapDelegateProvider: MapDelega
433433
}
434434
},
435435
CameraZoomAnimator(
436-
evaluator = { fraction, _, _ ->
437-
val safeFraction = fraction.getSafeFraction()
438-
// s: The distance traveled along the flight path, measured in
439-
// ρ-screenfuls.
440-
val s = safeFraction * S
441-
startZoom + (1 / w(s)).scaleZoom()
436+
evaluator = object : CameraTypeEvaluator<Double> {
437+
override fun canSkip(cameraCurrentValue: Any, startValue: Any, values: Array<*>): Boolean {
438+
return S == 0.0
439+
}
440+
441+
override fun evaluate(fraction: Float, startValue: Double, endValue: Double): Double {
442+
val safeFraction = fraction.getSafeFraction()
443+
// s: The distance traveled along the flight path, measured in
444+
// ρ-screenfuls.
445+
val s = safeFraction * S
446+
return startZoom + (1 / w(s)).scaleZoom()
447+
}
442448
},
443449
options = cameraAnimatorOptions(endZoom) {
444450
startValue(startZoom)

plugin-animation/src/main/java/com/mapbox/maps/plugin/animation/animator/CameraAnchorAnimator.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal class CameraAnchorAnimator(
1515
options: CameraAnimatorOptions<ScreenCoordinate>,
1616
block: (ValueAnimator.() -> Unit)? = null
1717
) : CameraAnimator<ScreenCoordinate>(
18-
Evaluators.SCREEN_COORDINATE,
18+
anchorEvaluator,
1919
options
2020
) {
2121

@@ -27,4 +27,15 @@ internal class CameraAnchorAnimator(
2727
* Animator type.
2828
*/
2929
override val type = CameraAnimatorType.ANCHOR
30+
31+
private companion object {
32+
private val anchorEvaluator = object : CameraTypeEvaluator<ScreenCoordinate> {
33+
override fun canSkip(cameraCurrentValue: Any, startValue: Any, values: Array<*>) = false
34+
override fun evaluate(
35+
fraction: Float,
36+
startValue: ScreenCoordinate?,
37+
endValue: ScreenCoordinate?,
38+
) = Evaluators.SCREEN_COORDINATE.evaluate(fraction, startValue, endValue)
39+
}
40+
}
3041
}

0 commit comments

Comments
 (0)