Skip to content

Commit a4389c0

Browse files
committed
[feat] Experimental v5 support main branch
1 parent c26b098 commit a4389c0

File tree

5 files changed

+428
-0
lines changed

5 files changed

+428
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
},
4444
"devDependencies": {
4545
"@commitlint/config-conventional": "^8.3.4",
46+
"@react-navigation/native": "^5.1.6",
47+
"@react-navigation/stack": "^5.2.13",
4648
"@types/react": "^16.9.23",
4749
"@types/react-native": "^0.61.17",
4850
"commitlint": "^8.3.5",

src/createSharedElementScene.tsx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Route, NavigationState } from "@react-navigation/native";
2+
import {
3+
StackNavigationProp,
4+
StackCardInterpolationProps
5+
} from "@react-navigation/stack";
6+
import hoistNonReactStatics from "hoist-non-react-statics";
7+
import * as React from "react";
8+
import { View, StyleSheet, InteractionManager } from "react-native";
9+
import { nodeFromRef } from "react-native-shared-element";
10+
11+
import { ISharedElementRendererData } from "./SharedElementRendererData";
12+
import SharedElementSceneContext from "./SharedElementSceneContext";
13+
import SharedElementSceneData from "./SharedElementSceneData";
14+
import {
15+
SharedElementEventSubscription,
16+
SharedElementSceneComponent,
17+
SharedElementRoute
18+
} from "./types";
19+
20+
const styles = StyleSheet.create({
21+
container: {
22+
flex: 1
23+
}
24+
});
25+
26+
type PropsType = {
27+
navigation: StackNavigationProp<any>;
28+
route: SharedElementRoute;
29+
};
30+
31+
function isValidNavigationState(
32+
state: Partial<NavigationState>
33+
): state is NavigationState {
34+
return "index" in state && "routes" in state;
35+
}
36+
37+
// Gets the current screen from navigation state
38+
function getActiveRoute(state: NavigationState): Route<any> {
39+
const route = state.routes[state.index];
40+
return route.state && isValidNavigationState(route.state)
41+
? getActiveRoute(route.state) // Dive into nested navigators
42+
: route;
43+
}
44+
45+
function isActiveRoute(
46+
navigation: StackNavigationProp<any>,
47+
route: Route<any>
48+
): boolean {
49+
const state = navigation.dangerouslyGetState();
50+
const activeRoute = getActiveRoute(state);
51+
return route.name === activeRoute.name;
52+
}
53+
54+
function createSharedElementScene(
55+
Component: SharedElementSceneComponent,
56+
rendererData: ISharedElementRendererData,
57+
AnimationContext: React.Context<StackCardInterpolationProps | undefined>,
58+
navigatorId: string,
59+
verbose: boolean
60+
): React.ComponentType<any> {
61+
class SharedElementSceneView extends React.PureComponent<PropsType> {
62+
private subscriptions: {
63+
[key: string]: SharedElementEventSubscription;
64+
} = {};
65+
private sceneData: SharedElementSceneData = new SharedElementSceneData(
66+
Component,
67+
this.props.route,
68+
navigatorId,
69+
rendererData.nestingDepth,
70+
verbose
71+
);
72+
73+
componentDidMount() {
74+
const { navigation } = this.props;
75+
this.subscriptions = {
76+
willFocus: navigation.addListener("focus", this.onWillFocus),
77+
willBlur: navigation.addListener("blur", this.onWillBlur),
78+
transitionStart: navigation.addListener(
79+
"transitionStart",
80+
this.transitionStart
81+
),
82+
transitionEnd: navigation.addListener(
83+
"transitionEnd",
84+
this.transitionEnd
85+
)
86+
};
87+
}
88+
89+
transitionStart({ data: { closing } }: any) {
90+
rendererData.startTransition(
91+
closing,
92+
navigatorId,
93+
rendererData.nestingDepth
94+
);
95+
}
96+
97+
transitionEnd = ({ data: { closing } }: any) => {
98+
rendererData.endTransition(
99+
closing,
100+
navigatorId,
101+
rendererData.nestingDepth
102+
);
103+
};
104+
105+
componentWillUnmount() {
106+
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
107+
}
108+
109+
render() {
110+
// console.log('SharedElementSceneView.render');
111+
return (
112+
<SharedElementSceneContext.Provider value={this.sceneData}>
113+
<View
114+
style={styles.container}
115+
collapsable={false}
116+
ref={this.onSetRef}
117+
>
118+
<AnimationContext.Consumer>
119+
{this.onRenderAnimationContext}
120+
</AnimationContext.Consumer>
121+
<Component {...this.props} />
122+
</View>
123+
</SharedElementSceneContext.Provider>
124+
);
125+
}
126+
127+
private onRenderAnimationContext = (
128+
value: StackCardInterpolationProps | undefined
129+
) => {
130+
this.sceneData.setAnimimationContextValue(value);
131+
return null;
132+
};
133+
134+
componentDidUpdate() {
135+
this.sceneData.updateRoute(this.props.route);
136+
}
137+
138+
private onSetRef = (ref: any) => {
139+
this.sceneData.setAncestor(nodeFromRef(ref));
140+
};
141+
142+
private onWillFocus = () => {
143+
const { navigation, route } = this.props;
144+
145+
//console.log('onWillFocus: ', navigation.state, activeRoute);
146+
if (isActiveRoute(navigation, route)) {
147+
this.sceneData.updateRoute(route);
148+
rendererData.updateSceneState(this.sceneData, "willFocus");
149+
InteractionManager.runAfterInteractions(() => {
150+
this.sceneData.updateRoute(this.props.route);
151+
rendererData.updateSceneState(this.sceneData, "didFocus");
152+
});
153+
}
154+
};
155+
156+
private onWillBlur = () => {
157+
const { route } = this.props;
158+
159+
//console.log('onWillBlur: ', navigation.state, activeRoute);
160+
this.sceneData.updateRoute(route);
161+
rendererData.updateSceneState(this.sceneData, "willBlur");
162+
};
163+
}
164+
165+
hoistNonReactStatics(SharedElementSceneView, Component);
166+
return SharedElementSceneView;
167+
}
168+
169+
export default createSharedElementScene;
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
useNavigationBuilder,
3+
createNavigatorFactory,
4+
StackRouter,
5+
DefaultNavigatorOptions,
6+
RouteConfig,
7+
StackRouterOptions,
8+
StackNavigationState
9+
} from "@react-navigation/native";
10+
import {
11+
CardAnimationContext,
12+
StackView,
13+
StackNavigationOptions
14+
} from "@react-navigation/stack";
15+
import {
16+
StackNavigationConfig,
17+
StackNavigationEventMap
18+
} from "@react-navigation/stack/lib/typescript/src/types";
19+
import * as React from "react";
20+
21+
import SharedElementRendererContext from "./SharedElementRendererContext";
22+
import SharedElementRendererData from "./SharedElementRendererData";
23+
import { SharedElementRendererProxy } from "./SharedElementRendererProxy";
24+
import SharedElementRendererView from "./SharedElementRendererView";
25+
import createSharedElementScene from "./createSharedElementScene";
26+
import {
27+
SharedElementSceneComponent,
28+
SharedElementsComponentConfig
29+
} from "./types";
30+
31+
let _navigatorId = 1;
32+
33+
export default function createSharedElementStackNavigator<
34+
ParamList extends Record<string, object | undefined>
35+
>(options?: { name?: string; debug?: boolean }) {
36+
const navigatorId =
37+
options && options.name ? options.name : `stack${_navigatorId}`;
38+
_navigatorId++;
39+
const debug = options?.debug || false;
40+
41+
const rendererDataProxy = new SharedElementRendererProxy();
42+
43+
type Props = DefaultNavigatorOptions<StackNavigationOptions> &
44+
StackRouterOptions &
45+
StackNavigationConfig;
46+
47+
function SharedElementStackNavigator({
48+
initialRouteName,
49+
children,
50+
screenOptions,
51+
...rest
52+
}: Props) {
53+
const { state, descriptors, navigation } = useNavigationBuilder<
54+
StackNavigationState,
55+
StackRouterOptions,
56+
StackNavigationOptions,
57+
StackNavigationEventMap
58+
>(StackRouter, {
59+
initialRouteName,
60+
children,
61+
screenOptions
62+
});
63+
64+
const rendererDataRef = React.useRef<SharedElementRendererData | null>(
65+
null
66+
);
67+
68+
return (
69+
<SharedElementRendererContext.Consumer>
70+
{rendererData => {
71+
// In case a renderer is already present higher up in the chain
72+
// then don't bother creating a renderer here, but use that one instead
73+
if (!rendererData) {
74+
rendererDataRef.current =
75+
rendererDataRef.current || new SharedElementRendererData();
76+
rendererDataProxy.source = rendererDataRef.current;
77+
} else {
78+
rendererDataProxy.source = rendererData;
79+
}
80+
return (
81+
<SharedElementRendererContext.Provider value={rendererDataProxy}>
82+
<StackView
83+
{...rest}
84+
state={state}
85+
navigation={navigation}
86+
descriptors={descriptors}
87+
/>
88+
{rendererDataRef.current ? (
89+
<SharedElementRendererView
90+
rendererData={rendererDataRef.current}
91+
/>
92+
) : (
93+
undefined
94+
)}
95+
</SharedElementRendererContext.Provider>
96+
);
97+
}}
98+
</SharedElementRendererContext.Consumer>
99+
);
100+
}
101+
102+
const navigatorFactory = createNavigatorFactory<
103+
StackNavigationState,
104+
StackNavigationOptions,
105+
StackNavigationEventMap,
106+
typeof SharedElementStackNavigator
107+
>(SharedElementStackNavigator);
108+
109+
const { Navigator, Screen } = navigatorFactory<ParamList>();
110+
111+
type ScreenProps<RouteName extends keyof ParamList> = Omit<
112+
RouteConfig<
113+
ParamList,
114+
RouteName,
115+
StackNavigationState,
116+
StackNavigationOptions,
117+
StackNavigationEventMap
118+
>,
119+
"component" | "children"
120+
> & {
121+
component: SharedElementSceneComponent;
122+
sharedElementsConfig?: SharedElementsComponentConfig;
123+
};
124+
125+
function wrapComponent(component: SharedElementSceneComponent) {
126+
return createSharedElementScene(
127+
component,
128+
rendererDataProxy,
129+
CardAnimationContext,
130+
navigatorId,
131+
debug
132+
);
133+
}
134+
135+
// Wrapping Screen to explicitly statically type a "Shared Element" Screen.
136+
function wrapScreen<RouteName extends keyof ParamList>(
137+
_: ScreenProps<RouteName>
138+
) {
139+
return null;
140+
}
141+
142+
type NavigatorProps = React.ComponentProps<typeof Navigator>;
143+
144+
function getSharedElementsChildrenProps(children: React.ReactNode) {
145+
return React.Children.toArray(children).reduce<any[]>((acc, child) => {
146+
if (React.isValidElement(child)) {
147+
if (child.type === wrapScreen) {
148+
acc.push(child.props);
149+
}
150+
151+
if (child.type === React.Fragment) {
152+
acc.push(...getSharedElementsChildrenProps(child.props.children));
153+
}
154+
}
155+
return acc;
156+
}, []);
157+
}
158+
159+
// react-navigation only allows the Screen component as direct children
160+
// of Navigator, this is why we need to wrap the Navigator
161+
function WrapNavigator(props: NavigatorProps) {
162+
const { children, ...rest } = props;
163+
const componentMapRef = React.useRef<Map<any, any>>(new Map());
164+
const screenChildrenProps = getSharedElementsChildrenProps(children);
165+
166+
return (
167+
<Navigator {...rest}>
168+
{screenChildrenProps.map(
169+
({ component, sharedElementsConfig, ...childrenProps }) => {
170+
if (sharedElementsConfig)
171+
component.sharedElements = sharedElementsConfig;
172+
173+
if (!componentMapRef.current.has(component)) {
174+
componentMapRef.current.set(component, wrapComponent(component));
175+
}
176+
177+
const wrappedComponent = componentMapRef.current.get(component);
178+
179+
return (
180+
<Screen
181+
key={childrenProps.name}
182+
{...childrenProps}
183+
component={wrappedComponent}
184+
/>
185+
);
186+
}
187+
)}
188+
</Navigator>
189+
);
190+
}
191+
192+
return {
193+
Navigator: WrapNavigator,
194+
Screen: wrapScreen
195+
};
196+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./types";
22

33
export { default as createSharedElementStackNavigator } from "./v4/createSharedElementStackNavigator";
4+
export { default as createSharedElementStackNavigator5 } from "./createSharedElementStackNavigator";
45

56
export { default as SharedElement } from "./SharedElement";

0 commit comments

Comments
 (0)