├── .nvmrc ├── .gitignore ├── .npmignore ├── src ├── index.ts └── Push.tsx ├── example ├── showcase.gif ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash-icon.png │ └── adaptive-icon.png ├── tsconfig.json ├── index.ts ├── .gitignore ├── package.json ├── app.json └── App.tsx ├── tsconfig.json ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── LICENSE ├── package.json ├── README.md └── yarn.lock /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src 3 | /showcase 4 | /tsconfig.json -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { WithPushTransition, WithPushTransitionProps } from "./Push"; 2 | -------------------------------------------------------------------------------- /example/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/showcase.gif -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/splash-icon.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheLartians/react-native-simple-transition/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "lib": ["es6", "es2019"], 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "pretty": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "declaration": true, 13 | "jsx": "react", 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist"], 17 | "lib": ["es2015"] 18 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: install 18 | run: yarn install 19 | 20 | - name: build 21 | run: yarn build 22 | 23 | - name: check style 24 | run: yarn check:style 25 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | .kotlin/ 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~53.0.20", 13 | "expo-status-bar": "~2.2.3", 14 | "react": "19.0.0", 15 | "react-native": "0.79.5", 16 | "react-native-simple-transition": "../" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.25.2", 20 | "@types/react": "~19.0.10", 21 | "typescript": "~5.8.3" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Publish new commits to npm. See https://github.com/mikeal/merge-release/blob/master/README.md for more info. 2 | 3 | name: Publish 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: install 17 | run: yarn install 18 | 19 | - name: build 20 | run: yarn build 21 | 22 | - name: Publish 23 | if: github.ref == 'refs/heads/master' 24 | uses: Github-Actions-Community/merge-release@main 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 28 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | }, 23 | "edgeToEdgeEnabled": true 24 | }, 25 | "web": { 26 | "favicon": "./assets/favicon.png" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lars Melchior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-simple-transition", 3 | "version": "0.1.0", 4 | "description": "A small component that automatically transitions child changes", 5 | "author": "Lars Melchior", 6 | "license": "MIT", 7 | "types": "dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/TheLartians/react-native-simple-transition" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "main": "dist/index.js", 16 | "devDependencies": { 17 | "@types/react-native": "^0.63.8", 18 | "prettier": "^2.0.5", 19 | "typescript": "^3.9.7" 20 | }, 21 | "scripts": { 22 | "build": "tsc", 23 | "watch": "tsc --watch", 24 | "prepublish": "npm run build", 25 | "check:style": "prettier --check \"src/**/*[!.d].ts\" \"src/**/*.tsx\"", 26 | "fix:style": "prettier --check \"src/**/*.ts\" \"src/**/*.tsx\" --write", 27 | "example": "yarn --cwd example" 28 | }, 29 | "keywords": [ 30 | "android", 31 | "animation", 32 | "fade", 33 | "ios", 34 | "mobile", 35 | "react-native", 36 | "react-native-component", 37 | "transition", 38 | "ui" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/TheLartians/react-native-simple-transition/workflows/Build/badge.svg) 2 | [![npm version](https://badge.fury.io/js/react-native-simple-transition.svg)](https://badge.fury.io/js/react-native-simple-transition) 3 | 4 | # react-native-simple-transition 5 | 6 | A minimalist and easy to use transition component for React Native. 7 | 8 | ![animated example](./example/showcase.gif) 9 | 10 | ## Usage 11 | 12 | Install the library 13 | 14 | ```bash 15 | yarn add react-native-simple-transition 16 | ``` 17 | 18 | Create a transition component and add the content as a child. 19 | New components will transition with an animation every time the content key changes. 20 | 21 | ```tsx 22 | import { WithPushTransition } from 'react-native-simple-transition'; 23 | 24 | const MyComponent = () => { 25 | const [count, setCount] = useState(0); 26 | 27 | return ( 28 | 29 | setCount(count+1)}> 30 | This component will smoothly transition on key changes. 31 | 32 | 33 | ) 34 | } 35 | ``` 36 | 37 | Currently the only transition component is `WithPushTransition`. 38 | More are planned to be added soon. 39 | 40 | ## WithPushTransition 41 | 42 | ### Optional properties 43 | 44 | - `contentKey`: alternative to updating the child's `key` property 45 | - `duration`: transition duration in milliseconds 46 | - `style`: the style given to the transition component 47 | - `easing`: an [easing function](https://reactnative.dev/docs/easing) for the transition 48 | - `direction`: the direction of the transition; can be `"left"`, `"right"`, `"up"` or `"down"` 49 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/prop-types@*": 6 | version "15.7.3" 7 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" 8 | integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== 9 | 10 | "@types/react-native@^0.63.8": 11 | version "0.63.8" 12 | resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.8.tgz#73ec087122c64c309eeaf150b565b8d755f0fb1f" 13 | integrity sha512-QRwGFRTyGafRVTUS+0GYyJrlpmS3boyBaFI0ULSc+mh/lQNxrzbdQvoL2k5X7+t9hxyqA4dTQAlP6l0ir/fNJQ== 14 | dependencies: 15 | "@types/react" "*" 16 | 17 | "@types/react@*": 18 | version "16.9.46" 19 | resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e" 20 | integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg== 21 | dependencies: 22 | "@types/prop-types" "*" 23 | csstype "^3.0.2" 24 | 25 | csstype@^3.0.2: 26 | version "3.0.2" 27 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7" 28 | integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw== 29 | 30 | prettier@^2.0.5: 31 | version "2.0.5" 32 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" 33 | integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== 34 | 35 | typescript@^3.9.7: 36 | version "3.9.7" 37 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" 38 | integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== 39 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | SafeAreaView, 4 | StyleSheet, 5 | Text, 6 | TouchableOpacity, 7 | View, 8 | } from 'react-native'; 9 | import {WithPushTransition} from 'react-native-simple-transition'; 10 | 11 | const App = () => { 12 | const [count, setCount] = useState(0); 13 | const direction = (['up', 'down', 'left', 'right'] as const)[count % 4]; 14 | 15 | return ( 16 | 17 | 22 | 27 | {count}: {direction} 28 | 29 | 30 | setCount(count + 1)}> 33 | Next 34 | 35 | 36 | ); 37 | }; 38 | 39 | const styles = StyleSheet.create({ 40 | appContainer: { 41 | flex: 1, 42 | justifyContent: 'center', 43 | backgroundColor: 'white', 44 | }, 45 | buttonContainer: { 46 | backgroundColor: 'blue', 47 | paddingVertical: 10, 48 | paddingHorizontal: 50, 49 | borderRadius: 100, 50 | justifyContent: 'center', 51 | marginTop: 0, 52 | margin: 10, 53 | }, 54 | buttonLabel: { 55 | color: 'white', 56 | fontWeight: 'bold', 57 | fontSize: 20, 58 | textAlign: 'center', 59 | }, 60 | innerWindow: { 61 | flex: 1, 62 | borderRadius: 20, 63 | overflow: 'hidden', 64 | margin: 10, 65 | }, 66 | innerContainer: { 67 | width: '100%', 68 | height: '100%', 69 | alignItems: 'center', 70 | justifyContent: 'center', 71 | }, 72 | innerLabel: {color: 'white', fontSize: 20, fontWeight: 'bold'}, 73 | }); 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /src/Push.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo, ReactNode } from "react"; 2 | import { 3 | Animated, 4 | View, 5 | Easing, 6 | ViewStyle, 7 | Text, 8 | EasingFunction, 9 | } from "react-native"; 10 | 11 | export type WithPushTransitionProps = { 12 | children: React.ReactNode; 13 | /** A unique key for the currently displayed content. When changing content update this key to trigger a transition. */ 14 | contentKey: string | number; 15 | /** The animation duration. */ 16 | duration?: number; 17 | /** The style for the container View of the content */ 18 | style?: ViewStyle; 19 | /** The easing function for the transition animation. */ 20 | easing?: EasingFunction; 21 | /** The direction for the push animation. */ 22 | direction?: "left" | "right" | "up" | "down"; 23 | }; 24 | 25 | function createTransform( 26 | value: Animated.Value, 27 | idx: number, 28 | width: number, 29 | height: number, 30 | direction: WithPushTransitionProps["direction"] 31 | ) { 32 | switch (direction) { 33 | case "right": 34 | return { 35 | translateX: value.interpolate({ 36 | inputRange: [0, 1], 37 | outputRange: [width * (idx - 1), width * idx], 38 | }), 39 | }; 40 | default: 41 | case "left": 42 | return { 43 | translateX: value.interpolate({ 44 | inputRange: [0, 1], 45 | outputRange: [width * (1 - idx), -width * idx], 46 | }), 47 | }; 48 | case "up": 49 | return { 50 | translateY: value.interpolate({ 51 | inputRange: [0, 1], 52 | outputRange: [height * (1 - idx), -height * idx], 53 | }), 54 | }; 55 | case "down": 56 | return { 57 | translateY: value.interpolate({ 58 | inputRange: [0, 1], 59 | outputRange: [height * (idx - 1), height * idx], 60 | }), 61 | }; 62 | } 63 | } 64 | 65 | export const WithPushTransition = ({ 66 | contentKey, 67 | children, 68 | duration, 69 | easing, 70 | style, 71 | direction = "left", 72 | }: WithPushTransitionProps): ReactNode => { 73 | const [width, setWidth] = useState(0); 74 | const [height, setHeight] = useState(0); 75 | 76 | const [current, setCurrent] = useState(children); 77 | const [currentKey, setCurrentKey] = useState(contentKey); 78 | const [previous, setPrevious] = useState(); 79 | 80 | const animatedValue = useMemo(() => new Animated.Value(1), []); 81 | 82 | const animation = useMemo( 83 | () => 84 | Animated.timing(animatedValue, { 85 | toValue: 1, 86 | duration: duration ?? 1000, 87 | easing: easing ?? Easing.inOut(Easing.ease), 88 | useNativeDriver: true, 89 | }), 90 | [animatedValue, duration, easing] 91 | ); 92 | 93 | useEffect(() => { 94 | if (currentKey !== contentKey) { 95 | // add the new children to the current views 96 | setPrevious(current); 97 | setCurrentKey(contentKey); 98 | setCurrent(children); 99 | 100 | animation.stop(); 101 | requestAnimationFrame(() => { 102 | animatedValue.setValue(0); 103 | requestAnimationFrame(() => { 104 | animation.start(() => { 105 | // finish the animation by removing the previous children. 106 | setPrevious(undefined); 107 | }); 108 | }); 109 | }); 110 | } else { 111 | // the key is identical so current view component needs to be updated 112 | // the animation should continue running 113 | setCurrent(children); 114 | } 115 | }, [currentKey, children]); 116 | 117 | return ( 118 | { 121 | setWidth(event.nativeEvent.layout.width); 122 | setHeight(event.nativeEvent.layout.height); 123 | }} 124 | > 125 | {previous && ( 126 | 136 | {previous} 137 | 138 | )} 139 | 149 | {current} 150 | 151 | 152 | ); 153 | }; 154 | --------------------------------------------------------------------------------