├── .npmrc
├── hooks
├── useColorScheme.ts
├── useColorScheme.web.ts
├── useGradualAnimation.ts
└── useThemeColor.ts
├── bun.lockb
├── assets
├── images
│ ├── icon.png
│ ├── favicon.png
│ ├── splash.png
│ ├── react-logo.png
│ ├── adaptive-icon.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ └── partial-react-logo.png
└── fonts
│ └── SpaceMono-Regular.ttf
├── expo-env.d.ts
├── babel.config.js
├── tsconfig.json
├── components
├── __tests__
│ ├── ThemedText-test.tsx
│ └── __snapshots__
│ │ └── ThemedText-test.tsx.snap
├── navigation
│ └── TabBarIcon.tsx
├── ThemedView.tsx
├── ExternalLink.tsx
├── HelloWave.tsx
├── Collapsible.tsx
├── ThemedText.tsx
├── MessageItem.tsx
└── ParallaxScrollView.tsx
├── .gitignore
├── constants
└── Colors.ts
├── app
├── +not-found.tsx
├── index.tsx
├── _layout.tsx
├── +html.tsx
├── advanced.tsx
├── advanced-toolbar.tsx
├── view-avoiding.tsx
└── basic.tsx
├── app.json
├── package.json
├── README.md
├── scripts
└── reset-project.js
└── messages.ts
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/bun.lockb
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/expo-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // NOTE: This file should not be edited and should be in your git ignore
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/keyboard-guide/HEAD/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { ThemedText } from '../ThemedText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | /ios
14 | /android
15 |
16 | # macOS
17 | .DS_Store
18 |
19 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
20 | # The following patterns were generated by expo-cli
21 |
22 | expo-env.d.ts
23 | # @end expo-cli
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2 |
3 | import Ionicons from '@expo/vector-icons/Ionicons';
4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet';
5 | import { type ComponentProps } from 'react';
6 |
7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 | return 'light';
8 | }
9 |
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/hooks/useGradualAnimation.ts:
--------------------------------------------------------------------------------
1 | import { useKeyboardHandler } from "react-native-keyboard-controller";
2 | import { useSharedValue } from "react-native-reanimated";
3 |
4 | const OFFSET = 42;
5 |
6 | export const useGradualAnimation = () => {
7 | const totalOffset = OFFSET;
8 |
9 | const height = useSharedValue(totalOffset);
10 |
11 | useKeyboardHandler(
12 | {
13 | onMove: (e) => {
14 | "worklet";
15 | height.value =
16 | e.height > 0 ? Math.max(e.height + OFFSET, totalOffset) : totalOffset;
17 | },
18 | },
19 | []
20 | );
21 | return { height };
22 | };
23 |
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { useColorScheme } from 'react-native';
7 |
8 | import { Colors } from '@/constants/Colors';
9 |
10 | export function useThemeColor(
11 | props: { light?: string; dark?: string },
12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
13 | ) {
14 | const theme = useColorScheme() ?? 'light';
15 | const colorFromProps = props[theme];
16 |
17 | if (colorFromProps) {
18 | return colorFromProps;
19 | } else {
20 | return Colors[theme][colorName];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { openBrowserAsync } from 'expo-web-browser';
3 | import { type ComponentProps } from 'react';
4 | import { Platform } from 'react-native';
5 |
6 | type Props = Omit, 'href'> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== 'web') {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: '#ECEDEE',
20 | background: '#151718',
21 | tint: tintColorDark,
22 | icon: '#9BA1A6',
23 | tabIconDefault: '#9BA1A6',
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from 'expo-router';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import Animated, {
3 | useSharedValue,
4 | useAnimatedStyle,
5 | withTiming,
6 | withRepeat,
7 | withSequence,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedText } from '@/components/ThemedText';
11 |
12 | export function HelloWave() {
13 | const rotationAnimation = useSharedValue(0);
14 |
15 | rotationAnimation.value = withRepeat(
16 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
17 | 4 // Run the animation 4 times
18 | );
19 |
20 | const animatedStyle = useAnimatedStyle(() => ({
21 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
22 | }));
23 |
24 | return (
25 |
26 | 👋
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | text: {
33 | fontSize: 28,
34 | lineHeight: 32,
35 | marginTop: -6,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "keyboard-handling",
4 | "slug": "keyboard-handling",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "splash": {
12 | "image": "./assets/images/splash.png",
13 | "resizeMode": "contain",
14 | "backgroundColor": "#ffffff"
15 | },
16 | "ios": {
17 | "supportsTablet": true,
18 | "bundleIdentifier": "dev.expo.keyboard.guide"
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/images/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | },
25 | "softwareKeyboardLayoutMode": "pan",
26 | "package": "dev.expo.keyboard.guide"
27 | },
28 | "web": {
29 | "bundler": "metro",
30 | "output": "static",
31 | "favicon": "./assets/images/favicon.png"
32 | },
33 | "plugins": ["expo-router", "expo-font", "expo-web-browser"],
34 | "experiments": {
35 | "typedRoutes": true
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, SafeAreaView, Button } from "react-native";
2 |
3 | import { Link } from "expo-router";
4 |
5 | export default function IndexScreen() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | const styles = StyleSheet.create({
28 | container: {
29 | flex: 1,
30 | alignItems: "center",
31 | justifyContent: "center",
32 | gap: 16,
33 | },
34 | listStyle: {
35 | padding: 16,
36 | gap: 16,
37 | },
38 | textInput: {
39 | width: "95%",
40 | height: 45,
41 | borderWidth: 1,
42 | borderRadius: 8,
43 | borderColor: "#d8d8d8",
44 | backgroundColor: "#fff",
45 | padding: 8,
46 | alignSelf: "center",
47 | marginBottom: 8,
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from '@expo/vector-icons/Ionicons';
2 | import { PropsWithChildren, useState } from 'react';
3 | import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
4 |
5 | import { ThemedText } from '@/components/ThemedText';
6 | import { ThemedView } from '@/components/ThemedView';
7 | import { Colors } from '@/constants/Colors';
8 |
9 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
10 | const [isOpen, setIsOpen] = useState(false);
11 | const theme = useColorScheme() ?? 'light';
12 |
13 | return (
14 |
15 | setIsOpen((value) => !value)}
18 | activeOpacity={0.8}>
19 |
24 | {title}
25 |
26 | {isOpen && {children}}
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | heading: {
33 | flexDirection: 'row',
34 | alignItems: 'center',
35 | gap: 6,
36 | },
37 | content: {
38 | marginTop: 6,
39 | marginLeft: 24,
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from "@react-navigation/native";
6 | import { useFonts } from "expo-font";
7 | import { Stack } from "expo-router";
8 | import * as SplashScreen from "expo-splash-screen";
9 | import { useEffect } from "react";
10 | import "react-native-reanimated";
11 |
12 | import { useColorScheme } from "@/hooks/useColorScheme";
13 | import { KeyboardProvider } from "react-native-keyboard-controller";
14 |
15 | // Prevent the splash screen from auto-hiding before asset loading is complete.
16 | SplashScreen.preventAutoHideAsync();
17 |
18 | export default function RootLayout() {
19 | const colorScheme = useColorScheme();
20 | const [loaded] = useFonts({
21 | SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
22 | });
23 |
24 | useEffect(() => {
25 | if (loaded) {
26 | SplashScreen.hideAsync();
27 | }
28 | }, [loaded]);
29 |
30 | if (!loaded) {
31 | return null;
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = 'default',
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: '600',
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: 'bold',
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: 'bold',
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: '#0a7ea4',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 | import { type PropsWithChildren } from 'react';
3 |
4 | /**
5 | * This file is web-only and used to configure the root HTML for every web page during static rendering.
6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
7 | */
8 | export default function Root({ children }: PropsWithChildren) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | {/*
17 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
18 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
19 | */}
20 |
21 |
22 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
23 |
24 | {/* Add any additional elements that you want globally available on web... */}
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | const responsiveBackground = `
32 | body {
33 | background-color: #fff;
34 | }
35 | @media (prefers-color-scheme: dark) {
36 | body {
37 | background-color: #000;
38 | }
39 | }`;
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keyboard-handling",
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 run:android",
9 | "ios": "expo run: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.1.0",
19 | "@react-navigation/native": "^7.1.9",
20 | "expo": "^53.0.9",
21 | "expo-constants": "~17.1.6",
22 | "expo-font": "~13.3.1",
23 | "expo-linking": "~7.1.5",
24 | "expo-router": "~5.0.7",
25 | "expo-splash-screen": "~0.30.8",
26 | "expo-status-bar": "~2.2.3",
27 | "expo-system-ui": "~5.0.7",
28 | "expo-web-browser": "~14.1.6",
29 | "react": "19.0.0",
30 | "react-dom": "19.0.0",
31 | "react-native": "0.79.2",
32 | "react-native-gesture-handler": "~2.24.0",
33 | "react-native-keyboard-controller": "^1.17.1",
34 | "react-native-reanimated": "~3.17.4",
35 | "react-native-safe-area-context": "5.4.0",
36 | "react-native-screens": "~4.10.0",
37 | "react-native-web": "^0.20.0"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "^7.20.0",
41 | "@types/jest": "^29.5.12",
42 | "@types/react": "~19.0.10",
43 | "@types/react-test-renderer": "^18.0.7",
44 | "jest": "^29.2.1",
45 | "jest-expo": "~53.0.5",
46 | "react-test-renderer": "18.2.0",
47 | "typescript": "~5.8.3"
48 | },
49 | "private": true
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/MessageItem.tsx:
--------------------------------------------------------------------------------
1 | import { BetoPhoto, KeithPhoto, Message } from "@/messages";
2 | import {
3 | Image,
4 | StyleSheet,
5 | Text,
6 | TextStyle,
7 | View,
8 | ViewStyle,
9 | } from "react-native";
10 |
11 | export default function MessageItem({ message }: { message: Message }) {
12 | const photoSrc = message.author === "Keith" ? KeithPhoto : BetoPhoto;
13 | const author = message.author === "Keith" ? "Keith" : "Beto";
14 |
15 | const containerStyle: ViewStyle = {
16 | gap: 8,
17 | maxWidth: "80%",
18 | flexDirection: "row",
19 | alignItems: "center",
20 | alignSelf: message.author === "Keith" ? "flex-start" : "flex-end",
21 | };
22 |
23 | const messageContainerStyle: ViewStyle = {
24 | backgroundColor: message.author === "Keith" ? "#218aff" : "#d8d8d8",
25 | };
26 |
27 | const textStyle: TextStyle = {
28 | color: message.author === "Keith" ? "#fff" : "#000",
29 | };
30 |
31 | return (
32 |
33 | {author === "Keith" && (
34 |
35 |
36 | {author}
37 |
38 | )}
39 |
45 | {message.image && (
46 |
55 | )}
56 | {message.message}
57 |
58 |
59 | );
60 | }
61 |
62 | const styles = StyleSheet.create({
63 | messageContainer: {
64 | borderRadius: 8,
65 | padding: 8,
66 | },
67 | photo: {
68 | width: 40,
69 | height: 40,
70 | borderRadius: 20,
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from 'react';
2 | import { StyleSheet, useColorScheme } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedView } from '@/components/ThemedView';
11 |
12 | const HEADER_HEIGHT = 250;
13 |
14 | type Props = PropsWithChildren<{
15 | headerImage: ReactElement;
16 | headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 |
19 | export default function ParallaxScrollView({
20 | children,
21 | headerImage,
22 | headerBackgroundColor,
23 | }: Props) {
24 | const colorScheme = useColorScheme() ?? 'light';
25 | const scrollRef = useAnimatedRef();
26 | const scrollOffset = useScrollViewOffset(scrollRef);
27 |
28 | const headerAnimatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [
31 | {
32 | translateY: interpolate(
33 | scrollOffset.value,
34 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
36 | ),
37 | },
38 | {
39 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
40 | },
41 | ],
42 | };
43 | });
44 |
45 | return (
46 |
47 |
48 |
54 | {headerImage}
55 |
56 | {children}
57 |
58 |
59 | );
60 | }
61 |
62 | const styles = StyleSheet.create({
63 | container: {
64 | flex: 1,
65 | },
66 | header: {
67 | height: 250,
68 | overflow: 'hidden',
69 | },
70 | content: {
71 | flex: 1,
72 | padding: 32,
73 | gap: 16,
74 | overflow: 'hidden',
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/app/advanced.tsx:
--------------------------------------------------------------------------------
1 | import MessageItem from "@/components/MessageItem";
2 | import { messages } from "@/messages";
3 | import {
4 | StyleSheet,
5 | Platform,
6 | FlatList,
7 | View,
8 | StatusBar,
9 | TextInput,
10 | } from "react-native";
11 | import { useKeyboardHandler } from "react-native-keyboard-controller";
12 | import Animated, {
13 | useAnimatedStyle,
14 | useSharedValue,
15 | } from "react-native-reanimated";
16 |
17 | const PADDING_BOTTOM = 20;
18 |
19 | const useGradualAnimation = () => {
20 | const height = useSharedValue(PADDING_BOTTOM);
21 |
22 | useKeyboardHandler(
23 | {
24 | onMove: (e) => {
25 | "worklet";
26 | // set height to min 10
27 | height.value = Math.max(e.height, PADDING_BOTTOM);
28 | },
29 | onEnd: (e) => {
30 | "worklet";
31 | height.value = e.height;
32 | },
33 | },
34 | []
35 | );
36 | return { height };
37 | };
38 |
39 | export default function TabTwoScreen() {
40 | const { height } = useGradualAnimation();
41 |
42 | const fakeView = useAnimatedStyle(() => {
43 | return {
44 | height: Math.abs(height.value),
45 | marginBottom: height.value > 0 ? 0 : PADDING_BOTTOM,
46 | };
47 | }, []);
48 |
49 | return (
50 |
51 | }
54 | keyExtractor={(item) => item.createdAt.toString()}
55 | contentContainerStyle={styles.listStyle}
56 | keyboardDismissMode="on-drag"
57 | inverted
58 | />
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | const styles = StyleSheet.create({
66 | container: {
67 | flex: 1,
68 | paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0,
69 | },
70 | listStyle: {
71 | padding: 16,
72 | gap: 16,
73 | },
74 | textInput: {
75 | width: "95%",
76 | height: 45,
77 | borderWidth: 1,
78 | borderRadius: 8,
79 | borderColor: "#d8d8d8",
80 | backgroundColor: "#fff",
81 | padding: 8,
82 | alignSelf: "center",
83 | marginBottom: 8,
84 | },
85 | });
86 |
--------------------------------------------------------------------------------
/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require('fs');
10 | const path = require('path');
11 |
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, 'app');
14 | const newDirPath = path.join(root, 'app-example');
15 | const newAppDirPath = path.join(root, 'app');
16 |
17 | const indexContent = `import { Text, View } from "react-native";
18 |
19 | export default function Index() {
20 | return (
21 |
28 | Edit app/index.tsx to edit this screen.
29 |
30 | );
31 | }
32 | `;
33 |
34 | const layoutContent = `import { Stack } from "expo-router";
35 |
36 | export default function RootLayout() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | `;
44 |
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 | if (error) {
47 | return console.error(`Error renaming directory: ${error}`);
48 | }
49 | console.log('/app moved to /app-example.');
50 |
51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 | if (error) {
53 | return console.error(`Error creating new app directory: ${error}`);
54 | }
55 | console.log('New /app directory created.');
56 |
57 | const indexPath = path.join(newAppDirPath, 'index.tsx');
58 | fs.writeFile(indexPath, indexContent, (error) => {
59 | if (error) {
60 | return console.error(`Error creating index.tsx: ${error}`);
61 | }
62 | console.log('app/index.tsx created.');
63 |
64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx');
65 | fs.writeFile(layoutPath, layoutContent, (error) => {
66 | if (error) {
67 | return console.error(`Error creating _layout.tsx: ${error}`);
68 | }
69 | console.log('app/_layout.tsx created.');
70 | });
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/app/advanced-toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, TextInput, View } from "react-native";
2 | import {
3 | KeyboardAwareScrollView,
4 | KeyboardToolbar,
5 | } from "react-native-keyboard-controller";
6 |
7 | export default function AdvancedToolbar() {
8 | return (
9 | <>
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | gap: 16,
47 | padding: 16,
48 | },
49 | listStyle: {
50 | padding: 16,
51 | gap: 16,
52 | },
53 | textInput: {
54 | width: "auto",
55 | flexGrow: 1,
56 | flexShrink: 1,
57 | height: 45,
58 | borderWidth: 1,
59 | borderRadius: 8,
60 | borderColor: "#d8d8d8",
61 | backgroundColor: "#fff",
62 | padding: 8,
63 | marginBottom: 8,
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/app/view-avoiding.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import { useGradualAnimation } from "@/hooks/useGradualAnimation";
3 | import {
4 | Button,
5 | Keyboard,
6 | StyleSheet,
7 | Text,
8 | TextInput,
9 | useColorScheme,
10 | View,
11 | } from "react-native";
12 | import { KeyboardToolbar } from "react-native-keyboard-controller";
13 | import Animated, { useAnimatedStyle } from "react-native-reanimated";
14 | import { Stack } from "expo-router";
15 | import { Colors } from "@/constants/Colors";
16 |
17 | export default function Schedule() {
18 | const { height } = useGradualAnimation();
19 | const colorScheme = useColorScheme();
20 | const isDark = colorScheme === "dark";
21 | const inputRef = useRef(null);
22 |
23 | const keyboardPadding = useAnimatedStyle(() => {
24 | return {
25 | height: height.value,
26 | };
27 | }, []);
28 |
29 | const toggleKeyboard = () => {
30 | if (Keyboard.isVisible()) {
31 | Keyboard.dismiss();
32 | } else {
33 | inputRef.current?.focus();
34 | }
35 | };
36 |
37 | return (
38 | <>
39 | (
42 |
43 | ),
44 | }}
45 | />
46 |
47 |
66 |
67 |
74 | {/* Footer or something */}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | This is a toolbar}
85 | showArrows={false}
86 | insets={{ left: 16, right: 0 }}
87 | doneText="Close keyboard"
88 | />
89 | >
90 | );
91 | }
92 |
93 | function Item({ name }: { name: string }) {
94 | return (
95 |
106 | {name}
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/app/basic.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | StyleSheet,
3 | Platform,
4 | FlatList,
5 | SafeAreaView,
6 | StatusBar,
7 | TextInput,
8 | Keyboard,
9 | KeyboardAvoidingView,
10 | KeyboardEventListener,
11 | Pressable,
12 | View,
13 | } from "react-native";
14 |
15 | import { messages } from "@/messages";
16 | import MessageItem from "@/components/MessageItem";
17 | import { useEffect, useRef } from "react";
18 | import { Ionicons } from "@expo/vector-icons";
19 |
20 | export default function HomeScreen() {
21 | const textInputRef = useRef(null);
22 |
23 | useEffect(() => {
24 | const showSubscription = Keyboard.addListener(
25 | "keyboardDidShow",
26 | handleKeyboardShow
27 | );
28 | const hideSubscription = Keyboard.addListener(
29 | "keyboardDidHide",
30 | handleKeyboardHide
31 | );
32 |
33 | return () => {
34 | showSubscription.remove();
35 | hideSubscription.remove();
36 | };
37 | }, []);
38 |
39 | // useEffect(() => {
40 | // // focus input on mount
41 | // textInputRef.current?.focus();
42 | // }, []);
43 |
44 | const handleKeyboardShow: KeyboardEventListener = (event) => {
45 | console.log("keyboard show", event.duration);
46 | };
47 |
48 | const handleKeyboardHide: KeyboardEventListener = (event) => {
49 | console.log("keyboard hide", event.duration);
50 | };
51 |
52 | return (
53 |
54 |
58 | }
61 | keyExtractor={(item) => item.createdAt.toString()}
62 | contentContainerStyle={styles.listStyle}
63 | keyboardDismissMode="on-drag"
64 | keyboardShouldPersistTaps="always"
65 | />
66 |
75 | Keyboard.dismiss()}>
76 |
77 |
78 | textInputRef.current?.focus()}>
79 |
80 |
81 |
82 |
87 |
88 |
89 | );
90 | }
91 |
92 | const styles = StyleSheet.create({
93 | container: {
94 | flex: 1,
95 | paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0,
96 | },
97 | listStyle: {
98 | padding: 16,
99 | gap: 16,
100 | },
101 | textInput: {
102 | width: "95%",
103 | height: 45,
104 | borderWidth: 1,
105 | borderRadius: 8,
106 | borderColor: "#d8d8d8",
107 | backgroundColor: "#fff",
108 | padding: 8,
109 | alignSelf: "center",
110 | marginBottom: 8,
111 | },
112 | });
113 |
--------------------------------------------------------------------------------
/messages.ts:
--------------------------------------------------------------------------------
1 | export interface Message {
2 | author: string;
3 | message: string;
4 | image: string | null;
5 | createdAt: Date;
6 | }
7 |
8 | export const KeithPhoto = "https://avatars.githubusercontent.com/u/8053974?v=4";
9 | export const BetoPhoto = "https://avatars.githubusercontent.com/u/43630417?v=4";
10 |
11 | export const messages: Message[] = [
12 | {
13 | author: "Keith",
14 | message:
15 | "Hey Beto, have you tried using Expo for React Native development?",
16 | image: null,
17 | createdAt: new Date("2024-08-08T10:00:00Z"),
18 | },
19 | {
20 | author: "Beto",
21 | message: "Yeah, I started using it last month. It's amazing!",
22 | image: null,
23 | createdAt: new Date("2024-08-08T10:02:00Z"),
24 | },
25 | {
26 | author: "Keith",
27 | message:
28 | "I know, right? The setup process is so smooth. No more dealing with native build tools.",
29 | image: null,
30 | createdAt: new Date("2024-08-08T10:04:00Z"),
31 | },
32 | {
33 | author: "Beto",
34 | message:
35 | "Absolutely! And the hot reloading feature is a game-changer. Look at this demo:",
36 | image:
37 | "https://as2.ftcdn.net/v2/jpg/05/34/48/37/1000_F_534483775_2hBgOxryd3El6t3tKOtbcM95Yq3OmTGG.jpg",
38 | createdAt: new Date("2024-08-08T10:07:00Z"),
39 | },
40 | {
41 | author: "Keith",
42 | message:
43 | "That's awesome! I love how Expo handles all the heavy lifting with notifications and updates too.",
44 | image: null,
45 | createdAt: new Date("2024-08-08T10:10:00Z"),
46 | },
47 | {
48 | author: "Beto",
49 | message:
50 | "Definitely. And have you tried Expo Go? Testing on physical devices has never been easier.",
51 | image: null,
52 | createdAt: new Date("2024-08-08T10:12:00Z"),
53 | },
54 | {
55 | author: "Keith",
56 | message:
57 | "Oh yeah, it's fantastic. No need to mess with Apple developer accounts just to test on iOS.",
58 | image: null,
59 | createdAt: new Date("2024-08-08T10:15:00Z"),
60 | },
61 | {
62 | author: "Beto",
63 | message:
64 | "Exactly! And look at this chart showing how much time I've saved since switching to Expo:",
65 | image: null,
66 | createdAt: new Date("2024-08-08T10:18:00Z"),
67 | },
68 | {
69 | author: "Keith",
70 | message:
71 | "Those are impressive numbers! The Expo SDK is so comprehensive too. It covers almost everything I need.",
72 | image: null,
73 | createdAt: new Date("2024-08-08T10:21:00Z"),
74 | },
75 | {
76 | author: "Beto",
77 | message:
78 | "True, and when you do need to add custom native code, EAS makes it straightforward.",
79 | image: null,
80 | createdAt: new Date("2024-08-08T10:24:00Z"),
81 | },
82 | {
83 | author: "Keith",
84 | message:
85 | "Expo has really transformed my React Native workflow. I can't imagine going back to the old way of doing things.",
86 | image: null,
87 | createdAt: new Date("2024-08-08T10:27:00Z"),
88 | },
89 | {
90 | author: "Beto",
91 | message:
92 | "Same here! It's made development so much more enjoyable. Cheers to Expo! 🎉",
93 | image:
94 | "https://cdn.prod.website-files.com/5e740d74e6787687577e9b38/63978bf83d789b4602145daf_maximizing-efficiency-and-productivity-with-expo-dev-tools-2.png",
95 | createdAt: new Date("2024-08-08T10:30:00Z"),
96 | },
97 | ];
98 |
--------------------------------------------------------------------------------