feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,86 @@
/**
* 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.
*
* @format
*/
import {ProcessedColorValue} from '../StyleSheet/processColor';
import {ColorValue} from '../StyleSheet/StyleSheet';
/**
* @see: https://reactnative.dev/docs/actionsheetios#content
*/
export interface ActionSheetIOSOptions {
title?: string | undefined;
options: string[];
cancelButtonIndex?: number | undefined;
destructiveButtonIndex?: number | number[] | undefined | null;
message?: string | undefined;
anchor?: number | undefined;
tintColor?: ColorValue | ProcessedColorValue | undefined;
cancelButtonTintColor?: ColorValue | ProcessedColorValue | undefined;
userInterfaceStyle?: 'light' | 'dark' | undefined;
disabledButtonIndices?: number[] | undefined;
}
export interface ShareActionSheetIOSOptions {
message?: string | undefined;
url?: string | undefined;
subject?: string | undefined;
anchor?: number | undefined;
/** The activities to exclude from the ActionSheet.
* For example: ['com.apple.UIKit.activity.PostToTwitter']
*/
excludedActivityTypes?: string[] | undefined;
}
/**
* @see https://reactnative.dev/docs/actionsheetios#content
*/
export interface ActionSheetIOSStatic {
/**
* Display an iOS action sheet. The `options` object must contain one or more
* of:
* - `options` (array of strings) - a list of button titles (required)
* - `cancelButtonIndex` (int) - index of cancel button in `options`
* - `destructiveButtonIndex` (int) - index of destructive button in `options`
* - `title` (string) - a title to show above the action sheet
* - `message` (string) - a message to show below the title
*/
showActionSheetWithOptions: (
options: ActionSheetIOSOptions,
callback: (buttonIndex: number) => void,
) => void;
/**
* Display the iOS share sheet. The `options` object should contain
* one or both of `message` and `url` and can additionally have
* a `subject` or `excludedActivityTypes`:
*
* - `url` (string) - a URL to share
* - `message` (string) - a message to share
* - `subject` (string) - a subject for the message
* - `excludedActivityTypes` (array) - the activities to exclude from the ActionSheet
*
* NOTE: if `url` points to a local file, or is a base64-encoded
* uri, the file it points to will be loaded and shared directly.
* In this way, you can share images, videos, PDF files, etc.
*/
showShareActionSheetWithOptions: (
options: ShareActionSheetIOSOptions,
failureCallback: (error: Error) => void,
successCallback: (success: boolean, method: string) => void,
) => void;
/**
* Dismisses the most upper iOS action sheet presented, if no action sheet is
* present a warning is displayed.
*/
dismissActionSheet: () => void;
}
export const ActionSheetIOS: ActionSheetIOSStatic;
export type ActionSheetIOS = ActionSheetIOSStatic;

View File

@@ -0,0 +1,162 @@
/**
* 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
* @format
*/
import type {ProcessedColorValue} from '../StyleSheet/processColor';
import type {ColorValue} from '../StyleSheet/StyleSheet';
import RCTActionSheetManager from './NativeActionSheetManager';
const processColor = require('../StyleSheet/processColor').default;
const invariant = require('invariant');
/**
* Display action sheets and share sheets on iOS.
*
* See https://reactnative.dev/docs/actionsheetios
*/
const ActionSheetIOS = {
/**
* Display an iOS action sheet.
*
* The `options` object must contain one or more of:
*
* - `options` (array of strings) - a list of button titles (required)
* - `cancelButtonIndex` (int) - index of cancel button in `options`
* - `destructiveButtonIndex` (int or array of ints) - index or indices of destructive buttons in `options`
* - `title` (string) - a title to show above the action sheet
* - `message` (string) - a message to show below the title
* - `disabledButtonIndices` (array of numbers) - a list of button indices which should be disabled
*
* The 'callback' function takes one parameter, the zero-based index
* of the selected item.
*
* See https://reactnative.dev/docs/actionsheetios#showactionsheetwithoptions
*/
showActionSheetWithOptions(
options: {|
+title?: ?string,
+message?: ?string,
+options: Array<string>,
+destructiveButtonIndex?: ?number | ?Array<number>,
+cancelButtonIndex?: ?number,
+anchor?: ?number,
+tintColor?: ColorValue | ProcessedColorValue,
+cancelButtonTintColor?: ColorValue | ProcessedColorValue,
+userInterfaceStyle?: string,
+disabledButtonIndices?: Array<number>,
|},
callback: (buttonIndex: number) => void,
) {
invariant(
typeof options === 'object' && options !== null,
'Options must be a valid object',
);
invariant(typeof callback === 'function', 'Must provide a valid callback');
invariant(RCTActionSheetManager, "ActionSheetManager doesn't exist");
const {
tintColor,
cancelButtonTintColor,
destructiveButtonIndex,
...remainingOptions
} = options;
let destructiveButtonIndices = null;
if (Array.isArray(destructiveButtonIndex)) {
destructiveButtonIndices = destructiveButtonIndex;
} else if (typeof destructiveButtonIndex === 'number') {
destructiveButtonIndices = [destructiveButtonIndex];
}
const processedTintColor = processColor(tintColor);
const processedCancelButtonTintColor = processColor(cancelButtonTintColor);
invariant(
processedTintColor == null || typeof processedTintColor === 'number',
'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions tintColor',
);
invariant(
processedCancelButtonTintColor == null ||
typeof processedCancelButtonTintColor === 'number',
'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions cancelButtonTintColor',
);
RCTActionSheetManager.showActionSheetWithOptions(
{
...remainingOptions,
// $FlowFixMe[incompatible-call]
tintColor: processedTintColor,
// $FlowFixMe[incompatible-call]
cancelButtonTintColor: processedCancelButtonTintColor,
destructiveButtonIndices,
},
callback,
);
},
/**
* Display the iOS share sheet. The `options` object should contain
* one or both of `message` and `url` and can additionally have
* a `subject` or `excludedActivityTypes`:
*
* - `url` (string) - a URL to share
* - `message` (string) - a message to share
* - `subject` (string) - a subject for the message
* - `excludedActivityTypes` (array) - the activities to exclude from
* the ActionSheet
* - `tintColor` (color) - tint color of the buttons
*
* The 'failureCallback' function takes one parameter, an error object.
* The only property defined on this object is an optional `stack` property
* of type `string`.
*
* The 'successCallback' function takes two parameters:
*
* - a boolean value signifying success or failure
* - a string that, in the case of success, indicates the method of sharing
*
* See https://reactnative.dev/docs/actionsheetios#showshareactionsheetwithoptions
*/
showShareActionSheetWithOptions(
options: Object,
failureCallback: Function,
successCallback: Function,
) {
invariant(
typeof options === 'object' && options !== null,
'Options must be a valid object',
);
invariant(
typeof failureCallback === 'function',
'Must provide a valid failureCallback',
);
invariant(
typeof successCallback === 'function',
'Must provide a valid successCallback',
);
invariant(RCTActionSheetManager, "ActionSheetManager doesn't exist");
RCTActionSheetManager.showShareActionSheetWithOptions(
{...options, tintColor: processColor(options.tintColor)},
failureCallback,
successCallback,
);
},
/**
* Dismisses the most upper iOS action sheet presented, if no action sheet is
* present a warning is displayed.
*/
dismissActionSheet: () => {
invariant(RCTActionSheetManager, "ActionSheetManager doesn't exist");
if (typeof RCTActionSheetManager.dismissActionSheet === 'function') {
RCTActionSheetManager.dismissActionSheet();
}
},
};
module.exports = ActionSheetIOS;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeActionSheetManager';
import NativeActionSheetManager from '../../src/private/specs/modules/NativeActionSheetManager';
export default NativeActionSheetManager;

View File

@@ -0,0 +1,34 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
Pod::Spec.new do |s|
s.name = "React-RCTActionSheet"
s.version = version
s.summary = "An API for displaying iOS action sheets and share sheets."
s.homepage = "https://reactnative.dev/"
s.documentation_url = "https://reactnative.dev/docs/actionsheetios"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "*.{m}"
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTActionSheet"
s.dependency "React-Core/RCTActionSheetHeaders", version
end

View File

@@ -0,0 +1,91 @@
/**
* 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.
*
* @format
*/
/**
* @see https://reactnative.dev/docs/alert#content
*/
export interface AlertButton {
text?: string | undefined;
onPress?: ((value?: string) => void) | undefined;
isPreferred?: boolean | undefined;
style?: 'default' | 'cancel' | 'destructive' | undefined;
}
interface AlertOptions {
/** @platform android */
cancelable?: boolean | undefined;
userInterfaceStyle?: 'unspecified' | 'light' | 'dark' | undefined;
/** @platform android */
onDismiss?: (() => void) | undefined;
}
/**
* Launches an alert dialog with the specified title and message.
*
* Optionally provide a list of buttons. Tapping any button will fire the
* respective onPress callback and dismiss the alert. By default, the only
* button will be an 'OK' button.
*
* This is an API that works both on iOS and Android and can show static
* alerts. On iOS, you can show an alert that prompts the user to enter
* some information.
*
* ## iOS
*
* On iOS you can specify any number of buttons. Each button can optionally
* specify a style, which is one of 'default', 'cancel' or 'destructive'.
*
* ## Android
*
* On Android at most three buttons can be specified. Android has a concept
* of a neutral, negative and a positive button:
*
* - If you specify one button, it will be the 'positive' one (such as 'OK')
* - Two buttons mean 'negative', 'positive' (such as 'Cancel', 'OK')
* - Three buttons mean 'neutral', 'negative', 'positive' (such as 'Later', 'Cancel', 'OK')
*
* ```
* // Works on both iOS and Android
* Alert.alert(
* 'Alert Title',
* 'My Alert Msg',
* [
* {text: 'Ask me later', onPress: () => console.log('Ask me later pressed')},
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
* {text: 'OK', onPress: () => console.log('OK Pressed')},
* ]
* )
* ```
*/
export interface AlertStatic {
alert: (
title: string,
message?: string,
buttons?: AlertButton[],
options?: AlertOptions,
) => void;
prompt: (
title: string,
message?: string,
callbackOrButtons?: ((text: string) => void) | AlertButton[],
type?: AlertType,
defaultValue?: string,
keyboardType?: string,
options?: AlertOptions,
) => void;
}
export type AlertType =
| 'default'
| 'plain-text'
| 'secure-text'
| 'login-password';
export const Alert: AlertStatic;
export type Alert = AlertStatic;

View File

@@ -0,0 +1,174 @@
/**
* 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.
*
* @format
* @flow
*/
import type {DialogOptions} from '../NativeModules/specs/NativeDialogManagerAndroid';
import Platform from '../Utilities/Platform';
import RCTAlertManager from './RCTAlertManager';
export type AlertType =
| 'default'
| 'plain-text'
| 'secure-text'
| 'login-password';
export type AlertButtonStyle = 'default' | 'cancel' | 'destructive';
export type Buttons = Array<{
text?: string,
onPress?: ?Function,
isPreferred?: boolean,
style?: AlertButtonStyle,
...
}>;
type Options = {
cancelable?: ?boolean,
userInterfaceStyle?: 'unspecified' | 'light' | 'dark',
onDismiss?: ?() => void,
...
};
/**
* Launches an alert dialog with the specified title and message.
*
* See https://reactnative.dev/docs/alert
*/
class Alert {
static alert(
title: ?string,
message?: ?string,
buttons?: Buttons,
options?: Options,
): void {
if (Platform.OS === 'ios') {
Alert.prompt(
title,
message,
buttons,
'default',
undefined,
undefined,
options,
);
} else if (Platform.OS === 'android') {
const NativeDialogManagerAndroid =
require('../NativeModules/specs/NativeDialogManagerAndroid').default;
if (!NativeDialogManagerAndroid) {
return;
}
const constants = NativeDialogManagerAndroid.getConstants();
const config: DialogOptions = {
title: title || '',
message: message || '',
cancelable: false,
};
if (options && options.cancelable) {
config.cancelable = options.cancelable;
}
// At most three buttons (neutral, negative, positive). Ignore rest.
// The text 'OK' should be probably localized. iOS Alert does that in native.
const defaultPositiveText = 'OK';
const validButtons: Buttons = buttons
? buttons.slice(0, 3)
: [{text: defaultPositiveText}];
const buttonPositive = validButtons.pop();
const buttonNegative = validButtons.pop();
const buttonNeutral = validButtons.pop();
if (buttonNeutral) {
config.buttonNeutral = buttonNeutral.text || '';
}
if (buttonNegative) {
config.buttonNegative = buttonNegative.text || '';
}
if (buttonPositive) {
config.buttonPositive = buttonPositive.text || defaultPositiveText;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const onAction = (action, buttonKey) => {
if (action === constants.buttonClicked) {
if (buttonKey === constants.buttonNeutral) {
buttonNeutral.onPress && buttonNeutral.onPress();
} else if (buttonKey === constants.buttonNegative) {
buttonNegative.onPress && buttonNegative.onPress();
} else if (buttonKey === constants.buttonPositive) {
buttonPositive.onPress && buttonPositive.onPress();
}
} else if (action === constants.dismissed) {
options && options.onDismiss && options.onDismiss();
}
};
const onError = (errorMessage: string) => console.warn(errorMessage);
NativeDialogManagerAndroid.showAlert(config, onError, onAction);
}
}
static prompt(
title: ?string,
message?: ?string,
callbackOrButtons?: ?(((text: string) => void) | Buttons),
type?: ?AlertType = 'plain-text',
defaultValue?: string,
keyboardType?: string,
options?: Options,
): void {
if (Platform.OS === 'ios') {
let callbacks: Array<?any> = [];
const buttons = [];
let cancelButtonKey;
let destructiveButtonKey;
let preferredButtonKey;
if (typeof callbackOrButtons === 'function') {
callbacks = [callbackOrButtons];
} else if (Array.isArray(callbackOrButtons)) {
callbackOrButtons.forEach((btn, index) => {
callbacks[index] = btn.onPress;
if (btn.style === 'cancel') {
cancelButtonKey = String(index);
} else if (btn.style === 'destructive') {
destructiveButtonKey = String(index);
}
if (btn.isPreferred) {
preferredButtonKey = String(index);
}
if (btn.text || index < (callbackOrButtons || []).length - 1) {
const btnDef: {[number]: string} = {};
btnDef[index] = btn.text || '';
buttons.push(btnDef);
}
});
}
RCTAlertManager.alertWithArgs(
{
title: title || '',
message: message || undefined,
buttons,
type: type || undefined,
defaultValue,
cancelButtonKey,
destructiveButtonKey,
preferredButtonKey,
keyboardType,
userInterfaceStyle: options?.userInterfaceStyle || undefined,
},
(id, value) => {
const cb = callbacks[id];
cb && cb(value);
},
);
}
}
}
module.exports = Alert;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeAlertManager';
import NativeAlertManager from '../../src/private/specs/modules/NativeAlertManager';
export default NativeAlertManager;

View File

@@ -0,0 +1,27 @@
/**
* 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.
*
* @format
*/
import NativeDialogManagerAndroid from '../NativeModules/specs/NativeDialogManagerAndroid';
function emptyCallback() {}
module.exports = {
alertWithArgs: function (args, callback) {
// TODO(5998984): Polyfill it correctly with DialogManagerAndroid
if (!NativeDialogManagerAndroid) {
return;
}
NativeDialogManagerAndroid.showAlert(
args,
emptyCallback,
callback || emptyCallback,
);
},
};

View File

@@ -0,0 +1,25 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
import type {Args} from './NativeAlertManager';
import NativeAlertManager from './NativeAlertManager';
module.exports = {
alertWithArgs(
args: Args,
callback: (id: number, value: string) => void,
): void {
if (NativeAlertManager == null) {
return;
}
NativeAlertManager.alertWithArgs(args, callback);
},
};

View File

@@ -0,0 +1,18 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
import type {Args} from './NativeAlertManager';
declare module.exports: {
alertWithArgs(
args: Args,
callback: (id: number, value: string) => void,
): void,
};

View File

@@ -0,0 +1,626 @@
/**
* 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.
*
* @format
*/
import type * as React from 'react';
import {ScrollView} from '../Components/ScrollView/ScrollView';
import {View} from '../Components/View/View';
import {Image} from '../Image/Image';
import {FlatListComponent, FlatListProps} from '../Lists/FlatList';
import {
DefaultSectionT,
SectionListComponent,
SectionListProps,
} from '../Lists/SectionList';
import {ColorValue} from '../StyleSheet/StyleSheet';
import {Text} from '../Text/Text';
import {NativeSyntheticEvent} from '../Types/CoreEventTypes';
export namespace Animated {
type AnimatedValue = Value;
type AnimatedValueXY = ValueXY;
class Animated {
// Internal class, no public API.
}
class AnimatedNode {
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*
* See https://reactnative.dev/docs/animatedvalue.html#addlistener
*/
addListener(callback: (value: any) => any): string;
/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See https://reactnative.dev/docs/animatedvalue.html#removelistener
*/
removeListener(id: string): void;
/**
* Remove all registered listeners.
*
* See https://reactnative.dev/docs/animatedvalue.html#removealllisteners
*/
removeAllListeners(): void;
hasListeners(): boolean;
}
class AnimatedWithChildren extends AnimatedNode {
// Internal class, no public API.
}
type RgbaValue = {
readonly r: number;
readonly g: number;
readonly b: number;
readonly a: number;
};
type RgbaAnimatedValue = {
readonly r: AnimatedValue;
readonly g: AnimatedValue;
readonly b: AnimatedValue;
readonly a: AnimatedValue;
};
type AnimatedConfig = {
readonly useNativeDriver: boolean;
};
class AnimatedColor extends AnimatedWithChildren {
r: AnimatedValue;
g: AnimatedValue;
b: AnimatedValue;
a: AnimatedValue;
constructor(
valueIn?: RgbaValue | RgbaAnimatedValue | ColorValue | null,
config?: AnimatedConfig | null,
);
nativeColor: unknown; // Unsure what to do here
setValue: (value: RgbaValue | ColorValue) => void;
setOffset: (offset: RgbaValue) => void;
flattenOffset: () => void;
extractOffset: () => void;
addListener: (callback: (value: ColorValue) => unknown) => string;
removeListener: (id: string) => void;
removeAllListeners: () => void;
stopAnimation: (callback: (value: ColorValue) => unknown) => void;
resetAnimation: (callback: (value: ColorValue) => unknown) => void;
}
class AnimatedInterpolation<
OutputT extends number | string,
> extends AnimatedWithChildren {
interpolate(
config: InterpolationConfigType,
): AnimatedInterpolation<OutputT>;
}
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
type InterpolationConfigType = {
inputRange: number[];
outputRange: number[] | string[];
easing?: ((input: number) => number) | undefined;
extrapolate?: ExtrapolateType | undefined;
extrapolateLeft?: ExtrapolateType | undefined;
extrapolateRight?: ExtrapolateType | undefined;
};
type ValueListenerCallback = (state: {value: number}) => void;
type Animation = {
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: EndCallback | null,
previousAnimation: Animation | null,
animatedValue: AnimatedValue,
): void;
stop(): void;
};
/**
* Standard value for driving animations. One `Animated.Value` can drive
* multiple properties in a synchronized fashion, but can only be driven by one
* mechanism at a time. Using a new mechanism (e.g. starting a new animation,
* or calling `setValue`) will stop any previous ones.
*/
export class Value extends AnimatedWithChildren {
constructor(value: number, config?: AnimatedConfig | null);
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*/
setValue(value: number): void;
/**
* Sets an offset that is applied on top of whatever value is set, whether via
* `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*/
setOffset(offset: number): void;
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*/
flattenOffset(): void;
/**
* Sets the offset value to the base value, and resets the base value to zero.
* The final output of the value is unchanged.
*/
extractOffset(): void;
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*/
addListener(callback: ValueListenerCallback): string;
removeListener(id: string): void;
removeAllListeners(): void;
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*/
stopAnimation(callback?: (value: number) => void): void;
/**
* Stops any animation and resets the value to its original.
*
* See https://reactnative.dev/docs/animatedvalue#resetanimation
*/
resetAnimation(callback?: (value: number) => void): void;
/**
* Interpolates the value before updating the property, e.g. mapping 0-1 to
* 0-10.
*/
interpolate<OutputT extends number | string>(
config: InterpolationConfigType,
): AnimatedInterpolation<OutputT>;
/**
* Typically only used internally, but could be used by a custom Animation
* class.
*
* See https://reactnative.dev/docs/animatedvalue#animate
*/
animate(animation: Animation, callback?: EndCallback | null): void;
}
type ValueXYListenerCallback = (value: {x: number; y: number}) => void;
/**
* 2D Value for driving 2D animations, such as pan gestures. Almost identical
* API to normal `Animated.Value`, but multiplexed. Contains two regular
* `Animated.Value`s under the hood.
*/
export class ValueXY extends AnimatedWithChildren {
x: AnimatedValue;
y: AnimatedValue;
constructor(
valueIn?: {x: number | AnimatedValue; y: number | AnimatedValue},
config?: AnimatedConfig | null,
);
setValue(value: {x: number; y: number}): void;
setOffset(offset: {x: number; y: number}): void;
flattenOffset(): void;
extractOffset(): void;
resetAnimation(callback?: (value: {x: number; y: number}) => void): void;
stopAnimation(callback?: (value: {x: number; y: number}) => void): void;
addListener(callback: ValueXYListenerCallback): string;
removeListener(id: string): void;
/**
* Converts `{x, y}` into `{left, top}` for use in style, e.g.
*
*```javascript
* style={this.state.anim.getLayout()}
*```
*/
getLayout(): {[key: string]: AnimatedValue};
/**
* Converts `{x, y}` into a useable translation transform, e.g.
*
*```javascript
* style={{
* transform: this.state.anim.getTranslateTransform()
* }}
*```
*/
getTranslateTransform(): [
{translateX: AnimatedValue},
{translateY: AnimatedValue},
];
}
type EndResult = {finished: boolean};
type EndCallback = (result: EndResult) => void;
export interface CompositeAnimation {
/**
* Animations are started by calling start() on your animation.
* start() takes a completion callback that will be called when the
* animation is done or when the animation is done because stop() was
* called on it before it could finish.
*
* @param callback - Optional function that will be called
* after the animation finished running normally or when the animation
* is done because stop() was called on it before it could finish
*
* @example
* Animated.timing({}).start(({ finished }) => {
* // completion callback
* });
*/
start: (callback?: EndCallback) => void;
/**
* Stops any running animation.
*/
stop: () => void;
/**
* Stops any running animation and resets the value to its original.
*/
reset: () => void;
}
interface AnimationConfig {
isInteraction?: boolean | undefined;
useNativeDriver: boolean;
}
/**
* Animates a value from an initial velocity to zero based on a decay
* coefficient.
*/
export function decay(
value: AnimatedValue | AnimatedValueXY,
config: DecayAnimationConfig,
): CompositeAnimation;
interface DecayAnimationConfig extends AnimationConfig {
velocity: number | {x: number; y: number};
deceleration?: number | undefined;
}
/**
* Animates a value along a timed easing curve. The `Easing` module has tons
* of pre-defined curves, or you can use your own function.
*/
export const timing: (
value: AnimatedValue | AnimatedValueXY,
config: TimingAnimationConfig,
) => CompositeAnimation;
interface TimingAnimationConfig extends AnimationConfig {
toValue:
| number
| AnimatedValue
| {x: number; y: number}
| AnimatedValueXY
| AnimatedInterpolation<number>;
easing?: ((value: number) => number) | undefined;
duration?: number | undefined;
delay?: number | undefined;
}
interface SpringAnimationConfig extends AnimationConfig {
toValue:
| number
| AnimatedValue
| {x: number; y: number}
| AnimatedValueXY
| RgbaValue
| AnimatedColor
| AnimatedInterpolation<number>;
overshootClamping?: boolean | undefined;
restDisplacementThreshold?: number | undefined;
restSpeedThreshold?: number | undefined;
velocity?: number | {x: number; y: number} | undefined;
bounciness?: number | undefined;
speed?: number | undefined;
tension?: number | undefined;
friction?: number | undefined;
stiffness?: number | undefined;
mass?: number | undefined;
damping?: number | undefined;
delay?: number | undefined;
}
interface LoopAnimationConfig {
iterations?: number | undefined; // default -1 for infinite
/**
* Defaults to `true`
*/
resetBeforeIteration?: boolean | undefined;
}
/**
* Creates a new Animated value composed from two Animated values added
* together.
*/
export function add<OutputT extends number | string>(
a: Animated,
b: Animated,
): AnimatedAddition<OutputT>;
class AnimatedAddition<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Creates a new Animated value composed by subtracting the second Animated
* value from the first Animated value.
*/
export function subtract<OutputT extends number | string>(
a: Animated,
b: Animated,
): AnimatedSubtraction<OutputT>;
class AnimatedSubtraction<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Creates a new Animated value composed by dividing the first Animated
* value by the second Animated value.
*/
export function divide<OutputT extends number | string>(
a: Animated,
b: Animated,
): AnimatedDivision<OutputT>;
class AnimatedDivision<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Creates a new Animated value composed from two Animated values multiplied
* together.
*/
export function multiply<OutputT extends number | string>(
a: Animated,
b: Animated,
): AnimatedMultiplication<OutputT>;
class AnimatedMultiplication<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Creates a new Animated value that is the (non-negative) modulo of the
* provided Animated value
*/
export function modulo<OutputT extends number | string>(
a: Animated,
modulus: number,
): AnimatedModulo<OutputT>;
class AnimatedModulo<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Create a new Animated value that is limited between 2 values. It uses the
* difference between the last value so even if the value is far from the bounds
* it will start changing when the value starts getting closer again.
* (`value = clamp(value + diff, min, max)`).
*
* This is useful with scroll events, for example, to show the navbar when
* scrolling up and to hide it when scrolling down.
*/
export function diffClamp<OutputT extends number | string>(
a: Animated,
min: number,
max: number,
): AnimatedDiffClamp<OutputT>;
class AnimatedDiffClamp<
OutputT extends number | string,
> extends AnimatedInterpolation<OutputT> {}
/**
* Starts an animation after the given delay.
*/
export function delay(time: number): CompositeAnimation;
/**
* Starts an array of animations in order, waiting for each to complete
* before starting the next. If the current running animation is stopped, no
* following animations will be started.
*/
export function sequence(
animations: Array<CompositeAnimation>,
): CompositeAnimation;
/**
* Array of animations may run in parallel (overlap), but are started in
* sequence with successive delays. Nice for doing trailing effects.
*/
export function stagger(
time: number,
animations: Array<CompositeAnimation>,
): CompositeAnimation;
/**
* Loops a given animation continuously, so that each time it reaches the end,
* it resets and begins again from the start. Can specify number of times to
* loop using the key 'iterations' in the config. Will loop without blocking
* the UI thread if the child animation is set to 'useNativeDriver'.
*/
export function loop(
animation: CompositeAnimation,
config?: LoopAnimationConfig,
): CompositeAnimation;
/**
* Spring animation based on Rebound and Origami. Tracks velocity state to
* create fluid motions as the `toValue` updates, and can be chained together.
*/
export function spring(
value: AnimatedValue | AnimatedValueXY,
config: SpringAnimationConfig,
): CompositeAnimation;
type ParallelConfig = {
stopTogether?: boolean | undefined; // If one is stopped, stop all. default: true
};
/**
* Starts an array of animations all at the same time. By default, if one
* of the animations is stopped, they will all be stopped. You can override
* this with the `stopTogether` flag.
*/
export function parallel(
animations: Array<CompositeAnimation>,
config?: ParallelConfig,
): CompositeAnimation;
type Mapping = {[key: string]: Mapping} | AnimatedValue;
interface EventConfig<T> {
listener?: ((event: NativeSyntheticEvent<T>) => void) | undefined;
useNativeDriver: boolean;
}
/**
* Takes an array of mappings and extracts values from each arg accordingly,
* then calls `setValue` on the mapped outputs. e.g.
*
*```javascript
* onScroll={Animated.event(
* [{nativeEvent: {contentOffset: {x: this._scrollX}}}]
* {listener}, // Optional async listener
* )
* ...
* onPanResponderMove: Animated.event([
* null, // raw event arg ignored
* {dx: this._panX}, // gestureState arg
* ]),
*```
*/
export function event<T>(
argMapping: Array<Mapping | null>,
config?: EventConfig<T>,
): (...args: any[]) => void;
export type ComponentProps<T> = T extends
| React.ComponentType<infer P>
| React.Component<infer P>
? P
: never;
export type LegacyRef<C> = {getNode(): C};
type Nullable = undefined | null;
type Primitive = string | number | boolean | symbol;
type Builtin = Function | Date | Error | RegExp;
interface WithAnimatedArray<P> extends Array<WithAnimatedValue<P>> {}
type WithAnimatedObject<T> = {
[K in keyof T]: WithAnimatedValue<T[K]>;
};
export type WithAnimatedValue<T> = T extends Builtin | Nullable
? T
: T extends Primitive
? T | Value | AnimatedInterpolation<number | string> // add `Value` and `AnimatedInterpolation` but also preserve original T
: T extends Array<infer P>
? WithAnimatedArray<P>
: T extends {}
? WithAnimatedObject<T>
: T; // in case it's something we don't yet know about (for .e.g bigint)
type NonAnimatedProps = 'key' | 'ref';
type TAugmentRef<T> = T extends React.Ref<infer R>
? unknown extends R
? never
: React.Ref<R | LegacyRef<R>>
: never;
export type AnimatedProps<T> = {
[key in keyof T]: key extends NonAnimatedProps
? key extends 'ref'
? TAugmentRef<T[key]>
: T[key]
: WithAnimatedValue<T[key]>;
};
export interface AnimatedComponent<T extends React.ComponentType<any>>
extends React.FC<AnimatedProps<React.ComponentPropsWithRef<T>>> {}
export type AnimatedComponentOptions = {
collapsable?: boolean | undefined;
};
/**
* Make any React component Animatable. Used to create `Animated.View`, etc.
*/
export function createAnimatedComponent<T extends React.ComponentType<any>>(
component: T,
options?: AnimatedComponentOptions,
): AnimatedComponent<T>;
/**
* Animated variants of the basic native views. Accepts Animated.Value for
* props and style.
*/
export const View: AnimatedComponent<typeof _View>;
export const Image: AnimatedComponent<typeof _Image>;
export const Text: AnimatedComponent<typeof _Text>;
export const ScrollView: AnimatedComponent<typeof _ScrollView>;
/**
* FlatList and SectionList infer generic Type defined under their `data` and `section` props.
*/
export class FlatList<ItemT = any> extends FlatListComponent<
ItemT,
AnimatedProps<FlatListProps<ItemT>>
> {}
export class SectionList<
ItemT = any,
SectionT = DefaultSectionT,
> extends SectionListComponent<
AnimatedProps<SectionListProps<ItemT, SectionT>>
> {}
}
// We need to alias these views so we can reference them in the Animated
// namespace where their names are shadowed.
declare const _View: typeof View;
declare const _Image: typeof Image;
declare const _Text: typeof Text;
declare const _ScrollView: typeof ScrollView;

View File

@@ -0,0 +1,48 @@
/**
* 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
* @format
*/
export type {CompositeAnimation, Numeric} from './AnimatedImplementation';
import typeof AnimatedFlatList from './components/AnimatedFlatList';
import typeof AnimatedImage from './components/AnimatedImage';
import typeof AnimatedScrollView from './components/AnimatedScrollView';
import typeof AnimatedSectionList from './components/AnimatedSectionList';
import typeof AnimatedText from './components/AnimatedText';
import typeof AnimatedView from './components/AnimatedView';
import Platform from '../Utilities/Platform';
import AnimatedImplementation from './AnimatedImplementation';
import AnimatedMock from './AnimatedMock';
const Animated: typeof AnimatedImplementation = Platform.isDisableAnimations
? AnimatedMock
: AnimatedImplementation;
export default {
get FlatList(): AnimatedFlatList {
return require('./components/AnimatedFlatList').default;
},
get Image(): AnimatedImage {
return require('./components/AnimatedImage').default;
},
get ScrollView(): AnimatedScrollView {
return require('./components/AnimatedScrollView').default;
},
get SectionList(): AnimatedSectionList {
return require('./components/AnimatedSectionList').default;
},
get Text(): AnimatedText {
return require('./components/AnimatedText').default;
},
get View(): AnimatedView {
return require('./components/AnimatedView').default;
},
...Animated,
};

View File

@@ -0,0 +1,256 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from './AnimatedPlatformConfig';
import {findNodeHandle} from '../ReactNative/RendererProxy';
import NativeAnimatedHelper from './NativeAnimatedHelper';
import AnimatedValue from './nodes/AnimatedValue';
import AnimatedValueXY from './nodes/AnimatedValueXY';
import invariant from 'invariant';
export type Mapping =
| {[key: string]: Mapping, ...}
| AnimatedValue
| AnimatedValueXY;
export type EventConfig = {
listener?: ?Function,
useNativeDriver: boolean,
platformConfig?: PlatformConfig,
};
export function attachNativeEvent(
viewRef: any,
eventName: string,
argMapping: $ReadOnlyArray<?Mapping>,
platformConfig: ?PlatformConfig,
): {detach: () => void} {
// Find animated values in `argMapping` and create an array representing their
// key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x'].
const eventMappings = [];
const traverse = (value: mixed, path: Array<string>) => {
if (value instanceof AnimatedValue) {
value.__makeNative(platformConfig);
eventMappings.push({
nativeEventPath: path,
animatedValueTag: value.__getNativeTag(),
});
} else if (value instanceof AnimatedValueXY) {
traverse(value.x, path.concat('x'));
traverse(value.y, path.concat('y'));
} else if (typeof value === 'object') {
for (const key in value) {
traverse(value[key], path.concat(key));
}
}
};
invariant(
argMapping[0] && argMapping[0].nativeEvent,
'Native driven events only support animated values contained inside `nativeEvent`.',
);
// Assume that the event containing `nativeEvent` is always the first argument.
traverse(argMapping[0].nativeEvent, []);
const viewTag = findNodeHandle(viewRef);
if (viewTag != null) {
eventMappings.forEach(mapping => {
NativeAnimatedHelper.API.addAnimatedEventToView(
viewTag,
eventName,
mapping,
);
});
}
return {
detach() {
if (viewTag != null) {
eventMappings.forEach(mapping => {
NativeAnimatedHelper.API.removeAnimatedEventFromView(
viewTag,
eventName,
// $FlowFixMe[incompatible-call]
mapping.animatedValueTag,
);
});
}
},
};
}
function validateMapping(argMapping: $ReadOnlyArray<?Mapping>, args: any) {
const validate = (recMapping: ?Mapping, recEvt: any, key: string) => {
if (recMapping instanceof AnimatedValue) {
invariant(
typeof recEvt === 'number',
'Bad mapping of event key ' +
key +
', should be number but got ' +
typeof recEvt,
);
return;
}
if (recMapping instanceof AnimatedValueXY) {
invariant(
typeof recEvt.x === 'number' && typeof recEvt.y === 'number',
'Bad mapping of event key ' + key + ', should be XY but got ' + recEvt,
);
return;
}
if (typeof recEvt === 'number') {
invariant(
recMapping instanceof AnimatedValue,
'Bad mapping of type ' +
typeof recMapping +
' for key ' +
key +
', event value must map to AnimatedValue',
);
return;
}
invariant(
typeof recMapping === 'object',
'Bad mapping of type ' + typeof recMapping + ' for key ' + key,
);
invariant(
typeof recEvt === 'object',
'Bad event of type ' + typeof recEvt + ' for key ' + key,
);
for (const mappingKey in recMapping) {
validate(recMapping[mappingKey], recEvt[mappingKey], mappingKey);
}
};
invariant(
args.length >= argMapping.length,
'Event has less arguments than mapping',
);
argMapping.forEach((mapping, idx) => {
validate(mapping, args[idx], 'arg' + idx);
});
}
export class AnimatedEvent {
_argMapping: $ReadOnlyArray<?Mapping>;
_listeners: Array<Function> = [];
_attachedEvent: ?{detach: () => void, ...};
__isNative: boolean;
__platformConfig: ?PlatformConfig;
constructor(argMapping: $ReadOnlyArray<?Mapping>, config: EventConfig) {
this._argMapping = argMapping;
if (config == null) {
console.warn('Animated.event now requires a second argument for options');
config = {useNativeDriver: false};
}
if (config.listener) {
this.__addListener(config.listener);
}
this._attachedEvent = null;
this.__isNative = NativeAnimatedHelper.shouldUseNativeDriver(config);
this.__platformConfig = config.platformConfig;
}
__addListener(callback: Function): void {
this._listeners.push(callback);
}
__removeListener(callback: Function): void {
this._listeners = this._listeners.filter(listener => listener !== callback);
}
__attach(viewRef: any, eventName: string): void {
invariant(
this.__isNative,
'Only native driven events need to be attached.',
);
this._attachedEvent = attachNativeEvent(
viewRef,
eventName,
this._argMapping,
this.__platformConfig,
);
}
__detach(viewTag: any, eventName: string): void {
invariant(
this.__isNative,
'Only native driven events need to be detached.',
);
this._attachedEvent && this._attachedEvent.detach();
}
__getHandler(): any | ((...args: any) => void) {
if (this.__isNative) {
if (__DEV__) {
let validatedMapping = false;
return (...args: any) => {
if (!validatedMapping) {
validateMapping(this._argMapping, args);
validatedMapping = true;
}
this._callListeners(...args);
};
} else {
return this._callListeners;
}
}
let validatedMapping = false;
return (...args: any) => {
if (__DEV__ && !validatedMapping) {
validateMapping(this._argMapping, args);
validatedMapping = true;
}
const traverse = (
recMapping: ?(Mapping | AnimatedValue),
recEvt: any,
) => {
if (recMapping instanceof AnimatedValue) {
if (typeof recEvt === 'number') {
recMapping.setValue(recEvt);
}
} else if (recMapping instanceof AnimatedValueXY) {
if (typeof recEvt === 'object') {
traverse(recMapping.x, recEvt.x);
traverse(recMapping.y, recEvt.y);
}
} else if (typeof recMapping === 'object') {
for (const mappingKey in recMapping) {
/* $FlowFixMe[prop-missing] (>=0.120.0) This comment suppresses an
* error found when Flow v0.120 was deployed. To see the error,
* delete this comment and run Flow. */
traverse(recMapping[mappingKey], recEvt[mappingKey]);
}
}
};
this._argMapping.forEach((mapping, idx) => {
traverse(mapping, args[idx]);
});
this._callListeners(...args);
};
}
_callListeners = (...args: any) => {
this._listeners.forEach(listener => listener(...args));
};
}

View File

@@ -0,0 +1,752 @@
/**
* 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
* @format
*/
'use strict';
import type {EventConfig, Mapping} from './AnimatedEvent';
import type {
AnimationConfig,
EndCallback,
EndResult,
} from './animations/Animation';
import type {DecayAnimationConfig} from './animations/DecayAnimation';
import type {SpringAnimationConfig} from './animations/SpringAnimation';
import type {TimingAnimationConfig} from './animations/TimingAnimation';
import {AnimatedEvent, attachNativeEvent} from './AnimatedEvent';
import DecayAnimation from './animations/DecayAnimation';
import SpringAnimation from './animations/SpringAnimation';
import TimingAnimation from './animations/TimingAnimation';
import createAnimatedComponent from './createAnimatedComponent';
import AnimatedAddition from './nodes/AnimatedAddition';
import AnimatedColor from './nodes/AnimatedColor';
import AnimatedDiffClamp from './nodes/AnimatedDiffClamp';
import AnimatedDivision from './nodes/AnimatedDivision';
import AnimatedInterpolation from './nodes/AnimatedInterpolation';
import AnimatedModulo from './nodes/AnimatedModulo';
import AnimatedMultiplication from './nodes/AnimatedMultiplication';
import AnimatedNode from './nodes/AnimatedNode';
import AnimatedSubtraction from './nodes/AnimatedSubtraction';
import AnimatedTracking from './nodes/AnimatedTracking';
import AnimatedValue from './nodes/AnimatedValue';
import AnimatedValueXY from './nodes/AnimatedValueXY';
export type CompositeAnimation = {
start: (callback?: ?EndCallback) => void,
stop: () => void,
reset: () => void,
_startNativeLoop: (iterations?: number) => void,
_isUsingNativeDriver: () => boolean,
...
};
const add = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedAddition {
return new AnimatedAddition(a, b);
};
const subtract = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedSubtraction {
return new AnimatedSubtraction(a, b);
};
const divide = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedDivision {
return new AnimatedDivision(a, b);
};
const multiply = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedMultiplication {
return new AnimatedMultiplication(a, b);
};
const modulo = function (a: AnimatedNode, modulus: number): AnimatedModulo {
return new AnimatedModulo(a, modulus);
};
const diffClamp = function (
a: AnimatedNode,
min: number,
max: number,
): AnimatedDiffClamp {
return new AnimatedDiffClamp(a, min, max);
};
const _combineCallbacks = function (
callback: ?EndCallback,
config: $ReadOnly<{...AnimationConfig, ...}>,
) {
if (callback && config.onComplete) {
return (...args: Array<EndResult>) => {
config.onComplete && config.onComplete(...args);
callback && callback(...args);
};
} else {
return callback || config.onComplete;
}
};
const maybeVectorAnim = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: Object,
anim: (value: AnimatedValue, config: Object) => CompositeAnimation,
): ?CompositeAnimation {
if (value instanceof AnimatedValueXY) {
const configX = {...config};
const configY = {...config};
for (const key in config) {
const {x, y} = config[key];
if (x !== undefined && y !== undefined) {
configX[key] = x;
configY[key] = y;
}
}
const aX = anim((value: AnimatedValueXY).x, configX);
const aY = anim((value: AnimatedValueXY).y, configY);
// We use `stopTogether: false` here because otherwise tracking will break
// because the second animation will get stopped before it can update.
return parallel([aX, aY], {stopTogether: false});
} else if (value instanceof AnimatedColor) {
const configR = {...config};
const configG = {...config};
const configB = {...config};
const configA = {...config};
for (const key in config) {
const {r, g, b, a} = config[key];
if (
r !== undefined &&
g !== undefined &&
b !== undefined &&
a !== undefined
) {
configR[key] = r;
configG[key] = g;
configB[key] = b;
configA[key] = a;
}
}
const aR = anim((value: AnimatedColor).r, configR);
const aG = anim((value: AnimatedColor).g, configG);
const aB = anim((value: AnimatedColor).b, configB);
const aA = anim((value: AnimatedColor).a, configA);
// We use `stopTogether: false` here because otherwise tracking will break
// because the second animation will get stopped before it can update.
return parallel([aR, aG, aB, aA], {stopTogether: false});
}
return null;
};
const spring = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: SpringAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: SpringAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
if (configuration.toValue instanceof AnimatedNode) {
singleValue.track(
new AnimatedTracking(
singleValue,
configuration.toValue,
SpringAnimation,
singleConfig,
callback,
),
);
} else {
singleValue.animate(new SpringAnimation(singleConfig), callback);
}
};
return (
maybeVectorAnim(value, config, spring) || {
start: function (callback?: ?EndCallback): void {
start(value, config, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return config.useNativeDriver || false;
},
}
);
};
const timing = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: TimingAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: TimingAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
if (configuration.toValue instanceof AnimatedNode) {
singleValue.track(
new AnimatedTracking(
singleValue,
configuration.toValue,
TimingAnimation,
singleConfig,
callback,
),
);
} else {
singleValue.animate(new TimingAnimation(singleConfig), callback);
}
};
return (
maybeVectorAnim(value, config, timing) || {
start: function (callback?: ?EndCallback): void {
start(value, config, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return config.useNativeDriver || false;
},
}
);
};
const decay = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: DecayAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: DecayAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
singleValue.animate(new DecayAnimation(singleConfig), callback);
};
return (
maybeVectorAnim(value, config, decay) || {
start: function (callback?: ?EndCallback): void {
start(value, config, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return config.useNativeDriver || false;
},
}
);
};
const sequence = function (
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function (callback?: ?EndCallback) {
const onComplete = function (result: EndResult) {
if (!result.finished) {
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function () {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function () {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function () {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function (): boolean {
return false;
},
};
};
type ParallelConfig = {
// If one is stopped, stop all. default: true
stopTogether?: boolean,
...
};
const parallel = function (
animations: Array<CompositeAnimation>,
config?: ?ParallelConfig,
): CompositeAnimation {
let doneCount = 0;
// Make sure we only call stop() at most once for each animation
const hasEnded: {[number]: boolean} = {};
const stopTogether = !(config && config.stopTogether === false);
const result = {
start: function (callback?: ?EndCallback) {
if (doneCount === animations.length) {
callback && callback({finished: true});
return;
}
animations.forEach((animation, idx) => {
const cb = function (endResult: EndResult) {
hasEnded[idx] = true;
doneCount++;
if (doneCount === animations.length) {
doneCount = 0;
callback && callback(endResult);
return;
}
if (!endResult.finished && stopTogether) {
result.stop();
}
};
if (!animation) {
cb({finished: true});
} else {
animation.start(cb);
}
});
},
stop: function (): void {
animations.forEach((animation, idx) => {
!hasEnded[idx] && animation.stop();
hasEnded[idx] = true;
});
},
reset: function (): void {
animations.forEach((animation, idx) => {
animation.reset();
hasEnded[idx] = false;
doneCount = 0;
});
},
_startNativeLoop: function (): empty {
throw new Error(
'Loops run using the native driver cannot contain Animated.parallel animations',
);
},
_isUsingNativeDriver: function (): boolean {
return false;
},
};
return result;
};
const delay = function (time: number): CompositeAnimation {
// Would be nice to make a specialized implementation
return timing(new AnimatedValue(0), {
toValue: 0,
delay: time,
duration: 0,
useNativeDriver: false,
});
};
const stagger = function (
time: number,
animations: Array<CompositeAnimation>,
): CompositeAnimation {
return parallel(
animations.map((animation, i) => {
return sequence([delay(time * i), animation]);
}),
);
};
type LoopAnimationConfig = {
iterations: number,
resetBeforeIteration?: boolean,
...
};
const loop = function (
animation: CompositeAnimation,
// $FlowFixMe[prop-missing]
{iterations = -1, resetBeforeIteration = true}: LoopAnimationConfig = {},
): CompositeAnimation {
let isFinished = false;
let iterationsSoFar = 0;
return {
start: function (callback?: ?EndCallback) {
const restart = function (result: EndResult = {finished: true}): void {
if (
isFinished ||
iterationsSoFar === iterations ||
result.finished === false
) {
callback && callback(result);
} else {
iterationsSoFar++;
resetBeforeIteration && animation.reset();
animation.start(restart);
}
};
if (!animation || iterations === 0) {
callback && callback({finished: true});
} else {
if (animation._isUsingNativeDriver()) {
animation._startNativeLoop(iterations);
} else {
restart(); // Start looping recursively on the js thread
}
}
},
stop: function (): void {
isFinished = true;
animation.stop();
},
reset: function (): void {
iterationsSoFar = 0;
isFinished = false;
animation.reset();
},
_startNativeLoop: function () {
throw new Error(
'Loops run using the native driver cannot contain Animated.loop animations',
);
},
_isUsingNativeDriver: function (): boolean {
return animation._isUsingNativeDriver();
},
};
};
function forkEvent(
event: ?AnimatedEvent | ?Function,
listener: Function,
): AnimatedEvent | Function {
if (!event) {
return listener;
} else if (event instanceof AnimatedEvent) {
event.__addListener(listener);
return event;
} else {
return (...args) => {
typeof event === 'function' && event(...args);
listener(...args);
};
}
}
function unforkEvent(
event: ?AnimatedEvent | ?Function,
listener: Function,
): void {
if (event && event instanceof AnimatedEvent) {
event.__removeListener(listener);
}
}
const event = function (
argMapping: $ReadOnlyArray<?Mapping>,
config: EventConfig,
): any {
const animatedEvent = new AnimatedEvent(argMapping, config);
if (animatedEvent.__isNative) {
return animatedEvent;
} else {
return animatedEvent.__getHandler();
}
};
// All types of animated nodes that represent scalar numbers and can be interpolated (etc)
type AnimatedNumeric =
| AnimatedAddition
| AnimatedDiffClamp
| AnimatedDivision
| AnimatedInterpolation<number>
| AnimatedModulo
| AnimatedMultiplication
| AnimatedSubtraction
| AnimatedValue;
export type {AnimatedNumeric as Numeric};
/**
* The `Animated` library is designed to make animations fluid, powerful, and
* easy to build and maintain. `Animated` focuses on declarative relationships
* between inputs and outputs, with configurable transforms in between, and
* simple `start`/`stop` methods to control time-based animation execution.
* If additional transforms are added, be sure to include them in
* AnimatedMock.js as well.
*
* See https://reactnative.dev/docs/animated
*/
export default {
/**
* Standard value class for driving animations. Typically initialized with
* `new Animated.Value(0);`
*
* See https://reactnative.dev/docs/animated#value
*/
Value: AnimatedValue,
/**
* 2D value class for driving 2D animations, such as pan gestures.
*
* See https://reactnative.dev/docs/animatedvaluexy
*/
ValueXY: AnimatedValueXY,
/**
* Value class for driving color animations.
*/
Color: AnimatedColor,
/**
* Exported to use the Interpolation type in flow.
*
* See https://reactnative.dev/docs/animated#interpolation
*/
Interpolation: AnimatedInterpolation,
/**
* Exported for ease of type checking. All animated values derive from this
* class.
*
* See https://reactnative.dev/docs/animated#node
*/
Node: AnimatedNode,
/**
* Animates a value from an initial velocity to zero based on a decay
* coefficient.
*
* See https://reactnative.dev/docs/animated#decay
*/
decay,
/**
* Animates a value along a timed easing curve. The Easing module has tons of
* predefined curves, or you can use your own function.
*
* See https://reactnative.dev/docs/animated#timing
*/
timing,
/**
* Animates a value according to an analytical spring model based on
* damped harmonic oscillation.
*
* See https://reactnative.dev/docs/animated#spring
*/
spring,
/**
* Creates a new Animated value composed from two Animated values added
* together.
*
* See https://reactnative.dev/docs/animated#add
*/
add,
/**
* Creates a new Animated value composed by subtracting the second Animated
* value from the first Animated value.
*
* See https://reactnative.dev/docs/animated#subtract
*/
subtract,
/**
* Creates a new Animated value composed by dividing the first Animated value
* by the second Animated value.
*
* See https://reactnative.dev/docs/animated#divide
*/
divide,
/**
* Creates a new Animated value composed from two Animated values multiplied
* together.
*
* See https://reactnative.dev/docs/animated#multiply
*/
multiply,
/**
* Creates a new Animated value that is the (non-negative) modulo of the
* provided Animated value.
*
* See https://reactnative.dev/docs/animated#modulo
*/
modulo,
/**
* Create a new Animated value that is limited between 2 values. It uses the
* difference between the last value so even if the value is far from the
* bounds it will start changing when the value starts getting closer again.
*
* See https://reactnative.dev/docs/animated#diffclamp
*/
diffClamp,
/**
* Starts an animation after the given delay.
*
* See https://reactnative.dev/docs/animated#delay
*/
delay,
/**
* Starts an array of animations in order, waiting for each to complete
* before starting the next. If the current running animation is stopped, no
* following animations will be started.
*
* See https://reactnative.dev/docs/animated#sequence
*/
sequence,
/**
* Starts an array of animations all at the same time. By default, if one
* of the animations is stopped, they will all be stopped. You can override
* this with the `stopTogether` flag.
*
* See https://reactnative.dev/docs/animated#parallel
*/
parallel,
/**
* Array of animations may run in parallel (overlap), but are started in
* sequence with successive delays. Nice for doing trailing effects.
*
* See https://reactnative.dev/docs/animated#stagger
*/
stagger,
/**
* Loops a given animation continuously, so that each time it reaches the
* end, it resets and begins again from the start.
*
* See https://reactnative.dev/docs/animated#loop
*/
loop,
/**
* Takes an array of mappings and extracts values from each arg accordingly,
* then calls `setValue` on the mapped outputs.
*
* See https://reactnative.dev/docs/animated#event
*/
event,
/**
* Make any React component Animatable. Used to create `Animated.View`, etc.
*
* See https://reactnative.dev/docs/animated#createanimatedcomponent
*/
createAnimatedComponent,
/**
* Imperative API to attach an animated value to an event on a view. Prefer
* using `Animated.event` with `useNativeDrive: true` if possible.
*
* See https://reactnative.dev/docs/animated#attachnativeevent
*/
attachNativeEvent,
/**
* Advanced imperative API for snooping on animated events that are passed in
* through props. Use values directly where possible.
*
* See https://reactnative.dev/docs/animated#forkevent
*/
forkEvent,
unforkEvent,
/**
* Expose Event class, so it can be used as a type for type checkers.
*/
Event: AnimatedEvent,
};

View File

@@ -0,0 +1,195 @@
/**
* 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
* @format
*/
'use strict';
import type {Numeric as AnimatedNumeric} from './AnimatedImplementation';
import type {EndResult} from './animations/Animation';
import type {EndCallback} from './animations/Animation';
import type {DecayAnimationConfig} from './animations/DecayAnimation';
import type {SpringAnimationConfig} from './animations/SpringAnimation';
import type {TimingAnimationConfig} from './animations/TimingAnimation';
import {AnimatedEvent, attachNativeEvent} from './AnimatedEvent';
import AnimatedImplementation from './AnimatedImplementation';
import createAnimatedComponent from './createAnimatedComponent';
import AnimatedColor from './nodes/AnimatedColor';
import AnimatedInterpolation from './nodes/AnimatedInterpolation';
import AnimatedNode from './nodes/AnimatedNode';
import AnimatedValue from './nodes/AnimatedValue';
import AnimatedValueXY from './nodes/AnimatedValueXY';
/**
* Animations are a source of flakiness in snapshot testing. This mock replaces
* animation functions from AnimatedImplementation with empty animations for
* predictability in tests. When possible the animation will run immediately
* to the final state.
*/
// Prevent any callback invocation from recursively triggering another
// callback, which may trigger another animation
let inAnimationCallback = false;
function mockAnimationStart(
start: (callback?: ?EndCallback) => void,
): (callback?: ?EndCallback) => void {
return callback => {
const guardedCallback =
callback == null
? callback
: (...args: Array<EndResult>) => {
if (inAnimationCallback) {
console.warn(
'Ignoring recursive animation callback when running mock animations',
);
return;
}
inAnimationCallback = true;
try {
callback(...args);
} finally {
inAnimationCallback = false;
}
};
start(guardedCallback);
};
}
export type CompositeAnimation = {
start: (callback?: ?EndCallback) => void,
stop: () => void,
reset: () => void,
_startNativeLoop: (iterations?: number) => void,
_isUsingNativeDriver: () => boolean,
...
};
const emptyAnimation = {
start: () => {},
stop: () => {},
reset: () => {},
_startNativeLoop: () => {},
_isUsingNativeDriver: () => {
return false;
},
};
const mockCompositeAnimation = (
animations: Array<CompositeAnimation>,
): CompositeAnimation => ({
...emptyAnimation,
start: mockAnimationStart((callback?: ?EndCallback): void => {
animations.forEach(animation => animation.start());
callback?.({finished: true});
}),
});
const spring = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: SpringAnimationConfig,
): CompositeAnimation {
const anyValue: any = value;
return {
...emptyAnimation,
start: mockAnimationStart((callback?: ?EndCallback): void => {
anyValue.setValue(config.toValue);
callback?.({finished: true});
}),
};
};
const timing = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: TimingAnimationConfig,
): CompositeAnimation {
const anyValue: any = value;
return {
...emptyAnimation,
start: mockAnimationStart((callback?: ?EndCallback): void => {
anyValue.setValue(config.toValue);
callback?.({finished: true});
}),
};
};
const decay = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: DecayAnimationConfig,
): CompositeAnimation {
return emptyAnimation;
};
const sequence = function (
animations: Array<CompositeAnimation>,
): CompositeAnimation {
return mockCompositeAnimation(animations);
};
type ParallelConfig = {stopTogether?: boolean, ...};
const parallel = function (
animations: Array<CompositeAnimation>,
config?: ?ParallelConfig,
): CompositeAnimation {
return mockCompositeAnimation(animations);
};
const delay = function (time: number): CompositeAnimation {
return emptyAnimation;
};
const stagger = function (
time: number,
animations: Array<CompositeAnimation>,
): CompositeAnimation {
return mockCompositeAnimation(animations);
};
type LoopAnimationConfig = {
iterations: number,
resetBeforeIteration?: boolean,
...
};
const loop = function (
animation: CompositeAnimation,
// $FlowFixMe[prop-missing]
{iterations = -1}: LoopAnimationConfig = {},
): CompositeAnimation {
return emptyAnimation;
};
export type {AnimatedNumeric as Numeric};
export default {
Value: AnimatedValue,
ValueXY: AnimatedValueXY,
Color: AnimatedColor,
Interpolation: AnimatedInterpolation,
Node: AnimatedNode,
decay,
timing,
spring,
add: AnimatedImplementation.add,
subtract: AnimatedImplementation.subtract,
divide: AnimatedImplementation.divide,
multiply: AnimatedImplementation.multiply,
modulo: AnimatedImplementation.modulo,
diffClamp: AnimatedImplementation.diffClamp,
delay,
sequence,
parallel,
stagger,
loop,
event: AnimatedImplementation.event,
createAnimatedComponent,
attachNativeEvent,
forkEvent: AnimatedImplementation.forkEvent,
unforkEvent: AnimatedImplementation.unforkEvent,
Event: AnimatedEvent,
};

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
'use strict';
export type PlatformConfig = {};

View File

@@ -0,0 +1,26 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
'use strict';
import AnimatedImplementation from './AnimatedImplementation';
export default {
...AnimatedImplementation,
/* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive
* types. Plain intrinsic components can't be typed like this */
div: (AnimatedImplementation.createAnimatedComponent('div'): $FlowFixMe),
/* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive
* types. Plain intrinsic components can't be typed like this */
span: (AnimatedImplementation.createAnimatedComponent('span'): $FlowFixMe),
/* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive
* types. Plain intrinsic components can't be typed like this */
img: (AnimatedImplementation.createAnimatedComponent('img'): $FlowFixMe),
};

View File

@@ -0,0 +1,37 @@
/**
* 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.
*
* @format
*/
/**
* This class implements common easing functions. The math is pretty obscure,
* but this cool website has nice visual illustrations of what they represent:
* http://xaedes.de/dev/transitions/
*/
export type EasingFunction = (value: number) => number;
export interface EasingStatic {
step0: EasingFunction;
step1: EasingFunction;
linear: EasingFunction;
ease: EasingFunction;
quad: EasingFunction;
cubic: EasingFunction;
poly(n: number): EasingFunction;
sin: EasingFunction;
circle: EasingFunction;
exp: EasingFunction;
elastic(bounciness: number): EasingFunction;
back(s: number): EasingFunction;
bounce: EasingFunction;
bezier(x1: number, y1: number, x2: number, y2: number): EasingFunction;
in(easing: EasingFunction): EasingFunction;
out(easing: EasingFunction): EasingFunction;
inOut(easing: EasingFunction): EasingFunction;
}
export type Easing = EasingStatic;
export const Easing: EasingStatic;

View File

@@ -0,0 +1,250 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
let ease;
/**
* The `Easing` module implements common easing functions. This module is used
* by [Animate.timing()](docs/animate.html#timing) to convey physically
* believable motion in animations.
*
* You can find a visualization of some common easing functions at
* http://easings.net/
*
* ### Predefined animations
*
* The `Easing` module provides several predefined animations through the
* following methods:
*
* - [`back`](docs/easing.html#back) provides a simple animation where the
* object goes slightly back before moving forward
* - [`bounce`](docs/easing.html#bounce) provides a bouncing animation
* - [`ease`](docs/easing.html#ease) provides a simple inertial animation
* - [`elastic`](docs/easing.html#elastic) provides a simple spring interaction
*
* ### Standard functions
*
* Three standard easing functions are provided:
*
* - [`linear`](docs/easing.html#linear)
* - [`quad`](docs/easing.html#quad)
* - [`cubic`](docs/easing.html#cubic)
*
* The [`poly`](docs/easing.html#poly) function can be used to implement
* quartic, quintic, and other higher power functions.
*
* ### Additional functions
*
* Additional mathematical functions are provided by the following methods:
*
* - [`bezier`](docs/easing.html#bezier) provides a cubic bezier curve
* - [`circle`](docs/easing.html#circle) provides a circular function
* - [`sin`](docs/easing.html#sin) provides a sinusoidal function
* - [`exp`](docs/easing.html#exp) provides an exponential function
*
* The following helpers are used to modify other easing functions.
*
* - [`in`](docs/easing.html#in) runs an easing function forwards
* - [`inOut`](docs/easing.html#inout) makes any easing function symmetrical
* - [`out`](docs/easing.html#out) runs an easing function backwards
*/
const Easing = {
/**
* A stepping function, returns 1 for any positive value of `n`.
*/
step0(n: number): number {
return n > 0 ? 1 : 0;
},
/**
* A stepping function, returns 1 if `n` is greater than or equal to 1.
*/
step1(n: number): number {
return n >= 1 ? 1 : 0;
},
/**
* A linear function, `f(t) = t`. Position correlates to elapsed time one to
* one.
*
* http://cubic-bezier.com/#0,0,1,1
*/
linear(t: number): number {
return t;
},
/**
* A simple inertial interaction, similar to an object slowly accelerating to
* speed.
*
* http://cubic-bezier.com/#.42,0,1,1
*/
ease(t: number): number {
if (!ease) {
ease = Easing.bezier(0.42, 0, 1, 1);
}
return ease(t);
},
/**
* A quadratic function, `f(t) = t * t`. Position equals the square of elapsed
* time.
*
* http://easings.net/#easeInQuad
*/
quad(t: number): number {
return t * t;
},
/**
* A cubic function, `f(t) = t * t * t`. Position equals the cube of elapsed
* time.
*
* http://easings.net/#easeInCubic
*/
cubic(t: number): number {
return t * t * t;
},
/**
* A power function. Position is equal to the Nth power of elapsed time.
*
* n = 4: http://easings.net/#easeInQuart
* n = 5: http://easings.net/#easeInQuint
*/
poly(n: number): (t: number) => number {
return (t: number) => Math.pow(t, n);
},
/**
* A sinusoidal function.
*
* http://easings.net/#easeInSine
*/
sin(t: number): number {
return 1 - Math.cos((t * Math.PI) / 2);
},
/**
* A circular function.
*
* http://easings.net/#easeInCirc
*/
circle(t: number): number {
return 1 - Math.sqrt(1 - t * t);
},
/**
* An exponential function.
*
* http://easings.net/#easeInExpo
*/
exp(t: number): number {
return Math.pow(2, 10 * (t - 1));
},
/**
* A simple elastic interaction, similar to a spring oscillating back and
* forth.
*
* Default bounciness is 1, which overshoots a little bit once. 0 bounciness
* doesn't overshoot at all, and bounciness of N > 1 will overshoot about N
* times.
*
* http://easings.net/#easeInElastic
*/
elastic(bounciness: number = 1): (t: number) => number {
const p = bounciness * Math.PI;
return t => 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
},
/**
* Use with `Animated.parallel()` to create a simple effect where the object
* animates back slightly as the animation starts.
*
* https://easings.net/#easeInBack
*/
back(s: number = 1.70158): (t: number) => number {
return t => t * t * ((s + 1) * t - s);
},
/**
* Provides a simple bouncing effect.
*
* http://easings.net/#easeInBounce
*/
bounce(t: number): number {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
}
if (t < 2 / 2.75) {
const t2 = t - 1.5 / 2.75;
return 7.5625 * t2 * t2 + 0.75;
}
if (t < 2.5 / 2.75) {
const t2 = t - 2.25 / 2.75;
return 7.5625 * t2 * t2 + 0.9375;
}
const t2 = t - 2.625 / 2.75;
return 7.5625 * t2 * t2 + 0.984375;
},
/**
* Provides a cubic bezier curve, equivalent to CSS Transitions'
* `transition-timing-function`.
*
* A useful tool to visualize cubic bezier curves can be found at
* http://cubic-bezier.com/
*/
bezier(
x1: number,
y1: number,
x2: number,
y2: number,
): (t: number) => number {
const _bezier = require('./bezier').default;
return _bezier(x1, y1, x2, y2);
},
/**
* Runs an easing function forwards.
*/
in(easing: (t: number) => number): (t: number) => number {
return easing;
},
/**
* Runs an easing function backwards.
*/
out(easing: (t: number) => number): (t: number) => number {
return t => 1 - easing(1 - t);
},
/**
* Makes any easing function symmetrical. The easing function will run
* forwards for half of the duration, then backwards for the rest of the
* duration.
*/
inOut(easing: (t: number) => number): (t: number) => number {
return t => {
if (t < 0.5) {
return easing(t * 2) / 2;
}
return 1 - easing((1 - t) * 2) / 2;
};
},
};
export default Easing;

View File

@@ -0,0 +1,611 @@
/**
* 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 {EventSubscription} from '../vendor/emitter/EventEmitter';
import type {EventConfig} from './AnimatedEvent';
import type {AnimationConfig, EndCallback} from './animations/Animation';
import type {
AnimatedNodeConfig,
AnimatingNodeConfig,
EventMapping,
} from './NativeAnimatedModule';
import type {InterpolationConfigType} from './nodes/AnimatedInterpolation';
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import Platform from '../Utilities/Platform';
import NativeAnimatedNonTurboModule from './NativeAnimatedModule';
import NativeAnimatedTurboModule from './NativeAnimatedTurboModule';
import invariant from 'invariant';
// TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%.
const NativeAnimatedModule =
NativeAnimatedNonTurboModule ?? NativeAnimatedTurboModule;
let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */
let __nativeAnimationIdCount = 1; /* used for started animations */
let nativeEventEmitter;
let waitingForQueuedOperations = new Set<string>();
let queueOperations = false;
let queue: Array<() => void> = [];
// $FlowFixMe
let singleOpQueue: Array<any> = [];
const useSingleOpBatching =
Platform.OS === 'android' &&
!!NativeAnimatedModule?.queueAndExecuteBatchedOperations &&
ReactNativeFeatureFlags.animatedShouldUseSingleOp();
let flushQueueTimeout = null;
const eventListenerGetValueCallbacks: {
[number]: (value: number) => void,
} = {};
const eventListenerAnimationFinishedCallbacks: {
[number]: EndCallback,
} = {};
let globalEventEmitterGetValueListener: ?EventSubscription = null;
let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null;
const nativeOps: ?typeof NativeAnimatedModule = useSingleOpBatching
? ((function () {
const apis = [
'createAnimatedNode', // 1
'updateAnimatedNodeConfig', // 2
'getValue', // 3
'startListeningToAnimatedNodeValue', // 4
'stopListeningToAnimatedNodeValue', // 5
'connectAnimatedNodes', // 6
'disconnectAnimatedNodes', // 7
'startAnimatingNode', // 8
'stopAnimation', // 9
'setAnimatedNodeValue', // 10
'setAnimatedNodeOffset', // 11
'flattenAnimatedNodeOffset', // 12
'extractAnimatedNodeOffset', // 13
'connectAnimatedNodeToView', // 14
'disconnectAnimatedNodeFromView', // 15
'restoreDefaultValues', // 16
'dropAnimatedNode', // 17
'addAnimatedEventToView', // 18
'removeAnimatedEventFromView', // 19
'addListener', // 20
'removeListener', // 21
];
return apis.reduce<{[string]: number}>((acc, functionName, i) => {
// These indices need to be kept in sync with the indices in native (see NativeAnimatedModule in Java, or the equivalent for any other native platform).
// $FlowFixMe[prop-missing]
acc[functionName] = i + 1;
return acc;
}, {});
})(): $FlowFixMe)
: NativeAnimatedModule;
/**
* Wrappers around NativeAnimatedModule to provide flow and autocomplete support for
* the native module methods, and automatic queue management on Android
*/
const API = {
getValue: function (
tag: number,
saveValueCallback: (value: number) => void,
): void {
invariant(nativeOps, 'Native animated module is not available');
if (useSingleOpBatching) {
if (saveValueCallback) {
eventListenerGetValueCallbacks[tag] = saveValueCallback;
}
// $FlowFixMe
API.queueOperation(nativeOps.getValue, tag);
} else {
API.queueOperation(nativeOps.getValue, tag, saveValueCallback);
}
},
setWaitingForIdentifier: function (id: string): void {
waitingForQueuedOperations.add(id);
queueOperations = true;
if (
ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() &&
flushQueueTimeout
) {
clearTimeout(flushQueueTimeout);
}
},
unsetWaitingForIdentifier: function (id: string): void {
waitingForQueuedOperations.delete(id);
if (waitingForQueuedOperations.size === 0) {
queueOperations = false;
API.disableQueue();
}
},
disableQueue: function (): void {
invariant(nativeOps, 'Native animated module is not available');
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
const prevTimeout = flushQueueTimeout;
clearImmediate(prevTimeout);
flushQueueTimeout = setImmediate(API.flushQueue);
} else {
API.flushQueue();
}
},
flushQueue: function (): void {
// TODO: (T136971132)
invariant(
NativeAnimatedModule || process.env.NODE_ENV === 'test',
'Native animated module is not available',
);
flushQueueTimeout = null;
// Early returns before calling any APIs
if (useSingleOpBatching && singleOpQueue.length === 0) {
return;
}
if (!useSingleOpBatching && queue.length === 0) {
return;
}
if (useSingleOpBatching) {
// Set up event listener for callbacks if it's not set up
if (
!globalEventEmitterGetValueListener ||
!globalEventEmitterAnimationFinishedListener
) {
setupGlobalEventEmitterListeners();
}
// Single op batching doesn't use callback functions, instead we
// use RCTDeviceEventEmitter. This reduces overhead of sending lots of
// JSI functions across to native code; but also, TM infrastructure currently
// does not support packing a function into native arrays.
NativeAnimatedModule?.queueAndExecuteBatchedOperations?.(singleOpQueue);
singleOpQueue.length = 0;
} else {
Platform.OS === 'android' &&
NativeAnimatedModule?.startOperationBatch?.();
for (let q = 0, l = queue.length; q < l; q++) {
queue[q]();
}
queue.length = 0;
Platform.OS === 'android' &&
NativeAnimatedModule?.finishOperationBatch?.();
}
},
queueOperation: <Args: $ReadOnlyArray<mixed>, Fn: (...Args) => void>(
fn: Fn,
...args: Args
): void => {
if (useSingleOpBatching) {
// Get the command ID from the queued function, and push that ID and any arguments needed to execute the operation
// $FlowFixMe: surprise, fn is actually a number
singleOpQueue.push(fn, ...args);
return;
}
// If queueing is explicitly on, *or* the queue has not yet
// been flushed, use the queue. This is to prevent operations
// from being executed out of order.
if (queueOperations || queue.length !== 0) {
queue.push(() => fn(...args));
} else {
fn(...args);
}
},
createAnimatedNode: function (tag: number, config: AnimatedNodeConfig): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.createAnimatedNode, tag, config);
},
updateAnimatedNodeConfig: function (
tag: number,
config: AnimatedNodeConfig,
): void {
invariant(nativeOps, 'Native animated module is not available');
if (nativeOps.updateAnimatedNodeConfig) {
API.queueOperation(nativeOps.updateAnimatedNodeConfig, tag, config);
}
},
startListeningToAnimatedNodeValue: function (tag: number) {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.startListeningToAnimatedNodeValue, tag);
},
stopListeningToAnimatedNodeValue: function (tag: number) {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.stopListeningToAnimatedNodeValue, tag);
},
connectAnimatedNodes: function (parentTag: number, childTag: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.connectAnimatedNodes, parentTag, childTag);
},
disconnectAnimatedNodes: function (
parentTag: number,
childTag: number,
): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.disconnectAnimatedNodes, parentTag, childTag);
},
startAnimatingNode: function (
animationId: number,
nodeTag: number,
config: AnimatingNodeConfig,
endCallback: EndCallback,
): void {
invariant(nativeOps, 'Native animated module is not available');
if (useSingleOpBatching) {
if (endCallback) {
eventListenerAnimationFinishedCallbacks[animationId] = endCallback;
}
// $FlowFixMe
API.queueOperation(
// $FlowFixMe[incompatible-call]
nativeOps.startAnimatingNode,
animationId,
nodeTag,
config,
);
} else {
API.queueOperation(
nativeOps.startAnimatingNode,
animationId,
nodeTag,
config,
endCallback,
);
}
},
stopAnimation: function (animationId: number) {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.stopAnimation, animationId);
},
setAnimatedNodeValue: function (nodeTag: number, value: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.setAnimatedNodeValue, nodeTag, value);
},
setAnimatedNodeOffset: function (nodeTag: number, offset: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.setAnimatedNodeOffset, nodeTag, offset);
},
flattenAnimatedNodeOffset: function (nodeTag: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.flattenAnimatedNodeOffset, nodeTag);
},
extractAnimatedNodeOffset: function (nodeTag: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.extractAnimatedNodeOffset, nodeTag);
},
connectAnimatedNodeToView: function (nodeTag: number, viewTag: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.connectAnimatedNodeToView, nodeTag, viewTag);
},
disconnectAnimatedNodeFromView: function (
nodeTag: number,
viewTag: number,
): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.disconnectAnimatedNodeFromView,
nodeTag,
viewTag,
);
},
restoreDefaultValues: function (nodeTag: number): void {
invariant(nativeOps, 'Native animated module is not available');
// Backwards compat with older native runtimes, can be removed later.
if (nativeOps.restoreDefaultValues != null) {
API.queueOperation(nativeOps.restoreDefaultValues, nodeTag);
}
},
dropAnimatedNode: function (tag: number): void {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(nativeOps.dropAnimatedNode, tag);
},
addAnimatedEventToView: function (
viewTag: number,
eventName: string,
eventMapping: EventMapping,
) {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.addAnimatedEventToView,
viewTag,
eventName,
eventMapping,
);
},
removeAnimatedEventFromView(
viewTag: number,
eventName: string,
animatedNodeTag: number,
) {
invariant(nativeOps, 'Native animated module is not available');
API.queueOperation(
nativeOps.removeAnimatedEventFromView,
viewTag,
eventName,
animatedNodeTag,
);
},
};
function setupGlobalEventEmitterListeners() {
globalEventEmitterGetValueListener = RCTDeviceEventEmitter.addListener(
'onNativeAnimatedModuleGetValue',
params => {
const {tag} = params;
const callback = eventListenerGetValueCallbacks[tag];
if (!callback) {
return;
}
callback(params.value);
delete eventListenerGetValueCallbacks[tag];
},
);
globalEventEmitterAnimationFinishedListener =
RCTDeviceEventEmitter.addListener(
'onNativeAnimatedModuleAnimationFinished',
params => {
// TODO: remove Array.isArray once native changes have propagated
const animations = Array.isArray(params) ? params : [params];
for (const animation of animations) {
const {animationId} = animation;
const callback = eventListenerAnimationFinishedCallbacks[animationId];
if (callback) {
callback(animation);
delete eventListenerAnimationFinishedCallbacks[animationId];
}
}
},
);
}
/**
* Styles allowed by the native animated implementation.
*
* In general native animated implementation should support any numeric or color property that
* doesn't need to be updated through the shadow view hierarchy (all non-layout properties).
*/
const SUPPORTED_COLOR_STYLES = {
backgroundColor: true,
borderBottomColor: true,
borderColor: true,
borderEndColor: true,
borderLeftColor: true,
borderRightColor: true,
borderStartColor: true,
borderTopColor: true,
color: true,
tintColor: true,
};
const SUPPORTED_STYLES = {
...SUPPORTED_COLOR_STYLES,
borderBottomEndRadius: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
borderBottomStartRadius: true,
borderEndEndRadius: true,
borderEndStartRadius: true,
borderRadius: true,
borderTopEndRadius: true,
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderTopStartRadius: true,
borderStartEndRadius: true,
borderStartStartRadius: true,
elevation: true,
opacity: true,
transform: true,
zIndex: true,
/* ios styles */
shadowOpacity: true,
shadowRadius: true,
/* legacy android transform properties */
scaleX: true,
scaleY: true,
translateX: true,
translateY: true,
};
const SUPPORTED_TRANSFORMS = {
translateX: true,
translateY: true,
scale: true,
scaleX: true,
scaleY: true,
rotate: true,
rotateX: true,
rotateY: true,
rotateZ: true,
perspective: true,
skewX: true,
skewY: true,
matrix: ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform(),
};
const SUPPORTED_INTERPOLATION_PARAMS = {
inputRange: true,
outputRange: true,
extrapolate: true,
extrapolateRight: true,
extrapolateLeft: true,
};
function addWhitelistedStyleProp(prop: string): void {
// $FlowFixMe[prop-missing]
SUPPORTED_STYLES[prop] = true;
}
function addWhitelistedTransformProp(prop: string): void {
// $FlowFixMe[prop-missing]
SUPPORTED_TRANSFORMS[prop] = true;
}
function addWhitelistedInterpolationParam(param: string): void {
// $FlowFixMe[prop-missing]
SUPPORTED_INTERPOLATION_PARAMS[param] = true;
}
function isSupportedColorStyleProp(prop: string): boolean {
return SUPPORTED_COLOR_STYLES[prop] === true;
}
function isSupportedStyleProp(prop: string): boolean {
return SUPPORTED_STYLES[prop] === true;
}
function isSupportedTransformProp(prop: string): boolean {
return SUPPORTED_TRANSFORMS[prop] === true;
}
function isSupportedInterpolationParam(param: string): boolean {
return SUPPORTED_INTERPOLATION_PARAMS[param] === true;
}
function validateTransform(
configs: Array<
| {
type: 'animated',
property: string,
nodeTag: ?number,
...
}
| {
type: 'static',
property: string,
value: number | string,
...
},
>,
): void {
configs.forEach(config => {
if (!isSupportedTransformProp(config.property)) {
throw new Error(
`Property '${config.property}' is not supported by native animated module`,
);
}
});
}
function validateStyles(styles: {[key: string]: ?number, ...}): void {
for (const key in styles) {
if (!isSupportedStyleProp(key)) {
throw new Error(
`Style property '${key}' is not supported by native animated module`,
);
}
}
}
function validateInterpolation<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): void {
for (const key in config) {
if (!isSupportedInterpolationParam(key)) {
throw new Error(
`Interpolation property '${key}' is not supported by native animated module`,
);
}
}
}
function generateNewNodeTag(): number {
return __nativeAnimatedNodeTagCount++;
}
function generateNewAnimationId(): number {
return __nativeAnimationIdCount++;
}
function assertNativeAnimatedModule(): void {
invariant(NativeAnimatedModule, 'Native animated module is not available');
}
let _warnedMissingNativeAnimated = false;
function shouldUseNativeDriver(
config: $ReadOnly<{...AnimationConfig, ...}> | EventConfig,
): boolean {
if (config.useNativeDriver == null) {
console.warn(
'Animated: `useNativeDriver` was not specified. This is a required ' +
'option and must be explicitly set to `true` or `false`',
);
}
if (config.useNativeDriver === true && !NativeAnimatedModule) {
if (process.env.NODE_ENV !== 'test') {
if (!_warnedMissingNativeAnimated) {
console.warn(
'Animated: `useNativeDriver` is not supported because the native ' +
'animated module is missing. Falling back to JS-based animation. To ' +
'resolve this, add `RCTAnimation` module to this app, or remove ' +
'`useNativeDriver`. ' +
'Make sure to run `bundle exec pod install` first. Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md',
);
_warnedMissingNativeAnimated = true;
}
}
return false;
}
return config.useNativeDriver || false;
}
function transformDataType(value: number | string): number | string {
// Change the string type to number type so we can reuse the same logic in
// iOS and Android platform
if (typeof value !== 'string') {
return value;
}
// Normalize degrees and radians to a number expressed in radians
if (value.endsWith('deg')) {
const degrees = parseFloat(value) || 0;
return (degrees * Math.PI) / 180.0;
} else if (value.endsWith('rad')) {
return parseFloat(value) || 0;
} else {
return value;
}
}
export default {
API,
isSupportedColorStyleProp,
isSupportedStyleProp,
isSupportedTransformProp,
isSupportedInterpolationParam,
addWhitelistedStyleProp,
addWhitelistedTransformProp,
addWhitelistedInterpolationParam,
validateStyles,
validateTransform,
validateInterpolation,
generateNewNodeTag,
generateNewAnimationId,
assertNativeAnimatedModule,
shouldUseNativeDriver,
transformDataType,
// $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppression
// $FlowExpectedError[missing-type-arg] - unsafe getter lint suppression
get nativeEventEmitter(): NativeEventEmitter {
if (!nativeEventEmitter) {
// $FlowFixMe[underconstrained-implicit-instantiation]
nativeEventEmitter = new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeAnimatedModule,
);
}
return nativeEventEmitter;
},
};

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeAnimatedModule';
import NativeAnimatedModule from '../../src/private/specs/modules/NativeAnimatedModule';
export default NativeAnimatedModule;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeAnimatedTurboModule';
import NativeAnimatedTurboModule from '../../src/private/specs/modules/NativeAnimatedTurboModule';
export default NativeAnimatedTurboModule;

View File

@@ -0,0 +1,98 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
type SpringConfigType = {
stiffness: number,
damping: number,
...
};
function stiffnessFromOrigamiValue(oValue: number) {
return (oValue - 30) * 3.62 + 194;
}
function dampingFromOrigamiValue(oValue: number) {
return (oValue - 8) * 3 + 25;
}
export function fromOrigamiTensionAndFriction(
tension: number,
friction: number,
): SpringConfigType {
return {
stiffness: stiffnessFromOrigamiValue(tension),
damping: dampingFromOrigamiValue(friction),
};
}
export function fromBouncinessAndSpeed(
bounciness: number,
speed: number,
): SpringConfigType {
function normalize(value: number, startValue: number, endValue: number) {
return (value - startValue) / (endValue - startValue);
}
function projectNormal(n: number, start: number, end: number) {
return start + n * (end - start);
}
function linearInterpolation(t: number, start: number, end: number) {
return t * end + (1 - t) * start;
}
function quadraticOutInterpolation(t: number, start: number, end: number) {
return linearInterpolation(2 * t - t * t, start, end);
}
function b3Friction1(x: number) {
return 0.0007 * Math.pow(x, 3) - 0.031 * Math.pow(x, 2) + 0.64 * x + 1.28;
}
function b3Friction2(x: number) {
return 0.000044 * Math.pow(x, 3) - 0.006 * Math.pow(x, 2) + 0.36 * x + 2;
}
function b3Friction3(x: number) {
return (
0.00000045 * Math.pow(x, 3) -
0.000332 * Math.pow(x, 2) +
0.1078 * x +
5.84
);
}
function b3Nobounce(tension: number) {
if (tension <= 18) {
return b3Friction1(tension);
} else if (tension > 18 && tension <= 44) {
return b3Friction2(tension);
} else {
return b3Friction3(tension);
}
}
let b = normalize(bounciness / 1.7, 0, 20);
b = projectNormal(b, 0, 0.8);
const s = normalize(speed / 1.7, 0, 20);
const bouncyTension = projectNormal(s, 0.5, 200);
const bouncyFriction = quadraticOutInterpolation(
b,
b3Nobounce(bouncyTension),
0.01,
);
return {
stiffness: stiffnessFromOrigamiValue(bouncyTension),
damping: dampingFromOrigamiValue(bouncyFriction),
};
}

View File

@@ -0,0 +1,126 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from '../nodes/AnimatedNode';
import type AnimatedValue from '../nodes/AnimatedValue';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedProps from '../nodes/AnimatedProps';
export type EndResult = {finished: boolean, value?: number, ...};
export type EndCallback = (result: EndResult) => void;
export type AnimationConfig = {
isInteraction?: boolean,
useNativeDriver: boolean,
platformConfig?: PlatformConfig,
onComplete?: ?EndCallback,
iterations?: number,
};
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 {
__active: boolean;
__isInteraction: boolean;
__onEnd: ?EndCallback;
__iterations: number;
_nativeId: number;
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {}
stop(): void {
if (this._nativeId) {
NativeAnimatedHelper.API.stopAnimation(this._nativeId);
}
}
__getNativeAnimationConfig(): any {
// Subclasses that have corresponding animation implementation done in native
// should override this method
throw new Error('This animation type cannot be offloaded to native');
}
// Helper function for subclasses to make sure onEnd is only called once.
__debouncedOnEnd(result: EndResult): void {
const onEnd = this.__onEnd;
this.__onEnd = null;
onEnd && onEnd(result);
}
__findAnimatedPropsNodes(node: AnimatedNode): Array<AnimatedProps> {
const result = [];
if (node instanceof AnimatedProps) {
result.push(node);
return result;
}
for (const child of node.__getChildren()) {
result.push(...this.__findAnimatedPropsNodes(child));
}
return result;
}
__startNativeAnimation(animatedValue: AnimatedValue): void {
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.__debouncedOnEnd(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);
// 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(),
);
}
},
);
} catch (e) {
throw e;
} finally {
NativeAnimatedHelper.API.unsetWaitingForIdentifier(
startNativeAnimationWaitId,
);
}
}
}

View File

@@ -0,0 +1,133 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedValue from '../nodes/AnimatedValue';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import Animation from './Animation';
export type DecayAnimationConfig = {
...AnimationConfig,
velocity:
| number
| {
x: number,
y: number,
...
},
deceleration?: number,
};
export type DecayAnimationConfigSingle = {
...AnimationConfig,
velocity: number,
deceleration?: number,
};
export default class DecayAnimation extends Animation {
_startTime: number;
_lastValue: number;
_fromValue: number;
_deceleration: number;
_velocity: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: DecayAnimationConfigSingle) {
super();
this._deceleration = config.deceleration ?? 0.998;
this._velocity = config.velocity;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
this.__iterations = config.iterations ?? 1;
}
__getNativeAnimationConfig(): {|
deceleration: number,
iterations: number,
platformConfig: ?PlatformConfig,
type: $TEMPORARY$string<'decay'>,
velocity: number,
|} {
return {
type: 'decay',
deceleration: this._deceleration,
velocity: this._velocity,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._lastValue = fromValue;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._startTime = Date.now();
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`',
);
}
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
onUpdate(): void {
const now = Date.now();
const value =
this._fromValue +
(this._velocity / (1 - this._deceleration)) *
(1 - Math.exp(-(1 - this._deceleration) * (now - this._startTime)));
this._onUpdate(value);
if (Math.abs(this._lastValue - value) < 0.1) {
this.__debouncedOnEnd({finished: true});
return;
}
this._lastValue = value;
if (this.__active) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
super.stop();
this.__active = false;
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}

View File

@@ -0,0 +1,378 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedInterpolation from '../nodes/AnimatedInterpolation';
import type AnimatedValue from '../nodes/AnimatedValue';
import type AnimatedValueXY from '../nodes/AnimatedValueXY';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedColor from '../nodes/AnimatedColor';
import * as SpringConfig from '../SpringConfig';
import Animation from './Animation';
import invariant from 'invariant';
export type SpringAnimationConfig = {
...AnimationConfig,
toValue:
| number
| AnimatedValue
| {
x: number,
y: number,
...
}
| AnimatedValueXY
| {
r: number,
g: number,
b: number,
a: number,
...
}
| AnimatedColor
| AnimatedInterpolation<number>,
overshootClamping?: boolean,
restDisplacementThreshold?: number,
restSpeedThreshold?: number,
velocity?:
| number
| {
x: number,
y: number,
...
},
bounciness?: number,
speed?: number,
tension?: number,
friction?: number,
stiffness?: number,
damping?: number,
mass?: number,
delay?: number,
};
export type SpringAnimationConfigSingle = {
...AnimationConfig,
toValue: number,
overshootClamping?: boolean,
restDisplacementThreshold?: number,
restSpeedThreshold?: number,
velocity?: number,
bounciness?: number,
speed?: number,
tension?: number,
friction?: number,
stiffness?: number,
damping?: number,
mass?: number,
delay?: number,
};
export default class SpringAnimation extends Animation {
_overshootClamping: boolean;
_restDisplacementThreshold: number;
_restSpeedThreshold: number;
_lastVelocity: number;
_startPosition: number;
_lastPosition: number;
_fromValue: number;
_toValue: number;
_stiffness: number;
_damping: number;
_mass: number;
_initialVelocity: number;
_delay: number;
_timeout: any;
_startTime: number;
_lastTime: number;
_frameTime: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: SpringAnimationConfigSingle) {
super();
this._overshootClamping = config.overshootClamping ?? false;
this._restDisplacementThreshold = config.restDisplacementThreshold ?? 0.001;
this._restSpeedThreshold = config.restSpeedThreshold ?? 0.001;
this._initialVelocity = config.velocity ?? 0;
this._lastVelocity = config.velocity ?? 0;
this._toValue = config.toValue;
this._delay = config.delay ?? 0;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
this.__iterations = config.iterations ?? 1;
if (
config.stiffness !== undefined ||
config.damping !== undefined ||
config.mass !== undefined
) {
invariant(
config.bounciness === undefined &&
config.speed === undefined &&
config.tension === undefined &&
config.friction === undefined,
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
);
this._stiffness = config.stiffness ?? 100;
this._damping = config.damping ?? 10;
this._mass = config.mass ?? 1;
} else if (config.bounciness !== undefined || config.speed !== undefined) {
// Convert the origami bounciness/speed values to stiffness/damping
// We assume mass is 1.
invariant(
config.tension === undefined &&
config.friction === undefined &&
config.stiffness === undefined &&
config.damping === undefined &&
config.mass === undefined,
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
);
const springConfig = SpringConfig.fromBouncinessAndSpeed(
config.bounciness ?? 8,
config.speed ?? 12,
);
this._stiffness = springConfig.stiffness;
this._damping = springConfig.damping;
this._mass = 1;
} else {
// Convert the origami tension/friction values to stiffness/damping
// We assume mass is 1.
const springConfig = SpringConfig.fromOrigamiTensionAndFriction(
config.tension ?? 40,
config.friction ?? 7,
);
this._stiffness = springConfig.stiffness;
this._damping = springConfig.damping;
this._mass = 1;
}
invariant(this._stiffness > 0, 'Stiffness value must be greater than 0');
invariant(this._damping > 0, 'Damping value must be greater than 0');
invariant(this._mass > 0, 'Mass value must be greater than 0');
}
__getNativeAnimationConfig(): {|
damping: number,
initialVelocity: number,
iterations: number,
mass: number,
platformConfig: ?PlatformConfig,
overshootClamping: boolean,
restDisplacementThreshold: number,
restSpeedThreshold: number,
stiffness: number,
toValue: any,
type: $TEMPORARY$string<'spring'>,
|} {
return {
type: 'spring',
overshootClamping: this._overshootClamping,
restDisplacementThreshold: this._restDisplacementThreshold,
restSpeedThreshold: this._restSpeedThreshold,
stiffness: this._stiffness,
damping: this._damping,
mass: this._mass,
initialVelocity: this._initialVelocity ?? this._lastVelocity,
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._startPosition = fromValue;
this._lastPosition = this._startPosition;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._lastTime = Date.now();
this._frameTime = 0.0;
if (previousAnimation instanceof SpringAnimation) {
const internalState = previousAnimation.getInternalState();
this._lastPosition = internalState.lastPosition;
this._lastVelocity = internalState.lastVelocity;
// Set the initial velocity to the last velocity
this._initialVelocity = this._lastVelocity;
this._lastTime = internalState.lastTime;
}
const start = () => {
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`',
);
}
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
this.onUpdate();
}
};
// If this._delay is more than 0, we start after the timeout.
if (this._delay) {
this._timeout = setTimeout(start, this._delay);
} else {
start();
}
}
getInternalState(): Object {
return {
lastPosition: this._lastPosition,
lastVelocity: this._lastVelocity,
lastTime: this._lastTime,
};
}
/**
* This spring model is based off of a damped harmonic oscillator
* (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
*
* We use the closed form of the second order differential equation:
*
* x'' + (2ζ⍵_0)x' + ⍵^2x = 0
*
* where
* ⍵_0 = √(k / m) (undamped angular frequency of the oscillator),
* ζ = c / 2√mk (damping ratio),
* c = damping constant
* k = stiffness
* m = mass
*
* The derivation of the closed form is described in detail here:
* http://planetmath.org/sites/default/files/texpdf/39745.pdf
*
* This algorithm happens to match the algorithm used by CASpringAnimation,
* a QuartzCore (iOS) API that creates spring animations.
*/
onUpdate(): void {
// If for some reason we lost a lot of frames (e.g. process large payload or
// stopped in the debugger), we only advance by 4 frames worth of
// computation and will continue on the next frame. It's better to have it
// running at faster speed than jumping to the end.
const MAX_STEPS = 64;
let now = Date.now();
if (now > this._lastTime + MAX_STEPS) {
now = this._lastTime + MAX_STEPS;
}
const deltaTime = (now - this._lastTime) / 1000;
this._frameTime += deltaTime;
const c: number = this._damping;
const m: number = this._mass;
const k: number = this._stiffness;
const v0: number = -this._initialVelocity;
const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio
const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms)
const omega1 = omega0 * Math.sqrt(1.0 - zeta * zeta); // exponential decay
const x0 = this._toValue - this._startPosition; // calculate the oscillation from x0 = 1 to x = 0
let position = 0.0;
let velocity = 0.0;
const t = this._frameTime;
if (zeta < 1) {
// Under damped
const envelope = Math.exp(-zeta * omega0 * t);
position =
this._toValue -
envelope *
(((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) +
x0 * Math.cos(omega1 * t));
// This looks crazy -- it's actually just the derivative of the
// oscillation function
velocity =
zeta *
omega0 *
envelope *
((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 +
x0 * Math.cos(omega1 * t)) -
envelope *
(Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) -
omega1 * x0 * Math.sin(omega1 * t));
} else {
// Critically damped
const envelope = Math.exp(-omega0 * t);
position = this._toValue - envelope * (x0 + (v0 + omega0 * x0) * t);
velocity =
envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
}
this._lastTime = now;
this._lastPosition = position;
this._lastVelocity = velocity;
this._onUpdate(position);
if (!this.__active) {
// a listener might have stopped us in _onUpdate
return;
}
// Conditions for stopping the spring animation
let isOvershooting = false;
if (this._overshootClamping && this._stiffness !== 0) {
if (this._startPosition < this._toValue) {
isOvershooting = position > this._toValue;
} else {
isOvershooting = position < this._toValue;
}
}
const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
let isDisplacement = true;
if (this._stiffness !== 0) {
isDisplacement =
Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this._stiffness !== 0) {
// Ensure that we end up with a round value
this._lastPosition = this._toValue;
this._lastVelocity = 0;
this._onUpdate(this._toValue);
}
this.__debouncedOnEnd({finished: true});
return;
}
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
stop(): void {
super.stop();
this.__active = false;
clearTimeout(this._timeout);
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}

View File

@@ -0,0 +1,180 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {RgbaValue} from '../nodes/AnimatedColor';
import type AnimatedInterpolation from '../nodes/AnimatedInterpolation';
import type AnimatedValue from '../nodes/AnimatedValue';
import type AnimatedValueXY from '../nodes/AnimatedValueXY';
import type {AnimationConfig, EndCallback} from './Animation';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedColor from '../nodes/AnimatedColor';
import Animation from './Animation';
export type TimingAnimationConfig = $ReadOnly<{
...AnimationConfig,
toValue:
| number
| AnimatedValue
| {
x: number,
y: number,
...
}
| AnimatedValueXY
| RgbaValue
| AnimatedColor
| AnimatedInterpolation<number>,
easing?: (value: number) => number,
duration?: number,
delay?: number,
}>;
export type TimingAnimationConfigSingle = $ReadOnly<{
...AnimationConfig,
toValue: number,
easing?: (value: number) => number,
duration?: number,
delay?: number,
}>;
let _easeInOut;
function easeInOut() {
if (!_easeInOut) {
const Easing = require('../Easing').default;
_easeInOut = Easing.inOut(Easing.ease);
}
return _easeInOut;
}
export default class TimingAnimation extends Animation {
_startTime: number;
_fromValue: number;
_toValue: number;
_duration: number;
_delay: number;
_easing: (value: number) => number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_timeout: any;
_useNativeDriver: boolean;
_platformConfig: ?PlatformConfig;
constructor(config: TimingAnimationConfigSingle) {
super();
this._toValue = config.toValue;
this._easing = config.easing ?? easeInOut();
this._duration = config.duration ?? 500;
this._delay = config.delay ?? 0;
this.__iterations = config.iterations ?? 1;
this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config);
this._platformConfig = config.platformConfig;
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
}
__getNativeAnimationConfig(): any {
const frameDuration = 1000.0 / 60.0;
const frames = [];
const numFrames = Math.round(this._duration / frameDuration);
for (let frame = 0; frame < numFrames; frame++) {
frames.push(this._easing(frame / numFrames));
}
frames.push(this._easing(1));
return {
type: 'frames',
frames,
toValue: this._toValue,
iterations: this.__iterations,
platformConfig: this._platformConfig,
};
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
animatedValue: AnimatedValue,
): void {
this.__active = true;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
const start = () => {
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`',
);
}
// Animations that sometimes have 0 duration and sometimes do not
// still need to use the native driver when duration is 0 so as to
// not cause intermixed JS and native animations.
if (this._duration === 0 && !this._useNativeDriver) {
this._onUpdate(this._toValue);
this.__debouncedOnEnd({finished: true});
} else {
this._startTime = Date.now();
if (this._useNativeDriver) {
this.__startNativeAnimation(animatedValue);
} else {
this._animationFrame = requestAnimationFrame(
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.onUpdate.bind(this),
);
}
}
};
if (this._delay) {
this._timeout = setTimeout(start, this._delay);
} else {
start();
}
}
onUpdate(): void {
const now = Date.now();
if (now >= this._startTime + this._duration) {
if (this._duration === 0) {
this._onUpdate(this._toValue);
} else {
this._onUpdate(
this._fromValue + this._easing(1) * (this._toValue - this._fromValue),
);
}
this.__debouncedOnEnd({finished: true});
return;
}
this._onUpdate(
this._fromValue +
this._easing((now - this._startTime) / this._duration) *
(this._toValue - this._fromValue),
);
if (this.__active) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
super.stop();
this.__active = false;
clearTimeout(this._timeout);
global.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}

View File

@@ -0,0 +1,164 @@
/**
* Portions 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
* @format
*/
/**
* BezierEasing - use bezier curve for transition easing function
* https://github.com/gre/bezier-easing
* @copyright 2014-2015 Gaëtan Renaudeau. MIT License.
*/
'use strict';
// These values are established by empiricism with tests (tradeoff: performance VS precision)
const NEWTON_ITERATIONS = 4;
const NEWTON_MIN_SLOPE = 0.001;
const SUBDIVISION_PRECISION = 0.0000001;
const SUBDIVISION_MAX_ITERATIONS = 10;
const kSplineTableSize = 11;
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
const float32ArraySupported = typeof Float32Array === 'function';
function A(aA1: number, aA2: number) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
function B(aA1: number, aA2: number) {
return 3.0 * aA2 - 6.0 * aA1;
}
function C(aA1: number) {
return 3.0 * aA1;
}
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
function calcBezier(aT: number, aA1: number, aA2: number) {
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
}
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
function getSlope(aT: number, aA1: number, aA2: number) {
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
function binarySubdivide(
aX: number,
_aA: number,
_aB: number,
mX1: number,
mX2: number,
) {
let currentX,
currentT,
i = 0,
aA = _aA,
aB = _aB;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
} while (
Math.abs(currentX) > SUBDIVISION_PRECISION &&
++i < SUBDIVISION_MAX_ITERATIONS
);
return currentT;
}
function newtonRaphsonIterate(
aX: number,
_aGuessT: number,
mX1: number,
mX2: number,
) {
let aGuessT = _aGuessT;
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
const currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope === 0.0) {
return aGuessT;
}
const currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
export default function bezier(
mX1: number,
mY1: number,
mX2: number,
mY2: number,
): (x: number) => number {
if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) {
throw new Error('bezier x values must be in [0, 1] range');
}
// Precompute samples table
const sampleValues = float32ArraySupported
? new Float32Array(kSplineTableSize)
: new Array<number>(kSplineTableSize);
if (mX1 !== mY1 || mX2 !== mY2) {
for (let i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
}
}
function getTForX(aX: number) {
let intervalStart = 0.0;
let currentSample = 1;
const lastSample = kSplineTableSize - 1;
for (
;
currentSample !== lastSample && sampleValues[currentSample] <= aX;
++currentSample
) {
intervalStart += kSampleStepSize;
}
--currentSample;
// Interpolate to provide an initial guess for t
const dist =
(aX - sampleValues[currentSample]) /
(sampleValues[currentSample + 1] - sampleValues[currentSample]);
const guessForT = intervalStart + dist * kSampleStepSize;
const initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
} else if (initialSlope === 0.0) {
return guessForT;
} else {
return binarySubdivide(
aX,
intervalStart,
intervalStart + kSampleStepSize,
mX1,
mX2,
);
}
}
return function BezierEasing(x: number): number {
if (mX1 === mY1 && mX2 === mY2) {
return x; // linear
}
// Because JavaScript number are imprecise, we should guarantee the extremes are right.
if (x === 0) {
return 0;
}
if (x === 1) {
return 1;
}
return calcBezier(getTForX(x), mY1, mY2);
};
}

View File

@@ -0,0 +1,20 @@
/**
* 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 {AnimatedComponentType} from '../createAnimatedComponent';
import FlatList from '../../Lists/FlatList';
import createAnimatedComponent from '../createAnimatedComponent';
import * as React from 'react';
export default (createAnimatedComponent(FlatList): AnimatedComponentType<
React.ElementConfig<typeof FlatList>,
React.ElementRef<typeof FlatList>,
>);

View File

@@ -0,0 +1,22 @@
/**
* 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 {AnimatedComponentType} from '../createAnimatedComponent';
import Image from '../../Image/Image';
import createAnimatedComponent from '../createAnimatedComponent';
import * as React from 'react';
export default (createAnimatedComponent(
(Image: $FlowFixMe),
): AnimatedComponentType<
React.ElementConfig<typeof Image>,
React.ElementRef<typeof Image>,
>);

View File

@@ -0,0 +1,136 @@
/**
* 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 {____ViewStyle_Internal} from '../../StyleSheet/StyleSheetTypes';
import type {AnimatedComponentType} from '../createAnimatedComponent';
import RefreshControl from '../../Components/RefreshControl/RefreshControl';
import ScrollView from '../../Components/ScrollView/ScrollView';
import flattenStyle from '../../StyleSheet/flattenStyle';
import splitLayoutProps from '../../StyleSheet/splitLayoutProps';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import createAnimatedComponent from '../createAnimatedComponent';
import useAnimatedProps from '../useAnimatedProps';
import * as React from 'react';
import {useMemo} from 'react';
type Props = React.ElementConfig<typeof ScrollView>;
type Instance = React.ElementRef<typeof ScrollView>;
/**
* @see https://github.com/facebook/react-native/commit/b8c8562
*/
const AnimatedScrollView: AnimatedComponentType<Props, Instance> =
React.forwardRef(
function AnimatedScrollViewWithOrWithoutInvertedRefreshControl(
props,
forwardedRef,
) {
// (Android only) When a ScrollView has a RefreshControl and
// any `style` property set with an Animated.Value, the CSS
// gets incorrectly applied twice. This is because ScrollView
// swaps the parent/child relationship of itself and the
// RefreshControl component (see ScrollView.js for more details).
if (
Platform.OS === 'android' &&
props.refreshControl != null &&
props.style != null
) {
return (
<AnimatedScrollViewWithInvertedRefreshControl
scrollEventThrottle={0.0001}
{...props}
ref={forwardedRef}
refreshControl={props.refreshControl}
/>
);
} else {
return (
<AnimatedScrollViewWithoutInvertedRefreshControl
scrollEventThrottle={0.0001}
{...props}
ref={forwardedRef}
/>
);
}
},
);
const AnimatedScrollViewWithInvertedRefreshControl = React.forwardRef(
// $FlowFixMe[incompatible-call]
function AnimatedScrollViewWithInvertedRefreshControl(
props: {
...React.ElementConfig<typeof ScrollView>,
// $FlowFixMe[unclear-type] Same Flow type as `refreshControl` in ScrollView
refreshControl: React.Element<any>,
},
forwardedRef:
| {current: Instance | null, ...}
| ((Instance | null) => mixed),
) {
// Split `props` into the animate-able props for the parent (RefreshControl)
// and child (ScrollView).
const {intermediatePropsForRefreshControl, intermediatePropsForScrollView} =
useMemo(() => {
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[incompatible-call]
const {outer, inner} = splitLayoutProps(flattenStyle(props.style));
return {
intermediatePropsForRefreshControl: {style: outer},
intermediatePropsForScrollView: {...props, style: inner},
};
}, [props]);
// Handle animated props on `refreshControl`.
const [refreshControlAnimatedProps, refreshControlRef] = useAnimatedProps<
{style: ?____ViewStyle_Internal},
$FlowFixMe,
>(intermediatePropsForRefreshControl);
// NOTE: Assumes that refreshControl.ref` and `refreshControl.style` can be
// safely clobbered.
const refreshControl: React.Element<typeof RefreshControl> =
React.cloneElement(props.refreshControl, {
...refreshControlAnimatedProps,
ref: refreshControlRef,
});
// Handle animated props on `NativeDirectionalScrollView`.
const [scrollViewAnimatedProps, scrollViewRef] = useAnimatedProps<
Props,
Instance,
>(intermediatePropsForScrollView);
const ref = useMergeRefs<Instance>(scrollViewRef, forwardedRef);
return (
// $FlowFixMe[incompatible-use] Investigate useAnimatedProps return value
<ScrollView
{...scrollViewAnimatedProps}
ref={ref}
refreshControl={refreshControl}
// Because `refreshControl` is a clone of `props.refreshControl` with
// `refreshControlAnimatedProps` added, we need to pass ScrollView.js
// the combined styles since it also splits the outer/inner styles for
// its parent/child, respectively. Without this, the refreshControl
// styles would be ignored.
style={StyleSheet.compose(
scrollViewAnimatedProps.style,
refreshControlAnimatedProps.style,
)}
/>
);
},
);
const AnimatedScrollViewWithoutInvertedRefreshControl =
createAnimatedComponent(ScrollView);
export default AnimatedScrollView;

View File

@@ -0,0 +1,20 @@
/**
* 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 {AnimatedComponentType} from '../createAnimatedComponent';
import SectionList from '../../Lists/SectionList';
import createAnimatedComponent from '../createAnimatedComponent';
import * as React from 'react';
export default (createAnimatedComponent(SectionList): AnimatedComponentType<
React.ElementConfig<typeof SectionList>,
React.ElementRef<typeof SectionList>,
>);

View File

@@ -0,0 +1,22 @@
/**
* 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 {AnimatedComponentType} from '../createAnimatedComponent';
import Text from '../../Text/Text';
import createAnimatedComponent from '../createAnimatedComponent';
import * as React from 'react';
export default (createAnimatedComponent(
(Text: $FlowFixMe),
): AnimatedComponentType<
React.ElementConfig<typeof Text>,
React.ElementRef<typeof Text>,
>);

View File

@@ -0,0 +1,20 @@
/**
* 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 {AnimatedComponentType} from '../createAnimatedComponent';
import View from '../../Components/View/View';
import createAnimatedComponent from '../createAnimatedComponent';
import * as React from 'react';
export default (createAnimatedComponent(View): AnimatedComponentType<
React.ElementConfig<typeof View>,
React.ElementRef<typeof View>,
>);

View File

@@ -0,0 +1,67 @@
/**
* 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
* @format
*/
import View from '../Components/View/View';
import useMergeRefs from '../Utilities/useMergeRefs';
import useAnimatedProps from './useAnimatedProps';
import * as React from 'react';
// $FlowFixMe[deprecated-type]
export type AnimatedProps<Props: {...}> = $ObjMap<
Props &
$ReadOnly<{
passthroughAnimatedPropExplicitValues?: React.ElementConfig<typeof View>,
}>,
() => any,
>;
export type AnimatedComponentType<
Props: {...},
+Instance = mixed,
> = React.AbstractComponent<AnimatedProps<Props>, Instance>;
export default function createAnimatedComponent<TProps: {...}, TInstance>(
Component: React.AbstractComponent<TProps, TInstance>,
): AnimatedComponentType<TProps, TInstance> {
const AnimatedComponent = React.forwardRef<AnimatedProps<TProps>, TInstance>(
(props, forwardedRef) => {
const [reducedProps, callbackRef] = useAnimatedProps<TProps, TInstance>(
// $FlowFixMe[incompatible-call]
props,
);
const ref = useMergeRefs<TInstance>(callbackRef, forwardedRef);
// Some components require explicit passthrough values for animation
// to work properly. For example, if an animated component is
// transformed and Pressable, onPress will not work after transform
// without these passthrough values.
// $FlowFixMe[prop-missing]
const {passthroughAnimatedPropExplicitValues, style} = reducedProps;
const {style: passthroughStyle, ...passthroughProps} =
passthroughAnimatedPropExplicitValues ?? {};
const mergedStyle = {...style, ...passthroughStyle};
return (
<Component
{...reducedProps}
{...passthroughProps}
style={mergedStyle}
ref={ref}
/>
);
},
);
AnimatedComponent.displayName = `Animated(${
Component.displayName || 'Anonymous'
})`;
return AnimatedComponent;
}

View File

@@ -0,0 +1,64 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedAddition extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() + this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'addition',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
};
}
}

View File

@@ -0,0 +1,320 @@
/**
* 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
* @format
*/
'use strict';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {NativeColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import normalizeColor from '../../StyleSheet/normalizeColor';
import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedValue, {flushValue} from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedColorConfig = $ReadOnly<{
useNativeDriver: boolean,
}>;
type ColorListenerCallback = (value: ColorValue) => mixed;
export type RgbaValue = {
+r: number,
+g: number,
+b: number,
+a: number,
...
};
type RgbaAnimatedValue = {
+r: AnimatedValue,
+g: AnimatedValue,
+b: AnimatedValue,
+a: AnimatedValue,
...
};
export type InputValue = ?(RgbaValue | RgbaAnimatedValue | ColorValue);
const NativeAnimatedAPI = NativeAnimatedHelper.API;
const defaultColor: RgbaValue = {r: 0, g: 0, b: 0, a: 1.0};
/* eslint no-bitwise: 0 */
function processColor(
color?: ?(ColorValue | RgbaValue),
): ?(RgbaValue | NativeColorValue) {
if (color === undefined || color === null) {
return null;
}
if (isRgbaValue(color)) {
// $FlowIgnore[incompatible-cast] - Type is verified above
return (color: RgbaValue);
}
let normalizedColor: ?ProcessedColorValue = normalizeColor(
// $FlowIgnore[incompatible-cast] - Type is verified above
(color: ColorValue),
);
if (normalizedColor === undefined || normalizedColor === null) {
return null;
}
if (typeof normalizedColor === 'object') {
const processedColorObj: ?NativeColorValue =
processColorObject(normalizedColor);
if (processedColorObj != null) {
return processedColorObj;
}
} else if (typeof normalizedColor === 'number') {
const r: number = (normalizedColor & 0xff000000) >>> 24;
const g: number = (normalizedColor & 0x00ff0000) >>> 16;
const b: number = (normalizedColor & 0x0000ff00) >>> 8;
const a: number = (normalizedColor & 0x000000ff) / 255;
return {r, g, b, a};
}
return null;
}
function isRgbaValue(value: any): boolean {
return (
value &&
typeof value.r === 'number' &&
typeof value.g === 'number' &&
typeof value.b === 'number' &&
typeof value.a === 'number'
);
}
function isRgbaAnimatedValue(value: any): boolean {
return (
value &&
value.r instanceof AnimatedValue &&
value.g instanceof AnimatedValue &&
value.b instanceof AnimatedValue &&
value.a instanceof AnimatedValue
);
}
export default class AnimatedColor extends AnimatedWithChildren {
r: AnimatedValue;
g: AnimatedValue;
b: AnimatedValue;
a: AnimatedValue;
nativeColor: ?NativeColorValue;
_suspendCallbacks: number = 0;
constructor(valueIn?: InputValue, config?: ?AnimatedColorConfig) {
super();
let value: RgbaValue | RgbaAnimatedValue | ColorValue =
valueIn ?? defaultColor;
if (isRgbaAnimatedValue(value)) {
// $FlowIgnore[incompatible-cast] - Type is verified above
const rgbaAnimatedValue: RgbaAnimatedValue = (value: RgbaAnimatedValue);
this.r = rgbaAnimatedValue.r;
this.g = rgbaAnimatedValue.g;
this.b = rgbaAnimatedValue.b;
this.a = rgbaAnimatedValue.a;
} else {
const processedColor: RgbaValue | NativeColorValue =
// $FlowIgnore[incompatible-cast] - Type is verified above
processColor((value: ColorValue | RgbaValue)) ?? defaultColor;
let initColor: RgbaValue = defaultColor;
if (isRgbaValue(processedColor)) {
// $FlowIgnore[incompatible-cast] - Type is verified above
initColor = (processedColor: RgbaValue);
} else {
// $FlowIgnore[incompatible-cast] - Type is verified above
this.nativeColor = (processedColor: NativeColorValue);
}
this.r = new AnimatedValue(initColor.r);
this.g = new AnimatedValue(initColor.g);
this.b = new AnimatedValue(initColor.b);
this.a = new AnimatedValue(initColor.a);
}
if (config?.useNativeDriver) {
this.__makeNative();
}
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*/
setValue(value: RgbaValue | ColorValue): void {
let shouldUpdateNodeConfig = false;
if (this.__isNative) {
const nativeTag = this.__getNativeTag();
NativeAnimatedAPI.setWaitingForIdentifier(nativeTag.toString());
}
const processedColor: RgbaValue | NativeColorValue =
processColor(value) ?? defaultColor;
this._withSuspendedCallbacks(() => {
if (isRgbaValue(processedColor)) {
// $FlowIgnore[incompatible-type] - Type is verified above
const rgbaValue: RgbaValue = processedColor;
this.r.setValue(rgbaValue.r);
this.g.setValue(rgbaValue.g);
this.b.setValue(rgbaValue.b);
this.a.setValue(rgbaValue.a);
if (this.nativeColor != null) {
this.nativeColor = null;
shouldUpdateNodeConfig = true;
}
} else {
// $FlowIgnore[incompatible-type] - Type is verified above
const nativeColor: NativeColorValue = processedColor;
if (this.nativeColor !== nativeColor) {
this.nativeColor = nativeColor;
shouldUpdateNodeConfig = true;
}
}
});
if (this.__isNative) {
const nativeTag = this.__getNativeTag();
if (shouldUpdateNodeConfig) {
NativeAnimatedAPI.updateAnimatedNodeConfig(
nativeTag,
this.__getNativeConfig(),
);
}
NativeAnimatedAPI.unsetWaitingForIdentifier(nativeTag.toString());
} else {
flushValue(this);
}
// $FlowFixMe[incompatible-call]
this.__callListeners(this.__getValue());
}
/**
* Sets an offset that is applied on top of whatever value is set, whether
* via `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*/
setOffset(offset: RgbaValue): void {
this.r.setOffset(offset.r);
this.g.setOffset(offset.g);
this.b.setOffset(offset.b);
this.a.setOffset(offset.a);
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*/
flattenOffset(): void {
this.r.flattenOffset();
this.g.flattenOffset();
this.b.flattenOffset();
this.a.flattenOffset();
}
/**
* Sets the offset value to the base value, and resets the base value to
* zero. The final output of the value is unchanged.
*/
extractOffset(): void {
this.r.extractOffset();
this.g.extractOffset();
this.b.extractOffset();
this.a.extractOffset();
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*/
stopAnimation(callback?: ColorListenerCallback): void {
this.r.stopAnimation();
this.g.stopAnimation();
this.b.stopAnimation();
this.a.stopAnimation();
callback && callback(this.__getValue());
}
/**
* Stops any animation and resets the value to its original.
*/
resetAnimation(callback?: ColorListenerCallback): void {
this.r.resetAnimation();
this.g.resetAnimation();
this.b.resetAnimation();
this.a.resetAnimation();
callback && callback(this.__getValue());
}
__getValue(): ColorValue {
if (this.nativeColor != null) {
return this.nativeColor;
} else {
return `rgba(${this.r.__getValue()}, ${this.g.__getValue()}, ${this.b.__getValue()}, ${this.a.__getValue()})`;
}
}
__attach(): void {
this.r.__addChild(this);
this.g.__addChild(this);
this.b.__addChild(this);
this.a.__addChild(this);
super.__attach();
}
__detach(): void {
this.r.__removeChild(this);
this.g.__removeChild(this);
this.b.__removeChild(this);
this.a.__removeChild(this);
super.__detach();
}
_withSuspendedCallbacks(callback: () => void) {
this._suspendCallbacks++;
callback();
this._suspendCallbacks--;
}
__callListeners(value: number): void {
if (this._suspendCallbacks === 0) {
super.__callListeners(value);
}
}
__makeNative(platformConfig: ?PlatformConfig) {
this.r.__makeNative(platformConfig);
this.g.__makeNative(platformConfig);
this.b.__makeNative(platformConfig);
this.a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getNativeConfig(): {...} {
return {
type: 'color',
r: this.r.__getNativeTag(),
g: this.g.__getNativeTag(),
b: this.b.__getNativeTag(),
a: this.a.__getNativeTag(),
nativeColor: this.nativeColor,
};
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedDiffClamp extends AnimatedWithChildren {
_a: AnimatedNode;
_min: number;
_max: number;
_value: number;
_lastValue: number;
constructor(a: AnimatedNode, min: number, max: number) {
super();
this._a = a;
this._min = min;
this._max = max;
this._value = this._lastValue = this._a.__getValue();
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__getValue(): number {
const value = this._a.__getValue();
const diff = value - this._lastValue;
this._lastValue = value;
this._value = Math.min(Math.max(this._value + diff, this._min), this._max);
return this._value;
}
__attach(): void {
this._a.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'diffclamp',
input: this._a.__getNativeTag(),
min: this._min,
max: this._max,
};
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedNode from './AnimatedNode';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedDivision extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
_warnedAboutDivideByZero: boolean = false;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) {
console.error('Detected potential division by zero in AnimatedDivision');
}
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
const a = this._a.__getValue();
const b = this._b.__getValue();
if (b === 0) {
// Prevent spamming the console/LogBox
if (!this._warnedAboutDivideByZero) {
console.error('Detected division by zero in AnimatedDivision');
this._warnedAboutDivideByZero = true;
}
// Passing infinity/NaN to Fabric will cause a native crash
return 0;
}
this._warnedAboutDivideByZero = false;
return a / b;
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'division',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
};
}
}

View File

@@ -0,0 +1,415 @@
/**
* 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
* @format
*/
/* eslint no-bitwise: 0 */
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from './AnimatedNode';
import normalizeColor from '../../StyleSheet/normalizeColor';
import processColor from '../../StyleSheet/processColor';
import Easing from '../Easing';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
export type InterpolationConfigType<OutputT: number | string> = $ReadOnly<{
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
easing?: (input: number) => number,
extrapolate?: ExtrapolateType,
extrapolateLeft?: ExtrapolateType,
extrapolateRight?: ExtrapolateType,
}>;
/**
* Very handy helper to map input ranges to output ranges with an easing
* function and custom behavior outside of the ranges.
*/
function createNumericInterpolation(
config: InterpolationConfigType<number>,
): (input: number) => number {
const outputRange: $ReadOnlyArray<number> = (config.outputRange: any);
const inputRange = config.inputRange;
const easing = config.easing || Easing.linear;
let extrapolateLeft: ExtrapolateType = 'extend';
if (config.extrapolateLeft !== undefined) {
extrapolateLeft = config.extrapolateLeft;
} else if (config.extrapolate !== undefined) {
extrapolateLeft = config.extrapolate;
}
let extrapolateRight: ExtrapolateType = 'extend';
if (config.extrapolateRight !== undefined) {
extrapolateRight = config.extrapolateRight;
} else if (config.extrapolate !== undefined) {
extrapolateRight = config.extrapolate;
}
return input => {
invariant(
typeof input === 'number',
'Cannot interpolation an input which is not a number',
);
const range = findRange(input, inputRange);
return (interpolate(
input,
inputRange[range],
inputRange[range + 1],
outputRange[range],
outputRange[range + 1],
easing,
extrapolateLeft,
extrapolateRight,
): any);
};
}
function interpolate(
input: number,
inputMin: number,
inputMax: number,
outputMin: number,
outputMax: number,
easing: (input: number) => number,
extrapolateLeft: ExtrapolateType,
extrapolateRight: ExtrapolateType,
) {
let result = input;
// Extrapolate
if (result < inputMin) {
if (extrapolateLeft === 'identity') {
return result;
} else if (extrapolateLeft === 'clamp') {
result = inputMin;
} else if (extrapolateLeft === 'extend') {
// noop
}
}
if (result > inputMax) {
if (extrapolateRight === 'identity') {
return result;
} else if (extrapolateRight === 'clamp') {
result = inputMax;
} else if (extrapolateRight === 'extend') {
// noop
}
}
if (outputMin === outputMax) {
return outputMin;
}
if (inputMin === inputMax) {
if (input <= inputMin) {
return outputMin;
}
return outputMax;
}
// Input Range
if (inputMin === -Infinity) {
result = -result;
} else if (inputMax === Infinity) {
result = result - inputMin;
} else {
result = (result - inputMin) / (inputMax - inputMin);
}
// Easing
result = easing(result);
// Output Range
if (outputMin === -Infinity) {
result = -result;
} else if (outputMax === Infinity) {
result = result + outputMin;
} else {
result = result * (outputMax - outputMin) + outputMin;
}
return result;
}
const numericComponentRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
// Maps string inputs an RGBA color or an array of numeric components
function mapStringToNumericComponents(
input: string,
):
| {isColor: true, components: [number, number, number, number]}
| {isColor: false, components: $ReadOnlyArray<number | string>} {
let normalizedColor = normalizeColor(input);
invariant(
normalizedColor == null || typeof normalizedColor !== 'object',
'PlatformColors are not supported',
);
if (typeof normalizedColor === 'number') {
normalizedColor = normalizedColor || 0;
const r = (normalizedColor & 0xff000000) >>> 24;
const g = (normalizedColor & 0x00ff0000) >>> 16;
const b = (normalizedColor & 0x0000ff00) >>> 8;
const a = (normalizedColor & 0x000000ff) / 255;
return {isColor: true, components: [r, g, b, a]};
} else {
const components: Array<string | number> = [];
let lastMatchEnd = 0;
let match: RegExp$matchResult;
while ((match = (numericComponentRegex.exec(input): any)) != null) {
if (match.index > lastMatchEnd) {
components.push(input.substring(lastMatchEnd, match.index));
}
components.push(parseFloat(match[0]));
lastMatchEnd = match.index + match[0].length;
}
invariant(
components.length > 0,
'outputRange must contain color or value with numeric component',
);
if (lastMatchEnd < input.length) {
components.push(input.substring(lastMatchEnd, input.length));
}
return {isColor: false, components};
}
}
/**
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
function createStringInterpolation(
config: InterpolationConfigType<string>,
): (input: number) => string {
invariant(config.outputRange.length >= 2, 'Bad output range');
const outputRange = config.outputRange.map(mapStringToNumericComponents);
const isColor = outputRange[0].isColor;
if (__DEV__) {
invariant(
outputRange.every(output => output.isColor === isColor),
'All elements of output range should either be a color or a string with numeric components',
);
const firstOutput = outputRange[0].components;
invariant(
outputRange.every(
output => output.components.length === firstOutput.length,
),
'All elements of output range should have the same number of components',
);
invariant(
outputRange.every(output =>
output.components.every(
(component, i) =>
// $FlowIgnoreMe[invalid-compare]
typeof component === 'number' || component === firstOutput[i],
),
),
'All elements of output range should have the same non-numeric components',
);
}
const numericComponents: $ReadOnlyArray<$ReadOnlyArray<number>> =
outputRange.map(output =>
isColor
? // $FlowIgnoreMe[incompatible-call]
output.components
: // $FlowIgnoreMe[incompatible-call]
output.components.filter(c => typeof c === 'number'),
);
const interpolations = numericComponents[0].map((_, i) =>
createNumericInterpolation({
...config,
outputRange: numericComponents.map(components => components[i]),
}),
);
if (!isColor) {
return input => {
const values = interpolations.map(interpolation => interpolation(input));
let i = 0;
return outputRange[0].components
.map(c => (typeof c === 'number' ? values[i++] : c))
.join('');
};
} else {
return input => {
const result = interpolations.map((interpolation, i) => {
const value = interpolation(input);
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
// round the opacity (4th column).
return i < 3 ? Math.round(value) : Math.round(value * 1000) / 1000;
});
return `rgba(${result[0]}, ${result[1]}, ${result[2]}, ${result[3]})`;
};
}
}
function findRange(input: number, inputRange: $ReadOnlyArray<number>) {
let i;
for (i = 1; i < inputRange.length - 1; ++i) {
if (inputRange[i] >= input) {
break;
}
}
return i - 1;
}
function checkValidRanges<OutputT: number | string>(
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
) {
checkInfiniteRange('outputRange', outputRange);
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);
invariant(
inputRange.length === outputRange.length,
'inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length',
);
}
function checkValidInputRange(arr: $ReadOnlyArray<number>) {
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
const message =
'inputRange must be monotonically non-decreasing ' + String(arr);
for (let i = 1; i < arr.length; ++i) {
invariant(arr[i] >= arr[i - 1], message);
}
}
function checkInfiniteRange<OutputT: number | string>(
name: string,
arr: $ReadOnlyArray<OutputT>,
) {
invariant(arr.length >= 2, name + ' must have at least 2 elements');
invariant(
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
/* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
* below this comment, one or both of the operands may be something that
* doesn't cleanly convert to a string, like undefined, null, and object,
* etc. If you really mean this implicit string conversion, you can do
* something like String(myThing) */
// $FlowFixMe[unsafe-addition]
name + 'cannot be ]-infinity;+infinity[ ' + arr,
);
}
export default class AnimatedInterpolation<
OutputT: number | string,
> extends AnimatedWithChildren {
_parent: AnimatedNode;
_config: InterpolationConfigType<OutputT>;
_interpolation: ?(input: number) => OutputT;
constructor(parent: AnimatedNode, config: InterpolationConfigType<OutputT>) {
super();
this._parent = parent;
this._config = config;
if (__DEV__) {
checkValidRanges(config.inputRange, config.outputRange);
// Create interpolation eagerly in dev, so we can signal errors faster
// even when using the native driver
this._getInterpolation();
}
}
_getInterpolation(): number => OutputT {
if (!this._interpolation) {
const config = this._config;
if (config.outputRange && typeof config.outputRange[0] === 'string') {
this._interpolation = (createStringInterpolation((config: any)): any);
} else {
this._interpolation = (createNumericInterpolation((config: any)): any);
}
}
return this._interpolation;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._parent.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): OutputT {
const parentValue: number = this._parent.__getValue();
invariant(
typeof parentValue === 'number',
'Cannot interpolate an input which is not a number.',
);
return this._getInterpolation()(parentValue);
}
interpolate<NewOutputT: number | string>(
config: InterpolationConfigType<NewOutputT>,
): AnimatedInterpolation<NewOutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._parent.__addChild(this);
}
__detach(): void {
this._parent.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
if (__DEV__) {
NativeAnimatedHelper.validateInterpolation(this._config);
}
// Only the `outputRange` can contain strings so we don't need to transform `inputRange` here
let outputRange = this._config.outputRange;
let outputType = null;
if (typeof outputRange[0] === 'string') {
// $FlowIgnoreMe[incompatible-cast]
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
const processedColor = processColor(value);
if (typeof processedColor === 'number') {
outputType = 'color';
return processedColor;
} else {
return NativeAnimatedHelper.transformDataType(value);
}
}): any);
}
return {
inputRange: this._config.inputRange,
outputRange,
outputType,
extrapolateLeft:
this._config.extrapolateLeft || this._config.extrapolate || 'extend',
extrapolateRight:
this._config.extrapolateRight || this._config.extrapolate || 'extend',
type: 'interpolation',
};
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedModulo extends AnimatedWithChildren {
_a: AnimatedNode;
_modulus: number;
constructor(a: AnimatedNode, modulus: number) {
super();
this._a = a;
this._modulus = modulus;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return (
((this._a.__getValue() % this._modulus) + this._modulus) % this._modulus
);
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'modulus',
input: this._a.__getNativeTag(),
modulus: this._modulus,
};
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedMultiplication extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() * this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'multiplication',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
};
}
}

View File

@@ -0,0 +1,197 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import invariant from 'invariant';
const NativeAnimatedAPI = NativeAnimatedHelper.API;
type ValueListenerCallback = (state: {value: number, ...}) => mixed;
let _uniqueId = 1;
// Note(vjeux): this would be better as an interface but flow doesn't
// support them yet
export default class AnimatedNode {
_listeners: {[key: string]: ValueListenerCallback, ...};
_platformConfig: ?PlatformConfig;
__nativeAnimatedValueListener: ?any;
__attach(): void {}
__detach(): void {
this.removeAllListeners();
if (this.__isNative && this.__nativeTag != null) {
NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag);
this.__nativeTag = undefined;
}
}
__getValue(): any {}
__getAnimatedValue(): any {
return this.__getValue();
}
__addChild(child: AnimatedNode) {}
__removeChild(child: AnimatedNode) {}
__getChildren(): $ReadOnlyArray<AnimatedNode> {
return [];
}
/* Methods and props used by native Animated impl */
__isNative: boolean;
__nativeTag: ?number;
__shouldUpdateListenersForNewNativeTag: boolean;
constructor() {
this._listeners = {};
}
__makeNative(platformConfig: ?PlatformConfig): void {
if (!this.__isNative) {
throw new Error('This node cannot be made a "native" animated node');
}
this._platformConfig = platformConfig;
if (this.hasListeners()) {
this._startListeningToNativeValueUpdates();
}
}
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*
* See https://reactnative.dev/docs/animatedvalue#addlistener
*/
addListener(callback: (value: any) => mixed): string {
const id = String(_uniqueId++);
this._listeners[id] = callback;
if (this.__isNative) {
this._startListeningToNativeValueUpdates();
}
return id;
}
/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See https://reactnative.dev/docs/animatedvalue#removelistener
*/
removeListener(id: string): void {
delete this._listeners[id];
if (this.__isNative && !this.hasListeners()) {
this._stopListeningForNativeValueUpdates();
}
}
/**
* Remove all registered listeners.
*
* See https://reactnative.dev/docs/animatedvalue#removealllisteners
*/
removeAllListeners(): void {
this._listeners = {};
if (this.__isNative) {
this._stopListeningForNativeValueUpdates();
}
}
hasListeners(): boolean {
return !!Object.keys(this._listeners).length;
}
_startListeningToNativeValueUpdates() {
if (
this.__nativeAnimatedValueListener &&
!this.__shouldUpdateListenersForNewNativeTag
) {
return;
}
if (this.__shouldUpdateListenersForNewNativeTag) {
this.__shouldUpdateListenersForNewNativeTag = false;
this._stopListeningForNativeValueUpdates();
}
NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag());
this.__nativeAnimatedValueListener =
NativeAnimatedHelper.nativeEventEmitter.addListener(
'onAnimatedValueUpdate',
data => {
if (data.tag !== this.__getNativeTag()) {
return;
}
this.__onAnimatedValueUpdateReceived(data.value);
},
);
}
__onAnimatedValueUpdateReceived(value: number) {
this.__callListeners(value);
}
__callListeners(value: number): void {
for (const key in this._listeners) {
this._listeners[key]({value});
}
}
_stopListeningForNativeValueUpdates() {
if (!this.__nativeAnimatedValueListener) {
return;
}
this.__nativeAnimatedValueListener.remove();
this.__nativeAnimatedValueListener = null;
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag());
}
__getNativeTag(): number {
NativeAnimatedHelper.assertNativeAnimatedModule();
invariant(
this.__isNative,
'Attempt to get native tag from node not marked as "native"',
);
const nativeTag =
this.__nativeTag ?? NativeAnimatedHelper.generateNewNodeTag();
if (this.__nativeTag == null) {
this.__nativeTag = nativeTag;
const config = this.__getNativeConfig();
if (this._platformConfig) {
config.platformConfig = this._platformConfig;
}
NativeAnimatedHelper.API.createAnimatedNode(nativeTag, config);
this.__shouldUpdateListenersForNewNativeTag = true;
}
return nativeTag;
}
__getNativeConfig(): Object {
throw new Error(
'This JS animated node type cannot be used as native animated node',
);
}
toJSON(): any {
return this.__getValue();
}
__getPlatformConfig(): ?PlatformConfig {
return this._platformConfig;
}
__setPlatformConfig(platformConfig: ?PlatformConfig) {
this._platformConfig = platformConfig;
}
}

View File

@@ -0,0 +1,146 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
import * as React from 'react';
const MAX_DEPTH = 5;
function isPlainObject(value: any): boolean {
return (
value !== null &&
typeof value === 'object' &&
Object.getPrototypeOf(value).isPrototypeOf(Object)
);
}
// Recurse through values, executing fn for any AnimatedNodes
function visit(value: any, fn: any => void, depth: number = 0): void {
if (depth >= MAX_DEPTH) {
return;
}
if (value instanceof AnimatedNode) {
fn(value);
} else if (Array.isArray(value)) {
value.forEach(element => {
visit(element, fn, depth + 1);
});
} else if (isPlainObject(value)) {
Object.values(value).forEach(element => {
visit(element, fn, depth + 1);
});
}
}
// Returns a copy of value with a transformation fn applied to any AnimatedNodes
function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any {
if (depth >= MAX_DEPTH) {
return value;
}
if (value instanceof AnimatedNode) {
return fn(value);
} else if (Array.isArray(value)) {
return value.map(element => mapAnimatedNodes(element, fn, depth + 1));
} else if (isPlainObject(value)) {
const result: {[string]: any} = {};
for (const key in value) {
result[key] = mapAnimatedNodes(value[key], fn, depth + 1);
}
return result;
} else {
return value;
}
}
export function hasAnimatedNode(value: any, depth: number = 0): boolean {
if (depth >= MAX_DEPTH) {
return false;
}
if (value instanceof AnimatedNode) {
return true;
} else if (Array.isArray(value)) {
for (const element of value) {
if (hasAnimatedNode(element, depth + 1)) {
return true;
}
}
} else if (isPlainObject(value)) {
// Don't consider React elements
if (React.isValidElement(value)) {
return false;
}
for (const key in value) {
if (hasAnimatedNode(value[key], depth + 1)) {
return true;
}
}
}
return false;
}
export default class AnimatedObject extends AnimatedWithChildren {
_value: any;
constructor(value: any) {
super();
this._value = value;
}
__getValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getValue();
});
}
__getAnimatedValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getAnimatedValue();
});
}
__attach(): void {
super.__attach();
visit(this._value, node => {
node.__addChild(this);
});
}
__detach(): void {
visit(this._value, node => {
node.__removeChild(this);
});
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig): void {
visit(this._value, value => {
value.__makeNative(platformConfig);
});
super.__makeNative(platformConfig);
}
__getNativeConfig(): any {
return {
type: 'object',
value: mapAnimatedNodes(this._value, node => {
return {nodeTag: node.__getNativeTag()};
}),
};
}
}

View File

@@ -0,0 +1,186 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
import {AnimatedEvent} from '../AnimatedEvent';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
import AnimatedObject, {hasAnimatedNode} from './AnimatedObject';
import AnimatedStyle from './AnimatedStyle';
import invariant from 'invariant';
function createAnimatedProps(inputProps: Object): Object {
const props: Object = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
props[key] = new AnimatedStyle(value);
} else if (value instanceof AnimatedNode) {
props[key] = value;
} else if (hasAnimatedNode(value)) {
props[key] = new AnimatedObject(value);
} else {
props[key] = value;
}
}
return props;
}
export default class AnimatedProps extends AnimatedNode {
_props: Object;
_animatedView: any;
_callback: () => void;
constructor(props: Object, callback: () => void) {
super();
this._props = createAnimatedProps(props);
this._callback = callback;
}
__getValue(): Object {
const props: {[string]: any | ((...args: any) => void)} = {};
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
props[key] = value.__getValue();
} else if (value instanceof AnimatedEvent) {
props[key] = value.__getHandler();
} else {
props[key] = value;
}
}
return props;
}
__getAnimatedValue(): Object {
const props: {[string]: any} = {};
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
props[key] = value.__getAnimatedValue();
}
}
return props;
}
__attach(): void {
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
value.__addChild(this);
}
}
}
__detach(): void {
if (this.__isNative && this._animatedView) {
this.__disconnectAnimatedView();
}
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
value.__removeChild(this);
}
}
super.__detach();
}
update(): void {
this._callback();
}
__makeNative(platformConfig: ?PlatformConfig): void {
for (const key in this._props) {
const value = this._props[key];
if (value instanceof AnimatedNode) {
value.__makeNative(platformConfig);
}
}
if (!this.__isNative) {
this.__isNative = true;
// Since this does not call the super.__makeNative, we need to store the
// supplied platformConfig here, before calling __connectAnimatedView
// where it will be needed to traverse the graph of attached values.
super.__setPlatformConfig(platformConfig);
if (this._animatedView) {
this.__connectAnimatedView();
}
}
}
setNativeView(animatedView: any): void {
if (this._animatedView === animatedView) {
return;
}
this._animatedView = animatedView;
if (this.__isNative) {
this.__connectAnimatedView();
}
}
__connectAnimatedView(): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
const nativeViewTag: ?number = findNodeHandle(this._animatedView);
invariant(
nativeViewTag != null,
'Unable to locate attached view in the native tree',
);
NativeAnimatedHelper.API.connectAnimatedNodeToView(
this.__getNativeTag(),
nativeViewTag,
);
}
__disconnectAnimatedView(): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
const nativeViewTag: ?number = findNodeHandle(this._animatedView);
invariant(
nativeViewTag != null,
'Unable to locate attached view in the native tree',
);
NativeAnimatedHelper.API.disconnectAnimatedNodeFromView(
this.__getNativeTag(),
nativeViewTag,
);
}
__restoreDefaultValues(): void {
// When using the native driver, view properties need to be restored to
// their default values manually since react no longer tracks them. This
// is needed to handle cases where a prop driven by native animated is removed
// after having been changed natively by an animation.
if (this.__isNative) {
NativeAnimatedHelper.API.restoreDefaultValues(this.__getNativeTag());
}
}
__getNativeConfig(): Object {
const propsConfig: {[string]: number} = {};
for (const propKey in this._props) {
const value = this._props[propKey];
if (value instanceof AnimatedNode) {
value.__makeNative(this.__getPlatformConfig());
propsConfig[propKey] = value.__getNativeTag();
}
}
return {
type: 'props',
props: propsConfig,
};
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import flattenStyle from '../../StyleSheet/flattenStyle';
import Platform from '../../Utilities/Platform';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
import AnimatedObject, {hasAnimatedNode} from './AnimatedObject';
import AnimatedTransform from './AnimatedTransform';
import AnimatedWithChildren from './AnimatedWithChildren';
function createAnimatedStyle(
inputStyle: any,
keepUnanimatedValues: boolean,
): Object {
// $FlowFixMe[underconstrained-implicit-instantiation]
const style = flattenStyle(inputStyle);
const animatedStyles: any = {};
for (const key in style) {
const value = style[key];
if (value != null && key === 'transform') {
animatedStyles[key] =
ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
? new AnimatedObject(value)
: new AnimatedTransform(value);
} else if (value instanceof AnimatedNode) {
animatedStyles[key] = value;
} else if (hasAnimatedNode(value)) {
animatedStyles[key] = new AnimatedObject(value);
} else if (keepUnanimatedValues) {
animatedStyles[key] = value;
}
}
return animatedStyles;
}
export default class AnimatedStyle extends AnimatedWithChildren {
_inputStyle: any;
_style: Object;
constructor(style: any) {
super();
this._inputStyle = style;
this._style = createAnimatedStyle(style, Platform.OS !== 'web');
}
__getValue(): Object | Array<Object> {
const result: {[string]: any} = {};
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
result[key] = value.__getValue();
} else {
result[key] = value;
}
}
return Platform.OS === 'web' ? [this._inputStyle, result] : result;
}
__getAnimatedValue(): Object {
const result: {[string]: any} = {};
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
result[key] = value.__getAnimatedValue();
}
}
return result;
}
__attach(): void {
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
value.__addChild(this);
}
}
}
__detach(): void {
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
value.__removeChild(this);
}
}
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig) {
for (const key in this._style) {
const value = this._style[key];
if (value instanceof AnimatedNode) {
value.__makeNative(platformConfig);
}
}
super.__makeNative(platformConfig);
}
__getNativeConfig(): Object {
const styleConfig: {[string]: ?number} = {};
for (const styleKey in this._style) {
if (this._style[styleKey] instanceof AnimatedNode) {
const style = this._style[styleKey];
style.__makeNative(this.__getPlatformConfig());
styleConfig[styleKey] = style.__getNativeTag();
}
// Non-animated styles are set using `setNativeProps`, no need
// to pass those as a part of the node config
}
NativeAnimatedHelper.validateStyles(styleConfig);
return {
type: 'style',
style: styleConfig,
};
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedSubtraction extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(a: AnimatedNode | number, b: AnimatedNode | number) {
super();
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() - this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'subtraction',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
};
}
}

View File

@@ -0,0 +1,100 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {EndCallback} from '../animations/Animation';
import type AnimatedValue from './AnimatedValue';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
export default class AnimatedTracking extends AnimatedNode {
_value: AnimatedValue;
_parent: AnimatedNode;
_callback: ?EndCallback;
_animationConfig: Object;
_animationClass: any;
_useNativeDriver: boolean;
constructor(
value: AnimatedValue,
parent: AnimatedNode,
animationClass: any,
animationConfig: Object,
callback?: ?EndCallback,
) {
super();
this._value = value;
this._parent = parent;
this._animationClass = animationClass;
this._animationConfig = animationConfig;
this._useNativeDriver =
NativeAnimatedHelper.shouldUseNativeDriver(animationConfig);
this._callback = callback;
this.__attach();
}
__makeNative(platformConfig: ?PlatformConfig) {
this.__isNative = true;
this._parent.__makeNative(platformConfig);
super.__makeNative(platformConfig);
this._value.__makeNative(platformConfig);
}
__getValue(): Object {
return this._parent.__getValue();
}
__attach(): void {
this._parent.__addChild(this);
if (this._useNativeDriver) {
// when the tracking starts we need to convert this node to a "native node"
// so that the parent node will be made "native" too. This is necessary as
// if we don't do this `update` method will get called. At that point it
// may be too late as it would mean the JS driver has already started
// updating node values
let {platformConfig} = this._animationConfig;
this.__makeNative(platformConfig);
}
}
__detach(): void {
this._parent.__removeChild(this);
super.__detach();
}
update(): void {
this._value.animate(
new this._animationClass({
...this._animationConfig,
toValue: (this._animationConfig.toValue: any).__getValue(),
}),
this._callback,
);
}
__getNativeConfig(): any {
const animation = new this._animationClass({
...this._animationConfig,
// remove toValue from the config as it's a ref to Animated.Value
toValue: undefined,
});
const animationConfig = animation.__getNativeAnimationConfig();
return {
type: 'tracking',
animationId: NativeAnimatedHelper.generateNewAnimationId(),
animationConfig,
toValue: this._parent.__getNativeTag(),
value: this._value.__getNativeTag(),
};
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedTransform extends AnimatedWithChildren {
_transforms: $ReadOnlyArray<Object>;
constructor(transforms: $ReadOnlyArray<Object>) {
super();
this._transforms = transforms;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._transforms.forEach(transform => {
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
value.__makeNative(platformConfig);
}
}
});
super.__makeNative(platformConfig);
}
__getValue(): $ReadOnlyArray<Object> {
return this._get(animatedNode => animatedNode.__getValue());
}
__getAnimatedValue(): $ReadOnlyArray<Object> {
return this._get(animatedNode => animatedNode.__getAnimatedValue());
}
__attach(): void {
this._transforms.forEach(transform => {
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
value.__addChild(this);
}
}
});
}
__detach(): void {
this._transforms.forEach(transform => {
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
value.__removeChild(this);
}
}
});
super.__detach();
}
__getNativeConfig(): any {
const transConfigs: Array<any> = [];
this._transforms.forEach(transform => {
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
transConfigs.push({
type: 'animated',
property: key,
nodeTag: value.__getNativeTag(),
});
} else {
transConfigs.push({
type: 'static',
property: key,
value: NativeAnimatedHelper.transformDataType(value),
});
}
}
});
NativeAnimatedHelper.validateTransform(transConfigs);
return {
type: 'transform',
transforms: transConfigs,
};
}
_get(getter: AnimatedNode => any): $ReadOnlyArray<Object> {
return this._transforms.map(transform => {
const result: {[string]: any} = {};
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
result[key] = getter(value);
} else if (Array.isArray(value)) {
result[key] = value.map(element => {
if (element instanceof AnimatedNode) {
return getter(element);
} else {
return element;
}
});
} else if (typeof value === 'object') {
result[key] = {};
for (const [nestedKey, nestedValue] of Object.entries(value)) {
if (nestedValue instanceof AnimatedNode) {
result[key][nestedKey] = getter(nestedValue);
} else {
result[key][nestedKey] = nestedValue;
}
}
} else {
result[key] = value;
}
}
return result;
});
}
}

View File

@@ -0,0 +1,302 @@
/**
* 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
* @format
*/
'use strict';
import type Animation, {EndCallback} from '../animations/Animation';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type AnimatedTracking from './AnimatedTracking';
import InteractionManager from '../../Interaction/InteractionManager';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedValueConfig = $ReadOnly<{
useNativeDriver: boolean,
}>;
const NativeAnimatedAPI = NativeAnimatedHelper.API;
/**
* Animated works by building a directed acyclic graph of dependencies
* transparently when you render your Animated components.
*
* new Animated.Value(0)
* .interpolate() .interpolate() new Animated.Value(1)
* opacity translateY scale
* style transform
* View#234 style
* View#123
*
* A) Top Down phase
* When an Animated.Value is updated, we recursively go down through this
* graph in order to find leaf nodes: the views that we flag as needing
* an update.
*
* B) Bottom Up phase
* When a view is flagged as needing an update, we recursively go back up
* in order to build the new value that it needs. The reason why we need
* this two-phases process is to deal with composite props such as
* transform which can receive values from multiple parents.
*/
export function flushValue(rootNode: AnimatedNode): void {
const leaves = new Set<{update: () => void, ...}>();
function findAnimatedStyles(node: AnimatedNode) {
// $FlowFixMe[prop-missing]
if (typeof node.update === 'function') {
leaves.add((node: any));
} else {
node.__getChildren().forEach(findAnimatedStyles);
}
}
findAnimatedStyles(rootNode);
leaves.forEach(leaf => leaf.update());
}
/**
* Some operations are executed only on batch end, which is _mostly_ scheduled when
* Animated component props change. For some of the changes which require immediate execution
* (e.g. setValue), we create a separate batch in case none is scheduled.
*/
function _executeAsAnimatedBatch(id: string, operation: () => void) {
NativeAnimatedAPI.setWaitingForIdentifier(id);
operation();
NativeAnimatedAPI.unsetWaitingForIdentifier(id);
}
/**
* Standard value for driving animations. One `Animated.Value` can drive
* multiple properties in a synchronized fashion, but can only be driven by one
* mechanism at a time. Using a new mechanism (e.g. starting a new animation,
* or calling `setValue`) will stop any previous ones.
*
* See https://reactnative.dev/docs/animatedvalue
*/
export default class AnimatedValue extends AnimatedWithChildren {
_value: number;
_startingValue: number;
_offset: number;
_animation: ?Animation;
_tracking: ?AnimatedTracking;
constructor(value: number, config?: ?AnimatedValueConfig) {
super();
if (typeof value !== 'number') {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
this._startingValue = this._value = value;
this._offset = 0;
this._animation = null;
if (config && config.useNativeDriver) {
this.__makeNative();
}
}
__detach() {
if (this.__isNative) {
NativeAnimatedAPI.getValue(this.__getNativeTag(), value => {
this._value = value - this._offset;
});
}
this.stopAnimation();
super.__detach();
}
__getValue(): number {
return this._value + this._offset;
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*
* See https://reactnative.dev/docs/animatedvalue#setvalue
*/
setValue(value: number): void {
if (this._animation) {
this._animation.stop();
this._animation = null;
}
this._updateValue(
value,
!this.__isNative /* don't perform a flush for natively driven values */,
);
if (this.__isNative) {
_executeAsAnimatedBatch(this.__getNativeTag().toString(), () =>
NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value),
);
}
}
/**
* Sets an offset that is applied on top of whatever value is set, whether via
* `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*
* See https://reactnative.dev/docs/animatedvalue#setoffset
*/
setOffset(offset: number): void {
this._offset = offset;
if (this.__isNative) {
NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset);
}
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvalue#flattenoffset
*/
flattenOffset(): void {
this._value += this._offset;
this._offset = 0;
if (this.__isNative) {
NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag());
}
}
/**
* Sets the offset value to the base value, and resets the base value to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvalue#extractoffset
*/
extractOffset(): void {
this._offset += this._value;
this._value = 0;
if (this.__isNative) {
NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag());
}
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*
* See https://reactnative.dev/docs/animatedvalue#stopanimation
*/
stopAnimation(callback?: ?(value: number) => void): void {
this.stopTracking();
this._animation && this._animation.stop();
this._animation = null;
if (callback) {
if (this.__isNative) {
NativeAnimatedAPI.getValue(this.__getNativeTag(), callback);
} else {
callback(this.__getValue());
}
}
}
/**
* Stops any animation and resets the value to its original.
*
* See https://reactnative.dev/docs/animatedvalue#resetanimation
*/
resetAnimation(callback?: ?(value: number) => void): void {
this.stopAnimation(callback);
this._value = this._startingValue;
if (this.__isNative) {
NativeAnimatedAPI.setAnimatedNodeValue(
this.__getNativeTag(),
this._startingValue,
);
}
}
__onAnimatedValueUpdateReceived(value: number): void {
this._updateValue(value, false /*flush*/);
}
/**
* Interpolates the value before updating the property, e.g. mapping 0-1 to
* 0-10.
*/
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
/**
* Typically only used internally, but could be used by a custom Animation
* class.
*
* See https://reactnative.dev/docs/animatedvalue#animate
*/
animate(animation: Animation, callback: ?EndCallback): void {
let handle = null;
if (animation.__isInteraction) {
handle = InteractionManager.createInteractionHandle();
}
const previousAnimation = this._animation;
this._animation && this._animation.stop();
this._animation = animation;
animation.start(
this._value,
value => {
// Natively driven animations will never call into that callback, therefore we can always
// pass flush = true to allow the updated value to propagate to native with setNativeProps
this._updateValue(value, true /* flush */);
},
result => {
this._animation = null;
if (handle !== null) {
InteractionManager.clearInteractionHandle(handle);
}
callback && callback(result);
},
previousAnimation,
this,
);
}
/**
* Typically only used internally.
*/
stopTracking(): void {
this._tracking && this._tracking.__detach();
this._tracking = null;
}
/**
* Typically only used internally.
*/
track(tracking: AnimatedTracking): void {
this.stopTracking();
this._tracking = tracking;
// Make sure that the tracking animation starts executing
this._tracking && this._tracking.update();
}
_updateValue(value: number, flush: boolean): void {
if (value === undefined) {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
this._value = value;
if (flush) {
flushValue(this);
}
this.__callListeners(this.__getValue());
}
__getNativeConfig(): Object {
return {
type: 'value',
value: this._value,
offset: this._offset,
};
}
}

View File

@@ -0,0 +1,236 @@
/**
* 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
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
export type AnimatedValueXYConfig = $ReadOnly<{
useNativeDriver: boolean,
}>;
type ValueXYListenerCallback = (value: {x: number, y: number, ...}) => mixed;
let _uniqueId = 1;
/**
* 2D Value for driving 2D animations, such as pan gestures. Almost identical
* API to normal `Animated.Value`, but multiplexed.
*
* See https://reactnative.dev/docs/animatedvaluexy
*/
export default class AnimatedValueXY extends AnimatedWithChildren {
x: AnimatedValue;
y: AnimatedValue;
_listeners: {
[key: string]: {
x: string,
y: string,
...
},
...
};
constructor(
valueIn?: ?{
+x: number | AnimatedValue,
+y: number | AnimatedValue,
...
},
config?: ?AnimatedValueXYConfig,
) {
super();
const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any`
if (typeof value.x === 'number' && typeof value.y === 'number') {
this.x = new AnimatedValue(value.x);
this.y = new AnimatedValue(value.y);
} else {
invariant(
value.x instanceof AnimatedValue && value.y instanceof AnimatedValue,
'AnimatedValueXY must be initialized with an object of numbers or ' +
'AnimatedValues.',
);
this.x = value.x;
this.y = value.y;
}
this._listeners = {};
if (config && config.useNativeDriver) {
this.__makeNative();
}
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*
* See https://reactnative.dev/docs/animatedvaluexy#setvalue
*/
setValue(value: {x: number, y: number, ...}) {
this.x.setValue(value.x);
this.y.setValue(value.y);
}
/**
* Sets an offset that is applied on top of whatever value is set, whether
* via `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*
* See https://reactnative.dev/docs/animatedvaluexy#setoffset
*/
setOffset(offset: {x: number, y: number, ...}) {
this.x.setOffset(offset.x);
this.y.setOffset(offset.y);
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvaluexy#flattenoffset
*/
flattenOffset(): void {
this.x.flattenOffset();
this.y.flattenOffset();
}
/**
* Sets the offset value to the base value, and resets the base value to
* zero. The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvaluexy#extractoffset
*/
extractOffset(): void {
this.x.extractOffset();
this.y.extractOffset();
}
__getValue(): {
x: number,
y: number,
...
} {
return {
x: this.x.__getValue(),
y: this.y.__getValue(),
};
}
/**
* Stops any animation and resets the value to its original.
*
* See https://reactnative.dev/docs/animatedvaluexy#resetanimation
*/
resetAnimation(
callback?: (value: {x: number, y: number, ...}) => void,
): void {
this.x.resetAnimation();
this.y.resetAnimation();
callback && callback(this.__getValue());
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*
* See https://reactnative.dev/docs/animatedvaluexy#stopanimation
*/
stopAnimation(callback?: (value: {x: number, y: number, ...}) => void): void {
this.x.stopAnimation();
this.y.stopAnimation();
callback && callback(this.__getValue());
}
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to synchronously read
* the value because it might be driven natively.
*
* Returns a string that serves as an identifier for the listener.
*
* See https://reactnative.dev/docs/animatedvaluexy#addlistener
*/
addListener(callback: ValueXYListenerCallback): string {
const id = String(_uniqueId++);
const jointCallback = ({value: number}: any) => {
callback(this.__getValue());
};
this._listeners[id] = {
x: this.x.addListener(jointCallback),
y: this.y.addListener(jointCallback),
};
return id;
}
/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See https://reactnative.dev/docs/animatedvaluexy#removelistener
*/
removeListener(id: string): void {
this.x.removeListener(this._listeners[id].x);
this.y.removeListener(this._listeners[id].y);
delete this._listeners[id];
}
/**
* Remove all registered listeners.
*
* See https://reactnative.dev/docs/animatedvaluexy#removealllisteners
*/
removeAllListeners(): void {
this.x.removeAllListeners();
this.y.removeAllListeners();
this._listeners = {};
}
/**
* Converts `{x, y}` into `{left, top}` for use in style.
*
* See https://reactnative.dev/docs/animatedvaluexy#getlayout
*/
getLayout(): {[key: string]: AnimatedValue, ...} {
return {
left: this.x,
top: this.y,
};
}
/**
* Converts `{x, y}` into a useable translation transform.
*
* See https://reactnative.dev/docs/animatedvaluexy#gettranslatetransform
*/
getTranslateTransform(): Array<{[key: string]: AnimatedValue, ...}> {
return [{translateX: this.x}, {translateY: this.y}];
}
__attach(): void {
this.x.__addChild(this);
this.y.__addChild(this);
super.__attach();
}
__detach(): void {
this.x.__removeChild(this);
this.y.__removeChild(this);
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig) {
this.x.__makeNative(platformConfig);
this.y.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import NativeAnimatedHelper from '../NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
export default class AnimatedWithChildren extends AnimatedNode {
_children: Array<AnimatedNode>;
constructor() {
super();
this._children = [];
}
__makeNative(platformConfig: ?PlatformConfig) {
if (!this.__isNative) {
this.__isNative = true;
for (const child of this._children) {
child.__makeNative(platformConfig);
NativeAnimatedHelper.API.connectAnimatedNodes(
this.__getNativeTag(),
child.__getNativeTag(),
);
}
}
super.__makeNative(platformConfig);
}
__addChild(child: AnimatedNode): void {
if (this._children.length === 0) {
this.__attach();
}
this._children.push(child);
if (this.__isNative) {
// Only accept "native" animated nodes as children
child.__makeNative(this.__getPlatformConfig());
NativeAnimatedHelper.API.connectAnimatedNodes(
this.__getNativeTag(),
child.__getNativeTag(),
);
}
}
__removeChild(child: AnimatedNode): void {
const index = this._children.indexOf(child);
if (index === -1) {
console.warn("Trying to remove a child that doesn't exist");
return;
}
if (this.__isNative && child.__isNative) {
NativeAnimatedHelper.API.disconnectAnimatedNodes(
this.__getNativeTag(),
child.__getNativeTag(),
);
}
this._children.splice(index, 1);
if (this._children.length === 0) {
this.__detach();
}
}
__getChildren(): $ReadOnlyArray<AnimatedNode> {
return this._children;
}
__callListeners(value: number): void {
super.__callListeners(value);
if (!this.__isNative) {
for (const child of this._children) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
if (child.__getValue) {
child.__callListeners(child.__getValue());
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
/**
* 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 Platform from '../Utilities/Platform';
function shouldUseTurboAnimatedModule(): boolean {
return Platform.OS === 'ios' && global.RN$Bridgeless === true;
}
export default shouldUseTurboAnimatedModule;

View File

@@ -0,0 +1,220 @@
/**
* 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
*/
'use strict';
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils';
import useRefEffect from '../Utilities/useRefEffect';
import {AnimatedEvent} from './AnimatedEvent';
import NativeAnimatedHelper from './NativeAnimatedHelper';
import AnimatedProps from './nodes/AnimatedProps';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
} from 'react';
type ReducedProps<TProps> = {
...TProps,
collapsable: boolean,
...
};
type CallbackRef<T> = T => mixed;
export default function useAnimatedProps<TProps: {...}, TInstance>(
props: TProps,
): [ReducedProps<TProps>, CallbackRef<TInstance | null>] {
const [, scheduleUpdate] = useReducer<number, void>(count => count + 1, 0);
const onUpdateRef = useRef<?() => void>(null);
const timerRef = useRef<TimeoutID | null>(null);
// TODO: Only invalidate `node` if animated props or `style` change. In the
// previous implementation, we permitted `style` to override props with the
// same name property name as styles, so we can probably continue doing that.
// The ordering of other props *should* not matter.
const node = useMemo(
() => new AnimatedProps(props, () => onUpdateRef.current?.()),
[props],
);
const useNativePropsInFabric =
ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
useAnimatedPropsLifecycle(node);
// TODO: This "effect" does three things:
//
// 1) Call `setNativeView`.
// 2) Update `onUpdateRef`.
// 3) Update listeners for `AnimatedEvent` props.
//
// Ideally, each of these would be separate "effects" so that they are not
// unnecessarily re-run when irrelevant dependencies change. For example, we
// should be able to hoist all `AnimatedEvent` props and only do #3 if either
// the `AnimatedEvent` props change or `instance` changes.
//
// But there is no way to transparently compose three separate callback refs,
// so we just combine them all into one for now.
const refEffect = useCallback(
(instance: TInstance) => {
// NOTE: This may be called more often than necessary (e.g. when `props`
// changes), but `setNativeView` already optimizes for that.
node.setNativeView(instance);
// NOTE: When using the JS animation driver, this callback is called on
// every animation frame. When using the native driver, this callback is
// called when the animation completes.
onUpdateRef.current = () => {
if (
process.env.NODE_ENV === 'test' ||
typeof instance !== 'object' ||
typeof instance?.setNativeProps !== 'function' ||
(isFabricInstance(instance) && !useNativePropsInFabric)
) {
// Schedule an update for this component to update `reducedProps`,
// but do not compute it immediately. If a parent also updated, we
// need to merge those new props in before updating.
scheduleUpdate();
} else if (!node.__isNative) {
// $FlowIgnore[not-a-function] - Assume it's still a function.
// $FlowFixMe[incompatible-use]
instance.setNativeProps(node.__getAnimatedValue());
if (isFabricInstance(instance)) {
// Keeping state of Fiber tree and Shadow tree in sync.
//
// This is done by calling `scheduleUpdate` which will trigger a commit.
// However, React commit is not fast enough to drive animations.
// This is where setNativeProps comes in handy but the state between
// Fiber tree and Shadow tree needs to be kept in sync.
// The goal is to call `scheduleUpdate` as little as possible to maintain
// performance but frequently enough to keep state in sync.
// Debounce is set to 48ms, which is 3 * the duration of a frame.
// 3 frames was the highest value where flickering state was not observed.
if (timerRef.current != null) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
timerRef.current = null;
scheduleUpdate();
}, 48);
}
}
};
const target = getEventTarget(instance);
const events = [];
for (const propName in props) {
const propValue = props[propName];
if (propValue instanceof AnimatedEvent && propValue.__isNative) {
propValue.__attach(target, propName);
events.push([propName, propValue]);
}
}
return () => {
onUpdateRef.current = null;
for (const [propName, propValue] of events) {
propValue.__detach(target, propName);
}
};
},
[props, node, useNativePropsInFabric],
);
const callbackRef = useRefEffect<TInstance>(refEffect);
return [reduceAnimatedProps<TProps>(node), callbackRef];
}
function reduceAnimatedProps<TProps>(
node: AnimatedProps,
): ReducedProps<TProps> {
// Force `collapsable` to be false so that the native view is not flattened.
// Flattened views cannot be accurately referenced by the native driver.
return {
...node.__getValue(),
collapsable: false,
};
}
/**
* Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach`
* and `__detach`. However, this is more complicated because `AnimatedProps`
* uses reference counting to determine when to recursively detach its children
* nodes. So in order to optimize this, we avoid detaching until the next attach
* unless we are unmounting.
*/
function useAnimatedPropsLifecycle(node: AnimatedProps): void {
const prevNodeRef = useRef<?AnimatedProps>(null);
const isUnmountingRef = useRef<boolean>(false);
useEffect(() => {
// It is ok for multiple components to call `flushQueue` because it noops
// if the queue is empty. When multiple animated components are mounted at
// the same time. Only first component flushes the queue and the others will noop.
NativeAnimatedHelper.API.flushQueue();
});
useLayoutEffect(() => {
isUnmountingRef.current = false;
return () => {
isUnmountingRef.current = true;
};
}, []);
useLayoutEffect(() => {
node.__attach();
if (prevNodeRef.current != null) {
const prevNode = prevNodeRef.current;
// TODO: Stop restoring default values (unless `reset` is called).
prevNode.__restoreDefaultValues();
prevNode.__detach();
prevNodeRef.current = null;
}
return () => {
if (isUnmountingRef.current) {
// NOTE: Do not restore default values on unmount, see D18197735.
node.__detach();
} else {
prevNodeRef.current = node;
}
};
}, [node]);
}
function getEventTarget<TInstance>(instance: TInstance): TInstance {
return typeof instance === 'object' &&
typeof instance?.getScrollableNode === 'function'
? // $FlowFixMe[incompatible-use] - Legacy instance assumptions.
instance.getScrollableNode()
: instance;
}
// $FlowFixMe[unclear-type] - Legacy instance assumptions.
function isFabricInstance(instance: any): boolean {
return (
isFabricPublicInstance(instance) ||
// Some components have a setNativeProps function but aren't a host component
// such as lists like FlatList and SectionList. These should also use
// forceUpdate in Fabric since setNativeProps doesn't exist on the underlying
// host component. This crazy hack is essentially special casing those lists and
// ScrollView itself to use forceUpdate in Fabric.
// If these components end up using forwardRef then these hacks can go away
// as instance would actually be the underlying host component and the above check
// would be sufficient.
isFabricPublicInstance(instance?.getNativeScrollRef?.()) ||
isFabricPublicInstance(
instance?.getScrollResponder?.()?.getNativeScrollRef?.(),
)
);
}

View File

@@ -0,0 +1,15 @@
/**
* 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.
*
* @format
*/
import type {Animated} from './Animated';
export function useAnimatedValue(
initialValue: number,
config?: Animated.AnimatedConfig,
): Animated.Value;

View File

@@ -0,0 +1,25 @@
/**
* 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 {AnimatedValueConfig} from './nodes/AnimatedValue';
import Animated from './Animated';
import {useRef} from 'react';
export default function useAnimatedValue(
initialValue: number,
config?: ?AnimatedValueConfig,
): Animated.Value {
const ref = useRef<null | Animated.Value>(null);
if (ref.current == null) {
ref.current = new Animated.Value(initialValue, config);
}
return ref.current;
}

View File

@@ -0,0 +1,162 @@
/*
* 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.
*/
#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>
#import "RCTRootViewFactory.h"
@class RCTBridge;
@protocol RCTBridgeDelegate;
@protocol RCTComponentViewProtocol;
@class RCTRootView;
@class RCTSurfacePresenterBridgeAdapter;
NS_ASSUME_NONNULL_BEGIN
/**
* The RCTAppDelegate is an utility class that implements some base configurations for all the React Native apps.
* It is not mandatory to use it, but it could simplify your AppDelegate code.
*
* To use it, you just need to make your AppDelegate a subclass of RCTAppDelegate:
*
* ```objc
* #import <React/RCTAppDelegate.h>
* @interface AppDelegate: RCTAppDelegate
* @end
* ```
*
* All the methods implemented by the RCTAppDelegate can be overridden by your AppDelegate if you need to provide a
custom implementation.
* If you need to customize the default implementation, you can invoke `[super <method_name>]` and use the returned
object.
*
* Overridable methods
* Shared:
* - (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary
*)launchOptions;
* - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString*)moduleName initProps:(NSDictionary
*)initProps;
* - (UIViewController *)createRootViewController;
* - (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController;
* New Architecture:
* - (BOOL)turboModuleEnabled;
* - (BOOL)fabricEnabled;
* - (NSDictionary *)prepareInitialProps
* - (Class)getModuleClassFromName:(const char *)name
* - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
* - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params
* - (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
*/
@interface RCTAppDelegate : UIResponder <UIApplicationDelegate, UISceneDelegate, RCTBridgeDelegate>
/// The window object, used to render the UViewControllers
@property (nonatomic, strong, nonnull) UIWindow *window;
@property (nonatomic, nullable) RCTBridge *bridge;
@property (nonatomic, strong, nullable) NSString *moduleName;
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
@property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory;
@property (nonatomic, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter;
/**
* It creates a `RCTBridge` using a delegate and some launch options.
* By default, it is invoked passing `self` as a delegate.
* You can override this function to customize the logic that creates the RCTBridge
*
* @parameter: delegate - an object that implements the `RCTBridgeDelegate` protocol.
* @parameter: launchOptions - a dictionary with a set of options.
*
* @returns: a newly created instance of RCTBridge.
*/
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions;
/**
* It creates a `UIView` starting from a bridge, a module name and a set of initial properties.
* By default, it is invoked using the bridge created by `createBridgeWithDelegate:launchOptions` and
* the name in the `self.moduleName` variable.
* You can override this function to customize the logic that creates the Root View.
*
* @parameter: bridge - an instance of the `RCTBridge` object.
* @parameter: moduleName - the name of the app, used by Metro to resolve the module.
* @parameter: initProps - a set of initial properties.
*
* @returns: a UIView properly configured with a bridge for React Native.
*/
- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initProps:(NSDictionary *)initProps;
/**
* This method can be used to customize the rootView that is passed to React Native.
* A typical example is to override this method in the AppDelegate to change the background color.
* To achieve this, add in your `AppDelegate.mm`:
* ```
* - (void)customizeRootView:(RCTRootView *)rootView
* {
* rootView.backgroundColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
* if ([traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) {
* return [UIColor blackColor];
* } else {
* return [UIColor whiteColor];
* }
* }];
* }
* ```
*
* @parameter: rootView - The root view to customize.
*/
- (void)customizeRootView:(RCTRootView *)rootView;
/**
* It creates the RootViewController.
* By default, it creates a new instance of a `UIViewController`.
* You can override it to provide your own initial ViewController.
*
* @return: an instance of `UIViewController`.
*/
- (UIViewController *)createRootViewController;
/**
* It assigns the rootView to the rootViewController
* By default, it assigns the rootView to the view property of the rootViewController
* If you are not using a simple UIViewController, then there could be other methods to use to setup the rootView.
* For example: UISplitViewController requires `setViewController(_:for:)`
*/
- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController;
/// This method returns a map of Component Descriptors and Components classes that needs to be registered in the
/// new renderer. The Component Descriptor is a string which represent the name used in JS to refer to the native
/// component. The default implementation returns an empty dictionary. Subclasses can override this method to register
/// the required components.
///
/// @return a dictionary that associate a component for the new renderer with his descriptor.
- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents;
/// This method controls whether the `turboModules` feature of the New Architecture is turned on or off.
///
/// @note: This is required to be rendering on Fabric (i.e. on the New Architecture).
/// @return: `true` if the Turbo Native Module are enabled. Otherwise, it returns `false`.
- (BOOL)turboModuleEnabled;
/// This method controls whether the App will use the Fabric renderer of the New Architecture or not.
///
/// @return: `true` if the Fabric Renderer is enabled. Otherwise, it returns `false`.
- (BOOL)fabricEnabled;
/// This method controls whether React Native's new initialization layer is enabled.
///
/// @return: `true` if the new initialization layer is enabled. Otherwise returns `false`.
- (BOOL)bridgelessEnabled;
/// Return the bundle URL for the main bundle.
- (NSURL *__nullable)bundleURL;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,261 @@
/*
* 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.
*/
#import "RCTAppDelegate.h"
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTLog.h>
#import <React/RCTRootView.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <React/RCTUtils.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import "RCTAppSetupUtils.h"
#if RN_DISABLE_OSS_PLUGIN_HEADER
#import <RCTTurboModulePlugin/RCTTurboModulePlugin.h>
#else
#import <React/CoreModulesPlugins.h>
#endif
#import <React/RCTBundleURLProvider.h>
#import <React/RCTComponentViewFactory.h>
#import <React/RCTComponentViewProtocol.h>
#import <React/RCTFabricSurface.h>
#import <React/RCTSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <ReactCommon/RCTContextContainerHandling.h>
#if USE_HERMES
#import <ReactCommon/RCTHermesInstance.h>
#else
#import <ReactCommon/RCTJscInstance.h>
#endif
#import <ReactCommon/RCTHost+Internal.h>
#import <ReactCommon/RCTHost.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import <react/renderer/runtimescheduler/RuntimeSchedulerCallInvoker.h>
#import <react/runtime/JSRuntimeFactory.h>
@interface RCTAppDelegate () <RCTComponentViewFactoryComponentProvider, RCTTurboModuleManagerDelegate>
@end
@implementation RCTAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTSetNewArchEnabled([self newArchEnabled]);
RCTAppSetupPrepareApp(application, self.turboModuleEnabled);
self.rootViewFactory = [self createRCTRootViewFactory];
UIView *rootView = [self.rootViewFactory viewWithModuleName:self.moduleName
initialProperties:self.initialProps
launchOptions:launchOptions];
if (self.newArchEnabled || self.fabricEnabled) {
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}
[self _logWarnIfCreateRootViewWithBridgeIsOverridden];
[self customizeRootView:(RCTRootView *)rootView];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self createRootViewController];
[self setRootView:rootView toRootViewController:rootViewController];
self.window.rootViewController = rootViewController;
self.window.windowScene.delegate = self;
[self.window makeKeyAndVisible];
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Noop
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
[NSException raise:@"RCTBridgeDelegate::sourceURLForBridge not implemented"
format:@"Subclasses must implement a valid sourceURLForBridge method"];
return nil;
}
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions
{
return [[RCTBridge alloc] initWithDelegate:delegate launchOptions:launchOptions];
}
- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initProps:(NSDictionary *)initProps
{
BOOL enableFabric = self.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
rootView.backgroundColor = [UIColor systemBackgroundColor];
return rootView;
}
// TODO T173939093 - Remove _logWarnIfCreateRootViewWithBridgeIsOverridden after 0.74 is cut
- (void)_logWarnIfCreateRootViewWithBridgeIsOverridden
{
SEL selector = @selector(createRootViewWithBridge:moduleName:initProps:);
IMP baseClassImp = method_getImplementation(class_getInstanceMethod([RCTAppDelegate class], selector));
IMP currentClassImp = method_getImplementation(class_getInstanceMethod([self class], selector));
if (currentClassImp != baseClassImp) {
NSString *warnMessage =
@"If you are using the `createRootViewWithBridge` to customize the root view appearence,"
"for example to set the backgroundColor, please migrate to `customiseView` method.\n"
"The `createRootViewWithBridge` method is not invoked in bridgeless.";
RCTLogWarn(@"%@", warnMessage);
}
}
- (UIViewController *)createRootViewController
{
return [UIViewController new];
}
- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController
{
rootViewController.view = rootView;
}
- (void)customizeRootView:(RCTRootView *)rootView
{
// Override point for customization after application launch.
}
#pragma mark - UISceneDelegate
- (void)windowScene:(UIWindowScene *)windowScene
didUpdateCoordinateSpace:(id<UICoordinateSpace>)previousCoordinateSpace
interfaceOrientation:(UIInterfaceOrientation)previousInterfaceOrientation
traitCollection:(UITraitCollection *)previousTraitCollection API_AVAILABLE(ios(13.0))
{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
}
#pragma mark - New Arch Enabled settings
- (BOOL)newArchEnabled
{
#if RCT_NEW_ARCH_ENABLED
return YES;
#else
return NO;
#endif
}
- (BOOL)turboModuleEnabled
{
return [self newArchEnabled];
}
- (BOOL)fabricEnabled
{
return [self newArchEnabled];
}
- (BOOL)bridgelessEnabled
{
return [self newArchEnabled];
}
- (NSURL *)bundleURL
{
[NSException raise:@"RCTAppDelegate::bundleURL not implemented"
format:@"Subclasses must implement a valid getBundleURL method"];
return nullptr;
}
#pragma mark - Bridge and Bridge Adapter properties
- (RCTBridge *)bridge
{
return self.rootViewFactory.bridge;
}
- (RCTSurfacePresenterBridgeAdapter *)bridgeAdapter
{
return self.rootViewFactory.bridgeAdapter;
}
- (void)setBridge:(RCTBridge *)bridge
{
self.rootViewFactory.bridge = bridge;
}
- (void)setBridgeAdapter:(RCTSurfacePresenterBridgeAdapter *)bridgeAdapter
{
self.rootViewFactory.bridgeAdapter = bridgeAdapter;
}
#pragma mark - RCTTurboModuleManagerDelegate
- (Class)getModuleClassFromName:(const char *)name
{
#if RN_DISABLE_OSS_PLUGIN_HEADER
return RCTTurboModulePluginClassProvider(name);
#else
return RCTCoreModulesClassProvider(name);
#endif
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return nullptr;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return RCTAppSetupDefaultModuleFromClass(moduleClass);
}
#pragma mark - RCTComponentViewFactoryComponentProvider
- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
return @{};
}
- (RCTRootViewFactory *)createRCTRootViewFactory
{
__weak __typeof(self) weakSelf = self;
RCTBundleURLBlock bundleUrlBlock = ^{
RCTAppDelegate *strongSelf = weakSelf;
return strongSelf.bundleURL;
};
RCTRootViewFactoryConfiguration *configuration =
[[RCTRootViewFactoryConfiguration alloc] initWithBundleURLBlock:bundleUrlBlock
newArchEnabled:self.fabricEnabled
turboModuleEnabled:self.turboModuleEnabled
bridgelessEnabled:self.bridgelessEnabled];
configuration.createRootViewWithBridge = ^UIView *(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps)
{
return [weakSelf createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
};
configuration.createBridgeWithDelegate = ^RCTBridge *(id<RCTBridgeDelegate> delegate, NSDictionary *launchOptions)
{
return [weakSelf createBridgeWithDelegate:delegate launchOptions:launchOptions];
};
return [[RCTRootViewFactory alloc] initWithConfiguration:configuration andTurboModuleManagerDelegate:self];
}
@end

View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#ifdef __cplusplus
#import <memory>
#if USE_HERMES
#if __has_include(<jsireact/HermesExecutorFactory.h>)
#import <jsireact/HermesExecutorFactory.h>
#elif __has_include(<reacthermes/HermesExecutorFactory.h>)
#import <reacthermes/HermesExecutorFactory.h>
#endif
#else // USE_HERMES
#import <React/JSCExecutorFactory.h>
#endif // USE_HERMES
#import <ReactCommon/RCTTurboModuleManager.h>
// Forward declaration to decrease compilation coupling
namespace facebook::react {
class RuntimeScheduler;
}
RCT_EXTERN id<RCTTurboModule> RCTAppSetupDefaultModuleFromClass(Class moduleClass);
std::unique_ptr<facebook::react::JSExecutorFactory> RCTAppSetupDefaultJsExecutorFactory(
RCTBridge *bridge,
RCTTurboModuleManager *turboModuleManager,
const std::shared_ptr<facebook::react::RuntimeScheduler> &runtimeScheduler);
std::unique_ptr<facebook::react::JSExecutorFactory> RCTAppSetupJsExecutorFactoryForOldArch(
RCTBridge *bridge,
const std::shared_ptr<facebook::react::RuntimeScheduler> &runtimeScheduler);
#endif // __cplusplus
RCT_EXTERN_C_BEGIN
void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled);
UIView *RCTAppSetupDefaultRootView(
RCTBridge *bridge,
NSString *moduleName,
NSDictionary *initialProperties,
BOOL fabricEnabled);
RCT_EXTERN_C_END

View File

@@ -0,0 +1,177 @@
/*
* 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.
*/
#import "RCTAppSetupUtils.h"
#import <React/RCTJSIExecutorRuntimeInstaller.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import <react/renderer/runtimescheduler/RuntimeSchedulerBinding.h>
// Turbo Module
#import <React/RCTBundleAssetImageLoader.h>
#import <React/RCTDataRequestHandler.h>
#import <React/RCTFileRequestHandler.h>
#import <React/RCTGIFImageDecoder.h>
#import <React/RCTHTTPRequestHandler.h>
#import <React/RCTImageLoader.h>
#import <React/RCTNetworking.h>
// Fabric
#import <React/RCTFabricSurface.h>
#import <React/RCTSurfaceHostingProxyRootView.h>
// jsinspector-modern
#import <jsinspector-modern/InspectorFlags.h>
#if __has_include(<React-Codegen/RCTModulesConformingToProtocolsProvider.h>)
#define USE_OSS_CODEGEN 1
#import <React-Codegen/RCTModulesConformingToProtocolsProvider.h>
#elif __has_include(<React_Codegen/RCTModulesConformingToProtocolsProvider.h>)
#define USE_OSS_CODEGEN 1
#import <React_Codegen/RCTModulesConformingToProtocolsProvider.h>
#else
// Meta internal system do not generate the RCTModulesConformingToProtocolsProvider.h file
#define USE_OSS_CODEGEN 0
#endif
void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
{
RCTEnableTurboModule(turboModuleEnabled);
#if DEBUG
// Disable idle timer in dev builds to avoid putting application in background and complicating
// Metro reconnection logic. Users only need this when running the application using our CLI tooling.
application.idleTimerDisabled = YES;
#endif
}
UIView *
RCTAppSetupDefaultRootView(RCTBridge *bridge, NSString *moduleName, NSDictionary *initialProperties, BOOL fabricEnabled)
{
if (fabricEnabled) {
id<RCTSurfaceProtocol> surface = [[RCTFabricSurface alloc] initWithBridge:bridge
moduleName:moduleName
initialProperties:initialProperties];
return [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface];
}
return [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
id<RCTTurboModule> RCTAppSetupDefaultModuleFromClass(Class moduleClass)
{
// private block used to filter out modules depending on protocol conformance
NSArray * (^extractModuleConformingToProtocol)(RCTModuleRegistry *, Protocol *) =
^NSArray *(RCTModuleRegistry *moduleRegistry, Protocol *protocol)
{
NSArray<NSString *> *classNames = @[];
#if USE_OSS_CODEGEN
if (protocol == @protocol(RCTImageURLLoader)) {
classNames = [RCTModulesConformingToProtocolsProvider imageURLLoaderClassNames];
} else if (protocol == @protocol(RCTImageDataDecoder)) {
classNames = [RCTModulesConformingToProtocolsProvider imageDataDecoderClassNames];
} else if (protocol == @protocol(RCTURLRequestHandler)) {
classNames = [RCTModulesConformingToProtocolsProvider URLRequestHandlerClassNames];
}
#endif
NSMutableArray *modules = [NSMutableArray new];
for (NSString *className in classNames) {
const char *cModuleName = [className cStringUsingEncoding:NSUTF8StringEncoding];
id moduleFromLibrary = [moduleRegistry moduleForName:cModuleName];
if (![moduleFromLibrary conformsToProtocol:protocol]) {
continue;
}
[modules addObject:moduleFromLibrary];
}
return modules;
};
// Set up the default RCTImageLoader and RCTNetworking modules.
if (moduleClass == RCTImageLoader.class) {
return [[moduleClass alloc] initWithRedirectDelegate:nil
loadersProvider:^NSArray<id<RCTImageURLLoader>> *(RCTModuleRegistry *moduleRegistry) {
NSArray *imageURLLoaderModules =
extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTImageURLLoader));
return [@[ [RCTBundleAssetImageLoader new] ] arrayByAddingObjectsFromArray:imageURLLoaderModules];
}
decodersProvider:^NSArray<id<RCTImageDataDecoder>> *(RCTModuleRegistry *moduleRegistry) {
NSArray *imageDataDecoder = extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTImageDataDecoder));
return [@[ [RCTGIFImageDecoder new] ] arrayByAddingObjectsFromArray:imageDataDecoder];
}];
} else if (moduleClass == RCTNetworking.class) {
return [[moduleClass alloc]
initWithHandlersProvider:^NSArray<id<RCTURLRequestHandler>> *(RCTModuleRegistry *moduleRegistry) {
NSArray *URLRequestHandlerModules =
extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTURLRequestHandler));
return [@[
[RCTHTTPRequestHandler new],
[RCTDataRequestHandler new],
[RCTFileRequestHandler new],
[moduleRegistry moduleForName:"BlobModule"],
] arrayByAddingObjectsFromArray:URLRequestHandlerModules];
}];
}
// No custom initializer here.
return [moduleClass new];
}
std::unique_ptr<facebook::react::JSExecutorFactory> RCTAppSetupDefaultJsExecutorFactory(
RCTBridge *bridge,
RCTTurboModuleManager *turboModuleManager,
const std::shared_ptr<facebook::react::RuntimeScheduler> &runtimeScheduler)
{
// Necessary to allow NativeModules to lookup TurboModules
[bridge setRCTTurboModuleRegistry:turboModuleManager];
#if RCT_DEV
/**
* Instantiating DevMenu has the side-effect of registering
* shortcuts for CMD + d, CMD + i, and CMD + n via RCTDevMenu.
* Therefore, when TurboModules are enabled, we must manually create this
* NativeModule.
*/
[turboModuleManager moduleForName:"RCTDevMenu"];
#endif // end RCT_DEV
#if USE_HERMES
return std::make_unique<facebook::react::HermesExecutorFactory>(
#else
return std::make_unique<facebook::react::JSCExecutorFactory>(
#endif // USE_HERMES
facebook::react::RCTJSIExecutorRuntimeInstaller(
[turboModuleManager, bridge, runtimeScheduler](facebook::jsi::Runtime &runtime) {
if (!bridge || !turboModuleManager) {
return;
}
if (runtimeScheduler) {
facebook::react::RuntimeSchedulerBinding::createAndInstallIfNeeded(runtime, runtimeScheduler);
}
[turboModuleManager installJSBindings:runtime];
}));
}
std::unique_ptr<facebook::react::JSExecutorFactory> RCTAppSetupJsExecutorFactoryForOldArch(
RCTBridge *bridge,
const std::shared_ptr<facebook::react::RuntimeScheduler> &runtimeScheduler)
{
#if USE_HERMES
return std::make_unique<facebook::react::HermesExecutorFactory>(
#else
return std::make_unique<facebook::react::JSCExecutorFactory>(
#endif // USE_HERMES
facebook::react::RCTJSIExecutorRuntimeInstaller([bridge, runtimeScheduler](facebook::jsi::Runtime &runtime) {
if (!bridge) {
return;
}
if (runtimeScheduler) {
facebook::react::RuntimeSchedulerBinding::createAndInstallIfNeeded(runtime, runtimeScheduler);
}
}));
}

View File

@@ -0,0 +1,133 @@
/*
* 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.
*/
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import <React/RCTUtils.h>
@protocol RCTCxxBridgeDelegate;
@protocol RCTComponentViewFactoryComponentProvider;
@protocol RCTTurboModuleManagerDelegate;
@class RCTBridge;
@class RCTRootView;
@class RCTSurfacePresenterBridgeAdapter;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Blocks' definitions
typedef UIView *_Nonnull (
^RCTCreateRootViewWithBridgeBlock)(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps);
typedef RCTBridge *_Nonnull (
^RCTCreateBridgeWithDelegateBlock)(id<RCTBridgeDelegate> delegate, NSDictionary *launchOptions);
typedef NSURL *_Nullable (^RCTSourceURLForBridgeBlock)(RCTBridge *bridge);
typedef NSURL *_Nullable (^RCTBundleURLBlock)(void);
typedef NSArray<id<RCTBridgeModule>> *_Nonnull (^RCTExtraModulesForBridgeBlock)(RCTBridge *bridge);
typedef NSDictionary<NSString *, Class> *_Nonnull (^RCTExtraLazyModuleClassesForBridge)(RCTBridge *bridge);
typedef BOOL (^RCTBridgeDidNotFindModuleBlock)(RCTBridge *bridge, NSString *moduleName);
#pragma mark - RCTRootViewFactory Configuration
@interface RCTRootViewFactoryConfiguration : NSObject
/// This property controls whether the App will use the Fabric renderer of the New Architecture or not.
@property (nonatomic, assign, readonly) BOOL fabricEnabled;
/// This property controls whether React Native's new initialization layer is enabled.
@property (nonatomic, assign, readonly) BOOL bridgelessEnabled;
/// This method controls whether the `turboModules` feature of the New Architecture is turned on or off
@property (nonatomic, assign, readonly) BOOL turboModuleEnabled;
/// Return the bundle URL for the main bundle.
@property (nonatomic, nonnull) RCTBundleURLBlock bundleURLBlock;
/**
* Use this method to initialize a new instance of `RCTRootViewFactoryConfiguration` by passing a `bundleURL`
*
* Which is the location of the JavaScript source file. When running from the packager
* this should be an absolute URL, e.g. `http://localhost:8081/index.ios.bundle`.
* When running from a locally bundled JS file, this should be a `file://` url
* pointing to a path inside the app resources, e.g. `file://.../main.jsbundle`.
*
*/
- (instancetype)initWithBundleURLBlock:(RCTBundleURLBlock)bundleURLBlock
newArchEnabled:(BOOL)newArchEnabled
turboModuleEnabled:(BOOL)turboModuleEnabled
bridgelessEnabled:(BOOL)bridgelessEnabled NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
newArchEnabled:(BOOL)newArchEnabled
turboModuleEnabled:(BOOL)turboModuleEnabled
bridgelessEnabled:(BOOL)bridgelessEnabled __deprecated;
/**
* Block that allows to override logic of creating root view instance.
* It creates a `UIView` starting from a bridge, a module name and a set of initial properties.
* By default, it is invoked using the bridge created by `RCTCreateBridgeWithDelegateBlock` (or the default
* implementation) and the `moduleName` variable comes from `viewWithModuleName:initialProperties:launchOptions` of
* `RCTRootViewFactory`.
*
* @parameter: bridge - an instance of the `RCTBridge` object.
* @parameter: moduleName - the name of the app, used by Metro to resolve the module.
* @parameter: initProps - a set of initial properties.
*
* @returns: a UIView properly configured with a bridge for React Native.
*/
@property (nonatomic, nullable) RCTCreateRootViewWithBridgeBlock createRootViewWithBridge;
/**
* Block that allows to override default behavior of creating bridge.
* It should return `RCTBridge` using a delegate and some launch options.
*
* By default, it is invoked passing `self` as a delegate.
*
* @parameter: delegate - an object that implements the `RCTBridgeDelegate` protocol.
* @parameter: launchOptions - a dictionary with a set of options.
*
* @returns: a newly created instance of RCTBridge.
*/
@property (nonatomic, nullable) RCTCreateBridgeWithDelegateBlock createBridgeWithDelegate;
@end
#pragma mark - RCTRootViewFactory
/**
* The RCTRootViewFactory is an utility class that encapsulates the logic of creating a new RCTRootView based on the
* current state of the environment. It allows you to initialize your app root view for old architecture, new
* architecture and bridgless mode.
*
* This class is used to initalize rootView in RCTAppDelegate, but you can also use it separately.
*
* Create a new instance of this class (make sure to retain it) and call the
* `viewWithModuleName:initialProperties:launchOptions` method to create new RCTRootView.
*/
@interface RCTRootViewFactory : NSObject
@property (nonatomic, strong, nullable) RCTBridge *bridge;
@property (nonatomic, strong, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter;
- (instancetype)initWithConfiguration:(RCTRootViewFactoryConfiguration *)configuration
andTurboModuleManagerDelegate:(id<RCTTurboModuleManagerDelegate>)turboModuleManagerDelegate;
/**
* This method can be used to create new RCTRootViews on demand.
*
* @parameter: moduleName - the name of the app, used by Metro to resolve the module.
* @parameter: initialProperties - a set of initial properties.
* @parameter: launchOptions - a dictionary with a set of options.
*/
- (UIView *_Nonnull)viewWithModuleName:(NSString *)moduleName
initialProperties:(NSDictionary *__nullable)initialProperties
launchOptions:(NSDictionary *__nullable)launchOptions;
- (UIView *_Nonnull)viewWithModuleName:(NSString *)moduleName
initialProperties:(NSDictionary *__nullable)initialProperties;
- (UIView *_Nonnull)viewWithModuleName:(NSString *)moduleName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,268 @@
/*
* 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.
*/
#import "RCTRootViewFactory.h"
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTLog.h>
#import <React/RCTRootView.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <React/RCTUtils.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import "RCTAppDelegate.h"
#import "RCTAppSetupUtils.h"
#if RN_DISABLE_OSS_PLUGIN_HEADER
#import <RCTTurboModulePlugin/RCTTurboModulePlugin.h>
#else
#import <React/CoreModulesPlugins.h>
#endif
#import <React/RCTBundleURLProvider.h>
#import <React/RCTComponentViewFactory.h>
#import <React/RCTComponentViewProtocol.h>
#import <React/RCTFabricSurface.h>
#import <React/RCTSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <ReactCommon/RCTContextContainerHandling.h>
#if USE_HERMES
#import <ReactCommon/RCTHermesInstance.h>
#else
#import <ReactCommon/RCTJscInstance.h>
#endif
#import <ReactCommon/RCTHost+Internal.h>
#import <ReactCommon/RCTHost.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
#import <react/renderer/runtimescheduler/RuntimeSchedulerCallInvoker.h>
#import <react/runtime/JSRuntimeFactory.h>
static NSString *const kRNConcurrentRoot = @"concurrentRoot";
static NSDictionary *updateInitialProps(NSDictionary *initialProps, BOOL isFabricEnabled)
{
NSMutableDictionary *mutableProps = initialProps != NULL ? [initialProps mutableCopy] : [NSMutableDictionary new];
// Hardcoding the Concurrent Root as it it not recommended to
// have the concurrentRoot turned off when Fabric is enabled.
mutableProps[kRNConcurrentRoot] = @(isFabricEnabled);
return mutableProps;
}
@implementation RCTRootViewFactoryConfiguration
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
newArchEnabled:(BOOL)newArchEnabled
turboModuleEnabled:(BOOL)turboModuleEnabled
bridgelessEnabled:(BOOL)bridgelessEnabled
{
return [self
initWithBundleURLBlock:^{
return bundleURL;
}
newArchEnabled:newArchEnabled
turboModuleEnabled:turboModuleEnabled
bridgelessEnabled:bridgelessEnabled];
}
- (instancetype)initWithBundleURLBlock:(RCTBundleURLBlock)bundleURLBlock
newArchEnabled:(BOOL)newArchEnabled
turboModuleEnabled:(BOOL)turboModuleEnabled
bridgelessEnabled:(BOOL)bridgelessEnabled
{
if (self = [super init]) {
_bundleURLBlock = bundleURLBlock;
_fabricEnabled = newArchEnabled;
_turboModuleEnabled = turboModuleEnabled;
_bridgelessEnabled = bridgelessEnabled;
}
return self;
}
@end
@interface RCTRootViewFactory () <RCTContextContainerHandling> {
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
facebook::react::ContextContainer::Shared _contextContainer;
}
@end
@interface RCTRootViewFactory () <RCTCxxBridgeDelegate> {
std::shared_ptr<facebook::react::RuntimeScheduler> _runtimeScheduler;
}
@end
@implementation RCTRootViewFactory {
RCTHost *_reactHost;
RCTRootViewFactoryConfiguration *_configuration;
__weak id<RCTTurboModuleManagerDelegate> _turboModuleManagerDelegate;
}
- (instancetype)initWithConfiguration:(RCTRootViewFactoryConfiguration *)configuration
andTurboModuleManagerDelegate:(id<RCTTurboModuleManagerDelegate>)turboModuleManagerDelegate
{
if (self = [super init]) {
_configuration = configuration;
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
_turboModuleManagerDelegate = turboModuleManagerDelegate;
}
return self;
}
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties
{
return [self viewWithModuleName:moduleName initialProperties:initialProperties launchOptions:nil];
}
- (UIView *)viewWithModuleName:(NSString *)moduleName
{
return [self viewWithModuleName:moduleName initialProperties:nil launchOptions:nil];
}
- (UIView *)viewWithModuleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions
{
NSDictionary *initProps = updateInitialProps(initialProperties, self->_configuration.fabricEnabled);
if (self->_configuration.bridgelessEnabled) {
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
RCTSetUseNativeViewConfigsInBridgelessMode(self->_configuration.fabricEnabled);
// Enable TurboModule interop by default in Bridgeless mode
RCTEnableTurboModuleInterop(YES);
RCTEnableTurboModuleInteropBridgeProxy(YES);
[self createReactHostIfNeeded:launchOptions];
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:moduleName initialProperties:initProps];
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
initWithSurface:surface
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
return surfaceHostingProxyRootView;
}
[self createBridgeIfNeeded:launchOptions];
[self createBridgeAdapterIfNeeded];
if (self->_configuration.createRootViewWithBridge != nil) {
return self->_configuration.createRootViewWithBridge(self.bridge, moduleName, initProps);
}
return [self createRootViewWithBridge:self.bridge moduleName:moduleName initProps:initProps];
}
- (RCTBridge *)createBridgeWithDelegate:(id<RCTBridgeDelegate>)delegate launchOptions:(NSDictionary *)launchOptions
{
return [[RCTBridge alloc] initWithDelegate:delegate launchOptions:launchOptions];
}
- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initProps:(NSDictionary *)initProps
{
BOOL enableFabric = self->_configuration.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
rootView.backgroundColor = [UIColor systemBackgroundColor];
return rootView;
}
#pragma mark - RCTCxxBridgeDelegate
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
_runtimeScheduler = std::make_shared<facebook::react::RuntimeScheduler>(RCTRuntimeExecutorFromBridge(bridge));
if (RCTIsNewArchEnabled()) {
std::shared_ptr<facebook::react::CallInvoker> callInvoker =
std::make_shared<facebook::react::RuntimeSchedulerCallInvoker>(_runtimeScheduler);
RCTTurboModuleManager *turboModuleManager =
[[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:_turboModuleManagerDelegate
jsInvoker:callInvoker];
_contextContainer->erase("RuntimeScheduler");
_contextContainer->insert("RuntimeScheduler", _runtimeScheduler);
return RCTAppSetupDefaultJsExecutorFactory(bridge, turboModuleManager, _runtimeScheduler);
} else {
return RCTAppSetupJsExecutorFactoryForOldArch(bridge, _runtimeScheduler);
}
}
- (void)createBridgeIfNeeded:(NSDictionary *)launchOptions
{
if (self.bridge != nil) {
return;
}
if (self->_configuration.createBridgeWithDelegate != nil) {
self.bridge = self->_configuration.createBridgeWithDelegate(self, launchOptions);
} else {
self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions];
}
}
- (void)createBridgeAdapterIfNeeded
{
if (!self->_configuration.fabricEnabled || self.bridgeAdapter) {
return;
}
self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge
contextContainer:_contextContainer];
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;
}
#pragma mark - New Arch Utilities
- (void)createReactHostIfNeeded:(NSDictionary *)launchOptions
{
if (_reactHost) {
return;
}
__weak __typeof(self) weakSelf = self;
_reactHost = [[RCTHost alloc] initWithBundleURLProvider:self->_configuration.bundleURLBlock
hostDelegate:nil
turboModuleManagerDelegate:_turboModuleManagerDelegate
jsEngineProvider:^std::shared_ptr<facebook::react::JSRuntimeFactory>() {
return [weakSelf createJSRuntimeFactory];
}
launchOptions:launchOptions];
[_reactHost setBundleURLProvider:^NSURL *() {
return [weakSelf bundleURL];
}];
[_reactHost setContextContainerHandler:self];
[_reactHost start];
}
- (std::shared_ptr<facebook::react::JSRuntimeFactory>)createJSRuntimeFactory
{
#if USE_HERMES
return std::make_shared<facebook::react::RCTHermesInstance>(_reactNativeConfig, nullptr);
#else
return std::make_shared<facebook::react::RCTJscInstance>();
#endif
}
- (void)didCreateContextContainer:(std::shared_ptr<facebook::react::ContextContainer>)contextContainer
{
contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
return self->_configuration.bundleURLBlock();
}
@end

View File

@@ -0,0 +1,99 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1"
use_hermes = ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == '1'
new_arch_enabled_flag = (is_new_arch_enabled ? " -DRCT_NEW_ARCH_ENABLED" : "")
is_fabric_enabled = true #is_new_arch_enabled || ENV["RCT_FABRIC_ENABLED"]
hermes_flag = (use_hermes ? " -DUSE_HERMES" : "")
other_cflags = "$(inherited)" + folly_compiler_flags + new_arch_enabled_flag + hermes_flag
header_search_paths = [
"$(PODS_TARGET_SRCROOT)/../../ReactCommon",
"$(PODS_ROOT)/Headers/Private/React-Core",
"$(PODS_ROOT)/boost",
"$(PODS_ROOT)/DoubleConversion",
"$(PODS_ROOT)/fmt/include",
"$(PODS_ROOT)/RCT-Folly",
"${PODS_ROOT}/Headers/Public/FlipperKit",
"$(PODS_ROOT)/Headers/Public/ReactCommon",
"$(PODS_ROOT)/Headers/Public/React-RCTFabric",
"$(PODS_ROOT)/Headers/Private/Yoga",
].concat(use_hermes ? [
"$(PODS_ROOT)/Headers/Public/React-hermes",
"$(PODS_ROOT)/Headers/Public/hermes-engine"
] : [])
Pod::Spec.new do |s|
s.name = "React-RCTAppDelegate"
s.version = version
s.summary = "An utility library to simplify common operations for the New Architecture"
s.homepage = "https://reactnative.dev/"
s.documentation_url = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "**/*.{c,h,m,mm,S,cpp}"
# This guard prevent to install the dependencies when we run `pod install` in the old architecture.
s.compiler_flags = other_cflags
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths,
"OTHER_CPLUSPLUSFLAGS" => other_cflags,
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"DEFINES_MODULE" => "YES"
}
s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\""}
use_hermes = ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1"
s.dependency "React-Core"
s.dependency "RCT-Folly", folly_version
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "React-RCTNetwork"
s.dependency "React-RCTImage"
s.dependency "React-CoreModules"
s.dependency "React-nativeconfig"
s.dependency "React-Codegen"
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")
add_dependency(s, "React-runtimescheduler")
add_dependency(s, "React-RCTFabric", :framework_name => "RCTFabric")
add_dependency(s, "React-RuntimeCore")
add_dependency(s, "React-RuntimeApple")
add_dependency(s, "React-Fabric", :additional_framework_paths => ["react/renderer/components/view/platform/cxx"])
add_dependency(s, "React-graphics", :additional_framework_paths => ["react/renderer/graphics/platform/ios"])
add_dependency(s, "React-utils")
add_dependency(s, "React-debug")
add_dependency(s, "React-rendererdebug")
if use_hermes
s.dependency "React-hermes"
s.dependency "React-RuntimeHermes"
else
s.dependency "React-jsc"
end
end

View File

@@ -0,0 +1,58 @@
/**
* 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.
*
* @format
*/
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
/**
* AppState can tell you if the app is in the foreground or background,
* and notify you when the state changes.
*
* AppState is frequently used to determine the intent and proper behavior
* when handling push notifications.
*
* App State Events
* change - This even is received when the app state has changed.
* focus [Android] - Received when the app gains focus (the user is interacting with the app).
* blur [Android] - Received when the user is not actively interacting with the app.
*
* App States
* active - The app is running in the foreground
* background - The app is running in the background. The user is either in another app or on the home screen
* inactive [iOS] - This is a transition state that happens when the app launches, is asking for permissions or when a call or SMS message is received.
* unknown [iOS] - Initial value until the current app state is determined
* extension [iOS] - The app is running as an app extension
*
* For more information, see Apple's documentation: https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html
*
* @see https://reactnative.dev/docs/appstate#app-states
*/
export type AppStateEvent = 'change' | 'memoryWarning' | 'blur' | 'focus';
export type AppStateStatus =
| 'active'
| 'background'
| 'inactive'
| 'unknown'
| 'extension';
export interface AppStateStatic {
currentState: AppStateStatus;
isAvailable: boolean;
/**
* Add a handler to AppState changes by listening to the change event
* type and providing the handler
*/
addEventListener(
type: AppStateEvent,
listener: (state: AppStateStatus) => void,
): NativeEventSubscription;
}
export const AppState: AppStateStatic;
export type AppState = AppStateStatic;

View File

@@ -0,0 +1,128 @@
/**
* 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 NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import logError from '../Utilities/logError';
import Platform from '../Utilities/Platform';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeAppState from './NativeAppState';
export type AppStateValues = 'inactive' | 'background' | 'active';
type AppStateEventDefinitions = {
change: [AppStateValues],
memoryWarning: [],
blur: [],
focus: [],
};
type NativeAppStateEventDefinitions = {
appStateDidChange: [{app_state: AppStateValues}],
appStateFocusChange: [boolean],
memoryWarning: [],
};
/**
* `AppState` can tell you if the app is in the foreground or background,
* and notify you when the state changes.
*
* See https://reactnative.dev/docs/appstate
*/
class AppState {
currentState: ?string = null;
isAvailable: boolean;
_emitter: ?NativeEventEmitter<NativeAppStateEventDefinitions>;
constructor() {
if (NativeAppState == null) {
this.isAvailable = false;
} else {
this.isAvailable = true;
const emitter: NativeEventEmitter<NativeAppStateEventDefinitions> =
new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeAppState,
);
this._emitter = emitter;
this.currentState = NativeAppState.getConstants().initialAppState;
let eventUpdated = false;
// TODO: this is a terrible solution - in order to ensure `currentState`
// prop is up to date, we have to register an observer that updates it
// whenever the state changes, even if nobody cares. We should just
// deprecate the `currentState` property and get rid of this.
emitter.addListener('appStateDidChange', appStateData => {
eventUpdated = true;
this.currentState = appStateData.app_state;
});
// TODO: see above - this request just populates the value of `currentState`
// when the module is first initialized. Would be better to get rid of the
// prop and expose `getCurrentAppState` method directly.
// $FlowExpectedError[incompatible-call]
NativeAppState.getCurrentAppState(appStateData => {
// It's possible that the state will have changed here & listeners need to be notified
if (!eventUpdated && this.currentState !== appStateData.app_state) {
this.currentState = appStateData.app_state;
// $FlowFixMe[incompatible-call]
emitter.emit('appStateDidChange', appStateData);
}
}, logError);
}
}
/**
* Add a handler to AppState changes by listening to the `change` event type
* and providing the handler.
*
* See https://reactnative.dev/docs/appstate#addeventlistener
*/
addEventListener<K: $Keys<AppStateEventDefinitions>>(
type: K,
handler: (...$ElementType<AppStateEventDefinitions, K>) => void,
): EventSubscription {
const emitter = this._emitter;
if (emitter == null) {
throw new Error('Cannot use AppState when `isAvailable` is false.');
}
switch (type) {
case 'change':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const changeHandler: AppStateValues => void = handler;
return emitter.addListener('appStateDidChange', appStateData => {
changeHandler(appStateData.app_state);
});
case 'memoryWarning':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const memoryWarningHandler: () => void = handler;
return emitter.addListener('memoryWarning', memoryWarningHandler);
case 'blur':
case 'focus':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const focusOrBlurHandler: () => void = handler;
return emitter.addListener('appStateFocusChange', hasFocus => {
if (type === 'blur' && !hasFocus) {
focusOrBlurHandler();
}
if (type === 'focus' && hasFocus) {
focusOrBlurHandler();
}
});
}
throw new Error('Trying to subscribe to unknown event: ' + type);
}
}
module.exports = (new AppState(): AppState);

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeAppState';
import NativeAppState from '../../src/private/specs/modules/NativeAppState';
export default NativeAppState;

View File

@@ -0,0 +1,28 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
const MessageQueue = require('./MessageQueue');
const BatchedBridge: MessageQueue = new MessageQueue();
// Wire up the batched bridge on the global object so that we can call into it.
// Ideally, this would be the inverse relationship. I.e. the native environment
// provides this global directly with its script embedded. Then this module
// would export it. A possible fix would be to trim the dependencies in
// MessageQueue to its minimal features and embed that in the native runtime.
Object.defineProperty(global, '__fbBatchedBridge', {
configurable: true,
value: BatchedBridge,
});
module.exports = BatchedBridge;

View File

@@ -0,0 +1,491 @@
/**
* 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
* @format
*/
'use strict';
const Systrace = require('../Performance/Systrace');
const deepFreezeAndThrowOnMutationInDev = require('../Utilities/deepFreezeAndThrowOnMutationInDev');
const stringifySafe = require('../Utilities/stringifySafe').default;
const warnOnce = require('../Utilities/warnOnce');
const ErrorUtils = require('../vendor/core/ErrorUtils');
const invariant = require('invariant');
export type SpyData = {
type: number,
module: ?string,
method: string | number,
args: mixed[],
...
};
const TO_JS = 0;
const TO_NATIVE = 1;
const MODULE_IDS = 0;
const METHOD_IDS = 1;
const PARAMS = 2;
const MIN_TIME_BETWEEN_FLUSHES_MS = 5;
// eslint-disable-next-line no-bitwise
const TRACE_TAG_REACT_APPS = 1 << 17;
const DEBUG_INFO_LIMIT = 32;
class MessageQueue {
_lazyCallableModules: {[key: string]: (void) => {...}, ...};
_queue: [number[], number[], mixed[], number];
_successCallbacks: Map<number, ?(...mixed[]) => void>;
_failureCallbacks: Map<number, ?(...mixed[]) => void>;
_callID: number;
_lastFlush: number;
_eventLoopStartTime: number;
_reactNativeMicrotasksCallback: ?() => void;
_debugInfo: {[number]: [number, number], ...};
_remoteModuleTable: {[number]: string, ...};
_remoteMethodTable: {[number]: $ReadOnlyArray<string>, ...};
__spy: ?(data: SpyData) => void;
constructor() {
this._lazyCallableModules = {};
this._queue = [[], [], [], 0];
this._successCallbacks = new Map();
this._failureCallbacks = new Map();
this._callID = 0;
this._lastFlush = 0;
this._eventLoopStartTime = Date.now();
this._reactNativeMicrotasksCallback = null;
if (__DEV__) {
this._debugInfo = {};
this._remoteModuleTable = {};
this._remoteMethodTable = {};
}
// $FlowFixMe[cannot-write]
this.callFunctionReturnFlushedQueue =
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.callFunctionReturnFlushedQueue.bind(this);
// $FlowFixMe[cannot-write]
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.flushedQueue = this.flushedQueue.bind(this);
// $FlowFixMe[cannot-write]
this.invokeCallbackAndReturnFlushedQueue =
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
this.invokeCallbackAndReturnFlushedQueue.bind(this);
}
/**
* Public APIs
*/
static spy(spyOrToggle: boolean | ((data: SpyData) => void)) {
if (spyOrToggle === true) {
MessageQueue.prototype.__spy = info => {
console.log(
`${info.type === TO_JS ? 'N->JS' : 'JS->N'} : ` +
`${info.module != null ? info.module + '.' : ''}${info.method}` +
`(${JSON.stringify(info.args)})`,
);
};
} else if (spyOrToggle === false) {
MessageQueue.prototype.__spy = null;
} else {
MessageQueue.prototype.__spy = spyOrToggle;
}
}
callFunctionReturnFlushedQueue(
module: string,
method: string,
args: mixed[],
): null | [Array<number>, Array<number>, Array<mixed>, number] {
this.__guard(() => {
this.__callFunction(module, method, args);
});
return this.flushedQueue();
}
invokeCallbackAndReturnFlushedQueue(
cbID: number,
args: mixed[],
): null | [Array<number>, Array<number>, Array<mixed>, number] {
this.__guard(() => {
this.__invokeCallback(cbID, args);
});
return this.flushedQueue();
}
flushedQueue(): null | [Array<number>, Array<number>, Array<mixed>, number] {
this.__guard(() => {
this.__callReactNativeMicrotasks();
});
const queue = this._queue;
this._queue = [[], [], [], this._callID];
return queue[0].length ? queue : null;
}
getEventLoopRunningTime(): number {
return Date.now() - this._eventLoopStartTime;
}
registerCallableModule(name: string, module: {...}) {
this._lazyCallableModules[name] = () => module;
}
registerLazyCallableModule(name: string, factory: void => interface {}) {
let module: interface {};
let getValue: ?(void) => interface {} = factory;
this._lazyCallableModules[name] = () => {
if (getValue) {
module = getValue();
getValue = null;
}
/* $FlowFixMe[class-object-subtyping] added when improving typing for
* this parameters */
return module;
};
}
getCallableModule(name: string): {...} | null {
const getValue = this._lazyCallableModules[name];
return getValue ? getValue() : null;
}
callNativeSyncHook(
moduleID: number,
methodID: number,
params: mixed[],
onFail: ?(...mixed[]) => void,
onSucc: ?(...mixed[]) => void,
): mixed {
if (__DEV__) {
invariant(
global.nativeCallSyncHook,
'Calling synchronous methods on native ' +
'modules is not supported in Chrome.\n\n Consider providing alternative ' +
'methods to expose this method in debug mode, e.g. by exposing constants ' +
'ahead-of-time.',
);
}
this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
return global.nativeCallSyncHook(moduleID, methodID, params);
}
processCallbacks(
moduleID: number,
methodID: number,
params: mixed[],
onFail: ?(...mixed[]) => void,
onSucc: ?(...mixed[]) => void,
): void {
if (onFail || onSucc) {
if (__DEV__) {
this._debugInfo[this._callID] = [moduleID, methodID];
if (this._callID > DEBUG_INFO_LIMIT) {
delete this._debugInfo[this._callID - DEBUG_INFO_LIMIT];
}
if (this._successCallbacks.size > 500) {
const info: {[number]: {method: string, module: string}} = {};
this._successCallbacks.forEach((_, callID) => {
const debug = this._debugInfo[callID];
const module = debug && this._remoteModuleTable[debug[0]];
const method = debug && this._remoteMethodTable[debug[0]][debug[1]];
info[callID] = {module, method};
});
warnOnce(
'excessive-number-of-pending-callbacks',
`Excessive number of pending callbacks: ${
this._successCallbacks.size
}. Some pending callbacks that might have leaked by never being called from native code: ${stringifySafe(
info,
)}`,
);
}
}
// Encode callIDs into pairs of callback identifiers by shifting left and using the rightmost bit
// to indicate fail (0) or success (1)
// eslint-disable-next-line no-bitwise
onFail && params.push(this._callID << 1);
// eslint-disable-next-line no-bitwise
onSucc && params.push((this._callID << 1) | 1);
this._successCallbacks.set(this._callID, onSucc);
this._failureCallbacks.set(this._callID, onFail);
}
if (__DEV__) {
global.nativeTraceBeginAsyncFlow &&
global.nativeTraceBeginAsyncFlow(
TRACE_TAG_REACT_APPS,
'native',
this._callID,
);
}
this._callID++;
}
enqueueNativeCall(
moduleID: number,
methodID: number,
params: mixed[],
onFail: ?(...mixed[]) => void,
onSucc: ?(...mixed[]) => void,
): void {
this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
if (__DEV__) {
// Validate that parameters passed over the bridge are
// folly-convertible. As a special case, if a prop value is a
// function it is permitted here, and special-cased in the
// conversion.
const isValidArgument = (val: mixed): boolean => {
switch (typeof val) {
case 'undefined':
case 'boolean':
case 'string':
return true;
case 'number':
return isFinite(val);
case 'object':
if (val == null) {
return true;
}
if (Array.isArray(val)) {
return val.every(isValidArgument);
}
for (const k in val) {
if (typeof val[k] !== 'function' && !isValidArgument(val[k])) {
return false;
}
}
return true;
case 'function':
return false;
default:
return false;
}
};
// Replacement allows normally non-JSON-convertible values to be
// seen. There is ambiguity with string values, but in context,
// it should at least be a strong hint.
const replacer = (key: string, val: $FlowFixMe) => {
const t = typeof val;
if (t === 'function') {
return '<<Function ' + val.name + '>>';
} else if (t === 'number' && !isFinite(val)) {
return '<<' + val.toString() + '>>';
} else {
return val;
}
};
// Note that JSON.stringify
invariant(
isValidArgument(params),
'%s is not usable as a native method argument',
JSON.stringify(params, replacer),
);
// The params object should not be mutated after being queued
deepFreezeAndThrowOnMutationInDev(params);
}
this._queue[PARAMS].push(params);
const now = Date.now();
if (
global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
) {
const queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
global.nativeFlushQueueImmediate(queue);
}
Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
if (__DEV__ && this.__spy && isFinite(moduleID)) {
// $FlowFixMe[not-a-function]
this.__spy({
type: TO_NATIVE,
module: this._remoteModuleTable[moduleID],
method: this._remoteMethodTable[moduleID][methodID],
args: params,
});
} else if (this.__spy) {
this.__spy({
type: TO_NATIVE,
module: moduleID + '',
method: methodID,
args: params,
});
}
}
createDebugLookup(
moduleID: number,
name: string,
methods: ?$ReadOnlyArray<string>,
) {
if (__DEV__) {
this._remoteModuleTable[moduleID] = name;
this._remoteMethodTable[moduleID] = methods || [];
}
}
// For JSTimers to register its callback. Otherwise a circular dependency
// between modules is introduced. Note that only one callback may be
// registered at a time.
setReactNativeMicrotasksCallback(fn: () => void) {
this._reactNativeMicrotasksCallback = fn;
}
/**
* Private methods
*/
__guard(fn: () => void) {
if (this.__shouldPauseOnThrow()) {
fn();
} else {
try {
fn();
} catch (error) {
ErrorUtils.reportFatalError(error);
}
}
}
// MessageQueue installs a global handler to catch all exceptions where JS users can register their own behavior
// This handler makes all exceptions to be propagated from inside MessageQueue rather than by the VM at their origin
// This makes stacktraces to be placed at MessageQueue rather than at where they were launched
// The parameter DebuggerInternal.shouldPauseOnThrow is used to check before catching all exceptions and
// can be configured by the VM or any Inspector
__shouldPauseOnThrow(): boolean {
return (
// $FlowFixMe[cannot-resolve-name]
typeof DebuggerInternal !== 'undefined' &&
// $FlowFixMe[cannot-resolve-name]
DebuggerInternal.shouldPauseOnThrow === true
);
}
__callReactNativeMicrotasks() {
Systrace.beginEvent('JSTimers.callReactNativeMicrotasks()');
try {
if (this._reactNativeMicrotasksCallback != null) {
this._reactNativeMicrotasksCallback();
}
} finally {
Systrace.endEvent();
}
}
__callFunction(module: string, method: string, args: mixed[]): void {
this._lastFlush = Date.now();
this._eventLoopStartTime = this._lastFlush;
if (__DEV__ || this.__spy) {
Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`);
} else {
Systrace.beginEvent(`${module}.${method}(...)`);
}
try {
if (this.__spy) {
this.__spy({type: TO_JS, module, method, args});
}
const moduleMethods = this.getCallableModule(module);
if (!moduleMethods) {
const callableModuleNames = Object.keys(this._lazyCallableModules);
const n = callableModuleNames.length;
const callableModuleNameList = callableModuleNames.join(', ');
// TODO(T122225939): Remove after investigation: Why are we getting to this line in bridgeless mode?
const isBridgelessMode =
global.RN$Bridgeless === true ? 'true' : 'false';
invariant(
false,
`Failed to call into JavaScript module method ${module}.${method}(). Module has not been registered as callable. Bridgeless Mode: ${isBridgelessMode}. Registered callable JavaScript modules (n = ${n}): ${callableModuleNameList}.
A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`,
);
}
if (!moduleMethods[method]) {
invariant(
false,
`Failed to call into JavaScript module method ${module}.${method}(). Module exists, but the method is undefined.`,
);
}
moduleMethods[method].apply(moduleMethods, args);
} finally {
Systrace.endEvent();
}
}
__invokeCallback(cbID: number, args: mixed[]): void {
this._lastFlush = Date.now();
this._eventLoopStartTime = this._lastFlush;
// The rightmost bit of cbID indicates fail (0) or success (1), the other bits are the callID shifted left.
// eslint-disable-next-line no-bitwise
const callID = cbID >>> 1;
// eslint-disable-next-line no-bitwise
const isSuccess = cbID & 1;
const callback = isSuccess
? this._successCallbacks.get(callID)
: this._failureCallbacks.get(callID);
if (__DEV__) {
const debug = this._debugInfo[callID];
const module = debug && this._remoteModuleTable[debug[0]];
const method = debug && this._remoteMethodTable[debug[0]][debug[1]];
invariant(
callback,
`No callback found with cbID ${cbID} and callID ${callID} for ` +
(method
? ` ${module}.${method} - most likely the callback was already invoked`
: `module ${module || '<unknown>'}`) +
`. Args: '${stringifySafe(args)}'`,
);
const profileName = debug
? '<callback for ' + module + '.' + method + '>'
: cbID;
if (callback && this.__spy) {
this.__spy({type: TO_JS, module: null, method: profileName, args});
}
Systrace.beginEvent(
`MessageQueue.invokeCallback(${profileName}, ${stringifySafe(args)})`,
);
}
try {
if (!callback) {
return;
}
this._successCallbacks.delete(callID);
this._failureCallbacks.delete(callID);
callback(...args);
} finally {
if (__DEV__) {
Systrace.endEvent();
}
}
}
}
module.exports = MessageQueue;

View File

@@ -0,0 +1,25 @@
/**
* 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.
*
* @format
*/
/**
* Interface for NativeModules which allows to augment NativeModules with type information.
* See react-native-sensor-manager for example.
*/
interface NativeModulesStatic {
[name: string]: any;
}
/**
* Native Modules written in ObjectiveC/Swift/Java exposed via the RCTBridge
* Define lazy getters for each module. These will return the module if already loaded, or load it if not.
* See https://reactnative.dev/docs/native-modules-ios
* @example
* const MyModule = NativeModules.ModuleName
*/
export const NativeModules: NativeModulesStatic;

View File

@@ -0,0 +1,210 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
import type {ExtendedError} from '../Core/ExtendedError';
const BatchedBridge = require('./BatchedBridge');
const invariant = require('invariant');
export type ModuleConfig = [
string /* name */,
?{...} /* constants */,
?$ReadOnlyArray<string> /* functions */,
?$ReadOnlyArray<number> /* promise method IDs */,
?$ReadOnlyArray<number> /* sync method IDs */,
];
export type MethodType = 'async' | 'promise' | 'sync';
function genModule(
config: ?ModuleConfig,
moduleID: number,
): ?{
name: string,
module?: {...},
...
} {
if (!config) {
return null;
}
const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
invariant(
!moduleName.startsWith('RCT') && !moduleName.startsWith('RK'),
"Module name prefixes should've been stripped by the native side " +
"but wasn't for " +
moduleName,
);
if (!constants && !methods) {
// Module contents will be filled in lazily later
return {name: moduleName};
}
const module: {[string]: mixed} = {};
methods &&
methods.forEach((methodName, methodID) => {
const isPromise =
(promiseMethods && arrayContains(promiseMethods, methodID)) || false;
const isSync =
(syncMethods && arrayContains(syncMethods, methodID)) || false;
invariant(
!isPromise || !isSync,
'Cannot have a method that is both async and a sync hook',
);
const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
module[methodName] = genMethod(moduleID, methodID, methodType);
});
Object.assign(module, constants);
if (module.getConstants == null) {
module.getConstants = () => constants || Object.freeze({});
} else {
console.warn(
`Unable to define method 'getConstants()' on NativeModule '${moduleName}'. NativeModule '${moduleName}' already has a constant or method called 'getConstants'. Please remove it.`,
);
}
if (__DEV__) {
BatchedBridge.createDebugLookup(moduleID, moduleName, methods);
}
return {name: moduleName, module};
}
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;
function loadModule(name: string, moduleID: number): ?{...} {
invariant(
global.nativeRequireModuleConfig,
"Can't lazily create module without nativeRequireModuleConfig",
);
const config = global.nativeRequireModuleConfig(name);
const info = genModule(config, moduleID);
return info && info.module;
}
function genMethod(moduleID: number, methodID: number, type: MethodType) {
let fn = null;
if (type === 'promise') {
fn = function promiseMethodWrapper(...args: Array<mixed>) {
// In case we reject, capture a useful stack trace here.
/* $FlowFixMe[class-object-subtyping] added when improving typing for
* this parameters */
const enqueueingFrameError: ExtendedError = new Error();
return new Promise((resolve, reject) => {
BatchedBridge.enqueueNativeCall(
moduleID,
methodID,
args,
data => resolve(data),
errorData =>
reject(
updateErrorWithErrorData(
(errorData: $FlowFixMe),
enqueueingFrameError,
),
),
);
});
};
} else {
fn = function nonPromiseMethodWrapper(...args: Array<mixed>) {
const lastArg = args.length > 0 ? args[args.length - 1] : null;
const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
const hasSuccessCallback = typeof lastArg === 'function';
const hasErrorCallback = typeof secondLastArg === 'function';
hasErrorCallback &&
invariant(
hasSuccessCallback,
'Cannot have a non-function arg after a function arg.',
);
// $FlowFixMe[incompatible-type]
const onSuccess: ?(mixed) => void = hasSuccessCallback ? lastArg : null;
// $FlowFixMe[incompatible-type]
const onFail: ?(mixed) => void = hasErrorCallback ? secondLastArg : null;
// $FlowFixMe[unsafe-addition]
const callbackCount = hasSuccessCallback + hasErrorCallback;
const newArgs = args.slice(0, args.length - callbackCount);
if (type === 'sync') {
return BatchedBridge.callNativeSyncHook(
moduleID,
methodID,
newArgs,
onFail,
onSuccess,
);
} else {
BatchedBridge.enqueueNativeCall(
moduleID,
methodID,
newArgs,
onFail,
onSuccess,
);
}
};
}
// $FlowFixMe[prop-missing]
fn.type = type;
return fn;
}
function arrayContains<T>(array: $ReadOnlyArray<T>, value: T): boolean {
return array.indexOf(value) !== -1;
}
function updateErrorWithErrorData(
errorData: {message: string, ...},
error: ExtendedError,
): ExtendedError {
/* $FlowFixMe[class-object-subtyping] added when improving typing for this
* parameters */
return Object.assign(error, errorData || {});
}
let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {};
if (global.nativeModuleProxy) {
NativeModules = global.nativeModuleProxy;
} else {
const bridgeConfig = global.__fbBatchedBridgeConfig;
invariant(
bridgeConfig,
'__fbBatchedBridgeConfig is not set, cannot invoke native modules',
);
const defineLazyObjectProperty = require('../Utilities/defineLazyObjectProperty');
(bridgeConfig.remoteModuleConfig || []).forEach(
(config: ModuleConfig, moduleID: number) => {
// Initially this config will only contain the module name when running in JSC. The actual
// configuration of the module will be lazily loaded.
const info = genModule(config, moduleID);
if (!info) {
return;
}
if (info.module) {
NativeModules[info.name] = info.module;
}
// If there's no module config, define a lazy getter
else {
defineLazyObjectProperty(NativeModules, info.name, {
get: () => loadModule(info.name, moduleID),
});
}
},
);
}
module.exports = NativeModules;

View File

@@ -0,0 +1,38 @@
/**
* 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
*/
// These don't actually exist anywhere in the code.
'use strict';
import type {ModuleConfig} from '../NativeModules';
const remoteModulesConfig: $ReadOnlyArray<ModuleConfig> = [
[
'RemoteModule1',
null,
['remoteMethod', 'promiseMethod', 'promiseReturningMethod', 'syncMethod'],
[2 /* promiseReturningMethod */],
[3 /* syncMethod */],
],
[
'RemoteModule2',
null,
['remoteMethod', 'promiseMethod', 'promiseReturningMethod', 'syncMethod'],
[2 /* promiseReturningMethod */],
[3 /* syncMethod */],
],
];
const MessageQueueTestConfig = {
remoteModuleConfig: remoteModulesConfig,
};
module.exports = MessageQueueTestConfig;

View File

@@ -0,0 +1,22 @@
/**
* 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.
*
* @format
*/
'use strict';
/**
* Dummy module that only exists for the sake of proving that the message queue
* correctly dispatches to commonJS modules. The `testHook` is overridden by test
* cases.
*/
const MessageQueueTestModule = {
testHook1: function () {},
testHook2: function () {},
};
module.exports = MessageQueueTestModule;

View File

@@ -0,0 +1,158 @@
/**
* 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
*/
'use strict';
import type {BlobData, BlobOptions} from './BlobTypes';
/**
* Opaque JS representation of some binary data in native.
*
* The API is modeled after the W3C Blob API, with one caveat
* regarding explicit deallocation. Refer to the `close()`
* method for further details.
*
* Example usage in a React component:
*
* class WebSocketImage extends React.Component {
* state = {blob: null};
* componentDidMount() {
* let ws = this.ws = new WebSocket(...);
* ws.binaryType = 'blob';
* ws.onmessage = (event) => {
* if (this.state.blob) {
* this.state.blob.close();
* }
* this.setState({blob: event.data});
* };
* }
* componentUnmount() {
* if (this.state.blob) {
* this.state.blob.close();
* }
* this.ws.close();
* }
* render() {
* if (!this.state.blob) {
* return <View />;
* }
* return <Image source={{uri: URL.createObjectURL(this.state.blob)}} />;
* }
* }
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob
*/
class Blob {
_data: ?BlobData;
/**
* Constructor for JS consumers.
* Currently we only support creating Blobs from other Blobs.
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
*/
constructor(parts: Array<Blob | string> = [], options?: BlobOptions) {
const BlobManager = require('./BlobManager');
this.data = BlobManager.createFromParts(parts, options).data;
}
/*
* This method is used to create a new Blob object containing
* the data in the specified range of bytes of the source Blob.
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice
*/
// $FlowFixMe[unsafe-getters-setters]
set data(data: ?BlobData) {
this._data = data;
}
// $FlowFixMe[unsafe-getters-setters]
get data(): BlobData {
if (!this._data) {
throw new Error('Blob has been closed and is no longer available');
}
return this._data;
}
slice(start?: number, end?: number, contentType: string = ''): Blob {
const BlobManager = require('./BlobManager');
let {offset, size} = this.data;
if (typeof start === 'number') {
if (start > size) {
// $FlowFixMe[reassign-const]
start = size;
}
offset += start;
size -= start;
if (typeof end === 'number') {
if (end < 0) {
// $FlowFixMe[reassign-const]
end = this.size + end;
}
if (end > this.size) {
// $FlowFixMe[reassign-const]
end = this.size;
}
size = end - start;
}
}
return BlobManager.createFromOptions({
blobId: this.data.blobId,
offset,
size,
type: contentType,
/* Since `blob.slice()` creates a new view onto the same binary
* data as the original blob, we should re-use the same collector
* object so that the underlying resource gets deallocated when
* the last view into the data is released, not the first.
*/
__collector: this.data.__collector,
});
}
/**
* This method is in the standard, but not actually implemented by
* any browsers at this point. It's important for how Blobs work in
* React Native, however, since we cannot de-allocate resources automatically,
* so consumers need to explicitly de-allocate them.
*
* Note that the semantics around Blobs created via `blob.slice()`
* and `new Blob([blob])` are different. `blob.slice()` creates a
* new *view* onto the same binary data, so calling `close()` on any
* of those views is enough to deallocate the data, whereas
* `new Blob([blob, ...])` actually copies the data in memory.
*/
close() {
const BlobManager = require('./BlobManager');
BlobManager.release(this.data.blobId);
this.data = null;
}
/**
* Size of the data contained in the Blob object, in bytes.
*/
// $FlowFixMe[unsafe-getters-setters]
get size(): number {
return this.data.size;
}
/*
* String indicating the MIME type of the data contained in the Blob.
* If the type is unknown, this string is empty.
*/
// $FlowFixMe[unsafe-getters-setters]
get type(): string {
return this.data.type || '';
}
}
module.exports = Blob;

View File

@@ -0,0 +1,179 @@
/**
* 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 {BlobCollector, BlobData, BlobOptions} from './BlobTypes';
import NativeBlobModule from './NativeBlobModule';
import invariant from 'invariant';
const Blob = require('./Blob');
const BlobRegistry = require('./BlobRegistry');
/*eslint-disable no-bitwise */
/*eslint-disable eqeqeq */
/**
* Based on the rfc4122-compliant solution posted at
* http://stackoverflow.com/questions/105034
*/
function uuidv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// **Temporary workaround**
// TODO(#24654): Use turbomodules for the Blob module.
// Blob collector is a jsi::HostObject that is used by native to know
// when the a Blob instance is deallocated. This allows to free the
// underlying native resources. This is a hack to workaround the fact
// that the current bridge infra doesn't allow to track js objects
// deallocation. Ideally the whole Blob object should be a jsi::HostObject.
function createBlobCollector(blobId: string): BlobCollector | null {
if (global.__blobCollectorProvider == null) {
return null;
} else {
return global.__blobCollectorProvider(blobId);
}
}
/**
* Module to manage blobs. Wrapper around the native blob module.
*/
class BlobManager {
/**
* If the native blob module is available.
*/
static isAvailable: boolean = !!NativeBlobModule;
/**
* Create blob from existing array of blobs.
*/
static createFromParts(
parts: Array<Blob | string>,
options?: BlobOptions,
): Blob {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
const blobId = uuidv4();
const items = parts.map(part => {
if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) {
throw new Error(
"Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported",
);
}
if (part instanceof Blob) {
return {
data: part.data,
type: 'blob',
};
} else {
return {
data: String(part),
type: 'string',
};
}
});
const size = items.reduce((acc, curr) => {
if (curr.type === 'string') {
return acc + global.unescape(encodeURI(curr.data)).length;
} else {
return acc + curr.data.size;
}
}, 0);
NativeBlobModule.createFromParts(items, blobId);
return BlobManager.createFromOptions({
blobId,
offset: 0,
size,
type: options ? options.type : '',
lastModified: options ? options.lastModified : Date.now(),
});
}
/**
* Create blob instance from blob data from native.
* Used internally by modules like XHR, WebSocket, etc.
*/
static createFromOptions(options: BlobData): Blob {
BlobRegistry.register(options.blobId);
// $FlowFixMe[prop-missing]
return Object.assign(Object.create(Blob.prototype), {
data:
// Reuse the collector instance when creating from an existing blob.
// This will make sure that the underlying resource is only deallocated
// when all blobs that refer to it are deallocated.
options.__collector == null
? {
...options,
__collector: createBlobCollector(options.blobId),
}
: options,
});
}
/**
* Deallocate resources for a blob.
*/
static release(blobId: string): void {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
BlobRegistry.unregister(blobId);
if (BlobRegistry.has(blobId)) {
return;
}
NativeBlobModule.release(blobId);
}
/**
* Inject the blob content handler in the networking module to support blob
* requests and responses.
*/
static addNetworkingHandler(): void {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
NativeBlobModule.addNetworkingHandler();
}
/**
* Indicate the websocket should return a blob for incoming binary
* messages.
*/
static addWebSocketHandler(socketId: number): void {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
NativeBlobModule.addWebSocketHandler(socketId);
}
/**
* Indicate the websocket should no longer return a blob for incoming
* binary messages.
*/
static removeWebSocketHandler(socketId: number): void {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
NativeBlobModule.removeWebSocketHandler(socketId);
}
/**
* Send a blob message to a websocket.
*/
static sendOverSocket(blob: Blob, socketId: number): void {
invariant(NativeBlobModule, 'NativeBlobModule is available.');
NativeBlobModule.sendOverSocket(blob.data, socketId);
}
}
module.exports = BlobManager;

View File

@@ -0,0 +1,43 @@
/**
* 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
* @format
*/
const registry: Map<string, number> = new Map();
const register = (id: string) => {
const used = registry.get(id);
if (used != null) {
registry.set(id, used + 1);
} else {
registry.set(id, 1);
}
};
const unregister = (id: string) => {
const used = registry.get(id);
if (used != null) {
if (used <= 1) {
registry.delete(id);
} else {
registry.set(id, used - 1);
}
}
};
const has = (id: string): number | boolean => {
return registry.get(id) || false;
};
module.exports = {
register,
unregister,
has,
};

View File

@@ -0,0 +1,30 @@
/**
* 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
* @format
*/
'use strict';
export opaque type BlobCollector = {...};
export type BlobData = {
blobId: string,
offset: number,
size: number,
name?: string,
type?: string,
lastModified?: number,
__collector?: ?BlobCollector,
...
};
export type BlobOptions = {
type: string,
lastModified: number,
...
};

View File

@@ -0,0 +1,55 @@
/**
* 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
* @format
*/
'use strict';
import type {BlobOptions} from './BlobTypes';
const Blob = require('./Blob');
const invariant = require('invariant');
/**
* The File interface provides information about files.
*/
class File extends Blob {
/**
* Constructor for JS consumers.
*/
constructor(
parts: Array<Blob | string>,
name: string,
options?: BlobOptions,
) {
invariant(
parts != null && name != null,
'Failed to construct `File`: Must pass both `parts` and `name` arguments.',
);
super(parts, options);
this.data.name = name;
}
/**
* Name of the file.
*/
get name(): string {
invariant(this.data.name != null, 'Files must have a name set.');
return this.data.name;
}
/*
* Last modified time of the file.
*/
get lastModified(): number {
return this.data.lastModified || 0;
}
}
module.exports = File;

View File

@@ -0,0 +1,186 @@
/**
* 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
* @format
*/
import type Blob from './Blob';
import NativeFileReaderModule from './NativeFileReaderModule';
import {toByteArray} from 'base64-js';
import EventTarget from 'event-target-shim';
type ReadyState =
| 0 // EMPTY
| 1 // LOADING
| 2; // DONE
type ReaderResult = string | ArrayBuffer;
const READER_EVENTS = [
'abort',
'error',
'load',
'loadstart',
'loadend',
'progress',
];
const EMPTY = 0;
const LOADING = 1;
const DONE = 2;
class FileReader extends (EventTarget(...READER_EVENTS): any) {
static EMPTY: number = EMPTY;
static LOADING: number = LOADING;
static DONE: number = DONE;
EMPTY: number = EMPTY;
LOADING: number = LOADING;
DONE: number = DONE;
_readyState: ReadyState;
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean = false;
constructor() {
super();
this._reset();
}
_reset(): void {
this._readyState = EMPTY;
this._error = null;
this._result = null;
}
_setReadyState(newState: ReadyState) {
this._readyState = newState;
this.dispatchEvent({type: 'readystatechange'});
if (newState === DONE) {
if (this._aborted) {
this.dispatchEvent({type: 'abort'});
} else if (this._error) {
this.dispatchEvent({type: 'error'});
} else {
this.dispatchEvent({type: 'load'});
}
this.dispatchEvent({type: 'loadend'});
}
}
readAsArrayBuffer(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
const base64 = text.split(',')[1];
const typedArray = toByteArray(base64);
this._result = typedArray.buffer;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsDataURL(blob: ?Blob): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void {
this._aborted = false;
if (blob == null) {
throw new TypeError(
"Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'",
);
}
NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
if (this._aborted) {
return;
}
this._error = error;
this._setReadyState(DONE);
},
);
}
abort() {
this._aborted = true;
// only call onreadystatechange if there is something to abort, as per spec
if (this._readyState !== EMPTY && this._readyState !== DONE) {
this._reset();
this._setReadyState(DONE);
}
// Reset again after, in case modified in handler
this._reset();
}
get readyState(): ReadyState {
return this._readyState;
}
get error(): ?Error {
return this._error;
}
get result(): ?ReaderResult {
return this._result;
}
}
module.exports = FileReader;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeBlobModule';
import NativeBlobModule from '../../src/private/specs/modules/NativeBlobModule';
export default NativeBlobModule;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeFileReaderModule';
import NativeFileReaderModule from '../../src/private/specs/modules/NativeFileReaderModule';
export default NativeFileReaderModule;

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
#import <jsi/jsi.h>
@class RCTBlobManager;
namespace facebook::react {
class JSI_EXPORT RCTBlobCollector : public jsi::HostObject {
public:
RCTBlobCollector(RCTBlobManager *blobManager, const std::string &blobId);
~RCTBlobCollector();
static void install(RCTBlobManager *blobManager);
private:
const std::string blobId_;
RCTBlobManager *blobManager_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
#import "RCTBlobCollector.h"
#import <React/RCTBlobManager.h>
#import <React/RCTBridge+Private.h>
namespace facebook::react {
RCTBlobCollector::RCTBlobCollector(RCTBlobManager *blobManager, const std::string &blobId)
: blobId_(blobId), blobManager_(blobManager)
{
}
RCTBlobCollector::~RCTBlobCollector()
{
RCTBlobManager *blobManager = blobManager_;
NSString *blobId = [NSString stringWithUTF8String:blobId_.c_str()];
dispatch_async([blobManager_ executionQueue], ^{
[blobManager remove:blobId];
});
}
void RCTBlobCollector::install(RCTBlobManager *blobManager)
{
__weak RCTCxxBridge *cxxBridge = (RCTCxxBridge *)blobManager.bridge;
[cxxBridge
dispatchBlock:^{
if (!cxxBridge || cxxBridge.runtime == nullptr) {
return;
}
jsi::Runtime &runtime = *(jsi::Runtime *)cxxBridge.runtime;
runtime.global().setProperty(
runtime,
"__blobCollectorProvider",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__blobCollectorProvider"),
1,
[blobManager](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
auto blobId = args[0].asString(rt).utf8(rt);
auto blobCollector = std::make_shared<RCTBlobCollector>(blobManager, blobId);
return jsi::Object::createFromHostObject(rt, blobCollector);
}));
}
queue:RCTJSThread];
}
} // namespace facebook::react

View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
#import <React/RCTBridge.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTInitializing.h>
#import <React/RCTURLRequestHandler.h>
RCT_EXTERN void RCTEnableBlobManagerProcessingQueue(BOOL enabled);
@interface RCTBlobManager : NSObject <RCTBridgeModule, RCTURLRequestHandler, RCTInitializing>
- (NSString *)store:(NSData *)data;
- (void)store:(NSData *)data withId:(NSString *)blobId;
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob;
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size;
- (NSData *)resolveURL:(NSURL *)url;
- (void)remove:(NSString *)blobId;
- (void)createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId;
- (dispatch_queue_t)executionQueue;
@end

View File

@@ -0,0 +1,352 @@
/*
* 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.
*/
#import <React/RCTBlobManager.h>
#import <mutex>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTMockDef.h>
#import <React/RCTNetworking.h>
#import <React/RCTUtils.h>
#import <React/RCTWebSocketModule.h>
#import "RCTBlobCollector.h"
#import "RCTBlobPlugins.h"
RCT_MOCK_DEF(RCTBlobManager, dispatch_async);
#define dispatch_async RCT_MOCK_USE(RCTBlobManager, dispatch_async)
static BOOL gBlobManagerProcessingQueueEnabled = NO;
RCT_EXTERN void RCTEnableBlobManagerProcessingQueue(BOOL enabled)
{
gBlobManagerProcessingQueueEnabled = enabled;
}
static NSString *const kBlobURIScheme = @"blob";
@interface RCTBlobManager () <
RCTNetworkingRequestHandler,
RCTNetworkingResponseHandler,
RCTWebSocketContentHandler,
NativeBlobModuleSpec>
@end
@implementation RCTBlobManager {
// Blobs should be thread safe since they are used from the websocket and networking module,
// make sure to use proper locking when accessing this.
NSMutableDictionary<NSString *, NSData *> *_blobs;
std::mutex _blobsMutex;
NSOperationQueue *_queue;
dispatch_queue_t _processingQueue;
}
RCT_EXPORT_MODULE(BlobModule)
@synthesize bridge = _bridge;
@synthesize moduleRegistry = _moduleRegistry;
@synthesize methodQueue = _methodQueue;
- (void)initialize
{
std::lock_guard<std::mutex> lock(_blobsMutex);
_blobs = [NSMutableDictionary new];
if (gBlobManagerProcessingQueueEnabled) {
_processingQueue = dispatch_queue_create("com.facebook.react.blobmanager.processing", DISPATCH_QUEUE_SERIAL);
}
facebook::react::RCTBlobCollector::install(self);
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (NSDictionary<NSString *, id> *)constantsToExport
{
return [self getConstants];
}
- (NSDictionary<NSString *, id> *)getConstants
{
return @{
@"BLOB_URI_SCHEME" : kBlobURIScheme,
@"BLOB_URI_HOST" : [NSNull null],
};
}
- (NSString *)store:(NSData *)data
{
NSString *blobId = [NSUUID UUID].UUIDString;
[self store:data withId:blobId];
return blobId;
}
- (void)store:(NSData *)data withId:(NSString *)blobId
{
std::lock_guard<std::mutex> lock(_blobsMutex);
_blobs[blobId] = data;
}
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
{
NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
return [self resolve:blobId offset:offset ? [offset integerValue] : 0 size:size ? [size integerValue] : -1];
}
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
{
NSData *data;
{
std::lock_guard<std::mutex> lock(_blobsMutex);
data = _blobs[blobId];
}
if (!data) {
return nil;
}
if (offset != 0 || (size != -1 && size != data.length)) {
data = [data subdataWithRange:NSMakeRange(offset, size)];
}
return data;
}
- (NSData *)resolveURL:(NSURL *)url
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
NSString *blobId = components.path;
NSInteger offset = 0;
NSInteger size = -1;
if (components.queryItems) {
for (NSURLQueryItem *queryItem in components.queryItems) {
if ([queryItem.name isEqualToString:@"offset"]) {
offset = [queryItem.value integerValue];
}
if ([queryItem.name isEqualToString:@"size"]) {
size = [queryItem.value integerValue];
}
}
}
if (blobId) {
return [self resolve:blobId offset:offset size:size];
}
return nil;
}
- (void)remove:(NSString *)blobId
{
std::lock_guard<std::mutex> lock(_blobsMutex);
[_blobs removeObjectForKey:blobId];
}
RCT_EXPORT_METHOD(addNetworkingHandler)
{
RCTNetworking *const networking = [_moduleRegistry moduleForName:"Networking"];
// TODO(T63516227): Why can methodQueue be nil here?
// We don't want to do anything when methodQueue is nil.
if (![networking requestQueue]) {
return;
}
dispatch_async([networking requestQueue], ^{
[networking addRequestHandler:self];
[networking addResponseHandler:self];
});
}
RCT_EXPORT_METHOD(addWebSocketHandler : (double)socketID)
{
dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{
[[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:self
forSocketID:[NSNumber numberWithDouble:socketID]];
});
}
RCT_EXPORT_METHOD(removeWebSocketHandler : (double)socketID)
{
dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{
[[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:nil
forSocketID:[NSNumber numberWithDouble:socketID]];
});
}
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
RCT_EXPORT_METHOD(sendOverSocket : (NSDictionary *)blob socketID : (double)socketID)
{
dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{
[[self->_moduleRegistry moduleForName:"WebSocketModule"] sendData:[self resolve:blob]
forSocketID:[NSNumber numberWithDouble:socketID]];
});
}
RCT_EXPORT_METHOD(createFromParts : (NSArray<NSDictionary<NSString *, id> *> *)parts withId : (NSString *)blobId)
{
NSMutableData *data = [NSMutableData new];
for (NSDictionary<NSString *, id> *part in parts) {
NSString *type = [RCTConvert NSString:part[@"type"]];
if ([type isEqualToString:@"blob"]) {
NSData *partData = [self resolve:part[@"data"]];
[data appendData:partData];
} else if ([type isEqualToString:@"string"]) {
NSData *partData = [[RCTConvert NSString:part[@"data"]] dataUsingEncoding:NSUTF8StringEncoding];
[data appendData:partData];
} else {
[NSException raise:@"Invalid type for blob" format:@"%@ is invalid", type];
}
}
dispatch_async([self executionQueue], ^{
[self store:data withId:blobId];
});
}
RCT_EXPORT_METHOD(release : (NSString *)blobId)
{
dispatch_async([self executionQueue], ^{
[self remove:blobId];
});
}
- (dispatch_queue_t)executionQueue
{
return gBlobManagerProcessingQueueEnabled ? _processingQueue : _methodQueue;
}
#pragma mark - RCTURLRequestHandler methods
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
return [request.URL.scheme caseInsensitiveCompare:kBlobURIScheme] == NSOrderedSame;
}
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
{
// Lazy setup
if (!_queue) {
_queue = [NSOperationQueue new];
_queue.maxConcurrentOperationCount = 2;
}
__weak __typeof(self) weakSelf = self;
__weak __block NSBlockOperation *weakOp;
__block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
__typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:nil
expectedContentLength:-1
textEncodingName:nil];
[delegate URLRequest:weakOp didReceiveResponse:response];
NSData *data = [strongSelf resolveURL:response.URL];
NSError *error;
if (data) {
[delegate URLRequest:weakOp didReceiveData:data];
} else {
error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
}
[delegate URLRequest:weakOp didCompleteWithError:error];
}];
weakOp = op;
[_queue addOperation:op];
return op;
}
- (void)cancelRequest:(NSOperation *)op
{
[op cancel];
}
#pragma mark - RCTNetworkingRequestHandler methods
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data
{
return data[@"blob"] != nil;
}
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data
{
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
NSDictionary *blob = [RCTConvert NSDictionary:data[@"blob"]];
NSString *contentType = @"application/octet-stream";
NSString *blobType = [RCTConvert NSString:RCTNilIfNull(blob[@"type"])];
if (blobType != nil && blobType.length > 0) {
contentType = blob[@"type"];
}
return @{@"body" : [self resolve:blob], @"contentType" : contentType};
}
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType
{
return [responseType isEqualToString:@"blob"];
}
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data
{
// An empty body will have nil for data, in this case we need to return
// an empty blob as per the XMLHttpRequest spec.
data = data ?: [NSData new];
return @{
@"blobId" : [self store:data],
@"offset" : @0,
@"size" : @(data.length),
@"name" : RCTNullIfNil([response suggestedFilename]),
@"type" : RCTNullIfNil([response MIMEType]),
};
}
#pragma mark - RCTWebSocketContentHandler methods
- (id)processWebsocketMessage:(id)message
forSocketID:(NSNumber *)socketID
withType:(NSString *__autoreleasing _Nonnull *)type
{
if (![message isKindOfClass:[NSData class]]) {
*type = @"text";
return message;
}
*type = @"blob";
return @{
@"blobId" : [self store:message],
@"offset" : @0,
@"size" : @(((NSData *)message).length),
};
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeBlobModuleSpecJSI>(params);
}
@end
Class RCTBlobManagerCls(void)
{
return RCTBlobManager.class;
}

View File

@@ -0,0 +1,41 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
// FB Internal: FBRCTBlobPlugins.h is autogenerated by the build system.
#import <React/FBRCTBlobPlugins.h>
#else
// OSS-compatibility layer
#import <Foundation/Foundation.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage"
#ifdef __cplusplus
extern "C" {
#endif
// RCTTurboModuleManagerDelegate should call this to resolve module classes.
Class RCTBlobClassProvider(const char *name);
// Lookup functions
Class RCTBlobManagerCls(void) __attribute__((used));
Class RCTFileReaderModuleCls(void) __attribute__((used));
#ifdef __cplusplus
}
#endif
#pragma GCC diagnostic pop
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -0,0 +1,34 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
// OSS-compatibility layer
#import "RCTBlobPlugins.h"
#import <string>
#import <unordered_map>
Class RCTBlobClassProvider(const char *name) {
// Intentionally leak to avoid crashing after static destructors are run.
static const auto sCoreModuleClassMap = new const std::unordered_map<std::string, Class (*)(void)>{
{"BlobModule", RCTBlobManagerCls},
{"FileReaderModule", RCTFileReaderModuleCls},
};
auto p = sCoreModuleClassMap->find(name);
if (p != sCoreModuleClassMap->end()) {
auto classFunc = p->second;
return classFunc();
}
return nil;
}
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
@interface RCTFileReaderModule : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,95 @@
/*
* 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.
*/
#import <React/RCTFileReaderModule.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTBlobManager.h>
#import "RCTBlobPlugins.h"
@interface RCTFileReaderModule () <NativeFileReaderModuleSpec>
@end
@implementation RCTFileReaderModule
RCT_EXPORT_MODULE(FileReaderModule)
@synthesize moduleRegistry = _moduleRegistry;
RCT_EXPORT_METHOD(readAsText
: (NSDictionary<NSString *, id> *)blob encoding
: (NSString *)encoding resolve
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject)
{
RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"];
dispatch_async([blobManager executionQueue], ^{
NSData *data = [blobManager resolve:blob];
if (data == nil) {
reject(
RCTErrorUnspecified,
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]],
nil);
} else {
NSStringEncoding stringEncoding;
if (encoding == nil) {
stringEncoding = NSUTF8StringEncoding;
} else {
stringEncoding =
CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)encoding));
}
NSString *text = [[NSString alloc] initWithData:data encoding:stringEncoding];
resolve(text);
}
});
}
RCT_EXPORT_METHOD(readAsDataURL
: (NSDictionary<NSString *, id> *)blob resolve
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject)
{
RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"];
dispatch_async([blobManager executionQueue], ^{
NSData *data = [blobManager resolve:blob];
if (data == nil) {
reject(
RCTErrorUnspecified,
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]],
nil);
} else {
NSString *type = [RCTConvert NSString:blob[@"type"]];
NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
type != nil && [type length] > 0 ? type : @"application/octet-stream",
[data base64EncodedStringWithOptions:0]];
resolve(text);
}
});
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeFileReaderModuleSpecJSI>(params);
}
@end
Class RCTFileReaderModuleCls(void)
{
return RCTFileReaderModule.class;
}

View File

@@ -0,0 +1,66 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
header_search_paths = [
"\"$(PODS_ROOT)/RCT-Folly\"",
"\"$(PODS_ROOT)/boost\"",
"\"$(PODS_ROOT)/DoubleConversion\"",
"\"$(PODS_ROOT)/fmt/include\"",
"\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"",
]
Pod::Spec.new do |s|
s.name = "React-RCTBlob"
s.version = version
s.summary = "An API for displaying iOS action sheets and share sheets."
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness'
s.source = source
s.source_files = "*.{h,m,mm}"
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTBlob"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"HEADER_SEARCH_PATHS" => header_search_paths.join(' ')
}
s.dependency "DoubleConversion"
s.dependency "fmt", "9.1.0"
s.dependency "RCT-Folly", folly_version
s.dependency "React-jsi"
s.dependency "React-Core/RCTBlobHeaders"
s.dependency "React-Core/RCTWebSocket"
s.dependency "React-RCTNetwork"
add_dependency(s, "React-Codegen")
add_dependency(s, "React-NativeModulesApple")
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"
end
end

View File

@@ -0,0 +1,233 @@
/**
* 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.
*
* @format
* @flow
*/
import type Blob from './Blob';
import NativeBlobModule from './NativeBlobModule';
let BLOB_URL_PREFIX = null;
if (
NativeBlobModule &&
typeof NativeBlobModule.getConstants().BLOB_URI_SCHEME === 'string'
) {
const constants = NativeBlobModule.getConstants();
// $FlowFixMe[incompatible-type] asserted above
// $FlowFixMe[unsafe-addition]
BLOB_URL_PREFIX = constants.BLOB_URI_SCHEME + ':';
if (typeof constants.BLOB_URI_HOST === 'string') {
BLOB_URL_PREFIX += `//${constants.BLOB_URI_HOST}/`;
}
}
/**
* To allow Blobs be accessed via `content://` URIs,
* you need to register `BlobProvider` as a ContentProvider in your app's `AndroidManifest.xml`:
*
* ```xml
* <manifest>
* <application>
* <provider
* android:name="com.facebook.react.modules.blob.BlobProvider"
* android:authorities="@string/blob_provider_authority"
* android:exported="false"
* />
* </application>
* </manifest>
* ```
* And then define the `blob_provider_authority` string in `res/values/strings.xml`.
* Use a dotted name that's entirely unique to your app:
*
* ```xml
* <resources>
* <string name="blob_provider_authority">your.app.package.blobs</string>
* </resources>
* ```
*/
// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/src
// The reference code bloat comes from Unicode issues with URLs, so those won't work here.
export class URLSearchParams {
_searchParams: Array<Array<string>> = [];
constructor(params: any) {
if (typeof params === 'object') {
Object.keys(params).forEach(key => this.append(key, params[key]));
}
}
append(key: string, value: string): void {
this._searchParams.push([key, value]);
}
delete(name: string): void {
throw new Error('URLSearchParams.delete is not implemented');
}
get(name: string): void {
throw new Error('URLSearchParams.get is not implemented');
}
getAll(name: string): void {
throw new Error('URLSearchParams.getAll is not implemented');
}
has(name: string): void {
throw new Error('URLSearchParams.has is not implemented');
}
set(name: string, value: string): void {
throw new Error('URLSearchParams.set is not implemented');
}
sort(): void {
throw new Error('URLSearchParams.sort is not implemented');
}
// $FlowFixMe[unsupported-syntax]
// $FlowFixMe[missing-local-annot]
[Symbol.iterator]() {
return this._searchParams[Symbol.iterator]();
}
toString(): string {
if (this._searchParams.length === 0) {
return '';
}
const last = this._searchParams.length - 1;
return this._searchParams.reduce((acc, curr, index) => {
return (
acc +
encodeURIComponent(curr[0]) +
'=' +
encodeURIComponent(curr[1]) +
(index === last ? '' : '&')
);
}, '');
}
}
function validateBaseUrl(url: string) {
// from this MIT-licensed gist: https://gist.github.com/dperini/729294
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test(
url,
);
}
export class URL {
_url: string;
_searchParamsInstance: ?URLSearchParams = null;
static createObjectURL(blob: Blob): string {
if (BLOB_URL_PREFIX === null) {
throw new Error('Cannot create URL for blob!');
}
return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}`;
}
static revokeObjectURL(url: string) {
// Do nothing.
}
// $FlowFixMe[missing-local-annot]
constructor(url: string, base: string | URL) {
let baseUrl = null;
if (!base || validateBaseUrl(url)) {
this._url = url;
if (!this._url.endsWith('/')) {
this._url += '/';
}
} else {
if (typeof base === 'string') {
baseUrl = base;
if (!validateBaseUrl(baseUrl)) {
throw new TypeError(`Invalid base URL: ${baseUrl}`);
}
} else {
baseUrl = base.toString();
}
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!url.startsWith('/')) {
url = `/${url}`;
}
if (baseUrl.endsWith(url)) {
url = '';
}
this._url = `${baseUrl}${url}`;
}
}
get hash(): string {
throw new Error('URL.hash is not implemented');
}
get host(): string {
throw new Error('URL.host is not implemented');
}
get hostname(): string {
throw new Error('URL.hostname is not implemented');
}
get href(): string {
return this.toString();
}
get origin(): string {
throw new Error('URL.origin is not implemented');
}
get password(): string {
throw new Error('URL.password is not implemented');
}
get pathname(): string {
throw new Error('URL.pathname not implemented');
}
get port(): string {
throw new Error('URL.port is not implemented');
}
get protocol(): string {
throw new Error('URL.protocol is not implemented');
}
get search(): string {
throw new Error('URL.search is not implemented');
}
get searchParams(): URLSearchParams {
if (this._searchParamsInstance == null) {
this._searchParamsInstance = new URLSearchParams();
}
return this._searchParamsInstance;
}
toJSON(): string {
return this.toString();
}
toString(): string {
if (this._searchParamsInstance === null) {
return this._url;
}
// $FlowFixMe[incompatible-use]
const instanceString = this._searchParamsInstance.toString();
const separator = this._url.indexOf('?') > -1 ? '&' : '?';
return this._url + separator + instanceString;
}
get username(): string {
throw new Error('URL.username is not implemented');
}
}

View File

@@ -0,0 +1,16 @@
/**
* 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
* @format
*/
const BlobModule = {
createFromParts() {},
release() {},
};
module.exports = BlobModule;

View File

@@ -0,0 +1,20 @@
/**
* 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
* @format
*/
const FileReaderModule = {
async readAsText(): Promise<string> {
return '';
},
async readAsDataURL(): Promise<string> {
return 'data:text/plain;base64,NDI=';
},
};
module.exports = FileReaderModule;

View File

@@ -0,0 +1,140 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import NativeRedBox from '../NativeModules/specs/NativeRedBox';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeBugReporting from './NativeBugReporting';
type ExtraData = {[key: string]: string, ...};
type SourceCallback = () => string;
type DebugData = {
extras: ExtraData,
files: ExtraData,
...
};
function defaultExtras() {
BugReporting.addFileSource('react_hierarchy.txt', () =>
require('./dumpReactTree')(),
);
}
/**
* A simple class for collecting bug report data. Components can add sources that will be queried when a bug report
* is created via `collectExtraData`. For example, a list component might add a source that provides the list of rows
* that are currently visible on screen. Components should also remember to call `remove()` on the object that is
* returned by `addSource` when they are unmounted.
*/
class BugReporting {
static _extraSources: Map<string, SourceCallback> = new Map();
static _fileSources: Map<string, SourceCallback> = new Map();
static _subscription: ?EventSubscription = null;
static _redboxSubscription: ?EventSubscription = null;
static _maybeInit() {
if (!BugReporting._subscription) {
BugReporting._subscription = RCTDeviceEventEmitter.addListener(
'collectBugExtraData',
// $FlowFixMe[method-unbinding]
BugReporting.collectExtraData,
null,
);
defaultExtras();
}
if (!BugReporting._redboxSubscription) {
BugReporting._redboxSubscription = RCTDeviceEventEmitter.addListener(
'collectRedBoxExtraData',
// $FlowFixMe[method-unbinding]
BugReporting.collectExtraData,
null,
);
}
}
/**
* Maps a string key to a simple callback that should return a string payload to be attached
* to a bug report. Source callbacks are called when `collectExtraData` is called.
*
* Returns an object to remove the source when the component unmounts.
*
* Conflicts trample with a warning.
*/
static addSource(
key: string,
callback: SourceCallback,
): {remove: () => void, ...} {
return this._addSource(key, callback, BugReporting._extraSources);
}
/**
* Maps a string key to a simple callback that should return a string payload to be attached
* to a bug report. Source callbacks are called when `collectExtraData` is called.
*
* Returns an object to remove the source when the component unmounts.
*
* Conflicts trample with a warning.
*/
static addFileSource(
key: string,
callback: SourceCallback,
): {remove: () => void, ...} {
return this._addSource(key, callback, BugReporting._fileSources);
}
static _addSource(
key: string,
callback: SourceCallback,
source: Map<string, SourceCallback>,
): {remove: () => void, ...} {
BugReporting._maybeInit();
if (source.has(key)) {
console.warn(
`BugReporting.add* called multiple times for same key '${key}'`,
);
}
source.set(key, callback);
return {
remove: () => {
source.delete(key);
},
};
}
/**
* This can be called from a native bug reporting flow, or from JS code.
*
* If available, this will call `NativeModules.BugReporting.setExtraData(extraData)`
* after collecting `extraData`.
*/
static collectExtraData(): DebugData {
const extraData: ExtraData = {};
for (const [key, callback] of BugReporting._extraSources) {
extraData[key] = callback();
}
const fileData: ExtraData = {};
for (const [key, callback] of BugReporting._fileSources) {
fileData[key] = callback();
}
if (NativeBugReporting != null && NativeBugReporting.setExtraData != null) {
NativeBugReporting.setExtraData(extraData, fileData);
}
if (NativeRedBox != null && NativeRedBox.setExtraData != null) {
NativeRedBox.setExtraData(extraData, 'From BugReporting.js');
}
return {extras: extraData, files: fileData};
}
}
module.exports = BugReporting;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../src/private/specs/modules/NativeBugReporting';
import NativeBugReporting from '../../src/private/specs/modules/NativeBugReporting';
export default NativeBugReporting;

View File

@@ -0,0 +1,151 @@
/**
* 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.
*
* @format
* @flow strict
*/
'use strict';
/*
const getReactData = require('getReactData');
const INDENTATION_SIZE = 2;
const MAX_DEPTH = 2;
const MAX_STRING_LENGTH = 50;
*/
/**
* Dump all React Native root views and their content. This function tries
* it best to get the content but ultimately relies on implementation details
* of React and will fail in future versions.
*/
function dumpReactTree(): string {
try {
return getReactTree();
} catch (e) {
return 'Failed to dump react tree: ' + e;
}
}
function getReactTree() {
// TODO(sema): Reenable tree dumps using the Fiber tree structure. #15945684
return (
'React tree dumps have been temporarily disabled while React is ' +
'upgraded to Fiber.'
);
/*
let output = '';
const rootIds = Object.getOwnPropertyNames(ReactNativeMount._instancesByContainerID);
for (const rootId of rootIds) {
const instance = ReactNativeMount._instancesByContainerID[rootId];
output += `============ Root ID: ${rootId} ============\n`;
output += dumpNode(instance, 0);
output += `============ End root ID: ${rootId} ============\n`;
}
return output;
*/
}
/*
function dumpNode(node: Object, indentation: number) {
const data = getReactData(node);
if (data.nodeType === 'Text') {
return indent(indentation) + data.text + '\n';
} else if (data.nodeType === 'Empty') {
return '';
}
let output = indent(indentation) + `<${data.name}`;
if (data.nodeType === 'Composite') {
for (const propName of Object.getOwnPropertyNames(data.props || {})) {
if (isNormalProp(propName)) {
try {
const value = convertValue(data.props[propName]);
if (value) {
output += ` ${propName}=${value}`;
}
} catch (e) {
const message = `[Failed to get property: ${e}]`;
output += ` ${propName}=${message}`;
}
}
}
}
let childOutput = '';
for (const child of data.children || []) {
childOutput += dumpNode(child, indentation + 1);
}
if (childOutput) {
output += '>\n' + childOutput + indent(indentation) + `</${data.name}>\n`;
} else {
output += ' />\n';
}
return output;
}
function isNormalProp(name: string): boolean {
switch (name) {
case 'children':
case 'key':
case 'ref':
return false;
default:
return true;
}
}
function convertObject(object: Object, depth: number) {
if (depth >= MAX_DEPTH) {
return '[...omitted]';
}
let output = '{';
let first = true;
for (const key of Object.getOwnPropertyNames(object)) {
if (!first) {
output += ', ';
}
output += `${key}: ${convertValue(object[key], depth + 1)}`;
first = false;
}
return output + '}';
}
function convertValue(value, depth = 0): ?string {
if (!value) {
return null;
}
switch (typeof value) {
case 'string':
return JSON.stringify(possiblyEllipsis(value).replace('\n', '\\n'));
case 'boolean':
case 'number':
return JSON.stringify(value);
case 'function':
return '[function]';
case 'object':
return convertObject(value, depth);
default:
return null;
}
}
function possiblyEllipsis(value: string) {
if (value.length > MAX_STRING_LENGTH) {
return value.slice(0, MAX_STRING_LENGTH) + '...';
} else {
return value;
}
}
function indent(size: number) {
return ' '.repeat(size * INDENTATION_SIZE);
}
*/
module.exports = dumpReactTree;

View File

@@ -0,0 +1,187 @@
/**
* 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.
*
* @format
* @flow
*/
'use strict';
/**
* Convert a react internal instance to a sanitized data object.
*
* This is shamelessly stolen from react-devtools:
* https://github.com/facebook/react-devtools/blob/HEAD/backend/getData.js
*/
function getData(element: Object): Object {
let children = null;
let props = null;
let state = null;
let context = null;
let updater = null;
let name = null;
let type = null;
let text = null;
let publicInstance = null;
let nodeType = 'Native';
// If the parent is a native node without rendered children, but with
// multiple string children, then the `element` that gets passed in here is
// a plain value -- a string or number.
if (typeof element !== 'object') {
nodeType = 'Text';
text = element + '';
} else if (
element._currentElement === null ||
element._currentElement === false
) {
nodeType = 'Empty';
} else if (element._renderedComponent) {
nodeType = 'NativeWrapper';
children = [element._renderedComponent];
props = element._instance.props;
state = element._instance.state;
context = element._instance.context;
if (context && Object.keys(context).length === 0) {
context = null;
}
} else if (element._renderedChildren) {
children = childrenList(element._renderedChildren);
} else if (element._currentElement && element._currentElement.props) {
// This is a native node without rendered children -- meaning the children
// prop is just a string or (in the case of the <option>) a list of
// strings & numbers.
children = element._currentElement.props.children;
}
if (!props && element._currentElement && element._currentElement.props) {
props = element._currentElement.props;
}
// != used deliberately here to catch undefined and null
if (element._currentElement != null) {
type = element._currentElement.type;
if (typeof type === 'string') {
name = type;
} else if (element.getName) {
nodeType = 'Composite';
name = element.getName();
// 0.14 top-level wrapper
// TODO(jared): The backend should just act as if these don't exist.
if (
element._renderedComponent &&
element._currentElement.props ===
element._renderedComponent._currentElement
) {
nodeType = 'Wrapper';
}
if (name === null) {
name = 'No display name';
}
} else if (element._stringText) {
nodeType = 'Text';
text = element._stringText;
} else {
name = type.displayName || type.name || 'Unknown';
}
}
if (element._instance) {
const inst = element._instance;
updater = {
setState: inst.setState && inst.setState.bind(inst),
forceUpdate: inst.forceUpdate && inst.forceUpdate.bind(inst),
setInProps: inst.forceUpdate && setInProps.bind(null, element),
setInState: inst.forceUpdate && setInState.bind(null, inst),
setInContext: inst.forceUpdate && setInContext.bind(null, inst),
};
publicInstance = inst;
// TODO: React ART currently falls in this bucket, but this doesn't
// actually make sense and we should clean this up after stabilizing our
// API for backends
if (inst._renderedChildren) {
children = childrenList(inst._renderedChildren);
}
}
return {
nodeType,
type,
name,
props,
state,
context,
children,
text,
updater,
publicInstance,
};
}
function setInProps(
internalInst: any,
path: Array<string | number>,
value: any,
) {
const element = internalInst._currentElement;
internalInst._currentElement = {
...element,
props: copyWithSet(element.props, path, value),
};
internalInst._instance.forceUpdate();
}
function setInState(inst: any, path: Array<string | number>, value: any) {
setIn(inst.state, path, value);
inst.forceUpdate();
}
function setInContext(inst: any, path: Array<string | number>, value: any) {
setIn(inst.context, path, value);
inst.forceUpdate();
}
function setIn(obj: Object, path: Array<string | number>, value: any) {
const last = path.pop();
const parent = path.reduce((obj_, attr) => (obj_ ? obj_[attr] : null), obj);
if (parent) {
parent[last] = value;
}
}
function childrenList(children: any) {
const res = [];
for (const name in children) {
res.push(children[name]);
}
return res;
}
function copyWithSetImpl(
obj: any | Array<any>,
path: Array<string | number>,
idx: number,
value: any,
): any {
if (idx >= path.length) {
return value;
}
const key = path[idx];
const updated = Array.isArray(obj) ? obj.slice() : {...obj};
// $FlowFixMe[incompatible-use] number or string is fine here
updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
return updated;
}
function copyWithSet(
obj: Object | Array<any>,
path: Array<string | number>,
value: any,
): Object | Array<any> {
return copyWithSetImpl(obj, path, 0, value);
}
module.exports = getData;

View File

@@ -0,0 +1,157 @@
/**
* 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.
*
* @format
*/
import type * as React from 'react';
import {HostComponent} from '../../../types/public/ReactNativeTypes';
import {EmitterSubscription} from '../../vendor/emitter/EventEmitter';
type AccessibilityChangeEventName =
| 'change' // deprecated, maps to screenReaderChanged
| 'boldTextChanged' // iOS-only Event
| 'grayscaleChanged' // iOS-only Event
| 'invertColorsChanged' // iOS-only Event
| 'reduceMotionChanged'
| 'screenReaderChanged'
| 'reduceTransparencyChanged'; // iOS-only Event
type AccessibilityChangeEvent = boolean;
type AccessibilityChangeEventHandler = (
event: AccessibilityChangeEvent,
) => void;
type AccessibilityAnnouncementEventName = 'announcementFinished'; // iOS-only Event
type AccessibilityAnnouncementFinishedEvent = {
announcement: string;
success: boolean;
};
type AccessibilityAnnouncementFinishedEventHandler = (
event: AccessibilityAnnouncementFinishedEvent,
) => void;
type AccessibilityEventTypes = 'click' | 'focus' | 'viewHoverEnter';
/**
* @see https://reactnative.dev/docs/accessibilityinfo
*/
export interface AccessibilityInfoStatic {
/**
* Query whether bold text is currently enabled.
*
* @platform ios
*/
isBoldTextEnabled: () => Promise<boolean>;
/**
* Query whether grayscale is currently enabled.
*
* @platform ios
*/
isGrayscaleEnabled: () => Promise<boolean>;
/**
* Query whether invert colors is currently enabled.
*
* @platform ios
*/
isInvertColorsEnabled: () => Promise<boolean>;
/**
* Query whether reduce motion is currently enabled.
*/
isReduceMotionEnabled: () => Promise<boolean>;
/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when prefer cross-fade transitions is enabled and `false` otherwise.
*/
prefersCrossFadeTransitions(): Promise<boolean>;
/**
* Query whether reduce transparency is currently enabled.
*
* @platform ios
*/
isReduceTransparencyEnabled: () => Promise<boolean>;
/**
* Query whether a screen reader is currently enabled.
*/
isScreenReaderEnabled: () => Promise<boolean>;
/**
* Query whether Accessibility Service is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when any service is enabled and `false` otherwise.
*
* @platform android
*/
isAccessibilityServiceEnabled(): Promise<boolean>;
/**
* Add an event handler. Supported events:
* - announcementFinished: iOS-only event. Fires when the screen reader has finished making an announcement.
* The argument to the event handler is a dictionary with these keys:
* - announcement: The string announced by the screen reader.
* - success: A boolean indicating whether the announcement was successfully made.
* - AccessibilityEventName constants other than announcementFinished: Fires on accessibility feature change.
* The argument to the event handler is a boolean.
* The boolean is true when the related event's feature is enabled and false otherwise.
*
*/
addEventListener(
eventName: AccessibilityChangeEventName,
handler: AccessibilityChangeEventHandler,
): EmitterSubscription;
addEventListener(
eventName: AccessibilityAnnouncementEventName,
handler: AccessibilityAnnouncementFinishedEventHandler,
): EmitterSubscription;
/**
* Set accessibility focus to a react component.
*/
setAccessibilityFocus: (reactTag: number) => void;
/**
* Post a string to be announced by the screen reader.
*/
announceForAccessibility: (announcement: string) => void;
/**
* Post a string to be announced by the screen reader.
* - `announcement`: The string announced by the screen reader.
* - `options`: An object that configures the reading options.
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
*/
announceForAccessibilityWithOptions(
announcement: string,
options: {queue?: boolean | undefined},
): void;
/**
* Gets the timeout in millisecond that the user needs.
* This value is set in "Time to take action (Accessibility timeout)" of "Accessibility" settings.
*
* @platform android
*/
getRecommendedTimeoutMillis: (originalTimeout: number) => Promise<number>;
sendAccessibilityEvent: (
handle: React.ElementRef<HostComponent<unknown>>,
eventType: AccessibilityEventTypes,
) => void;
}
export const AccessibilityInfo: AccessibilityInfoStatic;
export type AccessibilityInfo = AccessibilityInfoStatic;

View File

@@ -0,0 +1,421 @@
/**
* 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 {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
import type {ElementRef} from 'react';
import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter';
import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy';
import Platform from '../../Utilities/Platform';
import legacySendAccessibilityEvent from './legacySendAccessibilityEvent';
import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo';
import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
// Events that are only supported on Android.
type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
};
// Events that are only supported on iOS.
type AccessibilityEventDefinitionsIOS = {
announcementFinished: [{announcement: string, success: boolean}],
boldTextChanged: [boolean],
grayscaleChanged: [boolean],
invertColorsChanged: [boolean],
reduceTransparencyChanged: [boolean],
};
type AccessibilityEventDefinitions = {
...AccessibilityEventDefinitionsAndroid,
...AccessibilityEventDefinitionsIOS,
change: [boolean], // screenReaderChanged
reduceMotionChanged: [boolean],
screenReaderChanged: [boolean],
};
type AccessibilityEventTypes = 'click' | 'focus' | 'viewHoverEnter';
// Mapping of public event names to platform-specific event names.
const EventNames: Map<
$Keys<AccessibilityEventDefinitions>,
string,
> = Platform.OS === 'android'
? new Map([
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
])
: new Map([
['announcementFinished', 'announcementFinished'],
['boldTextChanged', 'boldTextChanged'],
['change', 'screenReaderChanged'],
['grayscaleChanged', 'grayscaleChanged'],
['invertColorsChanged', 'invertColorsChanged'],
['reduceMotionChanged', 'reduceMotionChanged'],
['reduceTransparencyChanged', 'reduceTransparencyChanged'],
['screenReaderChanged', 'screenReaderChanged'],
]);
/**
* Sometimes it's useful to know whether or not the device has a screen reader
* that is currently active. The `AccessibilityInfo` API is designed for this
* purpose. You can use it to query the current state of the screen reader as
* well as to register to be notified when the state of the screen reader
* changes.
*
* See https://reactnative.dev/docs/accessibilityinfo
*/
const AccessibilityInfo = {
/**
* Query whether bold text is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when bold text is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isBoldTextEnabled
*/
isBoldTextEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentBoldTextState(
resolve,
reject,
);
} else {
reject(null);
}
});
}
},
/**
* Query whether grayscale is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when grayscale is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isGrayscaleEnabled
*/
isGrayscaleEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentGrayscaleState(
resolve,
reject,
);
} else {
reject(null);
}
});
}
},
/**
* Query whether inverted colors are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when invert color is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isInvertColorsEnabled
*/
isInvertColorsEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentInvertColorsState(
resolve,
reject,
);
} else {
reject(null);
}
});
}
},
/**
* Query whether reduced motion is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a reduce motion is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isReduceMotionEnabled
*/
isReduceMotionEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid != null) {
NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve);
} else {
reject(null);
}
} else {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentReduceMotionState(
resolve,
reject,
);
} else {
reject(null);
}
}
});
},
/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when prefer cross-fade transitions is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#prefersCrossFadeTransitions
*/
prefersCrossFadeTransitions(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
if (
NativeAccessibilityManagerIOS?.getCurrentPrefersCrossFadeTransitionsState !=
null
) {
NativeAccessibilityManagerIOS.getCurrentPrefersCrossFadeTransitionsState(
resolve,
reject,
);
} else {
reject(null);
}
}
});
},
/**
* Query whether reduced transparency is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a reduce transparency is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isReduceTransparencyEnabled
*/
isReduceTransparencyEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentReduceTransparencyState(
resolve,
reject,
);
} else {
reject(null);
}
});
}
},
/**
* Query whether a screen reader is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a screen reader is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isScreenReaderEnabled
*/
isScreenReaderEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid != null) {
NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve);
} else {
reject(null);
}
} else {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentVoiceOverState(
resolve,
reject,
);
} else {
reject(null);
}
}
});
},
/**
* Query whether Accessibility Service is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when any service is enabled and `false` otherwise.
*
* @platform android
*
* See https://reactnative.dev/docs/accessibilityinfo/#isaccessibilityserviceenabled-android
*/
isAccessibilityServiceEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (
NativeAccessibilityInfoAndroid != null &&
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled != null
) {
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(resolve);
} else {
reject(null);
}
} else {
reject(null);
}
});
},
/**
* Add an event handler. Supported events:
*
* - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a reduce
* motion is enabled (or when "Transition Animation Scale" in "Developer options" is
* "Animation off") and `false` otherwise.
* - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument
* to the event handler is a boolean. The boolean is `true` when a screen
* reader is enabled and `false` otherwise.
*
* These events are only supported on iOS:
*
* - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a bold text
* is enabled and `false` otherwise.
* - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a gray scale
* is enabled and `false` otherwise.
* - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle
* changes. The argument to the event handler is a boolean. The boolean is `true` when a invert
* colors is enabled and `false` otherwise.
* - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency
* toggle changes. The argument to the event handler is a boolean. The boolean is `true`
* when a reduce transparency is enabled and `false` otherwise.
* - `announcementFinished`: iOS-only event. Fires when the screen reader has
* finished making an announcement. The argument to the event handler is a
* dictionary with these keys:
* - `announcement`: The string announced by the screen reader.
* - `success`: A boolean indicating whether the announcement was
* successfully made.
*
* See https://reactnative.dev/docs/accessibilityinfo#addeventlistener
*/
addEventListener<K: $Keys<AccessibilityEventDefinitions>>(
eventName: K,
// $FlowIssue[incompatible-type] - Flow bug with unions and generics (T128099423)
handler: (...$ElementType<AccessibilityEventDefinitions, K>) => void,
): EventSubscription {
const deviceEventName = EventNames.get(eventName);
return deviceEventName == null
? {remove(): void {}}
: // $FlowFixMe[incompatible-call]
RCTDeviceEventEmitter.addListener(deviceEventName, handler);
},
/**
* Set accessibility focus to a React component.
*
* See https://reactnative.dev/docs/accessibilityinfo#setaccessibilityfocus
*/
setAccessibilityFocus(reactTag: number): void {
legacySendAccessibilityEvent(reactTag, 'focus');
},
/**
* Send a named accessibility event to a HostComponent.
*/
sendAccessibilityEvent(
handle: ElementRef<HostComponent<mixed>>,
eventType: AccessibilityEventTypes,
) {
// iOS only supports 'focus' event types
if (Platform.OS === 'ios' && eventType === 'click') {
return;
}
// route through React renderer to distinguish between Fabric and non-Fabric handles
sendAccessibilityEvent(handle, eventType);
},
/**
* Post a string to be announced by the screen reader.
*
* See https://reactnative.dev/docs/accessibilityinfo#announceforaccessibility
*/
announceForAccessibility(announcement: string): void {
if (Platform.OS === 'android') {
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
} else {
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
}
},
/**
* Post a string to be announced by the screen reader.
* - `announcement`: The string announced by the screen reader.
* - `options`: An object that configures the reading options.
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
*/
announceForAccessibilityWithOptions(
announcement: string,
options: {queue?: boolean},
): void {
if (Platform.OS === 'android') {
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
} else {
if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) {
NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
announcement,
options,
);
} else {
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
}
}
},
/**
* Get the recommended timeout for changes to the UI needed by this user.
*
* See https://reactnative.dev/docs/accessibilityinfo#getrecommendedtimeoutmillis
*/
getRecommendedTimeoutMillis(originalTimeout: number): Promise<number> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis) {
NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis(
originalTimeout,
resolve,
);
} else {
resolve(originalTimeout);
}
});
} else {
return Promise.resolve(originalTimeout);
}
},
};
export default AccessibilityInfo;

View File

@@ -0,0 +1,13 @@
/**
* 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
* @format
*/
export * from '../../../src/private/specs/modules/NativeAccessibilityInfo';
import NativeAccessibilityInfo from '../../../src/private/specs/modules/NativeAccessibilityInfo';
export default NativeAccessibilityInfo;

View File

@@ -0,0 +1,13 @@
/**
* 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
*/
export * from '../../../src/private/specs/modules/NativeAccessibilityManager';
import NativeAccessibilityManager from '../../../src/private/specs/modules/NativeAccessibilityManager';
export default NativeAccessibilityManager;

Some files were not shown because too many files have changed in this diff Show More