22package com.mapbox.maps.plugin.animation
33
44import android.animation.Animator
5+ import android.animation.Animator.DURATION_INFINITE
6+ import android.animation.AnimatorSet
7+ import android.os.Build
58import androidx.annotation.RestrictTo
69import com.mapbox.common.Cancelable
10+ import com.mapbox.geojson.Point
11+ import com.mapbox.maps.CameraAnimationHint
12+ import com.mapbox.maps.CameraAnimationHintStage
713import com.mapbox.maps.CameraOptions
14+ import com.mapbox.maps.CameraState
15+ import com.mapbox.maps.EdgeInsets
816import com.mapbox.maps.ScreenCoordinate
17+ import com.mapbox.maps.logW
918import com.mapbox.maps.plugin.Plugin
1019import 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
1128import com.mapbox.maps.plugin.delegates.MapPluginExtensionsDelegate
1229import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate
1330
@@ -156,4 +173,178 @@ fun createCameraAnimationPlugin(): CameraAnimationsPlugin {
156173@JvmSynthetic
157174fun 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}
0 commit comments