Skip to content

Commit 06b2f97

Browse files
dzinadgithub-actions[bot]
authored andcommitted
NAVAND-5894: refactor calculateCameraAnimationHint, support more cases
GitOrigin-RevId: 042a1a046e3ecfa2a96716c0a4c48342a44e228b
1 parent f37958a commit 06b2f97

File tree

6 files changed

+1033
-102
lines changed

6 files changed

+1033
-102
lines changed

plugin-animation/api/plugin-animation.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
public final class com/mapbox/maps/plugin/animation/CameraAnimationsUtils {
2+
public static final synthetic fun calculateCameraAnimationHint (Landroid/animation/AnimatorSet;Ljava/util/List;Lcom/mapbox/maps/CameraState;)Lcom/mapbox/maps/CameraAnimationHint;
3+
public static final synthetic fun calculateCameraAnimationHint (Ljava/util/List;Ljava/util/List;Lcom/mapbox/maps/CameraState;)Lcom/mapbox/maps/CameraAnimationHint;
4+
public static synthetic fun calculateCameraAnimationHint$default (Landroid/animation/AnimatorSet;Ljava/util/List;Lcom/mapbox/maps/CameraState;ILjava/lang/Object;)Lcom/mapbox/maps/CameraAnimationHint;
5+
public static synthetic fun calculateCameraAnimationHint$default (Ljava/util/List;Ljava/util/List;Lcom/mapbox/maps/CameraState;ILjava/lang/Object;)Lcom/mapbox/maps/CameraAnimationHint;
26
public static final synthetic fun createCameraAnimationPlugin ()Lcom/mapbox/maps/plugin/animation/CameraAnimationsPlugin;
37
public static final fun easeTo (Lcom/mapbox/maps/plugin/delegates/MapPluginExtensionsDelegate;Lcom/mapbox/maps/CameraOptions;)Lcom/mapbox/common/Cancelable;
48
public static final fun easeTo (Lcom/mapbox/maps/plugin/delegates/MapPluginExtensionsDelegate;Lcom/mapbox/maps/CameraOptions;Lcom/mapbox/maps/plugin/animation/MapAnimationOptions;)Lcom/mapbox/common/Cancelable;

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

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,29 @@
22
package com.mapbox.maps.plugin.animation
33

44
import android.animation.Animator
5+
import android.animation.Animator.DURATION_INFINITE
6+
import android.animation.AnimatorSet
7+
import android.os.Build
58
import androidx.annotation.RestrictTo
69
import com.mapbox.common.Cancelable
10+
import com.mapbox.geojson.Point
11+
import com.mapbox.maps.CameraAnimationHint
12+
import com.mapbox.maps.CameraAnimationHintStage
713
import com.mapbox.maps.CameraOptions
14+
import com.mapbox.maps.CameraState
15+
import com.mapbox.maps.EdgeInsets
816
import com.mapbox.maps.ScreenCoordinate
17+
import com.mapbox.maps.logW
918
import com.mapbox.maps.plugin.Plugin
1019
import com.mapbox.maps.plugin.Plugin.Companion.MAPBOX_CAMERA_PLUGIN_ID
20+
import com.mapbox.maps.plugin.animation.CameraAnimationsPluginImpl.Companion.TAG
21+
import com.mapbox.maps.plugin.animation.animator.CameraAnchorAnimator
22+
import com.mapbox.maps.plugin.animation.animator.CameraAnimator
23+
import com.mapbox.maps.plugin.animation.animator.CameraBearingAnimator
24+
import com.mapbox.maps.plugin.animation.animator.CameraCenterAnimator
25+
import com.mapbox.maps.plugin.animation.animator.CameraPaddingAnimator
26+
import com.mapbox.maps.plugin.animation.animator.CameraPitchAnimator
27+
import com.mapbox.maps.plugin.animation.animator.CameraZoomAnimator
1128
import com.mapbox.maps.plugin.delegates.MapPluginExtensionsDelegate
1229
import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate
1330

@@ -156,4 +173,178 @@ fun createCameraAnimationPlugin(): CameraAnimationsPlugin {
156173
@JvmSynthetic
157174
fun CameraAnimationsPlugin.getCameraAnimatorsFactory(): CameraAnimatorsFactory {
158175
return (this as CameraAnimationsPluginImpl).cameraAnimationsFactory
176+
}
177+
178+
/**
179+
* Calculates camera animation hints for provided [AnimatorSet] at specified fractions.
180+
* This API must be called right before the animation is started: moving camera in-between should be avoided.
181+
*
182+
* Requirements:
183+
* 1. [AnimatorSet] must have startDelay = 0 (TODO support non-zero startDelay).
184+
* However, a child animator is allowed to have a non-zero startDelay.
185+
* 2. All child animations must be instances of [CameraAnimator].
186+
* 3. None of the child animations are allowed to have infinite duration.
187+
* Note: [AnimatorSet] itself may have infinite duration.
188+
*
189+
* If [AnimatorSet] has zero duration, no camera animation hints are calculated: null is returned.
190+
*
191+
* @param fractions list of fractions at which to calculate intermediate camera states
192+
* @param startCameraState start value of the animations.
193+
* Must be provided in case your [AnimatorSet]'s child animators do not have a startValue.
194+
*
195+
* @return [CameraAnimationHint] object, null if calculation could not be performed
196+
*/
197+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
198+
@JvmSynthetic
199+
fun AnimatorSet.calculateCameraAnimationHint(
200+
fractions: List<Float>,
201+
startCameraState: CameraState? = null,
202+
): CameraAnimationHint? {
203+
// Make sure all animators in animatorSet are camera animators
204+
val cameraAnimators = childAnimations.filterIsInstance<CameraAnimator<*>>()
205+
if (cameraAnimators.size != childAnimations.size) {
206+
logW(TAG, "Incompatible animators: all should be instances of CameraAnimator")
207+
return null
208+
}
209+
210+
if (startDelay != 0L) {
211+
logW(TAG, "AnimatorSets with non-zero startDelay are not supported.")
212+
return null
213+
}
214+
215+
if (childAnimations.isEmpty()) {
216+
logW(TAG, "AnimatorSet has no child animations.")
217+
return null
218+
}
219+
220+
val totalDuration = if (duration >= 0) {
221+
duration
222+
} else {
223+
cameraAnimators.maxOf { it.safeTotalDuration() }
224+
}
225+
return cameraAnimators.calculateCameraAnimationHint(fractions, totalDuration, startCameraState)
226+
}
227+
228+
/**
229+
* Calculates camera animation hints for provided list of [Animator] at specified fractions.
230+
* This API must be called right before the animation is started: moving camera in-between should be avoided.
231+
*
232+
* Requirements:
233+
* 1. None of the animations are allowed to have infinite duration.
234+
*
235+
* If all animators have zero duration, no camera animation hints are calculated: null is returned.
236+
*
237+
* @param fractions list of fractions at which to calculate intermediate camera states
238+
* @param startCameraState start value of the animations.
239+
* Must be provided in case your [AnimatorSet]'s child animators do not have a startValue.
240+
*
241+
* @return [CameraAnimationHint] object, null if calculation could not be performed
242+
*/
243+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
244+
@JvmSynthetic
245+
fun <T : CameraAnimator<*>> List<T>.calculateCameraAnimationHint(
246+
fractions: List<Float>,
247+
startCameraState: CameraState? = null,
248+
): CameraAnimationHint? {
249+
if (isEmpty()) {
250+
return null
251+
}
252+
val totalDuration = maxOf { it.safeTotalDuration() }
253+
return calculateCameraAnimationHint(fractions, totalDuration, startCameraState)
254+
}
255+
256+
private fun Animator.safeTotalDuration(): Long {
257+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
258+
totalDuration
259+
} else {
260+
// For pre-N devices, totalDuration is not available, so we use the duration of the animator.
261+
duration + startDelay
262+
}
263+
}
264+
265+
private fun <T : CameraAnimator<*>> List<T>.calculateCameraAnimationHint(
266+
fractions: List<Float>,
267+
totalDuration: Long,
268+
startCameraState: CameraState?,
269+
): CameraAnimationHint? {
270+
if (isEmpty()) {
271+
return null
272+
}
273+
274+
// No need to calculate camera animation hints if the animation is instant
275+
if (totalDuration == 0L) {
276+
return null
277+
}
278+
279+
if (totalDuration == DURATION_INFINITE || this.any { it.duration == DURATION_INFINITE }) {
280+
logW(
281+
TAG,
282+
"Animators with infinite duration are not supported. " +
283+
"Please use finite duration for all animators.",
284+
)
285+
return null
286+
}
287+
288+
val cameraOptionsBuilder = CameraOptions.Builder()
289+
290+
val stages = fractions.map { totalFraction ->
291+
cameraOptionsBuilder.clear()
292+
this.map { cameraAnimator ->
293+
try {
294+
val fraction = getRelativeFraction(cameraAnimator, totalDuration, totalFraction)
295+
val value = cameraAnimator.getAnimatedValueAt(fraction, startCameraState)
296+
updateCameraValue(cameraAnimator, value, cameraOptionsBuilder)
297+
} catch (e: UnsupportedOperationException) {
298+
logW(
299+
TAG,
300+
"Unable to calculate animated value ahead of time for ${cameraAnimator.type.name}: ${e.message}"
301+
)
302+
}
303+
}
304+
val camera = cameraOptionsBuilder.build()
305+
// We use totalDuration to keep track of the progress because that's the total duration of the animation. That is,
306+
// the time between `setUserAnimationInProgress(true)` and `setUserAnimationInProgress(false)`
307+
val progress = (totalDuration * totalFraction).toLong()
308+
CameraAnimationHintStage.Builder()
309+
.camera(camera)
310+
.progress(progress)
311+
.build()
312+
}
313+
return CameraAnimationHint.Builder().stages(stages).build()
314+
}
315+
316+
private fun getRelativeFraction(cameraAnimator: CameraAnimator<*>, totalDuration: Long, totalFraction: Float): Float {
317+
val childDuration = cameraAnimator.duration
318+
if (childDuration <= 0L) {
319+
return 1f
320+
}
321+
return ((totalFraction * totalDuration - cameraAnimator.startDelay) / childDuration).coerceIn(0f, 1f)
322+
}
323+
324+
internal fun updateCameraValue(
325+
cameraAnimator: CameraAnimator<*>,
326+
animatedValue: Any?,
327+
cameraOptionsBuilder: CameraOptions.Builder
328+
) {
329+
when (cameraAnimator) {
330+
is CameraCenterAnimator -> cameraOptionsBuilder.center(animatedValue as? Point)
331+
is CameraZoomAnimator -> cameraOptionsBuilder.zoom(animatedValue as? Double)
332+
is CameraAnchorAnimator -> cameraOptionsBuilder.anchor(animatedValue as? ScreenCoordinate)
333+
is CameraPaddingAnimator -> cameraOptionsBuilder.padding(animatedValue as? EdgeInsets)
334+
is CameraBearingAnimator -> cameraOptionsBuilder.bearing(animatedValue as? Double)
335+
is CameraPitchAnimator -> cameraOptionsBuilder.pitch(animatedValue as? Double)
336+
}
337+
}
338+
339+
/**
340+
* Convenience method to clear camera options so it can be reused instead of creating
341+
* new [CameraOptions.Builder].
342+
*/
343+
private fun CameraOptions.Builder.clear() {
344+
center(null)
345+
.padding(null)
346+
.anchor(null)
347+
.zoom(null)
348+
.bearing(null)
349+
.pitch(null)
159350
}

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

Lines changed: 9 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import androidx.annotation.VisibleForTesting
99
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
1010
import com.mapbox.common.Cancelable
1111
import com.mapbox.geojson.Point
12-
import com.mapbox.maps.CameraAnimationHint
13-
import com.mapbox.maps.CameraAnimationHintStage
1412
import com.mapbox.maps.CameraOptions
1513
import com.mapbox.maps.EdgeInsets
1614
import com.mapbox.maps.MapboxCameraAnimationException
@@ -80,6 +78,12 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
8078

8179
private val lifecycleListeners = CopyOnWriteArraySet<CameraAnimationsLifecycleListener>()
8280

81+
/**
82+
* Animation fraction at which the camera animation hints will be calculated.
83+
* Note: at least 1.0F should be present and entries should be in ascent order.
84+
*/
85+
private val cameraAnimationHintFractions = listOf(0.25F, 0.5F, 0.75F, 1.0F)
86+
8387
/**
8488
* If debug mode is enabled extra logs will be written about animation lifecycle and
8589
* some other events that may be useful for debugging.
@@ -355,21 +359,6 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
355359
CameraAnimatorType.PITCH -> animationObjectValues.all { Objects.equals(pitch, it) }
356360
}
357361

358-
private fun updateCameraValue(
359-
cameraAnimator: CameraAnimator<*>,
360-
animatedValue: Any?,
361-
cameraOptionsBuilder: CameraOptions.Builder
362-
) {
363-
when (cameraAnimator) {
364-
is CameraCenterAnimator -> cameraOptionsBuilder.center(animatedValue as? Point)
365-
is CameraZoomAnimator -> cameraOptionsBuilder.zoom(animatedValue as? Double)
366-
is CameraAnchorAnimator -> cameraOptionsBuilder.anchor(animatedValue as? ScreenCoordinate)
367-
is CameraPaddingAnimator -> cameraOptionsBuilder.padding(animatedValue as? EdgeInsets)
368-
is CameraBearingAnimator -> cameraOptionsBuilder.bearing(animatedValue as? Double)
369-
is CameraPitchAnimator -> cameraOptionsBuilder.pitch(animatedValue as? Double)
370-
}
371-
}
372-
373362
private fun registerInternalListener(animator: CameraAnimator<*>) {
374363
postOnAnimatorThread {
375364
animator.addInternalListener(object : Animator.AnimatorListener {
@@ -488,86 +477,6 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
488477
}
489478
}
490479

491-
/**
492-
* Animation fraction at which the camera animation hints will be calculated.
493-
* Note: at least 1.0F should be present and entries should be in ascent order.
494-
*/
495-
private val cameraAnimationHintFractions = listOf(0.25F, 0.5F, 0.75F, 1.0F)
496-
private fun calculateCameraAnimationHint(animatorSet: AnimatorSet) {
497-
// this method requires that all animators have:
498-
// 1. same duration
499-
// 2. no delay
500-
// 3. start value
501-
// TODO: support different duration, start delay and no start value
502-
503-
// No need to calculate camera animation hints if the animation is instant
504-
if (animatorSet.duration == 0L) {
505-
return
506-
}
507-
508-
// Make sure all animators in animatorSet are camera animators
509-
val cameraAnimators = animatorSet.childAnimations.map { it as CameraAnimator<*> }
510-
if (cameraAnimators.isEmpty() || cameraAnimators.size != animatorSet.childAnimations.size) {
511-
return
512-
}
513-
514-
val cameraOptionsBuilder = CameraOptions.Builder()
515-
// Keep track of the duration to make sure all animators have the same duration
516-
val duration = cameraAnimators[0].duration
517-
518-
val stages = cameraAnimationHintFractions.map { fraction ->
519-
cameraOptionsBuilder.clear()
520-
cameraAnimators.map { cameraAnimator ->
521-
if (cameraAnimator.startDelay != 0L) {
522-
logW(
523-
TAG,
524-
"Unable to calculate animated value ahead of time for ${cameraAnimator.type.name}: startDelay != 0 is not supported"
525-
)
526-
return
527-
}
528-
if (cameraAnimator.duration != duration) {
529-
logW(
530-
TAG,
531-
"Unable to calculate animated value ahead of time for ${cameraAnimator.type.name}: different duration is not supported"
532-
)
533-
return
534-
}
535-
try {
536-
val value = cameraAnimator.getAnimatedValueAt(fraction)
537-
updateCameraValue(cameraAnimator, value, cameraOptionsBuilder)
538-
} catch (e: UnsupportedOperationException) {
539-
logW(
540-
TAG,
541-
"Unable to calculate animated value ahead of time for ${cameraAnimator.type.name}: ${e.message}"
542-
)
543-
}
544-
}
545-
val camera = cameraOptionsBuilder.build()
546-
// We use animatorSet.duration to keep track of the progress because that's the total duration of the animation. That is,
547-
// the time between `setUserAnimationInProgress(true)` and `setUserAnimationInProgress(false)`
548-
val progress = (animatorSet.duration * fraction).toLong()
549-
CameraAnimationHintStage.Builder()
550-
.camera(camera)
551-
.progress(progress)
552-
.build()
553-
}
554-
val cameraAnimationHint = CameraAnimationHint.Builder().stages(stages).build()
555-
mapTransformDelegate.setCameraAnimationHint(cameraAnimationHint)
556-
}
557-
558-
/**
559-
* Convenience method to clear camera options so it can be reused instead of creating
560-
* new [CameraOptions.Builder].
561-
*/
562-
private fun CameraOptions.Builder.clear() {
563-
center(null)
564-
.padding(null)
565-
.anchor(null)
566-
.zoom(null)
567-
.bearing(null)
568-
.pitch(null)
569-
}
570-
571480
private fun registerInternalUpdateListener(animator: CameraAnimator<*>) {
572481
animator.addInternalUpdateListener {
573482
postOnMainThread { onAnimationUpdateInternal(animator, it) }
@@ -1140,7 +1049,9 @@ internal class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlu
11401049
}
11411050
playTogether(*animators)
11421051
}
1143-
calculateCameraAnimationHint(animatorSet)
1052+
animatorSet.calculateCameraAnimationHint(cameraAnimationHintFractions)?.let {
1053+
mapTransformDelegate.setCameraAnimationHint(it)
1054+
}
11441055

11451056
return HighLevelAnimatorSet(animationOptions?.owner, animatorSet).also {
11461057
highLevelAnimatorSet = it

0 commit comments

Comments
 (0)