├── .gitignore
├── README.md
├── example
├── .gitignore
├── index.html
├── index.tsx
├── package.json
└── tsconfig.json
├── package.json
├── src
└── index.tsx
├── test
└── blah.test.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | .rts2_cache_cjs
6 | .rts2_cache_esm
7 | .rts2_cache_umd
8 | dist
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # shared-element-rn
2 |
3 | Shared element transitions built around [`react-native-shared-element`](https://github.com/IjzerenHein/react-native-shared-element)
4 |
5 | This component somewhat simplifys the usage for other libraries or custom screens in your app.
6 |
7 | ## Installation
8 |
9 | ```bash
10 | yarn add shared-element-rn react-native-shared-element
11 | cd ios && pod install
12 | ```
13 |
14 | ## API
15 |
16 | `` will trigger a transition when its `activeIndex` prop changes.
17 |
18 | It will look for any `` children on the next screen and if there is a matching id (or ids) it will trigger the shared element transition.
19 |
20 | ```javascript
21 | import { SharedElements, SharedElement } from 'shared-element-rn';
22 |
23 | function MySharedElements() {
24 | const [activeIndex, setActiveIndex] = React.useState(0);
25 |
26 | return (
27 |
28 |
29 | setActiveIndex(1)} />
30 | setActiveIndex(0)} />
31 |
32 |
33 | );
34 | }
35 |
36 | function Screen1({ onTransition }) {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | function Screen2({ onTransition }) {
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 | ```
60 |
61 | ## Other stuff
62 |
63 | Often you'll want to add some custom animations to non-shared elements, for example a fade in or slide in effect.
64 |
65 | This library exports a `useInterpolation()` hook that can be used to animate non-shared elements in sync with the transitions:
66 |
67 | ```javascript
68 | import { useInterpolation } from 'shared-element-rn';
69 |
70 | const fadeIn = {
71 | opacity: {
72 | inputRange: [-1, 0, 1],
73 | outputRange: [0, 1, 0],
74 | },
75 | };
76 |
77 | function FadeIn({ children }) {
78 | const styles = useInterpolation(fadeIn);
79 | return {children};
80 | }
81 |
82 | const transitionBottom = {
83 | transform: [
84 | {
85 | translateY: {
86 | inputRange: [-1, 0, 1],
87 | outputRange: [500, 0, 500],
88 | },
89 | },
90 | ],
91 | };
92 |
93 | function TransitionBottom({ children }) {
94 | const styles = useInterpolation(transitionBottom);
95 |
96 | return {children};
97 | }
98 | ```
99 |
100 | ## Roadmap
101 |
102 | - gesture handling to dismiss views on swipe
103 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import { Thing } from '../.';
5 |
6 | const App = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | ReactDOM.render(, document.getElementById('root'));
15 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.8.15",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^1.12.3",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shared-element-rn",
3 | "version": "0.1.0",
4 | "main": "dist/index.js",
5 | "module": "dist/shared-element-rn.esm.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "start": "tsdx watch",
12 | "build": "tsdx build",
13 | "test": "tsdx test --env=jsdom",
14 | "lint": "tsdx lint"
15 | },
16 | "peerDependencies": {
17 | "react": ">=16",
18 | "react-native": "^0.62.0",
19 | "react-native-shared-element": "^0.5.6"
20 | },
21 | "husky": {
22 | "hooks": {
23 | "pre-commit": "pretty-quick --staged"
24 | }
25 | },
26 | "prettier": {
27 | "printWidth": 80,
28 | "semi": true,
29 | "singleQuote": true,
30 | "trailingComma": "es5"
31 | },
32 | "devDependencies": {
33 | "@types/jest": "^25.1.4",
34 | "@types/react": "^16.9.26",
35 | "@types/react-dom": "^16.9.5",
36 | "@types/react-native": "^0.61.23",
37 | "husky": "^4.2.3",
38 | "prettier": "^2.0.2",
39 | "pretty-quick": "^2.0.1",
40 | "react": "^16.13.1",
41 | "react-dom": "^16.13.1",
42 | "react-native": "^0.62.0",
43 | "react-native-shared-element": "^0.5.6",
44 | "tsdx": "^0.13.0",
45 | "tslib": "^1.11.1",
46 | "typescript": "^3.8.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | SharedElement as RNSharedElement,
5 | SharedElementTransition,
6 | nodeFromRef,
7 | SharedElementAlign,
8 | SharedElementAnimation,
9 | SharedElementResize,
10 | SharedElementNode,
11 | } from 'react-native-shared-element';
12 |
13 | import { StyleSheet, Animated, View, ViewStyle } from 'react-native';
14 |
15 | const IndexContext = React.createContext(-1);
16 | const AnimatedIndexContext = React.createContext(new Animated.Value(0));
17 | const TransitionContext = React.createContext({});
18 |
19 | interface ISharedElementConfig {
20 | align?: SharedElementAlign;
21 | resize?: SharedElementResize;
22 | animated?: SharedElementAnimation;
23 | debug?: boolean;
24 | }
25 |
26 | const DEFAULT_ELEMENT_CONFIG: ISharedElementConfig = {
27 | align: 'auto',
28 | resize: 'auto',
29 | animated: 'move',
30 | debug: false,
31 | };
32 |
33 | interface ITransitionContextProvider {
34 | children: React.ReactNode;
35 | }
36 |
37 | function TransitionContextProvider({ children }: ITransitionContextProvider) {
38 | const ancestors = React.useRef([]);
39 | const transitions = React.useRef>({});
40 | const configs = React.useRef>({});
41 | const childNodes = React.useRef>({});
42 |
43 | function registerAncestor(ref: any, index: number) {
44 | const node = nodeFromRef(ref);
45 | // @ts-ignore
46 | ancestors.current[index] = node;
47 | }
48 |
49 | function registerElement(
50 | node: any,
51 | index: number,
52 | id: string,
53 | child: any,
54 | config?: ISharedElementConfig
55 | ) {
56 | if (!transitions.current[id]) {
57 | transitions.current[id] = [];
58 | }
59 |
60 | transitions.current[id][index] = node;
61 |
62 | if (!childNodes.current[id]) {
63 | childNodes.current[id] = [];
64 | }
65 |
66 | childNodes.current[id][index] = child;
67 |
68 | if (!configs.current[id]) {
69 | configs.current[id] = [];
70 | }
71 |
72 | configs.current[id][index] = {
73 | ...DEFAULT_ELEMENT_CONFIG,
74 | ...config,
75 | };
76 | }
77 |
78 | return (
79 |
89 | {children}
90 |
91 | );
92 | }
93 |
94 | interface ISharedElements {
95 | activeIndex: number;
96 | animatedIndex: Animated.Value;
97 | transitionConfig?: ITransitionConfig;
98 | children: React.ReactNode;
99 | }
100 |
101 | function SharedElementsImpl({
102 | activeIndex,
103 | animatedIndex,
104 | transitionConfig = DEFAULT_TRANSITION_CONFIG,
105 | children,
106 | }: ISharedElements) {
107 | const transitionMethod =
108 | // @ts-ignore
109 | transitionConfig['duration'] !== undefined ? 'timing' : 'spring';
110 |
111 | const { transitions, ancestors, configs } = React.useContext(
112 | TransitionContext
113 | );
114 |
115 | const position = React.useRef(new Animated.Value(0));
116 |
117 | const [currentIndex, setCurrentIndex] = React.useState(activeIndex);
118 | const [nextIndex, setNextIndex] = React.useState(activeIndex);
119 |
120 | React.useEffect(() => {
121 | setNextIndex(activeIndex);
122 | }, [activeIndex]);
123 |
124 | const previousIndex = usePrevious(activeIndex);
125 |
126 | const transitioning =
127 | nextIndex !== currentIndex || previousIndex !== activeIndex;
128 |
129 | function renderTransitions() {
130 | if (currentIndex === nextIndex) {
131 | return null;
132 | }
133 |
134 | position.current.setValue(0);
135 |
136 | const nodes = Object.keys(transitions)
137 | .map((transitionId) => {
138 | const transition = transitions[transitionId];
139 | const startNode = transition[currentIndex];
140 | const endNode = transition[nextIndex];
141 |
142 | if (startNode && endNode) {
143 | const startAncestor = ancestors[currentIndex];
144 | const endAncestor = ancestors[nextIndex];
145 |
146 | const elementConfigs = configs[transitionId];
147 | let elementConfig;
148 |
149 | if (elementConfigs) {
150 | elementConfig = elementConfigs[currentIndex];
151 | }
152 |
153 | elementConfig = elementConfig || DEFAULT_ELEMENT_CONFIG;
154 |
155 | if (startAncestor && endAncestor) {
156 | return (
157 |
163 |
175 |
176 | );
177 | }
178 | }
179 |
180 | return null;
181 | })
182 | .filter(Boolean);
183 |
184 | if (nodes.length > 0) {
185 | Animated.parallel([
186 | Animated[transitionMethod](position.current, {
187 | ...transitionConfig,
188 | toValue: 1,
189 | }),
190 | Animated[transitionMethod](animatedIndex, {
191 | ...transitionConfig,
192 | toValue: nextIndex,
193 | }),
194 | ]).start(() => {
195 | setCurrentIndex(nextIndex);
196 | });
197 | }
198 |
199 | return nodes;
200 | }
201 |
202 | return (
203 |
204 |
205 | {React.Children.map(children, (child: any, index: number) => {
206 | if (!transitioning) {
207 | if (index > activeIndex) {
208 | return React.cloneElement(child, { children: null });
209 | }
210 | }
211 |
212 | return (
213 |
217 |
218 |
219 | {child}
220 |
221 |
222 |
223 | );
224 | })}
225 |
226 | {renderTransitions()}
227 |
228 |
229 | );
230 | }
231 |
232 | interface ISharedElementScreen {
233 | children: React.ReactNode;
234 | currentIndex: number;
235 | }
236 |
237 | function SharedElementScreen({ children, currentIndex }: ISharedElementScreen) {
238 | const index = React.useContext(IndexContext);
239 | const { registerAncestor } = React.useContext(TransitionContext);
240 |
241 | const animatedValue = React.useRef(new Animated.Value(0));
242 |
243 | // prevent initial flash on mount
244 | React.useEffect(() => {
245 | Animated.timing(animatedValue.current, {
246 | toValue: 1,
247 | duration: 100,
248 | }).start();
249 | }, [currentIndex]);
250 |
251 | return (
252 |
256 | registerAncestor(ref, index)}
260 | >
261 | {children}
262 |
263 |
264 | );
265 | }
266 |
267 | interface ISharedElement {
268 | children: React.ReactNode;
269 | id: string;
270 | config?: ISharedElementConfig;
271 | }
272 |
273 | function SharedElement({ children, id, config }: ISharedElement) {
274 | const index = React.useContext(IndexContext);
275 |
276 | const { registerElement } = React.useContext(TransitionContext);
277 |
278 | return (
279 | registerElement(node, index, id, children, config)}
281 | >
282 | {children}
283 |
284 | );
285 | }
286 |
287 | type ITransitionConfig =
288 | | Partial
289 | | Partial;
290 |
291 | const DEFAULT_TRANSITION_CONFIG: ITransitionConfig = {
292 | stiffness: 1000,
293 | damping: 500,
294 | mass: 3,
295 | overshootClamping: false,
296 | restDisplacementThreshold: 0.01,
297 | restSpeedThreshold: 0.01,
298 | useNativeDriver: true,
299 | };
300 |
301 | interface ISharedElements {
302 | activeIndex: number;
303 | children: React.ReactNode;
304 | animatedValue?: Animated.Value;
305 | transitionConfig?: ITransitionConfig;
306 | }
307 |
308 | function SharedElements({
309 | children,
310 | activeIndex,
311 | animatedValue,
312 | transitionConfig,
313 | }: ISharedElements) {
314 | const animatedIndex = React.useRef(
315 | animatedValue || new Animated.Value(activeIndex)
316 | );
317 |
318 | return (
319 |
320 |
325 | {children}
326 |
327 |
328 | );
329 | }
330 |
331 | export { SharedElements, SharedElement, useInterpolation };
332 |
333 | function usePrevious(value: any) {
334 | // The ref object is a generic container whose current property is mutable ...
335 | // ... and can hold any value, similar to an instance property on a class
336 | const ref = React.useRef(value);
337 |
338 | // Store current value in ref
339 | React.useEffect(() => {
340 | ref.current = value;
341 | }, [value]); // Only re-run if value changes
342 |
343 | // Return previous value (happens before update in useEffect above)
344 | return ref.current;
345 | }
346 |
347 | function useInterpolation(interpolation: any) {
348 | const index = React.useContext(IndexContext);
349 | const animatedIndex = React.useContext(AnimatedIndexContext);
350 |
351 | const offset = Animated.subtract(index, animatedIndex);
352 |
353 | const styles = React.useMemo(() => {
354 | return interpolateWithConfig(offset, interpolation);
355 | }, [interpolation, offset]);
356 |
357 | return styles;
358 | }
359 |
360 | function interpolateWithConfig(
361 | offset: Animated.AnimatedSubtraction,
362 | pageInterpolation?: any
363 | ): ViewStyle {
364 | if (!pageInterpolation) {
365 | return {};
366 | }
367 |
368 | return Object.keys(pageInterpolation).reduce((styles: any, key: any) => {
369 | const currentStyle = pageInterpolation[key];
370 |
371 | if (Array.isArray(currentStyle)) {
372 | const _style = currentStyle.map((interpolationConfig: any) =>
373 | interpolateWithConfig(offset, interpolationConfig)
374 | );
375 |
376 | styles[key] = _style;
377 | return styles;
378 | }
379 |
380 | if (typeof currentStyle === 'object') {
381 | styles[key] = offset.interpolate(currentStyle);
382 | return styles;
383 | }
384 |
385 | return styles;
386 | }, {});
387 | }
388 |
--------------------------------------------------------------------------------
/test/blah.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { Thing } from '../src';
4 |
5 | describe('it', () => {
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div');
8 | ReactDOM.render(, div);
9 | ReactDOM.unmountComponentAtNode(div);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./src",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "strictFunctionTypes": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitThis": true,
17 | "alwaysStrict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "./",
24 | "paths": {
25 | "*": ["src/*", "node_modules/*"]
26 | },
27 | "jsx": "react",
28 | "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------