From 9c4136d74a42f7981e50bb79d5136f805122ec09 Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Sat, 31 Aug 2019 18:31:32 +0200 Subject: [PATCH] feat: added new API which supports pop as well BREAKING CHANGE: The old API where the shared elements could be passed as props has been dropped. Instead you should define a static variable called `sharedElements` on your Screen component. That variable can be either a function or a static array. See the README for more details --- src/SharedElementRendererData.tsx | 92 +++++++++++++++-------- src/SharedElementRendererProxy.tsx | 14 ++-- src/SharedElementSceneData.tsx | 16 +++- src/createSharedElementScene.tsx | 42 +++-------- src/createSharedElementStackNavigator.tsx | 11 ++- src/types.tsx | 68 +++++++++++------ src/utils.tsx | 24 ------ 7 files changed, 149 insertions(+), 118 deletions(-) diff --git a/src/SharedElementRendererData.tsx b/src/SharedElementRendererData.tsx index 9d563ed..98a2e07 100644 --- a/src/SharedElementRendererData.tsx +++ b/src/SharedElementRendererData.tsx @@ -4,7 +4,9 @@ import { SharedElementsConfig, SharedElementAnimatedValue, SharedElementTransitionProps, + NavigationProp, } from './types'; +import { normalizeSharedElementsConfig } from './utils'; export type SharedElementRendererUpdateHandler = () => any; @@ -13,16 +15,33 @@ export interface ISharedElementRendererData { endTransition(): void; willActivateScene( sceneData: SharedElementSceneData, - sharedElements: SharedElementsConfig, - animValue?: SharedElementAnimatedValue + navigation: NavigationProp ): void; - didActivateScene(sceneData: SharedElementSceneData): void; + didActivateScene( + sceneData: SharedElementSceneData, + navigation: NavigationProp + ): void; +} + +function getSharedElements( + sceneData: SharedElementSceneData, + navigation: NavigationProp, + otherNavigation: NavigationProp, + show: boolean +): SharedElementsConfig | null { + const { sharedElements } = sceneData.Component; + if (!sharedElements) return null; + // TODO push/pop distinction? + return normalizeSharedElementsConfig( + sharedElements(navigation, otherNavigation, show) + ); } export default class SharedElementRendererData implements ISharedElementRendererData { private sceneData: SharedElementSceneData | null = null; private prevSceneData: SharedElementSceneData | null = null; + private prevNavigation: NavigationProp | null = null; private updateSubscribers = new Set(); private sceneSubscription: SharedElementEventSubscription | null = null; private sharedElements: SharedElementsConfig = []; @@ -38,22 +57,27 @@ export default class SharedElementRendererData willActivateScene( sceneData: SharedElementSceneData, - sharedElements: SharedElementsConfig, - animValue?: SharedElementAnimatedValue + navigation: NavigationProp ): void { /*console.log( 'SharedElementRendererData.willActivateScene: ', sceneData.name, ', previous: ', - this.prevSceneData ? this.prevSceneData.name : '', - ', sharedElements: ', - sharedElements + this.prevSceneData ? this.prevSceneData.name : '' );*/ - this.sceneData = sceneData; - if (!this.prevSceneData) return; - this.sharedElements = sharedElements; - if (animValue) this.animValue = animValue; - if (sharedElements.length) { + if (!this.prevSceneData || !this.prevNavigation) return; + const sharedElements = + getSharedElements(sceneData, navigation, this.prevNavigation, true) || + getSharedElements( + this.prevSceneData, + this.prevNavigation, + navigation, + false + ); + if (sharedElements && sharedElements.length) { + // console.log('sharedElements: ', sharedElements, sceneData); + this.sceneData = sceneData; + this.sharedElements = sharedElements; this.sceneSubscription = this.sceneData.addUpdateListener(() => { // TODO optimize this.emitUpdateEvent(); @@ -62,13 +86,17 @@ export default class SharedElementRendererData } } - didActivateScene(sceneData: SharedElementSceneData): void { - // console.log('SharedElementRendererData.didActivateScene: ', sceneData.name); + didActivateScene( + sceneData: SharedElementSceneData, + navigation: NavigationProp + ): void { + //console.log('SharedElementRendererData.didActivateScene: ', sceneData.name); if (this.sceneSubscription) { this.sceneSubscription.remove(); this.sceneSubscription = null; } this.prevSceneData = sceneData; + this.prevNavigation = navigation; if (this.sceneData) { this.sceneData = null; if (this.sharedElements.length) { @@ -93,20 +121,24 @@ export default class SharedElementRendererData getTransitions(): SharedElementTransitionProps[] { const { prevSceneData, sceneData } = this; - return this.sharedElements.map(({ id, sourceId, animation, debug }) => ({ - position: this.animValue, - start: { - ancestor: - (prevSceneData ? prevSceneData.getAncestor() : undefined) || null, - node: - (prevSceneData ? prevSceneData.getNode(sourceId) : undefined) || null, - }, - end: { - ancestor: (sceneData ? sceneData.getAncestor() : undefined) || null, - node: (sceneData ? sceneData.getNode(id) : undefined) || null, - }, - ...animation, - debug, - })); + // console.log('getTransitions: ', sceneData, prevSceneData); + return this.sharedElements.map(({ id, sourceId, animation, debug }) => { + return { + position: this.animValue, + start: { + ancestor: + (prevSceneData ? prevSceneData.getAncestor() : undefined) || null, + node: + (prevSceneData ? prevSceneData.getNode(sourceId) : undefined) || + null, + }, + end: { + ancestor: (sceneData ? sceneData.getAncestor() : undefined) || null, + node: (sceneData ? sceneData.getNode(id) : undefined) || null, + }, + ...animation, + debug, + }; + }); } } diff --git a/src/SharedElementRendererProxy.tsx b/src/SharedElementRendererProxy.tsx index 628af73..7ed900e 100644 --- a/src/SharedElementRendererProxy.tsx +++ b/src/SharedElementRendererProxy.tsx @@ -1,7 +1,7 @@ import SharedElementRendererData, { ISharedElementRendererData, } from './SharedElementRendererData'; -import { SharedElementAnimatedValue, SharedElementsConfig } from './types'; +import { SharedElementAnimatedValue, NavigationProp } from './types'; import SharedElementSceneData from './SharedElementSceneData'; export class SharedElementRendererProxy implements ISharedElementRendererData { @@ -29,8 +29,7 @@ export class SharedElementRendererProxy implements ISharedElementRendererData { willActivateScene( sceneData: SharedElementSceneData, - sharedElements: SharedElementsConfig, - animValue?: SharedElementAnimatedValue + navigation: NavigationProp ) { if (!this.data) { console.warn( @@ -38,17 +37,20 @@ export class SharedElementRendererProxy implements ISharedElementRendererData { ); return; } - return this.data.willActivateScene(sceneData, sharedElements, animValue); + return this.data.willActivateScene(sceneData, navigation); } - didActivateScene(sceneData: SharedElementSceneData) { + didActivateScene( + sceneData: SharedElementSceneData, + navigation: NavigationProp + ) { if (!this.data) { console.warn( 'SharedElementRendererProxy.didActivateScene called before Proxy was initialized' ); return; } - return this.data.didActivateScene(sceneData); + return this.data.didActivateScene(sceneData, navigation); } get source(): SharedElementRendererData | null { diff --git a/src/SharedElementSceneData.tsx b/src/SharedElementSceneData.tsx index e04e016..d90df1a 100644 --- a/src/SharedElementSceneData.tsx +++ b/src/SharedElementSceneData.tsx @@ -1,4 +1,8 @@ -import { SharedElementNode, SharedElementEventSubscription } from './types'; +import { + SharedElementNode, + SharedElementEventSubscription, + SharedElementSceneComponent, +} from './types'; export type SharedElementSceneUpdateHandlerEventType = | 'ancestor' @@ -17,10 +21,16 @@ export default class SharedElementSceneData { private nodes: { [key: string]: SharedElementNode; } = {}; + public readonly Component: SharedElementSceneComponent; public readonly name: string; - constructor(name: string) { - this.name = name; + constructor(Component: SharedElementSceneComponent) { + this.Component = Component; + this.name = + Component.displayName || + Component.name || + (Component.constructor ? Component.constructor.name : undefined) || + ''; } getAncestor(): SharedElementNode | undefined { diff --git a/src/createSharedElementScene.tsx b/src/createSharedElementScene.tsx index 4160036..1f64efb 100644 --- a/src/createSharedElementScene.tsx +++ b/src/createSharedElementScene.tsx @@ -4,9 +4,12 @@ import hoistNonReactStatics from 'hoist-non-react-statics'; import { nodeFromRef } from 'react-native-shared-element'; import SharedElementSceneData from './SharedElementSceneData'; import SharedElementSceneContext from './SharedElementSceneContext'; -import { SharedElementsConfig, SharedElementEventSubscription } from './types'; +import { + SharedElementEventSubscription, + NavigationProp, + SharedElementSceneComponent, +} from './types'; import { ISharedElementRendererData } from './SharedElementRendererData'; -import { normalizeSharedElementsConfig } from './utils'; const styles = StyleSheet.create({ container: { @@ -15,7 +18,7 @@ const styles = StyleSheet.create({ }); type PropsType = { - navigation: any; + navigation: NavigationProp; }; function getActiveRouteState(route: any): any { @@ -31,19 +34,15 @@ function getActiveRouteState(route: any): any { } function createSharedElementScene( - Component: React.ComponentType, + Component: SharedElementSceneComponent, rendererData: ISharedElementRendererData ): React.ComponentType { class SharedElementSceneView extends React.PureComponent { - private initial: boolean = true; private subscriptions: { [key: string]: SharedElementEventSubscription; } = {}; private sceneData: SharedElementSceneData = new SharedElementSceneData( - Component.displayName || - Component.name || - (Component.constructor ? Component.constructor.name : undefined) || - '' + Component ); componentDidMount() { @@ -79,34 +78,17 @@ function createSharedElementScene( this.sceneData.setAncestor(nodeFromRef(ref)); }; - private getSharedElements(): SharedElementsConfig | null { - const { navigation } = this.props; - let sharedElements = - // @ts-ignore - navigation.getParam('sharedElements') || Component.sharedElements; - if (!sharedElements) return null; - if (typeof sharedElements === 'function') { - sharedElements = sharedElements(navigation); - } - return sharedElements - ? normalizeSharedElementsConfig(sharedElements) - : null; - } - private onWillFocus = () => { - const sharedElements = this.getSharedElements(); - if (sharedElements && this.initial) { - this.initial = false; - rendererData.willActivateScene(this.sceneData, sharedElements); - } + const { navigation } = this.props; + rendererData.willActivateScene(this.sceneData, navigation); }; private onDidFocus = () => { const { navigation } = this.props; const activeRoute = getActiveRouteState(navigation.state); if (navigation.state.routeName === activeRoute.routeName) { - // console.log('onDidFocus: ', this.sceneData.name, activeRoute, this.props.navigation.state.routeName); - rendererData.didActivateScene(this.sceneData); + // console.log('onDidFocus: ', this.sceneData.name, navigation); + rendererData.didActivateScene(this.sceneData, navigation); } }; } diff --git a/src/createSharedElementStackNavigator.tsx b/src/createSharedElementStackNavigator.tsx index 4aaa80b..4e601c3 100644 --- a/src/createSharedElementStackNavigator.tsx +++ b/src/createSharedElementStackNavigator.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { Animated } from 'react-native'; import hoistNonReactStatics from 'hoist-non-react-statics'; import SharedElementRendererView from './SharedElementRendererView'; import SharedElementRendererData, { @@ -38,10 +37,14 @@ function createSharedElementEnabledNavigator( return createNavigator(wrappedRouteConfigs, { ...navigatorConfig, onTransitionStart: (transitionProps: any, prevTransitionProps: any) => { - if (transitionProps.index === prevTransitionProps.index) return; - // console.log('onTransitionStart: ', transitionProps, prevTransitionProps); + const { index, position } = transitionProps; + const prevIndex = prevTransitionProps.index; + if (index === prevIndex) return; rendererData.startTransition( - Animated.subtract(transitionProps.position, transitionProps.index - 1) + position.interpolate({ + inputRange: [index - 1, index], + outputRange: index > prevIndex ? [0, 1] : [2, 1], + }) ); if (navigatorConfig.onTransitionStart) { navigatorConfig.onTransitionStart(transitionProps, prevTransitionProps); diff --git a/src/types.tsx b/src/types.tsx index 8485683..0856db8 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -12,6 +12,46 @@ export { SharedElementTransitionProps, }; +export type Route = { + key: string; + routeName: string; +}; + +export type NavigationEventName = + | 'willFocus' + | 'didFocus' + | 'willBlur' + | 'didBlur'; + +export type NavigationState = { + key: string; + index: number; + routes: Route[]; + routeName: string; + transitions: { + pushing: string[]; + popping: string[]; + }; + params?: { [key: string]: unknown }; +}; + +export type NavigationProp = { + navigate(routeName: RouteName): void; + goBack(): void; + goBack(key: string | null): void; + addListener: ( + event: NavigationEventName, + callback: () => void + ) => { remove: () => void }; + isFocused(): boolean; + state: NavigationState; + setParams(params: Params): void; + getParam(): Params; + dispatch(action: { type: string }): void; + isFirstRouteInParent(): boolean; + dangerouslyGetParent(): NavigationProp | undefined; +}; + export type SharedElementEventSubscription = { remove(): void; }; @@ -33,26 +73,12 @@ export type SharedElementsConfig = SharedElementConfig[]; export type SharedElementAnimatedValue = any; -export type NavigationSpringConfig = { - damping: number; - mass: number; - stiffness: number; - restSpeedThreshold: number; - restDisplacementThreshold: number; - overshootClamping: boolean; -}; - -export type NavigationTimingConfig = { - duration: number; - easing: any; // Animated.EasingFunction; -}; - -export type NavigationTransitionSpec = - | { timing: 'spring'; config: NavigationSpringConfig } - | { timing: 'timing'; config: NavigationTimingConfig }; +export type SharedElementsComponentConfig = ( + navigation: NavigationProp, + otherNavigation: NavigationProp, + show: boolean +) => SharedElementsConfig | null; -export type NavigationLegacyTransitionSpec = { - timing: any; - duration?: number; - easing?: any; +export type SharedElementSceneComponent = React.ComponentType & { + sharedElements: SharedElementsComponentConfig; }; diff --git a/src/utils.tsx b/src/utils.tsx index 9764426..d117dd8 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,33 +1,9 @@ -import { Animated } from 'react-native'; import { - NavigationTransitionSpec, - NavigationLegacyTransitionSpec, SharedElementAnimationConfig, SharedElementConfig, SharedElementsConfig, } from './types'; -export function fromLegacyNavigationTransitionSpec( - spec: NavigationLegacyTransitionSpec -): NavigationTransitionSpec { - const { timing, ...other } = spec; - if (timing === Animated.timing) { - return { - timing: 'timing', - // @ts-ignore - config: other, - }; - } else if (spec.timing === Animated.spring) { - return { - timing: 'spring', - // @ts-ignore - config: other, - }; - } else { - throw new Error('Invalid transitionSpec'); - } -} - export function normalizeSharedElementAnimationConfig( animationConfig: any ): SharedElementAnimationConfig {