Closed stevengoldberg closed 1 year ago
@stevengoldberg
when shared element ID
change, you must update stackRouteParam to update SharedElementsConfig in Stack.Screen.
// navigation.goBack() will animate the current `shared element ID`
useEffect(() => {
navigation.setParams(
{ sharedElementId: `shared element ID` }
)
}, [`shared element ID`])
Thanks @thinh-nn3386 — I ultimately went with a similar solution. In my case, I use the route params to set the initialScrollIndex
of the FlatList, so I don't want to update that each time I scroll through the list, but I do want to update the title of the screen displaying the FlatList (in addition to selecting the correct shared element for the transition back). Here are some simplified code snippets of my approach:
Setting up the navigator:
export default function LibraryView({
}) {
return (
<Stack.Navigator initialRouteName="CalendarList">
<Stack.Screen
name="DayView"
component={DayView}
sharedElements={(route, otherRoute, showing) => {
if (showing) {
return [imagesByDateString[route.params.date].id]
}
}}
/>
<Stack.Screen
name="CalendarList"
component={CalendarView}
sharedElements={(route, otherRoute, showing) => {
if (showing) {
return [imagesByDateString[route.params.date].id]
}
}}
/>
</Stack.Navigator>
)
}
The component inside CalendarView
that sets up navigating from the overview to the expanded view list:
const DayWithImage = ({ dateString, uri, id, day, setImageLoaded }) => {
const navigation = useNavigation()
return (
<Center width="100%">
<Pressable
onPress={() => {
navigation.navigate('DayView', { date: dateString })
}}
width="100%"
>
<SharedElement id={id}>
<Image
alt={`image for ${dateString}`}
source={{ uri }}
width="100%"
aspectRatio={3 / 4}
borderWidth={1}
borderColor={'blue.600'}
borderRadius={4}
onLoadEnd={() => setImageLoaded(true)}
/>
</SharedElement>
<Text
width="100%"
color="white"
position="absolute"
textAlign="center"
top={0}
>
{day}
</Text>
</Pressable>
</Center>
)
}
The DayView, which displays details in a FlatList and keeps the screen header in sync:
function DayView({ route, navigation }) {
const imagesByDateString = useContext(ImageContext)
const { date: initialDate } = route.params
const dateStrings = Object.keys(imagesByDateString)
const sortedDateStrings = dateStrings.sort((a, b) => a - b)
const initialIndex = sortedDateStrings.indexOf(initialDate)
const windowWidth = Dimensions.get('window').width
const handleViewableItemsChanged = useCallback(
({ viewableItems, changed }) => {
if (viewableItems.length) {
const current = viewableItems[0].item
const time = imagesByDateString[current].creationTime
navigation.setOptions({
header: () => <DayCardHeader date={current} time={time} />,
})
}
},
[navigation, imagesByDateString]
)
const viewabilityConfigRef = useRef({
itemVisiblePercentThreshold: 100,
})
useEffect(() => {
const time = imagesByDateString[route.params.date].creationTime
navigation.setOptions({
header: ({ route, options, navigation }) => (
<DayCardHeader date={route.params.date} time={time} />
),
})
}, [navigation, imagesByDateString, route.params.date])
const handleRenderItem = useCallback(
({ item: dateString }) => {
const currentImage = imagesByDateString[dateString]
return (
<DayCard
key={currentImage.id}
imageCreationTime={currentImage.creationTime}
uri={currentImage.uri}
id={currentImage.id}
dateString={dateString}
creationTime={currentImage.creationTime}
windowWidth={windowWidth}
/>
)
},
[imagesByDateString, windowWidth]
)
return (
<FlatList
width={windowWidth}
data={sortedDateStrings}
horizontal
initialScrollIndex={initialIndex}
pagingEnabled
removeClippedSubviews
maxToRenderPerBatch={3}
initialNumToRender={1}
windowSize={3}
viewabilityConfig={viewabilityConfigRef.current}
getItemLayout={(data, index) => ({
length: windowWidth,
offset: windowWidth * index,
index,
})}
onViewableItemsChanged={handleViewableItemsChanged}
renderItem={handleRenderItem}
/>
)
}
And the DayCardHeader, which is responsible for navigating back to the CalendarView:
const DayCardHeader = ({ date, time }) => {
const navigation = useNavigation()
const [year, month, day] = date.split('-')
const displayTime = format(new Date(time), 'p')
const displayDate = isToday(new Date(year, month - 1, day))
? 'Today'
: format(new Date(year, month - 1, day), 'PPPP')
console.log(`${date} ${displayDate}`)
return (
<Row safeAreaTop pb={1}>
<Box pl={3} width={20} alignItems="flex-start">
<IconButton
icon={
<Ionicons
name="ios-close-circle"
size={24}
color="black"
/>
}
onPress={() =>
navigation.navigate('CalendarList', {
date,
})
}
>
Back
</IconButton>
</Box>
<Box flex={1}>
<Text flex={1} bold textAlign="center">
{displayDate}
</Text>
<Text flex={1} textAlign="center">
{displayTime}
</Text>
</Box>
<Box width={20} pr={3} />
</Row>
)
}
My app displays a grid of images. On pressing an image, it transitions to a new route which contains a full-sized view of the image and additional data. The full-sized view is displayed in a FlatList, so the list of images can be swiped through. Navigating back from here returns to the grid.
The behavior I want to achieve is like the iOS photos app: Use a shared element transition on the image to transition into and out of the full-sized image view. This is straightforward if the user taps an image and then returns to the grid without swiping in the FlatList, because the shared element is the same both times.
Here it is working as intended:
https://github.com/IjzerenHein/react-navigation-shared-element/assets/2230992/c0efa11e-c68a-47d2-8abb-d1cade169a17
However, if the user transitions to the full-sized view, swipes through the FlatList, then goes back, the shared element transition happens with the original element rather than the updated one. This video shows the issue:
https://github.com/IjzerenHein/react-navigation-shared-element/assets/2230992/bff2538c-2a0e-46b4-84bc-13af46ba9f2e
How can I solve this and keep the
sharedElements
in sync? I'm successfully keeping track of the correct shared element ID using theonViewableItemsChanged
prop ofFlatList
. But given the arguments to thesharedElements
function, it seems like I'd need to pass the id via the route params. Any way I do that, though, seems to cause unnecessary re-renders. What am I missing?