diff --git a/README.md b/README.md index a933223..1ba6de2 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ You can also animate imperatively by using the `animate()` function on the eleme this.view.animate({ 0: { opacity: 0 }, 1: { opacity: 1 } }); ``` +The `animate()` method accepts two extra optional arguments after the animation definition: + +* **`duration`** – overrides the default duration in milliseconds. +* **`iterationDelay`** – pause between iterations in milliseconds, useful for chained loops. + +```js +// Run a custom animation for 600ms with a 200ms pause between each of 4 iterations. +this.view.animate({ 0: { opacity: 0 }, 1: { opacity: 1 } }, 600, 200); +``` + #### Generic transitions ##### `transition(fromValues, toValues[[, duration], easing])` diff --git a/createAnimatableComponent.js b/createAnimatableComponent.js index 7be96ea..490521d 100644 --- a/createAnimatableComponent.js +++ b/createAnimatableComponent.js @@ -75,7 +75,7 @@ function getCompiledAnimation(animation) { if (typeof animation === 'string') { const compiledAnimation = getAnimationByName(animation); if (!compiledAnimation) { - throw new Error(`No animation registred by the name of ${animation}`); + throw new Error(`No animation registered by the name of ${animation}`); } return compiledAnimation; } @@ -123,6 +123,17 @@ function transitionToValue( } // Make (almost) any component animatable, similar to Animated.createAnimatedComponent +/** + * Higher-order component factory that makes any React Native + * component animatable. The returned class component mirrors the + * `Animated.createAnimatedComponent` API but also accepts the full + * set of `AnimatableProps` and exposes every registered animation + * as an imperative method on its ref. + * + * @param {React.ComponentType} WrappedComponent - Component to wrap. + * @returns {React.ComponentClass} An animatable component class + * with `displayName` of the form `withAnimatable()`. + */ export default function createAnimatableComponent(WrappedComponent) { const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; diff --git a/createAnimation.js b/createAnimation.js index 81a5b89..257983f 100644 --- a/createAnimation.js +++ b/createAnimation.js @@ -24,6 +24,18 @@ function parsePosition(value) { const cache = {}; +/** + * Compile a {@link CustomAnimation} definition into a structure + * suitable for `Animated.Value.interpolate()`. The compiled output + * is cached on a stringified-definition key, so calling this with + * the same definition repeatedly is cheap. + * + * @param {object} definition - The custom animation to compile. + * Keyframes may be keyed by `'from'`, `'to'`, or any number in + * the `[0, 1]` range. + * @returns {object} The compiled animation, keyed by style property. + * @throws {Error} If fewer than two valid keyframes are present. + */ export default function createAnimation(definition) { const cacheKey = JSON.stringify(definition); if (cache[cacheKey]) { diff --git a/index.js b/index.js index 8e84d91..5ff8ff5 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,10 @@ +/** + * @file Public entry point for `react-native-animatable`. + * + * The library automatically registers all built-in animation + * definitions exported from `./definitions` on module load, so the + * names listed in the `Animation` type are usable immediately. + */ import { View as CoreView, Text as CoreText, @@ -9,9 +16,13 @@ import * as ANIMATION_DEFINITIONS from './definitions'; initializeRegistryWithDefinitions(ANIMATION_DEFINITIONS); +/** Compose any React component into an animatable component. */ export const createAnimatableComponent = createComponent; +/** Precomposed animatable `View`. */ export const View = createComponent(CoreView); +/** Precomposed animatable `Text`. */ export const Text = createComponent(CoreText); +/** Precomposed animatable `Image`. */ export const Image = createComponent(CoreImage); export { default as createAnimation } from './createAnimation'; export { diff --git a/registry.js b/registry.js index 825a9fc..d3e94be 100644 --- a/registry.js +++ b/registry.js @@ -2,18 +2,48 @@ import createAnimation from './createAnimation'; const animationRegistry = {}; +/** + * Register a single custom animation under the given name so it can + * later be referenced by string from the `animation` prop. + * + * @param {string} animationName - The lookup key for the animation. + * @param {object} animation - A {@link CustomAnimation} definition + * that will be compiled via `createAnimation` before being stored. + */ export function registerAnimation(animationName, animation) { animationRegistry[animationName] = animation; } +/** + * Look up a previously registered animation by name. Returns + * `undefined` if no animation has been registered under that name. + * + * @param {string} animationName - The name passed to `registerAnimation`. + * @returns {object|undefined} The compiled animation, or `undefined`. + */ export function getAnimationByName(animationName) { return animationRegistry[animationName]; } +/** + * Return the list of all animation names currently registered, both + * the built-in ones and any user-registered custom animations. + * + * @returns {string[]} Array of animation names. + */ export function getAnimationNames() { return Object.keys(animationRegistry); } +/** + * Bulk-register a map of animation definitions. Each value is + * compiled with `createAnimation` and stored under its key. Existing + * animations with the same name will be replaced, which is useful + * for tweaking built-in animations. + * + * @param {{[key: string]: object}} definitions - Map of name to + * {@link CustomAnimation} definition. + */ export function initializeRegistryWithDefinitions(definitions) { Object.keys(definitions).forEach((animationName) => { registerAnimation( diff --git a/typings/react-native-animatable.d.ts b/typings/react-native-animatable.d.ts index af76e97..d59644b 100644 --- a/typings/react-native-animatable.d.ts +++ b/typings/react-native-animatable.d.ts @@ -14,7 +14,18 @@ import { Component, } from 'react'; +/** + * A function that maps animation progress (`t` in `[0, 1]`) to a + * transformed progress value, mirroring the shape of React Native's + * `Easing` functions. + */ export type EasingFunction = { (t: number): number }; + +/** + * The set of supported named easing functions plus any custom + * `EasingFunction`. Use the strings for the built-in easings shipped + * with this library, or supply a function for full control. + */ export type Easing = | 'linear' | 'ease' @@ -47,6 +58,11 @@ export type Easing = | 'ease-in-out-back' | EasingFunction; +/** + * All built-in animation names shipped with `react-native-animatable`. + * Custom animations registered via `registerAnimation` may be passed + * using the `string` overload of `AnimatableProps.animation`. + */ export type Animation = | 'bounce' | 'flash' @@ -111,12 +127,18 @@ export type Animation = | 'zoomOutLeft' | 'zoomOutRight'; +/** + * Direction an animation plays. `alternate` and `alternate-reverse` + * are most useful with `iterationCount` greater than one, producing + * a smooth ping-pong effect. + */ export type Direction = | 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'; +/** Style transform keys that may be used in a `transition` prop. */ type TransformKeys = | 'perspective' | 'rotate' @@ -132,29 +154,70 @@ type TransformKeys = | 'skewY' | 'matrix'; +/** + * Result passed to the `onAnimationEnd` / animation `Promise` + * resolution. `finished` is `false` when the animation was + * cancelled (for example by `stopAnimation()` or unmounting). + */ +export interface EndState { + finished: boolean; +} + +/** + * Props accepted by every component created with + * `createAnimatableComponent`. These are passed to the underlying + * component in addition to its own props. + */ interface AnimatableProps { + /** + * Name of the animation, an inline {@link CustomAnimation} + * definition, or a previously registered custom animation name. + */ animation?: Animation | string | CustomAnimation; + /** Animation duration in milliseconds. */ duration?: number; + /** Delay before the animation starts, in milliseconds. */ delay?: number; + /** Direction the animation plays. Defaults to `normal`. */ direction?: Direction; + /** Timing function used for the animation. */ easing?: Easing; + /** Number of iterations or the string `'infinite'`. */ iterationCount?: number | 'infinite'; + /** Delay between iterations, in milliseconds. */ iterationDelay?: number; + /** + * Style property (or array of properties) to transition between + * declarative style changes. + */ transition?: | (keyof S | TransformKeys) | ReadonlyArray; + /** Whether to use the native animation driver. */ useNativeDriver?: boolean; + /** + * Whether the animation should create an "interaction handle" on + * React Native's `InteractionManager`. When `undefined` the + * component defaults to `true` for non-looping animations and + * `false` for looping ones. + */ isInteraction?: boolean; - onAnimationBegin?: Function; - onAnimationEnd?: Function; + /** Called when the animation begins, after any configured `delay`. */ + onAnimationBegin?: () => void; + /** Called when the animation ends, whether finished or cancelled. */ + onAnimationEnd?: (endState: EndState) => void; + /** Called when a transition for `property` begins. */ onTransitionBegin?: (property: string) => void; + /** Called when a transition for `property` ends. */ onTransitionEnd?: (property: string) => void; } +/** Per-animation imperative methods exposed on the component ref. */ type AnimatableAnimationMethods = Partial<{ - [k in Animation]: (duration?: number) => Promise<{ finished: boolean }>; + [k in Animation]: (duration?: number) => Promise; }>; +/** Public shape of an animatable component. */ interface AnimatableComponent

extends NativeMethods, AnimatableAnimationMethods, @@ -164,14 +227,21 @@ interface AnimatableComponent

[key: string]: Component; }; + /** Stop the current animation and clear the animation style. */ stopAnimation(): void; + /** + * Imperatively run a custom animation. The returned promise + * resolves with `endState` describing whether the animation + * finished or was cancelled. + */ animate( animation: Animation | CustomAnimation, duration?: number, iterationDelay?: number, - ): Promise; + ): Promise; + /** Transition between two full style snapshots. */ transition( fromValues: T, toValues: T, @@ -179,6 +249,7 @@ interface AnimatableComponent

easing?: Easing, ): void; + /** Transition to a new style snapshot, inferring the start state. */ transitionTo( toValues: T, duration?: number, @@ -186,6 +257,11 @@ interface AnimatableComponent

): void; } +/** + * User-defined animation description. Keyframes may use numeric + * positions between `0` and `1`, or the convenience keys `from` + * (equivalent to `0`) and `to` (equivalent to `1`). + */ export interface CustomAnimation { from?: T; to?: T; @@ -194,18 +270,36 @@ export interface CustomAnimation { [progress: number]: T; } +/** + * Compile a {@link CustomAnimation} into a structure suitable for + * `Animated.Value.interpolate()`. Cached internally by stringified + * definition for performance. + */ export function createAnimation(animation: CustomAnimation): object; +/** Register a custom animation under `name` for declarative lookup. */ export function registerAnimation( name: string, animation: CustomAnimation, ): void; +/** + * Bulk-register a map of animations, replacing any previously + * registered definitions with the same name. Useful for tweaking + * built-in animations. + */ export function initializeRegistryWithDefinitions(animations: { [key: string]: CustomAnimation; }): void; type GetPropertyType = B[K]; + +/** + * Compose any component into an animatable component. The returned + * component exposes all props of `Component` plus the + * `AnimatableProps` listed above, and all built-in animation + * methods on its ref. + */ export function createAnimatableComponent< P extends { style?: any }, S = GetPropertyType,