mapbox-maps-android icon indicating copy to clipboard operation
mapbox-maps-android copied to clipboard

PuckAnimator animates with `duration=0` when called with an empty `options` lambda.

Open tomaszrybakiewicz opened this issue 3 years ago • 2 comments

tl;dr; Android SDK (31) ValueAnimator.clone() method doesn't copy duration value, causing PuckAnimator.userConfiguredAnimator.duration = 0.

Environment

  • Android OS version: Android 12 (API 31)
  • Devices affected: Pixel 6
  • Maps SDK Version: 10.6.0

Observed behavior and steps to reproduce

We detected this issue when working on a patch for a Location puck's velocity.

Puck Velocity Fix relies on a mechanism that allows customization of ValueAnimator via LocationConsumer.onLocationUpdated() callback.

When NavigationLocationProvider calls LocationConsumer.onLocationUpdated(vararg location: Point, options: (ValueAnimator.() -> Unit)? = null) with non-null options, the PuckAnimator uses a separate animator instance (userConfiguredAnimator) to drive the animation (PuckAnimator.animate()).

// PuckAnimator.kt
class PuckAnimator(...) : ValueAnimator() {
  ... 
  init {
    setObjectValues(emptyArray<Any>())
    setEvaluator(evaluator)
    addUpdateListener {
      @Suppress("UNCHECKED_CAST")
      val updatedValue = it.animatedValue as T
      updateLayer(it.animatedFraction, updatedValue)
      updateListener?.invoke(updatedValue)
    }
    duration = LocationComponentConstants.DEFAULT_INTERVAL_MILLIS
    interpolator = DEFAULT_INTERPOLATOR
    userConfiguredAnimator = clone() // <---------- THIS doesn't copy duration value
  }
  ...
  fun animate(
    vararg targets: T,
    options: (ValueAnimator.() -> Unit)? = null
  ) {
    cancelRunning()
    if (options == null) {
      setObjectValues(*targets)
      start()
    } else {  // <------------ 
      options.invoke(userConfiguredAnimator)
      userConfiguredAnimator.setObjectValues(*targets)
      userConfiguredAnimator.start()
    }
  }
  ...
}

The userConfiguredAnimator instance is created via ValueAnimator.clone() method in the PuckAnimator constructor. It appears that cloned version doesn't copy duration field, leaving userConfiguredAnimator.duration = 0.

(screenshots of debbuger session showing PuckAnimator duration fields)

Screen Shot 2022-06-17 at 3 51 07 PM Screen Shot 2022-06-17 at 3 51 18 PM
Expand for glitch recording

https://user-images.githubusercontent.com/2678039/174207047-6719eeb9-5d77-422f-bcb3-529ae8bb6cfc.mp4

Expected behaviour

Calling PuckPositionAnimator.animate() with an empty options block should animate Puck position with default duration (LocationComponentConstants.DEFAULT_INTERVAL_MILLIS).

Both animate calls below should result in the same animator behaviour:

var positionAnimator = PuckPositionAnimator(...)
val point = Point.fromLngLat(..., ...)

// First call without options
positionAnimator.animate(point)    

// Second call with empty options block
positionAnimator.animate(point, options = {})

Notes / preliminary analysis

After further investigation, we identified that in Android 12 SDK (API 31) neither the ValueAnimator.clone(), not its super class Animator.clone() clones the duration value, leaving it 0.

Proposed solution

PuckAnimator should set userConfiguredAnimator.duration default value to match PuckAnimator.duration.

// PuckAnimator.kt
class PuckAnimator(...) : ValueAnimator() {
  ... 
  init {
    setObjectValues(emptyArray<Any>())
    setEvaluator(evaluator)
    addUpdateListener {
      @Suppress("UNCHECKED_CAST")
      val updatedValue = it.animatedValue as T
      updateLayer(it.animatedFraction, updatedValue)
      updateListener?.invoke(updatedValue)
    }
    duration = LocationComponentConstants.DEFAULT_INTERVAL_MILLIS
    interpolator = DEFAULT_INTERPOLATOR
    userConfiguredAnimator = clone() // <---------- THIS doesn't copy duration value
    userConfiguredAnimator.duration = duration  // <---- MANUALLY copy duration value
  }
...
}

tomaszrybakiewicz avatar Jun 21 '22 19:06 tomaszrybakiewicz

@tomaszrybakiewicz I assume it's also connected with https://github.com/mapbox/mapbox-maps-android/issues/1411 - I did also mention workaround there, could you please check if that will help in your case?

kiryldz avatar Jun 22 '22 09:06 kiryldz

@kiryldz, Thanks. This issue is not a blocker and we already implemented a workaround in NavSDK. However this issue might still affect developers that decide to use their LocationProvider implementation with LocationComponentPlugin, instead suggested NavigationLocationProvider.

tomaszrybakiewicz avatar Jun 22 '22 14:06 tomaszrybakiewicz