From f154a93f44f655e8f465abdbf696e563557af8ac Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Wed, 4 Sep 2019 00:10:30 +0200 Subject: [PATCH] fix: fixed no or stale transition when switching route (goBack) too fast --- src/SharedElementRendererData.tsx | 184 +++++++++++++++------- src/SharedElementRendererProxy.tsx | 22 +-- src/createSharedElementScene.tsx | 22 +-- src/createSharedElementStackNavigator.tsx | 16 +- src/utils.tsx | 13 ++ 5 files changed, 175 insertions(+), 82 deletions(-) diff --git a/src/SharedElementRendererData.tsx b/src/SharedElementRendererData.tsx index 89a50a6..f6893a5 100644 --- a/src/SharedElementRendererData.tsx +++ b/src/SharedElementRendererData.tsx @@ -4,16 +4,21 @@ import { SharedElementsStrictConfig, SharedElementAnimatedValue, SharedElementTransitionProps, + Route, } from './types'; import { normalizeSharedElementsConfig } from './utils'; export type SharedElementRendererUpdateHandler = () => any; export interface ISharedElementRendererData { - startTransition(animValue: SharedElementAnimatedValue): void; - endTransition(): void; - willActivateScene(sceneData: SharedElementSceneData): void; - didActivateScene(sceneData: SharedElementSceneData): void; + startTransition( + animValue: SharedElementAnimatedValue, + route: Route, + prevRoute: Route + ): void; + endTransition(route: Route, prevRoute: Route): void; + willActivateScene(sceneData: SharedElementSceneData, route: Route): void; + didActivateScene(sceneData: SharedElementSceneData, route: Route): void; } function getSharedElements( @@ -28,67 +33,138 @@ function getSharedElements( ); } +const NO_SHARED_ELEMENTS: any[] = []; + +type SceneRoute = { + scene: SharedElementSceneData; + route: Route; + subscription: SharedElementEventSubscription | null; +}; + export default class SharedElementRendererData implements ISharedElementRendererData { - private sceneData: SharedElementSceneData | null = null; - private prevSceneData: SharedElementSceneData | null = null; + private scenes: SceneRoute[] = []; private updateSubscribers = new Set(); - private sceneSubscription: SharedElementEventSubscription | null = null; - private sharedElements: SharedElementsStrictConfig = []; + private sharedElements: SharedElementsStrictConfig | null = null; private isShowing: boolean = true; private animValue: SharedElementAnimatedValue; + private route: Route | null = null; + private prevRoute: Route | null = null; + private scene: SharedElementSceneData | null = null; + private prevScene: SharedElementSceneData | null = null; - startTransition(animValue: SharedElementAnimatedValue) { + startTransition( + animValue: SharedElementAnimatedValue, + route: Route, + // @ts-ignore + prevRoute: Route //eslint-disable-line @typescript-eslint/no-unused-vars + ) { + //console.log('startTransition, route: ', route.key); this.animValue = animValue; + this.prevRoute = this.route; + this.route = route; + this.updateSceneListeners(); + this.updateSharedElements(); + } + + endTransition( + // @ts-ignore + route: Route, //eslint-disable-line @typescript-eslint/no-unused-vars + // @ts-ignore + prevRoute: Route //eslint-disable-line @typescript-eslint/no-unused-vars + ) { + //console.log('endTransition, route: ', route.key); + if (this.prevRoute != null) { + this.prevRoute = null; + this.animValue = null; + this.updateSceneListeners(); + this.updateSharedElements(); + } + } + + willActivateScene(sceneData: SharedElementSceneData, route: Route): void { + //console.log('willActivateScene, route: ', route.key); + this.registerScene(sceneData, route); + } + + didActivateScene(sceneData: SharedElementSceneData, route: Route): void { + //console.log('didActivateScene, route: ', route.key); + this.prevRoute = null; + this.registerScene(sceneData, route); } - endTransition() { - // Nothing to do + private registerScene(sceneData: SharedElementSceneData, route: Route) { + this.scenes.push({ + scene: sceneData, + route, + subscription: null, + }); + if (this.scenes.length > 5) { + const { subscription } = this.scenes[0]; + this.scenes.splice(0, 1); + if (subscription) subscription.remove(); + } + this.updateSceneListeners(); + this.updateSharedElements(); } - willActivateScene(sceneData: SharedElementSceneData): void { - /*console.log( - 'SharedElementRendererData.willActivateScene: ', - sceneData.name, - ', previous: ', - this.prevSceneData ? this.prevSceneData.name : '' - );*/ - if (!this.prevSceneData) return; + private updateSceneListeners() { + this.scenes.forEach(sceneRoute => { + const { scene, route, subscription } = sceneRoute; + const isActive = + (this.route && this.route.key === route.key) || + (this.prevRoute && this.prevRoute.key === route.key); + if (isActive && !subscription) { + sceneRoute.subscription = scene.addUpdateListener(() => { + // TODO optimize + this.emitUpdateEvent(); + }); + } else if (!isActive && subscription) { + sceneRoute.subscription = null; + subscription.remove(); + } + }); + } + + private updateSharedElements() { + const { route, prevRoute, animValue } = this; + const sceneRoute = route + ? this.scenes.find(sc => sc.route.key === route.key) + : undefined; + const prevSceneRoute = prevRoute + ? this.scenes.find(sc => sc.route.key === prevRoute.key) + : undefined; + const scene = sceneRoute ? sceneRoute.scene : null; + const prevScene = prevSceneRoute ? prevSceneRoute.scene : null; + + // Update current scene & previous scene + if (scene === this.scene && prevScene === this.prevScene) return; + this.scene = scene; + this.prevScene = prevScene; + + // Update shared elements + let sharedElements: SharedElementsStrictConfig | null = null; let isShowing = true; - let sharedElements = getSharedElements(sceneData, this.prevSceneData, true); - if (!sharedElements) { - isShowing = false; - sharedElements = getSharedElements(this.prevSceneData, sceneData, false); + if (animValue && scene && prevScene) { + sharedElements = getSharedElements(scene, prevScene, true); + if (!sharedElements) { + isShowing = false; + sharedElements = getSharedElements(prevScene, scene, false); + } } - if (sharedElements && sharedElements.length) { - // console.log('sharedElements: ', sharedElements, sceneData); - this.sceneData = sceneData; + if (this.sharedElements !== sharedElements) { this.sharedElements = sharedElements; this.isShowing = isShowing; - this.sceneSubscription = this.sceneData.addUpdateListener(() => { - // TODO optimize - this.emitUpdateEvent(); - }); + /*console.log( + 'updateSharedElements: ', + sharedElements, + ' ,isShowing: ', + isShowing + );*/ this.emitUpdateEvent(); } } - didActivateScene(sceneData: SharedElementSceneData): void { - //console.log('SharedElementRendererData.didActivateScene: ', sceneData.name); - if (this.sceneSubscription) { - this.sceneSubscription.remove(); - this.sceneSubscription = null; - } - this.prevSceneData = sceneData; - if (this.sceneData) { - this.sceneData = null; - if (this.sharedElements.length) { - this.sharedElements = []; - this.emitUpdateEvent(); - } - } - } - addUpdateListener( handler: SharedElementRendererUpdateHandler ): SharedElementEventSubscription { @@ -103,23 +179,21 @@ export default class SharedElementRendererData } getTransitions(): SharedElementTransitionProps[] { - const { sharedElements, prevSceneData, sceneData, isShowing } = this; + const { sharedElements, prevScene, scene, isShowing, animValue } = this; // console.log('getTransitions: ', sharedElements); + if (!sharedElements || !scene || !prevScene) return NO_SHARED_ELEMENTS; return sharedElements.map(({ id, otherId, ...other }) => { const startId = isShowing ? otherId || id : id; const endId = isShowing ? id : otherId || id; return { - position: this.animValue, + position: animValue, start: { - ancestor: - (prevSceneData ? prevSceneData.getAncestor() : undefined) || null, - node: - (prevSceneData ? prevSceneData.getNode(startId) : undefined) || - null, + ancestor: (prevScene ? prevScene.getAncestor() : undefined) || null, + node: (prevScene ? prevScene.getNode(startId) : undefined) || null, }, end: { - ancestor: (sceneData ? sceneData.getAncestor() : undefined) || null, - node: (sceneData ? sceneData.getNode(endId) : undefined) || null, + ancestor: (scene ? scene.getAncestor() : undefined) || null, + node: (scene ? scene.getNode(endId) : undefined) || null, }, ...other, }; diff --git a/src/SharedElementRendererProxy.tsx b/src/SharedElementRendererProxy.tsx index a2d5583..e9d72df 100644 --- a/src/SharedElementRendererProxy.tsx +++ b/src/SharedElementRendererProxy.tsx @@ -1,50 +1,54 @@ import SharedElementRendererData, { ISharedElementRendererData, } from './SharedElementRendererData'; -import { SharedElementAnimatedValue } from './types'; +import { SharedElementAnimatedValue, Route } from './types'; import SharedElementSceneData from './SharedElementSceneData'; export class SharedElementRendererProxy implements ISharedElementRendererData { private data: SharedElementRendererData | null = null; - startTransition(animValue: SharedElementAnimatedValue) { + startTransition( + animValue: SharedElementAnimatedValue, + route: Route, + prevRoute: Route + ) { if (!this.data) { console.warn( 'SharedElementRendererProxy.startTransition called before Proxy was initialized' ); return; } - return this.data.startTransition(animValue); + return this.data.startTransition(animValue, route, prevRoute); } - endTransition() { + endTransition(route: Route, prevRoute: Route) { if (!this.data) { console.warn( 'SharedElementRendererProxy.endTransition called before Proxy was initialized' ); return; } - return this.data.endTransition(); + return this.data.endTransition(route, prevRoute); } - willActivateScene(sceneData: SharedElementSceneData) { + willActivateScene(sceneData: SharedElementSceneData, route: Route) { if (!this.data) { console.warn( 'SharedElementRendererProxy.willActivateScene called before Proxy was initialized' ); return; } - return this.data.willActivateScene(sceneData); + return this.data.willActivateScene(sceneData, route); } - didActivateScene(sceneData: SharedElementSceneData) { + didActivateScene(sceneData: SharedElementSceneData, route: Route) { if (!this.data) { console.warn( 'SharedElementRendererProxy.didActivateScene called before Proxy was initialized' ); return; } - return this.data.didActivateScene(sceneData); + return this.data.didActivateScene(sceneData, route); } get source(): SharedElementRendererData | null { diff --git a/src/createSharedElementScene.tsx b/src/createSharedElementScene.tsx index 0f7e0c3..c0070c2 100644 --- a/src/createSharedElementScene.tsx +++ b/src/createSharedElementScene.tsx @@ -10,6 +10,7 @@ import { SharedElementSceneComponent, } from './types'; import { ISharedElementRendererData } from './SharedElementRendererData'; +import { getActiveRouteState } from './utils'; const styles = StyleSheet.create({ container: { @@ -21,18 +22,6 @@ type PropsType = { navigation: NavigationProp; }; -function getActiveRouteState(route: any): any { - if ( - !route.routes || - route.routes.length === 0 || - route.index >= route.routes.length - ) { - return route; - } else { - return getActiveRouteState(route.routes[route.index]); - } -} - function createSharedElementScene( Component: SharedElementSceneComponent, rendererData: ISharedElementRendererData @@ -84,7 +73,12 @@ function createSharedElementScene( }; private onWillFocus = () => { - rendererData.willActivateScene(this.sceneData); + const { navigation } = this.props; + const activeRoute = getActiveRouteState(navigation.state); + if (navigation.state.routeName === activeRoute.routeName) { + // console.log('onWillFocus: ', navigation.state, activeRoute); + rendererData.willActivateScene(this.sceneData, navigation.state); + } }; private onDidFocus = () => { @@ -92,7 +86,7 @@ function createSharedElementScene( const activeRoute = getActiveRouteState(navigation.state); if (navigation.state.routeName === activeRoute.routeName) { // console.log('onDidFocus: ', this.sceneData.name, navigation); - rendererData.didActivateScene(this.sceneData); + rendererData.didActivateScene(this.sceneData, navigation.state); } }; } diff --git a/src/createSharedElementStackNavigator.tsx b/src/createSharedElementStackNavigator.tsx index 58e755e..d9860df 100644 --- a/src/createSharedElementStackNavigator.tsx +++ b/src/createSharedElementStackNavigator.tsx @@ -7,6 +7,7 @@ import SharedElementRendererData, { import createSharedElementScene from './createSharedElementScene'; import SharedElementRendererContext from './SharedElementRendererContext'; import { SharedElementRendererProxy } from './SharedElementRendererProxy'; +import { getActiveRouteState } from './utils'; function createSharedElementEnabledNavigator( createNavigator: any, @@ -44,15 +45,22 @@ function createSharedElementEnabledNavigator( position.interpolate({ inputRange: [index - 1, index], outputRange: index > prevIndex ? [0, 1] : [2, 1], - }) + }), + getActiveRouteState(transitionProps.scene.route), + getActiveRouteState(prevTransitionProps.scene.route) ); if (navigatorConfig && navigatorConfig.onTransitionStart) { - navigatorConfig.onTransitionStart(transitionProps, prevTransitionProps); + navigatorConfig.onTransitionStart( + getActiveRouteState(transitionProps.scene.route), + getActiveRouteState(prevTransitionProps.scene.route) + ); } }, onTransitionEnd: (transitionProps: any, prevTransitionProps: any) => { - // console.log('onTransitionEnd: ', transitionProps, prevTransitionProps); - rendererData.endTransition(); + rendererData.endTransition( + transitionProps.scene.route, + prevTransitionProps.scene.route + ); if (navigatorConfig && navigatorConfig.onTransitionEnd) { navigatorConfig.onTransitionEnd(transitionProps, prevTransitionProps); } diff --git a/src/utils.tsx b/src/utils.tsx index 74f0a23..6445f40 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -3,6 +3,7 @@ import { SharedElementsConfig, SharedElementStrictConfig, SharedElementsStrictConfig, + Route, } from './types'; export function normalizeSharedElementConfig( @@ -31,3 +32,15 @@ export function normalizeSharedElementsConfig( if (!sharedElementsConfig || !sharedElementsConfig.length) return null; return sharedElementsConfig.map(normalizeSharedElementConfig); } + +export function getActiveRouteState(route: any): Route { + if ( + !route.routes || + route.routes.length === 0 || + route.index >= route.routes.length + ) { + return route; + } else { + return getActiveRouteState(route.routes[route.index]); + } +}