How to do shared element transitions in React Native Web?

807 Views Asked by At

I'm using react-native-web to develop a web&mobile application. I've reached the point when I'd like to introduce better transitions between the screens, and for that reason I'd like to do shared element transitions like the one below:

goal animation

Because I started with @react-navigation/stack for my routing, I wanted to use a library that would support the same structure I have. It took me quite some time to get the react-navigation-shared-element library working, but it has finally compiled. Unfortunately, the animations aren't looking like I wanted them to:

current animation current animation (no background)

As you can see, there is no animation really. It simply switches the page. I've spent weeks on this issue attempting different config, libraries, and generally everything, and I have nearly given up and started looking for web-only libraries to split the code into two different versions.

Therefore, I kindly request assistance to get the code working with one library or, if you think there are better solutions, an alternative selection of web-only and mobile-only libraries which I could use to keep roughly the same codebase (for example by wrapping library components into something I can use in the code).

Here's the code I have for my current animation:

import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createSharedElementStackNavigator, SharedElement} from 'react-navigation-shared-element';
import {View, TouchableOpacity, Text} from 'react-native';

const Stack = createSharedElementStackNavigator();

function A({navigation}) {
  return (
    <View>
      <TouchableOpacity onPress={() => navigation.navigate('B')}>GO</TouchableOpacity>
      <SharedElement id={'shared'}>
        <Text style={{position: 'absolute', top: 50, background: 'yellow'}}>Start screen</Text>
      </SharedElement>
    </View>
  );
}

function B({navigation}) {
  return (
    <View>
      <TouchableOpacity onPress={() => navigation.navigate('A')}>GO</TouchableOpacity>
      <SharedElement id={'shared'}>
        <Text style={{position: 'absolute', top: 100, background: 'yellow'}}>Start screen</Text>
      </SharedElement>
    </View>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name={'A'} component={A} />
        <Stack.Screen
          name={'B'}
          component={B}
          sharedElements={(route, otherRoute, showing) => {
            return [
              {
                id: 'shared',
                animation: 'move',
              },
            ];
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Here are the relevant libraries I am using:

"@react-navigation/native": "^6.0.2",
"@react-navigation/stack": "^6.0.7",
"react-native-shared-element": "^0.8.2",
"react-navigation-shared-element": "^3.1.3"

And, finally, here's my webpack config, which I added it only to support the shared elements library, so I have no idea if it's good or not. I'm quite new to React, so I don't know if there are things like loaders or presets required to support the shared elements animations missing.

const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Dotenv = require('dotenv-webpack');

const appDirectory = path.resolve(__dirname);

const babelLoaderConfiguration = {
  test: /\.js$/,
  include: [
    path.resolve(appDirectory, 'src'),
  ],
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['module:metro-react-native-babel-preset'],
      plugins: ['react-native-web'],
    },
  },
};

const imageLoaderConfiguration = {
  test: /\.(gif|jpe?g|png|svg)$/,
  use: {
    loader: 'url-loader',
    options: {
      name: '[name].[ext]',
      esModule: false,
    },
  },
};

module.exports = {
  entry: path.resolve(appDirectory, 'src', 'index.js'),
  output: {
    filename: 'bundle.web.js',
    path: path.resolve(appDirectory, 'build'),
  },
  devtool: 'source-map',
  module: {
    rules: [babelLoaderConfiguration, imageLoaderConfiguration],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(appDirectory, 'public', 'index.html'),
      filename: 'index.html',
    }),
    new Dotenv({
      path: path.resolve(appDirectory, '.env'),
      safe: true,
    }),
  ],
  resolve: {
    alias: {
      'react-native$': 'react-native-web',
    },
    extensions: ['.web.js', '.js'],
  },
};
1

There are 1 best solutions below

3
On

Try to use minus margin with useState in one component :

LIVE example - https://snack.expo.dev/8ZCuner6B

import React, { Component } from 'react';
import { Alert, Button, Text, TouchableOpacity, TextInput, View, StyleSheet } from 'react-native';


 const App = () => {

   const [animationStyle , setAnimationStyle] = React.useState({
    width: 200,
    fontFamily: 'Baskerville',
    fontSize: 20,
    height: 44,
    padding: 10,
    borderWidth: 1,
    borderColor: 'white',
    marginTop:30,
    marginVertical: 10,
    zIndex:2,
  })

  const frogotPasswordHandler = () => {
  let margin = 30;
  let step = 3.5
  const timerId = setInterval(() =>{
    if (margin > -50){
      margin = margin - step
      updateMarginStyle(margin);
    }
    if (margin < -50) {
      console.log("cleared")
      clearInterval(timerId)
    }
  } , 2);

  }

  const updateMarginStyle = (num) =>{
    setAnimationStyle({
    ...animationStyle,
    marginTop:num,
  })
  }

    return (
      <View style={styles.container}>
      <Text style={styles.titleText}>Hi, Welcome To</Text>
        <Text style={styles.titleText}>Momento</Text>
        <View    style={styles.textWrapper}>
        <TextInput
          keyboardType = 'email-address'
          onChangeText={(email) => this.setState({ email })}
          placeholder='email'
          placeholderTextColor = 'white'
          style={styles.input}
        />
        </View>
        <TextInput
          onChangeText={(password) => this.setState({ password })}
          placeholder={'password'}
          secureTextEntry={true}
          placeholderTextColor = 'white'
          style={animationStyle}
        />

        <TouchableOpacity
          style={styles.forgotButton}
          onPress={frogotPasswordHandler}
       >
        <Text style={styles.defaultText}>Forgot password</Text>
       </TouchableOpacity>
        

     
        <TouchableOpacity
          style={styles.button}
       >
         <Text style={styles.buttonText}> Sign Up / Login </Text>
       </TouchableOpacity>
        
      </View>
    );
  
}


export default App;



const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'salmon',
  },
  textWrapper:{
    backgroundColor:'green',
        zIndex:10,

  },
  titleText:{
    fontFamily: 'Baskerville',
    fontSize: 50,
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    alignItems: 'center',
    backgroundColor: 'powderblue',
    width: 200,
    height: 44,
    padding: 10,
    borderWidth: 1,
    borderColor: 'white',
    borderRadius: 25,
    marginBottom: 10,
  },
  buttonText:{
    fontFamily: 'Baskerville',
    fontSize: 20,
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {
    width: 200,
    fontFamily: 'Baskerville',
    fontSize: 20,
    height: 44,
    padding: 10,
    marginTop:20,
    borderWidth: 1,
    borderColor: 'white',
    marginVertical: 10,
  },
  defaultText:{
  },
  forgotButton:{
    backgroundColor:'gray',
    marginBottom:20,
    marginTop:10,
  }
});