├── .gitignore ├── README.md ├── app.json ├── app ├── _layout.tsx └── index.tsx ├── assets ├── fonts │ ├── Nunito.ttf │ └── Quicksand.ttf └── images │ ├── adaptive-icon.png │ ├── icon.png │ └── splash-icon.png ├── components └── PressableScale.tsx ├── constants └── anagramWords.ts ├── package-lock.json ├── package.json └── tsconfig.json /.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 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | app-example 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-text-transition", 4 | "slug": "expo-text-transition", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true 13 | }, 14 | "android": { 15 | "adaptiveIcon": { 16 | "foregroundImage": "./assets/images/adaptive-icon.png", 17 | "backgroundColor": "#ffffff" 18 | } 19 | }, 20 | "web": { 21 | "bundler": "metro", 22 | "output": "static", 23 | "favicon": "./assets/images/favicon.png" 24 | }, 25 | "plugins": [ 26 | "expo-router", 27 | [ 28 | "expo-splash-screen", 29 | { 30 | "image": "./assets/images/splash-icon.png", 31 | "imageWidth": 200, 32 | "resizeMode": "contain", 33 | "backgroundColor": "#ffffff" 34 | } 35 | ] 36 | ], 37 | "experiments": { 38 | "typedRoutes": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from "@react-navigation/native"; 6 | import { SplashScreen, Stack } from "expo-router"; 7 | import { StatusBar } from "expo-status-bar"; 8 | import { useEffect } from "react"; 9 | import { useColorScheme } from "react-native"; 10 | import { useFonts } from "expo-font"; 11 | import "react-native-reanimated"; 12 | 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | export default function RootLayout() { 16 | const colorScheme = useColorScheme(); 17 | const [loaded, error] = useFonts({ 18 | Quicksand: require("../assets/fonts/Quicksand.ttf"), 19 | Nunito: require("../assets/fonts/Nunito.ttf"), 20 | }); 21 | 22 | useEffect(() => { 23 | if (loaded || error) { 24 | SplashScreen.hideAsync(); 25 | } 26 | }, [loaded, error]); 27 | 28 | if (!loaded && !error) { 29 | return null; 30 | } 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { LogBox, StyleSheet, Text, View } from "react-native"; 2 | import React, { useState } from "react"; 3 | import Animated, { 4 | LayoutAnimationConfig, 5 | LinearTransition, 6 | withTiming, 7 | } from "react-native-reanimated"; 8 | import { useTheme } from "@react-navigation/native"; 9 | import { PressableScale } from "@/components/PressableScale"; 10 | import { anagramWords } from "@/constants/anagramWords"; 11 | 12 | LogBox.ignoreAllLogs(); 13 | 14 | const Exiting = () => { 15 | "worklet"; 16 | const animations = { 17 | opacity: withTiming(0, { duration: 250 }), 18 | transform: [{ scale: withTiming(0.5, { duration: 250 }) }], 19 | }; 20 | const initialValues = { 21 | opacity: 1, 22 | transform: [{ scale: 1 }], 23 | }; 24 | return { 25 | initialValues, 26 | animations, 27 | }; 28 | }; 29 | 30 | const Entering = () => { 31 | "worklet"; 32 | const animations = { 33 | opacity: withTiming(1, { duration: 250 }), 34 | transform: [{ scale: withTiming(1, { duration: 250 }) }], 35 | }; 36 | const initialValues = { 37 | opacity: 0, 38 | transform: [{ scale: 0.5 }], 39 | }; 40 | return { 41 | initialValues, 42 | animations, 43 | }; 44 | }; 45 | 46 | export default function () { 47 | const theme = useTheme(); 48 | const [value, setValue] = useState(anagramWords[0]); 49 | const valueArray = value.toString().split(""); 50 | 51 | return ( 52 | 53 | 54 | 58 | {valueArray.map((digit) => { 59 | return ( 60 | 66 | 67 | {digit} 68 | 69 | 70 | ); 71 | })} 72 | 73 | 74 | {anagramWords.map((word) => { 75 | const isActive = value === word; 76 | return ( 77 | setValue(word)} 81 | > 82 | 93 | {word} 94 | 95 | 96 | ); 97 | })} 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | const styles = StyleSheet.create({ 105 | container: { 106 | flex: 1, 107 | alignItems: "center", 108 | justifyContent: "center", 109 | }, 110 | value: { 111 | fontSize: 84, 112 | fontWeight: "bold", 113 | marginBottom: 20, 114 | fontFamily: "Nunito", 115 | }, 116 | buttonContainer: { 117 | flexDirection: "row", 118 | flexWrap: "wrap", 119 | alignItems: "center", 120 | justifyContent: "center", 121 | width: "90%", 122 | }, 123 | button: { 124 | padding: 15, 125 | paddingVertical: 8, 126 | borderRadius: 10, 127 | textAlign: "center", 128 | }, 129 | buttonText: { 130 | fontSize: 18, 131 | fontFamily: "Nunito", 132 | fontWeight: "600", 133 | }, 134 | }); 135 | -------------------------------------------------------------------------------- /assets/fonts/Nunito.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-text-transition/382945069abae6b74943b99ff162979f03e55b3e/assets/fonts/Nunito.ttf -------------------------------------------------------------------------------- /assets/fonts/Quicksand.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-text-transition/382945069abae6b74943b99ff162979f03e55b3e/assets/fonts/Quicksand.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-text-transition/382945069abae6b74943b99ff162979f03e55b3e/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-text-transition/382945069abae6b74943b99ff162979f03e55b3e/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunabhverma/expo-text-transition/382945069abae6b74943b99ff162979f03e55b3e/assets/images/splash-icon.png -------------------------------------------------------------------------------- /components/PressableScale.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import React from "react"; 3 | import type { PressableProps, StyleProp, ViewStyle } from "react-native"; 4 | import { Pressable } from "react-native"; 5 | import Animated, { 6 | cancelAnimation, 7 | runOnJS, 8 | useAnimatedStyle, 9 | useReducedMotion, 10 | useSharedValue, 11 | withTiming, 12 | } from "react-native-reanimated"; 13 | 14 | const DEFAULT_TARGET_SCALE = 0.95; 15 | 16 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable); 17 | 18 | export const PressableScale = ( 19 | params: { 20 | targetScale?: number; 21 | style?: StyleProp; 22 | } & Exclude 23 | ): ReactNode => { 24 | const { 25 | targetScale = DEFAULT_TARGET_SCALE, 26 | children, 27 | style, 28 | onPressIn, 29 | onPressOut, 30 | ...rest 31 | } = params; 32 | const reducedMotion = useReducedMotion(); 33 | 34 | const scale = useSharedValue(1); 35 | 36 | const animatedStyle = useAnimatedStyle(() => ({ 37 | transform: [{ scale: scale.value }], 38 | })); 39 | 40 | return ( 41 | { 44 | "worklet"; 45 | if (onPressIn) { 46 | runOnJS(onPressIn)(e); 47 | } 48 | cancelAnimation(scale); 49 | scale.value = withTiming(targetScale, { duration: 60 }); 50 | }} 51 | onPressOut={(e) => { 52 | "worklet"; 53 | if (onPressOut) { 54 | runOnJS(onPressOut)(e); 55 | } 56 | cancelAnimation(scale); 57 | scale.value = withTiming(1, { duration: 120 }); 58 | }} 59 | style={[!reducedMotion && animatedStyle, style]} 60 | {...rest} 61 | > 62 | {children} 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /constants/anagramWords.ts: -------------------------------------------------------------------------------- 1 | export const anagramWords = [ 2 | "curse", 3 | "tamers", 4 | "least", 5 | "desserts", 6 | "sisters", 7 | "stressed", 8 | "secure", 9 | "crate", 10 | "recuse", 11 | "react", 12 | "cater", 13 | "caret", 14 | "trace", 15 | "stales", 16 | "steals", 17 | "resists", 18 | "rescue", 19 | "master", 20 | "slates", 21 | "stream", 22 | ]; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-text-transition", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@expo/vector-icons": "^14.0.2", 19 | "@react-navigation/bottom-tabs": "^7.2.0", 20 | "@react-navigation/native": "^7.0.14", 21 | "expo": "~52.0.37", 22 | "expo-blur": "~14.0.3", 23 | "expo-constants": "~17.0.7", 24 | "expo-font": "~13.0.4", 25 | "expo-haptics": "~14.0.1", 26 | "expo-linking": "~7.0.5", 27 | "expo-router": "~4.0.17", 28 | "expo-splash-screen": "~0.29.22", 29 | "expo-status-bar": "~2.0.1", 30 | "expo-symbols": "~0.2.2", 31 | "expo-system-ui": "~4.0.8", 32 | "expo-web-browser": "~14.0.2", 33 | "react": "18.3.1", 34 | "react-dom": "18.3.1", 35 | "react-native": "0.76.7", 36 | "react-native-gesture-handler": "~2.20.2", 37 | "react-native-reanimated": "~3.16.1", 38 | "react-native-safe-area-context": "4.12.0", 39 | "react-native-screens": "~4.4.0", 40 | "react-native-web": "~0.19.13", 41 | "react-native-webview": "13.12.5", 42 | "@react-native-community/slider": "4.5.5" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.25.2", 46 | "@types/jest": "^29.5.12", 47 | "@types/react": "~18.3.12", 48 | "@types/react-test-renderer": "^18.3.0", 49 | "jest": "^29.2.1", 50 | "jest-expo": "~52.0.4", 51 | "react-test-renderer": "18.3.1", 52 | "typescript": "^5.3.3" 53 | }, 54 | "private": true, 55 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 56 | } 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------