UNPKG

11.6 kBPlain TextView Raw
1'use strict';
2import { withTiming } from '../../animation';
3import type {
4 SharedTransitionAnimationsFunction,
5 SharedTransitionAnimationsValues,
6 CustomProgressAnimation,
7 ProgressAnimation,
8 LayoutAnimationsOptions,
9} from '../animationBuilder/commonTypes';
10import {
11 LayoutAnimationType,
12 SharedTransitionType,
13} from '../animationBuilder/commonTypes';
14import type { StyleProps } from '../../commonTypes';
15import { ReduceMotion } from '../../commonTypes';
16import { ProgressTransitionManager } from './ProgressTransitionManager';
17import { updateLayoutAnimations } from '../../UpdateLayoutAnimations';
18import { getReduceMotionFromConfig } from '../../animation/util';
19
20const SUPPORTED_PROPS = [
21 'width',
22 'height',
23 'originX',
24 'originY',
25 'transform',
26 'borderRadius',
27 'borderTopLeftRadius',
28 'borderTopRightRadius',
29 'borderBottomLeftRadius',
30 'borderBottomRightRadius',
31] as const;
32
33type AnimationFactory = (
34 values: SharedTransitionAnimationsValues
35) => StyleProps;
36
37/**
38 * A SharedTransition builder class.
39 *
40 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
41 * @experimental
42 */
43export class SharedTransition {
44 private _customAnimationFactory: AnimationFactory | null = null;
45 private _animation: SharedTransitionAnimationsFunction | null = null;
46 private _transitionDuration = 500;
47 private _reduceMotion: ReduceMotion = ReduceMotion.System;
48 private _customProgressAnimation?: ProgressAnimation = undefined;
49 private _progressAnimation?: ProgressAnimation = undefined;
50 private _defaultTransitionType?: SharedTransitionType = undefined;
51 private static _progressTransitionManager = new ProgressTransitionManager();
52
53 public custom(customAnimationFactory: AnimationFactory): SharedTransition {
54 this._customAnimationFactory = customAnimationFactory;
55 return this;
56 }
57
58 public progressAnimation(
59 progressAnimationCallback: CustomProgressAnimation
60 ): SharedTransition {
61 this._customProgressAnimation = (viewTag, values, progress) => {
62 'worklet';
63 const newStyles = progressAnimationCallback(values, progress);
64 global._notifyAboutProgress(viewTag, newStyles, true);
65 };
66 return this;
67 }
68
69 public duration(duration: number): SharedTransition {
70 this._transitionDuration = duration;
71 return this;
72 }
73
74 public reduceMotion(_reduceMotion: ReduceMotion): this {
75 this._reduceMotion = _reduceMotion;
76 return this;
77 }
78
79 public defaultTransitionType(
80 transitionType: SharedTransitionType
81 ): SharedTransition {
82 this._defaultTransitionType = transitionType;
83 return this;
84 }
85
86 public registerTransition(
87 viewTag: number,
88 sharedTransitionTag: string,
89 isUnmounting = false
90 ) {
91 if (getReduceMotionFromConfig(this.getReduceMotion())) {
92 return;
93 }
94
95 const transitionAnimation = this.getTransitionAnimation();
96 const progressAnimation = this.getProgressAnimation();
97 if (!this._defaultTransitionType) {
98 if (this._customAnimationFactory && !this._customProgressAnimation) {
99 this._defaultTransitionType = SharedTransitionType.ANIMATION;
100 } else {
101 this._defaultTransitionType = SharedTransitionType.PROGRESS_ANIMATION;
102 }
103 }
104 const layoutAnimationType =
105 this._defaultTransitionType === SharedTransitionType.ANIMATION
106 ? LayoutAnimationType.SHARED_ELEMENT_TRANSITION
107 : LayoutAnimationType.SHARED_ELEMENT_TRANSITION_PROGRESS;
108 updateLayoutAnimations(
109 viewTag,
110 layoutAnimationType,
111 transitionAnimation,
112 sharedTransitionTag,
113 isUnmounting
114 );
115 SharedTransition._progressTransitionManager.addProgressAnimation(
116 viewTag,
117 progressAnimation
118 );
119 }
120
121 public unregisterTransition(viewTag: number, isUnmounting = false): void {
122 const layoutAnimationType =
123 this._defaultTransitionType === SharedTransitionType.ANIMATION
124 ? LayoutAnimationType.SHARED_ELEMENT_TRANSITION
125 : LayoutAnimationType.SHARED_ELEMENT_TRANSITION_PROGRESS;
126 updateLayoutAnimations(
127 viewTag,
128 layoutAnimationType,
129 undefined,
130 undefined,
131 isUnmounting
132 );
133 SharedTransition._progressTransitionManager.removeProgressAnimation(
134 viewTag,
135 isUnmounting
136 );
137 }
138
139 public getReduceMotion(): ReduceMotion {
140 return this._reduceMotion;
141 }
142
143 private getTransitionAnimation(): SharedTransitionAnimationsFunction {
144 if (!this._animation) {
145 this.buildAnimation();
146 }
147 return this._animation!;
148 }
149
150 private getProgressAnimation(): ProgressAnimation {
151 if (!this._progressAnimation) {
152 this.buildProgressAnimation();
153 }
154 return this._progressAnimation!;
155 }
156
157 private buildAnimation() {
158 const animationFactory = this._customAnimationFactory;
159 const transitionDuration = this._transitionDuration;
160 const reduceMotion = this._reduceMotion;
161 this._animation = (values: SharedTransitionAnimationsValues) => {
162 'worklet';
163 let animations: {
164 [key: string]: unknown;
165 } = {};
166 const initialValues: {
167 [key: string]: unknown;
168 } = {};
169
170 if (animationFactory) {
171 animations = animationFactory(values);
172 for (const key in animations) {
173 if (!(SUPPORTED_PROPS as readonly string[]).includes(key)) {
174 throw new Error(
175 `[Reanimated] The prop '${key}' is not supported yet.`
176 );
177 }
178 }
179 } else {
180 for (const propName of SUPPORTED_PROPS) {
181 if (propName === 'transform') {
182 const matrix = values.targetTransformMatrix;
183 animations.transformMatrix = withTiming(matrix, {
184 reduceMotion,
185 duration: transitionDuration,
186 });
187 } else {
188 const capitalizedPropName = `${propName
189 .charAt(0)
190 .toUpperCase()}${propName.slice(
191 1
192 )}` as Capitalize<LayoutAnimationsOptions>;
193 const keyToTargetValue = `target${capitalizedPropName}` as const;
194 animations[propName] = withTiming(values[keyToTargetValue], {
195 reduceMotion,
196 duration: transitionDuration,
197 });
198 }
199 }
200 }
201
202 for (const propName in animations) {
203 if (propName === 'transform') {
204 initialValues.transformMatrix = values.currentTransformMatrix;
205 } else {
206 const capitalizedPropName = (propName.charAt(0).toUpperCase() +
207 propName.slice(1)) as Capitalize<LayoutAnimationsOptions>;
208 const keyToCurrentValue = `current${capitalizedPropName}` as const;
209 initialValues[propName] = values[keyToCurrentValue];
210 }
211 }
212
213 return { initialValues, animations };
214 };
215 }
216
217 private buildProgressAnimation() {
218 if (this._customProgressAnimation) {
219 this._progressAnimation = this._customProgressAnimation;
220 return;
221 }
222 this._progressAnimation = (viewTag, values, progress) => {
223 'worklet';
224 const newStyles: { [key: string]: number | number[] } = {};
225 for (const propertyName of SUPPORTED_PROPS) {
226 if (propertyName === 'transform') {
227 // this is not the perfect solution, but at this moment it just interpolates the whole
228 // matrix instead of interpolating scale, translate, rotate, etc. separately
229 const currentMatrix = values.currentTransformMatrix;
230 const targetMatrix = values.targetTransformMatrix;
231 const newMatrix = new Array(9);
232 for (let i = 0; i < 9; i++) {
233 newMatrix[i] =
234 progress * (targetMatrix[i] - currentMatrix[i]) +
235 currentMatrix[i];
236 }
237 newStyles.transformMatrix = newMatrix;
238 } else {
239 // PropertyName == propertyName with capitalized fist letter, (width -> Width)
240 const PropertyName = (propertyName.charAt(0).toUpperCase() +
241 propertyName.slice(1)) as Capitalize<LayoutAnimationsOptions>;
242 const currentPropertyName = `current${PropertyName}` as const;
243 const targetPropertyName = `target${PropertyName}` as const;
244
245 const currentValue = values[currentPropertyName];
246 const targetValue = values[targetPropertyName];
247
248 newStyles[propertyName] =
249 progress * (targetValue - currentValue) + currentValue;
250 }
251 }
252 global._notifyAboutProgress(viewTag, newStyles, true);
253 };
254 }
255
256 // static builder methods i.e. shared transition modifiers
257
258 /**
259 * Lets you create a custom shared transition animation. Other shared transition modifiers can be chained alongside this modifier.
260 *
261 * @param customAnimationFactory - Callback function that have to return an object with styles for the custom shared transition.
262 * @returns A {@link SharedTransition} object. Styles returned from this function need to be to the `sharedTransitionStyle` prop.
263 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
264 * @experimental
265 */
266 public static custom(
267 customAnimationFactory: AnimationFactory
268 ): SharedTransition {
269 return new SharedTransition().custom(customAnimationFactory);
270 }
271
272 /**
273 * Lets you change the duration of the shared transition. Other shared transition modifiers can be chained alongside this modifier.
274 *
275 * @param duration - The duration of the shared transition animation in milliseconds.
276 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
277 * @experimental
278 */
279 public static duration(duration: number): SharedTransition {
280 return new SharedTransition().duration(duration);
281 }
282
283 /**
284 * Lets you create a shared transition animation bound to the progress between navigation screens. Other shared transition modifiers can be chained alongside this modifier.
285 *
286 * @param progressAnimationCallback - A callback called with the current progress value on every animation frame. It should return an object with styles for the shared transition.
287 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
288 * @experimental
289 */
290 public static progressAnimation(
291 progressAnimationCallback: CustomProgressAnimation
292 ): SharedTransition {
293 return new SharedTransition().progressAnimation(progressAnimationCallback);
294 }
295
296 /**
297 * Whether the transition is progress-bound or not. Other shared transition modifiers can be chained alongside this modifier.
298 *
299 * @param transitionType - Type of the transition. Configured with {@link SharedTransitionType} enum.
300 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
301 * @experimental
302 */
303 public static defaultTransitionType(
304 transitionType: SharedTransitionType
305 ): SharedTransition {
306 return new SharedTransition().defaultTransitionType(transitionType);
307 }
308
309 /**
310 * Lets you adjust the behavior when the device's reduced motion accessibility setting is turned on. Other shared transition modifiers can be chained alongside this modifier.
311 *
312 * @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
313 * @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
314 * @experimental
315 */
316 public static reduceMotion(reduceMotion: ReduceMotion): SharedTransition {
317 return new SharedTransition().reduceMotion(reduceMotion);
318 }
319}