@Dmitry Vasilevsky

После обновления react navigation до v6, при использовании native stack пропадает возможность использовать https://github.com/IjzerenHein/react-navigation-shared-element. При этом сейчас данный пакет уже не имеет поддержки.

Все же эффект shared element transition не так сложно с анимировать вручную. Для этого необходимо будет сделать всего несколько шагов.

Шаг 1

Предположим нужно сделать подобный эффект для картинки из списка на первом экране, анимируя всё это на второй экран.

Untitled

Untitled

Первоначально нужно что-то сделать с дефолтной анимацией второго экрана. Можно поставить анимацию непрозрачности, тогда не придется создавать её самим.

<StackTabOne.Screen
	name="FoxScreen"
	component={FoxScreen}
	options={{ animation: "fade" }}
/>

Если убрать анимацию вовсе, нужно также добавить непрозрачность для фона экранов и придумать свою анимацию. Так делает оригинальный пакет. Помимо этого он перехватывает анимацию возвращения и добавляет обратный эффект. Этого мы делать не будем.

Шаг 2

Теперь при нажатии на элемент списка нужно передать информацию о размерах и положении картинки, для этого мы обернём её в дополнительный View и повесим ref.

// Item.jsx
<View ref={ref} collapsable={false}>
	<Image
		source={{ uri: item.image }}
		style={styles.image}
	/>
</View>

При нажатии на элемент будем вычислять нужное с помощью функции measure ****и передавать вычисления на следующий экран.

// Item.jsx
const ref = React.useRef<View>(null);

const pressHandler = () => {
	ref.current?.measure((...measurements: number[]) => {
		navigation.navigate("FoxScreen", { item, measurements });
	});
};

Шаг 3

Напишем компонент-обертку <SharedView></SharedView> , который будет принимать вычисления компонента на первом экране и анимировать всё в начальное положение компонента на втором экране. Хочется сразу отметить, что onLayout высчитывает относительные координаты, а measure глобальные, поэтому данный компонент не универсален. Это просто пример, ведь для разных ситуаций нужно анимировать по разному.

import * as React from "react";
import { Animated, ViewProps } from "react-native";

interface LayoutValues {
  width: number;
  height: number;
  x: number;
  y: number;
}

interface SharedViewProps extends ViewProps {
  measurements: number[];
}

const SharedView = (props: SharedViewProps) => {
  const { measurements, style, ...otherProps } = props;
  const [layout, setLayout] = React.useState<LayoutValues>();
  const animation = React.useRef(new Animated.Value(0)).current;

  const animatedStyles = () => {
    if (!layout) return { opacity: 0 };
    const [, , width, height, x, y] = measurements;

    const scaleX = width / layout.width;
    const scaleY = height / layout.height;
    const translateX = x + width / 2 - (layout.x + layout.width / 2);
    const translateY = y + height / 2 - (layout.y + layout.height / 2);

    return {
      transform: [
        {
          translateX: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [translateX, 0],
          }),
        },
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [translateY, 0],
          }),
        },
        {
          scaleX: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [scaleX, 1],
          }),
        },
        {
          scaleY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [scaleY, 1],
          }),
        },
      ],
    };
  };

  React.useEffect(() => {
    Animated.timing(animation, {
      toValue: 1,
      duration: 300,
      useNativeDriver: false,
    }).start();
  }, [layout]);

  return (
    <Animated.View
      style={[style, animatedStyles()]}
      onLayout={(event) => setLayout(event.nativeEvent.layout)}
      {...otherProps}
    />
  );
};

export default SharedView;

Шаг 4

Обернем картинку на втором экране.

<SharedView measurements={measurements}>
	<Image
		source={{ uri: item.image }}
		style={styles.image}
	/>
</SharedView>