diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationAnimatorCoordinator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationAnimatorCoordinator.java index d53782c61e8..c48a15b519a 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationAnimatorCoordinator.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationAnimatorCoordinator.java @@ -31,6 +31,7 @@ import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_COMPASS_BEARING; import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_GPS_BEARING; import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_LAYER_LATLNG; +import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_PADDING; import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_PULSING_CIRCLE; import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_TILT; import static org.maplibre.android.location.MapLibreAnimator.ANIMATOR_ZOOM; @@ -217,6 +218,12 @@ void feedNewZoomLevel(double targetZoomLevel, @NonNull CameraPosition currentCam playAnimators(animationDuration, ANIMATOR_ZOOM); } + void feedNewPadding(double[] padding, @NonNull CameraPosition currentCameraPosition, long animationDuration, + @Nullable MapLibreMap.CancelableCallback callback) { + updatePaddingAnimator(padding, currentCameraPosition.padding, callback); + playAnimators(animationDuration, ANIMATOR_PADDING); + } + void feedNewTilt(double targetTilt, @NonNull CameraPosition currentCameraPosition, long animationDuration, @Nullable MapLibreMap.CancelableCallback callback) { updateTiltAnimator((float) targetTilt, (float) currentCameraPosition.tilt, callback); @@ -317,6 +324,11 @@ private void updateZoomAnimator(float targetZoomLevel, float previousZoomLevel, createNewCameraAdapterAnimator(ANIMATOR_ZOOM, new Float[] {previousZoomLevel, targetZoomLevel}, cancelableCallback); } + private void updatePaddingAnimator(double[] targetPadding, double[] previousPadding, + @Nullable MapLibreMap.CancelableCallback cancelableCallback) { + createNewPaddingAnimator(ANIMATOR_PADDING, new double[][] {previousPadding, targetPadding}, cancelableCallback); + } + private void updateTiltAnimator(float targetTilt, float previousTiltLevel, @Nullable MapLibreMap.CancelableCallback cancelableCallback) { createNewCameraAdapterAnimator(ANIMATOR_TILT, new Float[] {previousTiltLevel, targetTilt}, cancelableCallback); @@ -356,6 +368,16 @@ private void createNewCameraAdapterAnimator(@MapLibreAnimator.Type int animatorT } } + private void createNewPaddingAnimator(@MapLibreAnimator.Type int animatorType, + @NonNull @Size(min = 2) double[][] values, + @Nullable MapLibreMap.CancelableCallback cancelableCallback) { + cancelAnimator(animatorType); + MapLibreAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType); + if (listener != null) { + animatorArray.put(animatorType, animatorProvider.paddingAnimator(values, listener, cancelableCallback)); + } + } + private float checkGpsNorth(boolean isGpsNorth, float targetCameraBearing) { if (isGpsNorth) { targetCameraBearing = 0; @@ -479,6 +501,10 @@ void cancelZoomAnimation() { cancelAnimator(ANIMATOR_ZOOM); } + void cancelPaddingAnimation() { + cancelAnimator(ANIMATOR_PADDING); + } + void cancelTiltAnimation() { cancelAnimator(ANIMATOR_TILT); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationCameraController.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationCameraController.java index ac4868f328c..3bd85ed6845 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationCameraController.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationCameraController.java @@ -224,6 +224,15 @@ private void setZoom(float zoom) { onCameraMoveInvalidateListener.onInvalidateCameraMove(); } + private void setPadding(double[] padding) { + if (isTransitioning) { + return; + } + + transform.moveCamera(maplibreMap, CameraUpdateFactory.paddingTo(padding), null); + onCameraMoveInvalidateListener.onInvalidateCameraMove(); + } + private void setTilt(float tilt) { if (isTransitioning) { return; @@ -266,20 +275,13 @@ public void onNewAnimationValue(Float value) { }; private final MapLibreAnimator.AnimationsValueChangeListener zoomValueListener = - new MapLibreAnimator.AnimationsValueChangeListener() { - @Override - public void onNewAnimationValue(Float value) { - setZoom(value); - } - }; + value -> setZoom(value); + + private final MapLibreAnimator.AnimationsValueChangeListener paddingValueListener = + value -> setPadding(value); private final MapLibreAnimator.AnimationsValueChangeListener tiltValueListener = - new MapLibreAnimator.AnimationsValueChangeListener() { - @Override - public void onNewAnimationValue(Float value) { - setTilt(value); - } - }; + value -> setTilt(value); Set getAnimationListeners() { Set holders = new HashSet<>(); @@ -299,6 +301,7 @@ Set getAnimationListeners() { holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_ZOOM, zoomValueListener)); holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_TILT, tiltValueListener)); + holders.add(new AnimatorListenerHolder(MapLibreAnimator.ANIMATOR_PADDING, paddingValueListener)); return holders; } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponent.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponent.java index 9812ddbdd06..5809d13f12a 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponent.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponent.java @@ -47,6 +47,7 @@ import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static org.maplibre.android.location.LocationComponentConstants.DEFAULT_FASTEST_INTERVAL_MILLIS; import static org.maplibre.android.location.LocationComponentConstants.DEFAULT_INTERVAL_MILLIS; +import static org.maplibre.android.location.LocationComponentConstants.DEFAULT_TRACKING_PADDING_ANIM_DURATION; import static org.maplibre.android.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION; import static org.maplibre.android.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION; import static org.maplibre.android.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS; @@ -603,6 +604,84 @@ public void cancelZoomWhileTrackingAnimation() { locationAnimatorCoordinator.cancelZoomAnimation(); } + /** + * Sets the padding. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapLibreMap#moveCamera(CameraUpdate)}, + * {@link MapLibreMap#easeCamera(CameraUpdate)} or {@link MapLibreMap#animateCamera(CameraUpdate)} instead. + *

+ * If the camera is transitioning when the padding change is requested, the call is going to be ignored. + * Use {@link CameraTransitionListener} to chain the animations, or provide the padding as a camera change argument. + *

+ * + * @param padding The desired padding. + */ + public void paddingWhileTracking(double[] padding) { + paddingWhileTracking(padding, DEFAULT_TRACKING_PADDING_ANIM_DURATION, null); + } + + /** + * Sets the padding. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapLibreMap#moveCamera(CameraUpdate)}, + * {@link MapLibreMap#easeCamera(CameraUpdate)} or {@link MapLibreMap#animateCamera(CameraUpdate)} instead. + *

+ * If the camera is transitioning when the padding change is requested, the call is going to be ignored. + * Use {@link CameraTransitionListener} to chain the animations, or provide the padding as a camera change argument. + *

+ * + * @param padding The desired padding. + * @param animationDuration The padding animation duration. + */ + public void paddingWhileTracking(double[] padding, long animationDuration) { + paddingWhileTracking(padding, animationDuration, null); + } + + /** + * Sets the padding. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapLibreMap#moveCamera(CameraUpdate)}, + * {@link MapLibreMap#easeCamera(CameraUpdate)} or {@link MapLibreMap#animateCamera(CameraUpdate)} instead. + *

+ * If the camera is transitioning when the padding change is requested, the call is going to be ignored. + * Use {@link CameraTransitionListener} to chain the animations, or provide the padding as a camera change argument. + *

+ * + * @param padding The desired padding. + * @param animationDuration The padding animation duration. + * @param callback The callback with finish/cancel information + */ + public void paddingWhileTracking(double[] padding, long animationDuration, + @Nullable MapLibreMap.CancelableCallback callback) { + checkActivationState(); + if (!isLayerReady) { + notifyUnsuccessfulCameraOperation(callback, null); + return; + } else if (getCameraMode() == CameraMode.NONE) { + notifyUnsuccessfulCameraOperation(callback, String.format("%s%s", + "LocationComponent#paddingWhileTracking method can only be used", + " when a camera mode other than CameraMode#NONE is engaged.")); + return; + } else if (locationCameraController.isTransitioning()) { + notifyUnsuccessfulCameraOperation(callback, + "LocationComponent#paddingWhileTracking method call is ignored because the camera mode is transitioning"); + return; + } + + locationAnimatorCoordinator.feedNewPadding(padding, maplibreMap.getCameraPosition(), animationDuration, callback); + } + + /** + * Cancels animation started by {@link #paddingWhileTracking(double[], long, MapLibreMap.CancelableCallback)}. + */ + public void cancelPaddingWhileTrackingAnimation() { + checkActivationState(); + locationAnimatorCoordinator.cancelPaddingAnimation(); + } + /** * Tilts the camera. * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. @@ -732,10 +811,10 @@ public void forceLocationUpdate(@Nullable List locations, boolean look * Example usage: *
    * {@code
-   * mapboxMap.addOnCameraIdleListener(new MapboxMap.OnCameraIdleListener() {
+   * MapLibreMap.addOnCameraIdleListener(new MapLibreMap.OnCameraIdleListener() {
    *   {@literal @}Override
    *   public void onCameraIdle() {
-   *     double zoom = mapboxMap.getCameraPosition().zoom;
+   *     double zoom = MapLibreMap.getCameraPosition().zoom;
    *     int maxAnimationFps;
    *     if (zoom < 5) {
    *       maxAnimationFps = 3;
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponentConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponentConstants.java
index 032de3ca1bc..e7c40950fd5 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponentConstants.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/LocationComponentConstants.java
@@ -20,6 +20,9 @@ public final class LocationComponentConstants {
   // Default animation duration for zooming while tracking.
   static final long DEFAULT_TRACKING_ZOOM_ANIM_DURATION = 750;
 
+  // Default animation duration for updating padding while tracking.
+  static final long DEFAULT_TRACKING_PADDING_ANIM_DURATION = 750;
+
   // Default animation duration for tilting while tracking.
   static final long DEFAULT_TRACKING_TILT_ANIM_DURATION = 1250;
 
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimator.java
index f74a3120878..fcac273740f 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimator.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimator.java
@@ -17,7 +17,7 @@
  *
  * @param  Data type that will be animated.
  */
-abstract class MapLibreAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
+public abstract class MapLibreAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
   @Retention(RetentionPolicy.SOURCE)
   @IntDef( {
     ANIMATOR_LAYER_LATLNG,
@@ -29,7 +29,8 @@ abstract class MapLibreAnimator extends ValueAnimator implements ValueAnimato
     ANIMATOR_LAYER_ACCURACY,
     ANIMATOR_ZOOM,
     ANIMATOR_TILT,
-    ANIMATOR_PULSING_CIRCLE
+    ANIMATOR_PULSING_CIRCLE,
+    ANIMATOR_PADDING
   })
   @interface Type {
   }
@@ -44,6 +45,7 @@ abstract class MapLibreAnimator extends ValueAnimator implements ValueAnimato
   static final int ANIMATOR_ZOOM = 7;
   static final int ANIMATOR_TILT = 8;
   static final int ANIMATOR_PULSING_CIRCLE = 9;
+  static final int ANIMATOR_PADDING = 10;
 
   private final AnimationsValueChangeListener updateListener;
   private final K target;
@@ -59,7 +61,7 @@ abstract class MapLibreAnimator extends ValueAnimator implements ValueAnimato
    */
   private boolean invalid;
 
-  MapLibreAnimator(@NonNull @Size(min = 2) K[] values, @NonNull AnimationsValueChangeListener updateListener,
+  public MapLibreAnimator(@NonNull @Size(min = 2) K[] values, @NonNull AnimationsValueChangeListener updateListener,
                    int maxAnimationFps) {
     minUpdateInterval = 1E9 / maxAnimationFps;
     setObjectValues((Object[]) values);
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorListener.kt b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorListener.kt
new file mode 100644
index 00000000000..a8fb340a696
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorListener.kt
@@ -0,0 +1,22 @@
+package org.maplibre.android.location
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import org.maplibre.android.maps.MapLibreMap
+
+internal class MapLibreAnimatorListener(cancelableCallback: MapLibreMap.CancelableCallback?) :
+    AnimatorListenerAdapter() {
+    private val cancelableCallback: MapLibreMap.CancelableCallback?
+
+    init {
+        this.cancelableCallback = cancelableCallback
+    }
+
+    override fun onAnimationCancel(animation: Animator) {
+        cancelableCallback?.onCancel()
+    }
+
+    override fun onAnimationEnd(animation: Animator) {
+        cancelableCallback?.onFinish()
+    }
+}
\ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorProvider.java
index 9fd256a61bc..fb38ed17bca 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorProvider.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreAnimatorProvider.java
@@ -39,6 +39,12 @@ MapLibreCameraAnimatorAdapter cameraAnimator(Float[] values,
     return new MapLibreCameraAnimatorAdapter(values, updateListener, cancelableCallback);
   }
 
+  MapLibrePaddingAnimator paddingAnimator(double[][] values,
+                                        MapLibreAnimator.AnimationsValueChangeListener updateListener,
+                                        @Nullable MapLibreMap.CancelableCallback cancelableCallback) {
+    return new MapLibrePaddingAnimator(values, updateListener, cancelableCallback);
+  }
+
   /**
    * This animator is for the LocationComponent pulsing circle.
    *
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreCameraAnimatorAdapter.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreCameraAnimatorAdapter.java
index c4647ae4246..8fdee32208e 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreCameraAnimatorAdapter.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibreCameraAnimatorAdapter.java
@@ -1,8 +1,5 @@
 package org.maplibre.android.location;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Size;
@@ -10,30 +7,11 @@
 import org.maplibre.android.maps.MapLibreMap;
 
 class MapLibreCameraAnimatorAdapter extends MapLibreFloatAnimator {
-  @Nullable
-  private final MapLibreMap.CancelableCallback cancelableCallback;
 
   MapLibreCameraAnimatorAdapter(@NonNull @Size(min = 2) Float[] values,
                                 AnimationsValueChangeListener updateListener,
                                 @Nullable MapLibreMap.CancelableCallback cancelableCallback) {
     super(values, updateListener, Integer.MAX_VALUE);
-    this.cancelableCallback = cancelableCallback;
-    addListener(new MapLibreAnimatorListener());
-  }
-
-  private final class MapLibreAnimatorListener extends AnimatorListenerAdapter {
-    @Override
-    public void onAnimationCancel(Animator animation) {
-      if (cancelableCallback != null) {
-        cancelableCallback.onCancel();
-      }
-    }
-
-    @Override
-    public void onAnimationEnd(Animator animation) {
-      if (cancelableCallback != null) {
-        cancelableCallback.onFinish();
-      }
-    }
+    addListener(new MapLibreAnimatorListener(cancelableCallback));
   }
 }
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibrePaddingAnimator.kt b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibrePaddingAnimator.kt
new file mode 100644
index 00000000000..c142e82abd7
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/MapLibrePaddingAnimator.kt
@@ -0,0 +1,20 @@
+package org.maplibre.android.location
+
+import android.animation.TypeEvaluator
+import androidx.annotation.Size
+import org.maplibre.android.maps.MapLibreMap.CancelableCallback
+
+class MapLibrePaddingAnimator internal constructor(
+    @Size(min = 2) values: Array,
+    updateListener: AnimationsValueChangeListener,
+    cancelableCallback: CancelableCallback?
+) :
+    MapLibreAnimator(values, updateListener, Int.MAX_VALUE) {
+    init {
+        addListener(MapLibreAnimatorListener(cancelableCallback))
+    }
+
+    public override fun provideEvaluator(): TypeEvaluator {
+        return PaddingEvaluator()
+    }
+}
\ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/PaddingEvaluator.kt b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/PaddingEvaluator.kt
new file mode 100644
index 00000000000..57b4f55ecd7
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/location/PaddingEvaluator.kt
@@ -0,0 +1,18 @@
+package org.maplibre.android.location
+
+import android.animation.TypeEvaluator
+import androidx.annotation.Size
+
+internal class PaddingEvaluator : TypeEvaluator {
+    private val padding = DoubleArray(4)
+    override fun evaluate(
+        fraction: Float, @Size(min = 4) startValue: DoubleArray,
+        @Size(min = 4) endValue: DoubleArray
+    ): DoubleArray {
+        padding[0] = startValue[0] + (endValue[0] - startValue[0]) * fraction
+        padding[1] = startValue[1] + (endValue[1] - startValue[1]) * fraction
+        padding[2] = startValue[2] + (endValue[2] - startValue[2]) * fraction
+        padding[3] = startValue[3] + (endValue[3] - startValue[3]) * fraction
+        return padding
+    }
+}
\ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/location/LocationAnimatorCoordinatorTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/location/LocationAnimatorCoordinatorTest.kt
index b9d0fa99838..37e40ad674d 100644
--- a/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/location/LocationAnimatorCoordinatorTest.kt
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/location/LocationAnimatorCoordinatorTest.kt
@@ -18,6 +18,7 @@ import org.junit.Assert.*
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.maplibre.android.location.LocationComponentConstants.DEFAULT_TRACKING_PADDING_ANIM_DURATION
 import org.mockito.Mockito
 import org.robolectric.RobolectricTestRunner
 import org.maplibre.testUtils.Assert as MapLibreAssert
@@ -58,7 +59,8 @@ class LocationAnimatorCoordinatorTest {
                 ANIMATOR_CAMERA_COMPASS_BEARING,
                 ANIMATOR_LAYER_ACCURACY,
                 ANIMATOR_ZOOM,
-                ANIMATOR_TILT
+                ANIMATOR_TILT,
+                ANIMATOR_PADDING
             )
         )
     }
@@ -110,6 +112,19 @@ class LocationAnimatorCoordinatorTest {
                 null
             )
         }
+
+        val doubleArraySlot = slot>()
+        val doubleArrayListenerSlot = slot>()
+        every {
+            animatorProvider.paddingAnimator(capture(doubleArraySlot), capture(doubleArrayListenerSlot), capture(callback))
+        } answers {
+            MapLibrePaddingAnimator(doubleArraySlot.captured, doubleArrayListenerSlot.captured, callback.captured)
+        }
+        every {
+            animatorProvider.paddingAnimator(capture(doubleArraySlot), capture(doubleArrayListenerSlot), null)
+        } answers {
+            MapLibrePaddingAnimator(doubleArraySlot.captured, doubleArrayListenerSlot.captured, null)
+        }
     }
 
     @Test
@@ -504,6 +519,33 @@ class LocationAnimatorCoordinatorTest {
         verify { animatorSetProvider.startAnimation(eq(listOf(animator)), any(), DEFAULT_TRACKING_ZOOM_ANIM_DURATION) }
     }
 
+    @Test
+    fun feedNewPadding_animatorsCreated() {
+        locationAnimatorCoordinator.feedNewPadding(
+            doubleArrayOf(100.0, 200.0, 300.0, 400.0),
+            cameraPosition,
+            DEFAULT_TRACKING_PADDING_ANIM_DURATION,
+            null
+        )
+
+        assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_PADDING] != null)
+    }
+
+    @Test
+    fun feedNewPadding_animatorValue() {
+        val padding = doubleArrayOf(100.0, 200.0, 300.0, 400.0)
+        locationAnimatorCoordinator.feedNewPadding(
+            padding,
+            cameraPosition,
+            DEFAULT_TRACKING_PADDING_ANIM_DURATION,
+            null
+        )
+
+        val animator = locationAnimatorCoordinator.animatorArray[ANIMATOR_PADDING]
+        assertTrue(padding.contentEquals(animator.target as DoubleArray))
+        verify { animatorSetProvider.startAnimation(eq(listOf(animator)), any(), DEFAULT_TRACKING_PADDING_ANIM_DURATION) }
+    }
+
     @Test
     fun feedNewTiltLevel_animatorsCreated() {
         locationAnimatorCoordinator.feedNewTilt(
@@ -556,6 +598,21 @@ class LocationAnimatorCoordinatorTest {
         assertFalse(locationAnimatorCoordinator.animatorArray[ANIMATOR_ZOOM].isStarted)
     }
 
+    @Test
+    fun cancelPaddingAnimators() {
+        locationAnimatorCoordinator.feedNewPadding(
+            doubleArrayOf(100.0, 200.0, 300.0, 400.0),
+            cameraPosition,
+            DEFAULT_TRACKING_PADDING_ANIM_DURATION,
+            null
+        )
+        assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_PADDING].isStarted)
+
+        locationAnimatorCoordinator.cancelPaddingAnimation()
+
+        assertFalse(locationAnimatorCoordinator.animatorArray[ANIMATOR_PADDING].isStarted)
+    }
+
     @Test
     fun cancelTiltAnimation() {
         locationAnimatorCoordinator.feedNewTilt(
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/org/maplibre/android/location/LocationComponentTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/org/maplibre/android/location/LocationComponentTest.kt
index 803b328a20e..70f8739f491 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/org/maplibre/android/location/LocationComponentTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/org/maplibre/android/location/LocationComponentTest.kt
@@ -31,6 +31,7 @@ import org.maplibre.android.utils.BitmapUtils
 import org.maplibre.android.utils.ColorUtils
 import org.hamcrest.CoreMatchers.*
 import org.junit.*
+import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.runner.RunWith
@@ -1608,6 +1609,122 @@ class LocationComponentTest : EspressoTest() {
         executeComponentTest(componentAction)
     }
 
+    @Test
+    fun animators_dontPaddingWhileNotTracking() {
+        val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+            override fun onLocationComponentAction(
+                component: LocationComponent,
+                maplibreMap: MapLibreMap,
+                style: Style,
+                uiController: UiController,
+                context: Context
+            ) {
+                component.activateLocationComponent(LocationComponentActivationOptions
+                    .builder(context, style)
+                    .useDefaultLocationEngine(false)
+                    .build())
+                component.isLocationComponentEnabled = true
+                component.cameraMode = CameraMode.NONE
+                val padding = maplibreMap.cameraPosition.padding
+                component.paddingWhileTracking(doubleArrayOf(100.0, 200.0, 300.0, 400.0))
+                uiController.loopMainThreadForAtLeast(DEFAULT_TRACKING_PADDING_ANIM_DURATION)
+                TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+                assertArrayEquals(padding, maplibreMap.cameraPosition.padding, 0.1)
+            }
+        }
+
+        executeComponentTest(componentAction)
+    }
+
+    @Test
+    fun animators_dontPaddingWhileStopped() {
+        val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+            override fun onLocationComponentAction(
+                component: LocationComponent,
+                maplibreMap: MapLibreMap,
+                style: Style,
+                uiController: UiController,
+                context: Context
+            ) {
+                component.activateLocationComponent(LocationComponentActivationOptions
+                    .builder(context, style)
+                    .useDefaultLocationEngine(false)
+                    .build())
+                component.isLocationComponentEnabled = true
+                component.cameraMode = CameraMode.TRACKING
+                val padding = maplibreMap.cameraPosition.padding
+
+                component.onStop()
+                component.paddingWhileTracking(doubleArrayOf(100.0, 200.0, 300.0, 400.0))
+                uiController.loopMainThreadForAtLeast(DEFAULT_TRACKING_PADDING_ANIM_DURATION)
+                TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+                assertArrayEquals(padding, maplibreMap.cameraPosition.padding, 0.1)
+            }
+        }
+
+        executeComponentTest(componentAction)
+    }
+
+    @Test
+    fun animators_dontPaddingWhileTransitioning() {
+        val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+            override fun onLocationComponentAction(
+                component: LocationComponent,
+                maplibreMap: MapLibreMap,
+                style: Style,
+                uiController: UiController,
+                context: Context
+            ) {
+                component.activateLocationComponent(LocationComponentActivationOptions
+                    .builder(context, style)
+                    .useDefaultLocationEngine(false)
+                    .build())
+                component.isLocationComponentEnabled = true
+                component.forceLocationUpdate(location)
+
+                val padding = maplibreMap.cameraPosition.padding
+                component.setCameraMode(CameraMode.TRACKING_GPS, 500L, null, null, null, null)
+                component.paddingWhileTracking(doubleArrayOf(100.0, 200.0, 300.0, 400.0), 1000L)
+                uiController.loopMainThreadForAtLeast(1000L)
+                TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+                assertArrayEquals(padding, maplibreMap.cameraPosition.padding, 0.1)
+            }
+        }
+
+        executeComponentTest(componentAction)
+    }
+
+    @Test
+    fun animators_paddingWhileTracking() {
+        val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+            override fun onLocationComponentAction(
+                component: LocationComponent,
+                maplibreMap: MapLibreMap,
+                style: Style,
+                uiController: UiController,
+                context: Context
+            ) {
+                component.activateLocationComponent(LocationComponentActivationOptions
+                    .builder(context, style)
+                    .useDefaultLocationEngine(false)
+                    .build())
+                component.isLocationComponentEnabled = true
+                component.cameraMode = CameraMode.TRACKING
+                val padding = doubleArrayOf(100.0, 200.0, 300.0, 400.0)
+                component.paddingWhileTracking(padding)
+                uiController.loopMainThreadForAtLeast(DEFAULT_TRACKING_PADDING_ANIM_DURATION)
+                TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+                assertArrayEquals(padding, maplibreMap.cameraPosition.padding, 0.01)
+            }
+        }
+
+        executeComponentTest(componentAction)
+    }
+
     @Test
     fun cameraPositionAdjustedToTrackingModeWhenComponentEnabled() {
         val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/org/maplibre/android/testapp/activity/location/LocationModesActivity.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/org/maplibre/android/testapp/activity/location/LocationModesActivity.kt
index 9505f3bcccb..a84759b0174 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/org/maplibre/android/testapp/activity/location/LocationModesActivity.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/org/maplibre/android/testapp/activity/location/LocationModesActivity.kt
@@ -25,12 +25,13 @@ import org.maplibre.android.location.modes.CameraMode
 import org.maplibre.android.location.modes.RenderMode
 import org.maplibre.android.location.permissions.PermissionsListener
 import org.maplibre.android.location.permissions.PermissionsManager
-import org.maplibre.android.maps.MapView
 import org.maplibre.android.maps.MapLibreMap
 import org.maplibre.android.maps.MapLibreMap.CancelableCallback
+import org.maplibre.android.maps.MapView
 import org.maplibre.android.maps.OnMapReadyCallback
 import org.maplibre.android.maps.Style
 import org.maplibre.android.testapp.R
+import java.util.Random
 
 class LocationModesActivity :
     AppCompatActivity(),
@@ -193,6 +194,27 @@ class LocationModesActivity :
                 Toast.makeText(this, "Not possible to animate - not tracking", Toast.LENGTH_SHORT)
                     .show()
             }
+        } else if (id == R.id.action_component_padding_animation_while_tracking) {
+            val paddingRandom = Random()
+            locationComponent!!.paddingWhileTracking(
+                doubleArrayOf(
+                    paddingRandom.nextDouble() * 500,
+                    paddingRandom.nextDouble() * 500,
+                    paddingRandom.nextDouble() * 500,
+                    paddingRandom.nextDouble() * 500
+                ), 1000L, object : CancelableCallback {
+                    override fun onCancel() {
+                        // No impl
+                    }
+
+                    override fun onFinish() {
+                        locationComponent!!.zoomWhileTracking(16.0)
+                    }
+                })
+            if (locationComponent!!.getCameraMode() == CameraMode.NONE) {
+                Toast.makeText(this, "Not possible to animate - not tracking", Toast.LENGTH_SHORT)
+                    .show()
+            }
         }
         return super.onOptionsItemSelected(item)
     }
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_mode.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_mode.xml
index 535679a1c4a..97d7b78455a 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_mode.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_mode.xml
@@ -36,4 +36,8 @@
     
+
+    
 
\ No newline at end of file