/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedNode from '../nodes/AnimatedNode'; import type AnimatedValue from '../nodes/AnimatedValue'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import AnimatedProps from '../nodes/AnimatedProps'; export type EndResult = {finished: boolean, value?: number, ...}; export type EndCallback = (result: EndResult) => void; export type AnimationConfig = $ReadOnly<{ isInteraction?: boolean, useNativeDriver: boolean, platformConfig?: PlatformConfig, onComplete?: ?EndCallback, iterations?: number, isLooping?: boolean, ... }>; let startNativeAnimationNextId = 1; // Important note: start() and stop() will only be called at most once. // Once an animation has been stopped or finished its course, it will // not be reused. export default class Animation { #nativeID: ?number; #onEnd: ?EndCallback; #useNativeDriver: boolean; __active: boolean; __isInteraction: boolean; __isLooping: ?boolean; __iterations: number; constructor(config: AnimationConfig) { this.#useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config); this.__active = false; this.__isInteraction = config.isInteraction ?? !this.#useNativeDriver; this.__isLooping = config.isLooping; this.__iterations = config.iterations ?? 1; } start( fromValue: number, onUpdate: (value: number) => void, onEnd: ?EndCallback, previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void { if (!this.#useNativeDriver && animatedValue.__isNative === true) { throw new Error( 'Attempting to run JS driven animation on animated node ' + 'that has been moved to "native" earlier by starting an ' + 'animation with `useNativeDriver: true`', ); } this.#onEnd = onEnd; this.__active = true; } stop(): void { if (this.#nativeID != null) { NativeAnimatedHelper.API.stopAnimation(this.#nativeID); } this.__active = false; } __getNativeAnimationConfig(): $ReadOnly<{ platformConfig: ?PlatformConfig, ... }> { // Subclasses that have corresponding animation implementation done in native // should override this method throw new Error('This animation type cannot be offloaded to native'); } __findAnimatedPropsNodes(node: AnimatedNode): Array { const result = []; if (node instanceof AnimatedProps) { result.push(node); return result; } for (const child of node.__getChildren()) { result.push(...this.__findAnimatedPropsNodes(child)); } return result; } __startAnimationIfNative(animatedValue: AnimatedValue): boolean { if (!this.#useNativeDriver) { return false; } const startNativeAnimationWaitId = `${startNativeAnimationNextId}:startAnimation`; startNativeAnimationNextId += 1; NativeAnimatedHelper.API.setWaitingForIdentifier( startNativeAnimationWaitId, ); try { const config = this.__getNativeAnimationConfig(); animatedValue.__makeNative(config.platformConfig); this.#nativeID = NativeAnimatedHelper.generateNewAnimationId(); NativeAnimatedHelper.API.startAnimatingNode( this.#nativeID, animatedValue.__getNativeTag(), config, result => { this.__notifyAnimationEnd(result); // When using natively driven animations, once the animation completes, // we need to ensure that the JS side nodes are synced with the updated // values. const {value} = result; if (value != null) { animatedValue.__onAnimatedValueUpdateReceived(value); if ( ReactNativeFeatureFlags.shouldSkipStateUpdatesForLoopingAnimations() && this.__isLooping === true ) { return; } // Once the JS side node is synced with the updated values, trigger an // update on the AnimatedProps nodes to call any registered callbacks. this.__findAnimatedPropsNodes(animatedValue).forEach(node => node.update(), ); } }, ); return true; } catch (e) { throw e; } finally { NativeAnimatedHelper.API.unsetWaitingForIdentifier( startNativeAnimationWaitId, ); } } /** * Notify the completion callback that the animation has ended. The completion * callback will never be called more than once. */ __notifyAnimationEnd(result: EndResult): void { const callback = this.#onEnd; if (callback != null) { this.#onEnd = null; callback(result); } } }