Skip to content

Instantly share code, notes, and snippets.

@mfbx9da4
Created Feb 21, 2022
Embed
What would you like to do?
Shared element transition overlay
import React, { useEffect } from 'react'
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native'
import {
nodeFromRef,
SharedElement as OriginalSharedElement,
SharedElementNode,
SharedElementTransition,
} from 'react-native-shared-element'
import { assert } from './utils/assert'
import { createObservable } from './utils/observable'
import { sleep } from './utils/sleep'
let startAncestor: any = null
let endAncestor: any
const startNodes = new Map<string, { node?: SharedElementNode; ancestor?: SharedElementNode }>()
const endNodes = new Map<string, { node?: SharedElementNode; ancestor?: SharedElementNode }>()
export function setStartAncestor(r: any) {
startAncestor = nodeFromRef(r)
}
function setEndAncestor(r: any) {
endAncestor = nodeFromRef(r)
}
function getStartSharedElement(sharedElementId: string) {
return { node: null, ancestor: null, ...startNodes.get(sharedElementId) }
}
function getEndSharedElement(sharedElementId: string) {
return { node: null, ancestor: null, ...endNodes.get(sharedElementId) }
}
let duration = 300
export type OverlayState = {
progress: Animated.Value
isInProgress: boolean
isShown: boolean
sharedElementIds: string[]
}
export const overlayState$ = createObservable<OverlayState>({
progress: new Animated.Value(0),
isInProgress: false,
isShown: false,
sharedElementIds: [] as string[],
})
export function showOverlay(sharedElementIds: string[]) {
assert(
sharedElementIds.length === new Set([...sharedElementIds]).size,
'Shared element ids must be unique'
)
overlayState$.patch({ isShown: true, isInProgress: true, sharedElementIds })
Animated.timing(overlayState$.value.progress, {
toValue: 1,
duration,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) overlayState$.patch({ isInProgress: false })
})
}
export function hideOverlay() {
overlayState$.patch({ isInProgress: true })
Animated.timing(overlayState$.value.progress, {
toValue: 0,
duration,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) overlayState$.patch({ isShown: false, isInProgress: false })
})
}
function SharedElementInner({ id, children }: { id: string; children: React.ReactNode }) {
const { isInsideOverlay } = React.useContext(OverlayContext)
useEffect(() => {
console.log('mount', { id, isInsideOverlay })
return () => console.log('unmount', { id, isInsideOverlay })
}, [])
return (
<OriginalSharedElement
onNode={x => {
if (isInsideOverlay) {
endNodes.set(id, { ...endNodes.get(id), node: x || undefined })
} else {
startNodes.set(id, { ...startNodes.get(id), node: x || undefined })
}
overlayState$.set(state => ({ ...state }))
}}
>
{children}
</OriginalSharedElement>
)
}
export const SharedElement = React.memo(SharedElementInner)
const OverlayContext = React.createContext({ isInsideOverlay: false })
export function Overlay(props: {
OverlayContent: React.ComponentType<OverlayState>
OverlayContainer?: React.ComponentType<OverlayContainerProps>
duration?: number
}) {
const [state] = overlayState$.useState()
const OverlayContainer = props.OverlayContainer || DefaultOverlayContainer
if (typeof props.duration !== 'undefined') {
duration = props.duration
}
return (
<OverlayContext.Provider value={{ isInsideOverlay: true }}>
{state.isShown ? <OverlayContainer OverlayContent={props.OverlayContent} {...state} /> : null}
{state.isInProgress ? (
<View
style={{
...StyleSheet.absoluteFillObject,
zIndex: 5,
// backgroundColor: 'peachpuff'
}}
pointerEvents="none"
>
{state.sharedElementIds.map(x => (
<SharedElementTransition
key={x}
start={{ ...getStartSharedElement(x), ancestor: startAncestor }}
end={{ ...getEndSharedElement(x), ancestor: endAncestor }}
position={state.progress}
animation="move"
resize="auto"
align="auto"
/>
))}
</View>
) : null}
</OverlayContext.Provider>
)
}
export type OverlayContainerProps = {
OverlayContent: React.ComponentType<OverlayState>
} & OverlayState
function DefaultOverlayContainer(props: OverlayContainerProps) {
console.log('DefaultOverlayContainer')
return (
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
zIndex: 3,
backgroundColor: 'black',
opacity: props.progress,
}}
>
<View
ref={x => {
// console.log('endAncestor', !!endNodes.get(id)?.ancestor)
// endNodes.set(id, { ...endNodes.get(id), ancestor: nodeFromRef(x) || undefined })
// if (x) overlayState$.set(state => ({ ...state }))
setEndAncestor(x)
}}
style={StyleSheet.absoluteFill}
>
<props.OverlayContent
progress={props.progress}
isInProgress={props.isInProgress}
isShown={props.isShown}
sharedElementIds={props.sharedElementIds}
/>
</View>
{/* <SharedElement id="myId">
<TouchableOpacity
onPress={hideOverlay}
style={{
backgroundColor: 'coral',
margin: 60,
marginTop: 200,
width: 160,
height: 160,
}}
/>
</SharedElement> */}
</Animated.View>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment