Files
react-native/Libraries/LayoutAnimation/LayoutAnimation.js
T
Joshua Gross 6eea5974cf LayoutAnimations.js: ensure that onComplete callback is always called, even if LayoutAnimations is disabled on iOS
Summary:
See comments. On iOS in Fabric, LayoutAnimations is only conditionally enabled; whereas on Android it's always enabled. That means for code on iOS that relies on the onComplete callback, there might be bugs.

Ensure the callback is always called on iOS by racing a timer with the animation completion.

This will be deleted before Fabric "ships" fully.

Impact is minimal since this change is scoped to only run on iOS and under Fabric.

Changelog: [Internal]

Reviewed By: sammy-SC

Differential Revision: D26166237

fbshipit-source-id: 9a07a402845c379e1511f199eef3d3e249e1eb06
2021-02-01 11:57:33 -08:00

188 lines
5.3 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its 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
*/
'use strict';
const UIManager = require('../ReactNative/UIManager');
import type {Spec as FabricUIManagerSpec} from '../ReactNative/FabricUIManager';
import type {
LayoutAnimationConfig as LayoutAnimationConfig_,
LayoutAnimationType,
LayoutAnimationProperty,
} from '../Renderer/shims/ReactNativeTypes';
import Platform from '../Utilities/Platform';
// Reexport type
export type LayoutAnimationConfig = LayoutAnimationConfig_;
type OnAnimationDidEndCallback = () => void;
type OnAnimationDidFailCallback = () => void;
/**
* Configures the next commit to be animated.
*
* onAnimationDidEnd is guaranteed to be called when the animation completes.
* onAnimationDidFail is *never* called in the classic, pre-Fabric renderer,
* and never has been. In the new renderer (Fabric) it is called only if configuration
* parsing fails.
*/
function configureNext(
config: LayoutAnimationConfig,
onAnimationDidEnd?: OnAnimationDidEndCallback,
onAnimationDidFail?: OnAnimationDidFailCallback,
) {
if (Platform.isTesting) {
return;
}
if (UIManager?.configureNextLayoutAnimation) {
UIManager.configureNextLayoutAnimation(
config,
onAnimationDidEnd ?? function() {},
onAnimationDidFail ??
function() {} /* this should never be called in Non-Fabric */,
);
}
// In Fabric, LayoutAnimations are unconditionally enabled for Android, and
// conditionally enabled on iOS (pending fully shipping; this is a temporary state).
const FabricUIManager: FabricUIManagerSpec = global?.nativeFabricUIManager;
if (FabricUIManager?.configureNextLayoutAnimation) {
// Since LayoutAnimations may possibly be disabled for now on iOS, we race
// a setTimeout with animation completion, in case onComplete is never called
// from native. Once LayoutAnimations unconditionally ship everywhere, we can
// delete this mechanism.
// TODO: (T65643440) remove timeout once LayoutAnimation ships on iOS.
let animationCompletionHasRun = false;
const onAnimationComplete = () => {
if (Platform.OS === 'ios') {
if (animationCompletionHasRun) {
return;
}
animationCompletionHasRun = true;
clearTimeout(raceWithAnimationId);
}
onAnimationDidEnd?.();
};
const raceWithAnimationId =
Platform.OS === 'ios'
? setTimeout(
onAnimationComplete,
(config.duration ?? 0) + 17 /* one frame + 1ms */,
)
: null;
global?.nativeFabricUIManager?.configureNextLayoutAnimation(
config,
onAnimationComplete,
onAnimationDidFail ??
function() {} /* this will only be called if configuration parsing fails */,
);
}
}
function create(
duration: number,
type: LayoutAnimationType,
property: LayoutAnimationProperty,
): LayoutAnimationConfig {
return {
duration,
create: {type, property},
update: {type},
delete: {type, property},
};
}
const Presets = {
easeInEaseOut: (create(
300,
'easeInEaseOut',
'opacity',
): LayoutAnimationConfig),
linear: (create(500, 'linear', 'opacity'): LayoutAnimationConfig),
spring: {
duration: 700,
create: {
type: 'linear',
property: 'opacity',
},
update: {
type: 'spring',
springDamping: 0.4,
},
delete: {
type: 'linear',
property: 'opacity',
},
},
};
/**
* Automatically animates views to their new positions when the
* next layout happens.
*
* A common way to use this API is to call it before calling `setState`.
*
* Note that in order to get this to work on **Android** you need to set the following flags via `UIManager`:
*
* UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
*/
const LayoutAnimation = {
/**
* Schedules an animation to happen on the next layout.
*
* @param config Specifies animation properties:
*
* - `duration` in milliseconds
* - `create`, `AnimationConfig` for animating in new views
* - `update`, `AnimationConfig` for animating views that have been updated
*
* @param onAnimationDidEnd Called when the animation finished.
* Only supported on iOS.
* @param onError Called on error. Only supported on iOS.
*/
configureNext,
/**
* Helper for creating a config for `configureNext`.
*/
create,
Types: Object.freeze({
spring: 'spring',
linear: 'linear',
easeInEaseOut: 'easeInEaseOut',
easeIn: 'easeIn',
easeOut: 'easeOut',
keyboard: 'keyboard',
}),
Properties: Object.freeze({
opacity: 'opacity',
scaleX: 'scaleX',
scaleY: 'scaleY',
scaleXY: 'scaleXY',
}),
checkConfig(...args: Array<mixed>) {
console.error('LayoutAnimation.checkConfig(...) has been disabled.');
},
Presets,
easeInEaseOut: (configureNext.bind(null, Presets.easeInEaseOut): (
onAnimationDidEnd?: OnAnimationDidEndCallback,
) => void),
linear: (configureNext.bind(null, Presets.linear): (
onAnimationDidEnd?: OnAnimationDidEndCallback,
) => void),
spring: (configureNext.bind(null, Presets.spring): (
onAnimationDidEnd?: OnAnimationDidEndCallback,
) => void),
};
module.exports = LayoutAnimation;