React Native 基础入门之路由

作者:user 发布日期:2025年11月3日 22:23 浏览量:106

1、安装配置 Expo Router

  • 安装 Expo Router
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
  • 设置路由入口
    打开 package.json,将 main 的值修改成:
{
    "main": "expo-router/entry"
}
  • 清理默认入口文件
    在创建空白项目时,项目的入口文件是根目录的 index.js,现在由Expo Router路由来接管,所以在根目录下的 index.js 和 App.js 两个文件就可以直接删掉了。
  • 新建目录
    在根目录里,新建一个 app 目录,再在该目录下新建一个 index.js 文件。完成上述package.json 配置 main 之后,项目的入口会自动变成 app/index.js 文件。
// app/index.js
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>
        欢迎来到 Expo Router 课程!
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 50,
    width: 200,
    fontWeight: 'bold',
    color: '#e29447',
  }
});
  • 运行项目
npx expo start

// 或者 
npm run start
  • Android 报错
    使用 Android 设备的,屏幕底部可能会出现报错信息,此错误没有任何影响,后面的介绍中会解决该报错。
Invalid prop 'style' supplied to 'React.Fragment...'
  • 修改深度链接标识
    可以打开根目录的 app.json 文件,在里面增加上 scheme:
{
  "expo": {
    "name": "dk-bms-app",
    "slug": "dk-bms-app",
    "scheme": "bms",
    "version": "1.0.0",
    // ...
  }
}

这个配置叫做深度链接,这么配置之后,将来可以通过以下地址,从别的应用或者浏览器,直接跳转到该App对应的页面来。

bms://user
  • 项目路由文件结构
app
    index.js
    details.js
  • 使用 Link 组件跳转
    首先需要从 expo-router 引入 Link 组件;
    跳转的路径,就在 href 里写 /details;
    路径都是相对于 app 目录来写的,也就是说 app 目录就是 /。
    下面这行返回,路径写的是../,它的意思就是返回上一级。
// app/index.js
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Link } from 'expo-router';

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是首页</Text>

      <Link href="/details" style={styles.link}>
        跳转到详情页Link
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 40,
    fontWeight: 'bold',
    color: '#e29447',
  },
  link: {
    marginTop: 20,
    fontSize: 20,
    color: '#1f99b0',
  },
  buttonText: {
    marginTop: 20,
    fontSize: 20,
    color: '#ff7f6f',
  },
});
// app/details.js
import { StyleSheet, Text, View } from 'react-native';
import { Link } from "expo-router";

export default function Details() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是详情页</Text>

      <Link href="../" style={styles.backText}>
        返回
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 40,
    fontWeight: 'bold',
    color: '#4f9df7',
  },
  backText: {
    marginTop: 20,
    fontSize: 20,
    color: '#67c1b5',
  }
});
  • Link 组件的 asChild
    很多时候,需要跳转的组件比较复杂。例如要在里面嵌套按钮或者其他东西。这种情况下就可以在 Link 组件里加上 asChild:
export default function Index() {
  return (
    <View style={styles.container}>
            // ...

      <Link href="/details" asChild>
        <TouchableOpacity>
          <Text style={styles.buttonText}>
            跳转到详情页(Link + asChild)
          </Text>
        </TouchableOpacity>
      </Link>
    </View>
  );
}

3、布局文件、Stack 与 Slot

在上述使用 Link 组件来跳转,点击链接之后,页面之间的切换没有任何动画效果,体验不太友好。另外,平常使用的那些App,页面都有个头部,里面有页面的标题之类的东西。

  • 新建 _layout.js 文件
    app 目录里,新建一个 _layout.js 文件,这是项目的布局文件。这个文件名称是固定的,前面必须有一个下划线
    布局的意思是,所有的页面都归它管理,而且是最先运行的文件,我们可以在里面做各种配置。

  • Stack 页面堆栈
    expo-router 里,引用了一个叫做 Stack 的东西。Stack 是用于管理应用中的页面堆栈的。
    在布局文件里加上Stack后,所有页面都会被Stack管理。进入新页面会从右侧 推入(Push),返回时 弹出(Pop) 页面,形成后进先出的这种结构。

// app/_layout.js
import { Stack } from 'expo-router';

export default function Layout() {
  return <Stack />;
}
  • iOS与Android效果不一致
    在使用 Stack后,iOS 中添加顶部自动出现了导航头(Header),上面的标题显示的是 index。跳转页面之后,详情页左上角也有了返回按钮,点击后,也能返回到首页。
    在Android设备,它顶部的标题,显示到最左边了,而不是中间。切换页面,感觉没有什么动画效果。因此继续加点配置,修改 app/_layout.js 文件,加上这两行:
import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerTitleAlign: 'center',     // 安卓标题栏居中
        animation: 'slide_from_right',  // 安卓使用左右切屏
      }}
    />
  );
}
  • Slot 插槽
    布局文件里,还有一种常见的东西,叫做插槽。如果大家用过Vue,那对这个Slot一定不陌生了。它其实就是一个占位符,各个页面,都会渲染在它里面。大家修改_layout.js文件:
import { StyleSheet, Text } from 'react-native';
import { Slot } from 'expo-router';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';

export default function Layout() {
  return (
    <SafeAreaProvider>
      <SafeAreaView style={styles.container}>
        <Text style={styles.header}>头部信息</Text>
        <Slot/>
        <Text style={styles.footer}>© Changle Weiyang Inc.</Text>
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    fontSize: 24,
    textAlign: 'center'
  },
  footer: {
    fontSize: 18,
    textAlign: 'center'
  }
});

4、router 方法跳转

除了使用 Link 组件进行页面跳转外,很多时候也会使用 JS 代码实现跳转。
使用 router 之前,需要引入并声明定义

// ...
import { useRouter } from 'expo-router';

export default function Index() {
  const router = useRouter();

  // ...
}

4-1、router.navigate 跳转

最常用的方法就是 router.navigate,在代码里加上一个按钮。
这种写法,与 Link组件的方式没有任何区别。它会将详情页加入Stack之中,进入详情页后,也是可以返回首页的。

export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      // ...

      <TouchableOpacity onPress={() => router.navigate('/details')}>
        <Text style={styles.buttonText}>
          跳转(navigate         </Text>
      </TouchableOpacity>
    </View>
  );
}  
  • 再次 router.navigate
    如果在详情页 details.js 再次用router.navigate往详情页跳,点击之后会发现没有变化,这就说明,如果details页面已经在Stack里了,使用router.navigate是不能再次跳转的。
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useRouter } from 'expo-router';

export default function Details() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是详情页</Text>

      <TouchableOpacity onPress={() => router.navigate('/details')}>
        <Text style={styles.buttonText}>
          再次跳转navigate
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 40,
    fontWeight: 'bold',
    color: '#4f9df7',
  },
  buttonText: {
    marginTop: 20,
    fontSize: 25,
    color: '#ff7f6f',
  },
  backText: {
    marginTop: 20,
    fontSize: 25,
    color: '#67c1b5',
  }
});

4-2、router.replace 跳转

replace 是替换的意思。用这种方式跳转,Stack里的所有页面都会被它替换掉。现在Stack里就只剩下详情页一个页面了,所以也就不能返回了。
那么现在我们只能按 r 键,刷新App,才能回到首页了。

export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
            // ...

      <TouchableOpacity onPress={() => router.replace('/details')}>
        <Text style={styles.buttonText}>
          替换(replace)
        </Text>
      </TouchableOpacity>
    </View>
  );
}

4-3、router.push 强制推入

使用router.push,哪怕页面已经在Stack中了,也会继续强制将页面推入到Stack中。

export default function Details() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      // ...

      <TouchableOpacity onPress={() => router.push('/details')}>
        <Text style={styles.buttonText}>
          强制推入(push)
        </Text>
      </TouchableOpacity>
    </View>
  );
}

4-4、router.back 返回

使用代码也可以实现返回操作,方法很简单,加上router.back,点击后,就可以一层一层的返回。

export default function Details() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      // ...

      <TouchableOpacity onPress={() => router.back()}>
        <Text style={styles.backText}>
          返回(back)
        </Text>
      </TouchableOpacity>
    </View>
  );
}

5、动态路由与参数传递

5-1、动态路由

  • 项目文件结构
app
    _layout.js
    index.js
    courses
        [id].js
  • 动态路由文件
    如上目录结构所示,对于用方括号包裹的文件名,就是动态路由文件。

5-2、接收参数

使用 useLocalSearchParams,可以从里面获取到传递过来的id。将id参数获取到后,显示到页面上。

import { View, Text, StyleSheet } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Course() {
  const { id } = useLocalSearchParams();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是课程页</Text>

      <Text style={styles.info}>课程ID: {id}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 40,
    fontWeight: 'bold',
    color: '#4f9df7',
  },
  info: {
    marginTop: 20,
    fontSize: 20,
    color: '#67c1b5',
  },
  buttonText: {
    marginTop: 20,
    fontSize: 20,
    color: '#ff7f6f',
  },
});
  • 普通传参
    直接在路由后面拼接参数
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Link, useRouter } from 'expo-router';

export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是首页</Text>

      <Link style={styles.link} href="/courses/1">
        跳转传参Link
      </Link>
    </View>
  );
}
  • Link 使用 params
    href,可以拆分成pathname和params。
    pathname,就要把路径写全,包括后面的[id]。
    params,就是要传递的参数了。我们这次传个2过去。
export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      // ...

      <Link
        style={styles.link}
        href={{
          pathname: '/courses/[id]',
          params: { id: 2 }
        }}>
        跳转传参Link 使用 params
      </Link>
    </View>
  );
}

5-4、router.navigate 参数传递

export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      // ...

      <TouchableOpacity onPress={() => router.navigate('/courses/3')}>
        <Text style={styles.buttonText}>
          跳转传参navigate 
        </Text>
      </TouchableOpacity>

      <TouchableOpacity onPress={() => router.navigate({
        pathname: '/courses/[id]',
        params: { id: 4 }
      })}>
        <Text style={styles.buttonText}>
          跳转传参navigate 使用 params 
        </Text>
      </TouchableOpacity>
    </View>
  );
}

6、设置导航栏标题

使用了Stack之后,就自动出现了顶部的这个导航栏。它里面的内容,也都是可以配置的。

6-1、设置顶部标题

  • 首先引入 Stack
import { Link, useRouter, Stack } from 'expo-router';
  • 然后在组件中添加 Stack.screen,里面的 options 就可以设置各种配置了。这里设置 title 为 首页
// app/index.js
export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: '首页' }}/>

      // ...
    </View>
  );
}

6-2、使用路由参数设置标题

可以在传参时将标题作为参数一起传递,然后在页面中获取传递的参数并设置。

export default function Index() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      //...

      <Link style={styles.link} href="/courses/1?title=Node.js">
        跳转传参Link
      </Link>

      <Link
        style={styles.link}
        href={{
          pathname: '/courses/[id]',
          params: { id: 2, title: 'React Native' }
        }}>
        跳转传参Link 使用 params
      </Link>

      <TouchableOpacity onPress={() => router.navigate('/courses/3?title=Vue.js')}>
        <Text style={styles.buttonText}>
          跳转传参navigate 
        </Text>
      </TouchableOpacity>

      <TouchableOpacity onPress={() => router.navigate({
        pathname: '/courses/[id]',
        params: { id: 4, title: '微信小程序' }
      })}>
        <Text style={styles.buttonText}>
          跳转传参navigate 使用 params 
        </Text>
      </TouchableOpacity>
    </View>
  );
}
export default function Course() {
  const { id, title } = useLocalSearchParams();

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: title }}/>
      // ...

    </View>
  );
}

6-3、使用 navigate 设置标题

有些时候,title设置了又想要修改它,可以通过navigation来实现

export default function Course() {
  const navigation = useNavigation();
  // ...

  return (
    <View style={styles.container}>
      // ...

      <TouchableOpacity onPress={() => navigation.setOptions({ title: '课程太好了!' })}>
        <Text style={styles.buttonText}>修改标题</Text>
      </TouchableOpacity>
    </View>
  );
}

6-4、导航栏设置样式

在options里,除了title以外,还加上了这些样式。

<Stack.Screen
  options={{
    title: '首页',
    headerStyle: {                  // 导航栏整体样式
      backgroundColor: '#e29447'
    },
    headerTintColor: '#fff',        // 导航栏中文字、按钮、图标的颜色
    headerTitleStyle: {             // 导航栏标题样式
      fontWeight: 'bold',
    },
  }}
/>

7、使用布局文件配置导航栏

7-1、公共样式

打开app/_layout.js,在screenOptions里加上以下配置,就可以看出布局文件里的screenOptions,是各个页面的公共配置。

import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerTitleAlign: 'center',     // 安卓标题栏居中
        animation: 'slide_from_right',  // 安卓使用左右切屏
        headerStyle: {                  // 导航栏整体样式
          backgroundColor: '#e29447'
        },
        headerTintColor: '#fff',        // 导航栏中文字按钮图标的颜色
        headerTitleStyle: {             // 导航栏标题样式
          fontWeight: 'bold',
        },
      }}
    />
  );
}

7-2、布局文件页面标题设置

先将原页面文件的Stack.Screen删掉,文件顶部的引用也删掉。

// 原页面文件的Stack.Screen
// <Stack.Screen options={{ title: '首页' }}/>

// 文件顶部的引用
// import { Stack } from 'expo-router';

然后在布局文件 _layout.js 的 Stack 中加上以下代码:

export default function Layout() {
  return (
    <Stack
      // ...
    >
      <Stack.Screen name="index" options={{ title: '首页' }} />
    </Stack>
  );
}

这里要加一个name,name的值就和首页文件的路径对应。
因为首页文件和布局文件在同一目录下,所以直接写index就行,不用写其他路径。
最底下要改为 \<\/Stack>

对于需要传递参数的,也可以按照如下方式进行修改:

import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      // ...
    >
            // ...

      <Stack.Screen
        name="courses/[id]"
        options={({ route }) => ({
          title: route.params?.title || '详情页', // 使用 params 中的 title如果没有则显示默认值
        })}
      />
    </Stack>
  );
}

name 还是要对应课程页文件所在的路径。这里因为和布局文件不在同一层,所以要补全路径 courses/。
接着设置标题,要先写个options,也就是详情页的设置了。里面写个route,这样就可以使用router.params?.title获取到路由参数里的标题了。如果没有传标题过来,就默认设置成详情页。

8、使用自定义组件配置导航栏

  • 自定义一个Logo组件
  • 找到首页的 Stack.Screen,将title 修改为headerTitle
import { Stack } from 'expo-router';
import { Image } from 'expo-image';
import { StyleSheet } from 'react-native';

export default function Layout() {
    return (
        <Stack
            screenOptions={{
                headerTitleAlign: 'center',     // 安卓标题栏居中
                animation: 'slide_from_right',  // 安卓使用左右切屏
                headerTintColor: '#1f99b0',     // 导航栏中文字按钮图标的颜色
                // 标题组件的样式
                headerTitleStyle: {
                    fontWeight: '400',
                    color: '#2A2929',
                    fontSize: 16,
                },
            }}
        >

            <Stack.Screen name='index' options={{
                headerTitle: props => <LogoTitle {...props} />
            }} />
        </Stack>
    );
}

/**
 * 导航栏Logo
 */
function LogoTitle() {
    return <Image style={style.logo} containFit="contain" source={require('../assets/favicon.png')} />
}

const style = StyleSheet.create({
    logo: {
        width: 30,
        height: 30,
    },
});

8-2、自定义左侧组件

定义导航栏左侧按钮就是在 Stack.screen 的 options 增加一个 headerLeft 配置。

/**
 * 导航栏左侧组件
 */
function HeaderLeft() {
  return (
    <Link asChild href="/articles" style={style.headerLeft}>
      <TouchableOpacity>
        <SimpleLineIcons size={20} color="#1f99b0" name="bell"/>
      </TouchableOpacity>
    </Link>
  );
}

8-3、自定义右侧组件

定义导航栏右侧组件就是在 Stack.screen 的 options 增加一个 headerRight 配置。
如果想配置多个,就在组件中定义多个。

/**
 * 导航栏右侧组件
 */
function HeaderRight() {
  return (
    <>
      <Link href="/search"  style={style.headerRight} asChild>
        <TouchableOpacity>
          <SimpleLineIcons size={20} color="#1f99b0" name="magnifier"/>
        </TouchableOpacity>
      </Link>

      <Link href="/settings"  style={style.headerRight} asChild>
        <TouchableOpacity>
          <SimpleLineIcons size={20} color="#1f99b0" name="options"/>
        </TouchableOpacity>
      </Link>
    </>
  )
}

8-4、封装顶部按钮组件

  • 封装组件
/**
 * 导航栏按钮组件
 * @param props
 */
function HeaderButton(props) {
  const { name, ...rest } = props;

  return (
    <Link asChild {...rest} >
      <TouchableOpacity>
        <SimpleLineIcons size={20} color="#1f99b0" name={name} />
      </TouchableOpacity>
    </Link>
  );
}
  • 使用示例
<Stack.Screen
  name="index"
  options={{
    headerTitle: props => <LogoTitle {...props} />,
    headerLeft: () => <HeaderButton name="bell" href="/articles" style={style.headerLeft} />,
    headerRight: () => (
      <>
        <HeaderButton name="magnifier" href="/search" style={style.headerRight} />
        <HeaderButton name="options" href="/settings" style={style.headerRight} />
      </>
    ),
  }}
/>

8-5、配置默认标题、返回按钮提示文字

因为没有给当前页面设置title,所以中间的标题,显示的是路径:articles/index。iOS左侧的返回按钮,在箭头后面跟了个index。而在Android上,返回按钮后面又没有这个index。

这两个地方的默认显示,都让人感觉比较奇怪。我们添加点配置,打开app/_layout.js,找到最外层的screenOptions:

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        title: '',                      // 默认标题为空
        //...
        headerBackButtonDisplayMode: 'minimal', // 设置返回按钮只显示箭头,不显示文字
      }}
    >
      //...
    </Stack>
  );
}

将title设置成空,这样如果当前页面没有设置标题,就什么都不要显示。下面,增加了一个headerBackButtonDisplayMode属性。这样设置后,导航栏的返回按钮就只显示箭头,不会自己带上提示文字了。

9、TabBar 的标准配置方法

9-1、目录结构

  • 在app目录里新建一个 (tabs) 文件夹。注意了,名字上有一对英文的小括号,千万别打成中文的了。
  • 里面新建一个_layout.js布局文件,这里就专门放TabBar配置
  • 然后将index.js,挪动到(tabs)里面。注意是挪动,而不是复制。确保现在app目录里,没有index.js了,只在(tabs)目录中有。
  • 在(tabs)里,再新建一个videos.js和users.js文件。

9-2、路由分组

这个 (tabs) 目录,就是专门用来放各个Tab页的。

现在这样做,就是有三个Tab页,一个首页,一个视频课程页,一个用户页。

(tabs)目录,名字上的这个小括号,在Expo Router里是有特殊意义的,它叫做路由分组:
- 利用它,将一些相关的文件,组织在一起。
- 特别要注意,这种带小括号的目录名,在URL里不计算路径,你要当它不存在!
index.js的URL,依然还是/index,就像还在app目录里一样,它依然还是首页。
videos.js文件的URL,其实是/videos,而不是/(tabs)/videos。
同样的道理,users.js文件的URL,是/users。

9-3、修改 app/_layout.js

打开app/_layout.js。Stack 增加了 TabBar 的配置。
- 注意这里有个headerShown,配置成了false。这是因为TabBar也会自带一个导航栏。如果不隐藏,它会和Stack的导航栏同时出现,这就会出来两个导航栏了。
- 底下给各个页面都添加上了title。
- 注意注释这里有个Cards,在Stack里页面是有两种形式的:
跳转时,这种页面左右滑动切换的就叫Cards
另一种页面从屏幕底部弹出的,叫做模态(Modal)

import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        title: '',                      // 默认标题为空
        headerTitleAlign: 'center',     // 安卓标题栏居中
        animation: 'slide_from_right',  // 安卓使用左右切屏
        headerTintColor: '#1f99b0',     // 导航栏中文字按钮图标的颜色
        // 标题组件的样式
        headerTitleStyle: {
          fontWeight: '400',
          color: '#2A2929',
          fontSize: 16,
        },
        headerBackButtonDisplayMode: 'minimal', // 设置返回按钮只显示箭头不显示文字
      }}
    >
      {/* Tabs */}
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />

      {/* Cards */}
      <Stack.Screen name="articles/index" options={{ title: '通知' }} />
      <Stack.Screen name="settings/index" options={{ title: '设置' }} />
      <Stack.Screen name="courses/[id]" options={{ title: '课程详情' }} />
      <Stack.Screen name="search/index" options={{ title: '搜索' }} />
    </Stack>
  );
}

9-4、修改 app/(tabs)/_layout.js

  • 和app/_layout.js中的不同,app中布局文件里使用的是Stack、Stack.Screen。这里的 app/(tabs)/_layout.js 要用Tabs和Tabs.Screen
    然后将顶部的按钮,配置到了最外层的screenOptions里,这样所有的Tab页在导航栏上,都会有Logo和按钮。
    底下就给三个页面,简单的设置下标题好了。首页就叫做发现,另外两个就设置成视频课程和我的。
import { Tabs, Link } from 'expo-router';
import { Image } from 'expo-image';
import { SimpleLineIcons } from '@expo/vector-icons';
import { StyleSheet, TouchableOpacity } from 'react-native';

/**
 * 导航栏 Logo 组件
 */
function LogoTitle() {
  return <Image style={style.logo} contentFit="contain" source={require('../../assets/logo-light.png')}/>;
}

/**
 * 导航栏按钮组件
 * @param props
 */
function HeaderButton(props) {
  const { name, ...rest } = props;

  return (
    <Link asChild {...rest} >
      <TouchableOpacity>
        <SimpleLineIcons size={20} color="#1f99b0" name={name} />
      </TouchableOpacity>
    </Link>
  );
}

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        headerTitleAlign: 'center',       // 安卓标题栏居中
        headerTitle: props => <LogoTitle {...props} />,
        headerLeft: () => <HeaderButton name="bell" href="/articles" style={style.headerLeft} />,
        headerRight: () => (
          <>
            <HeaderButton name="magnifier" href="/search" style={style.headerRight} />
            <HeaderButton name="options" href="/settings" style={style.headerRight} />
          </>
        ),
      }}
    >
      <Tabs.Screen
        name="index"
        options={{ title: '发现' }}
      />
      <Tabs.Screen
        name="videos"
        options={{ title: '视频课程' }}
      />
      <Tabs.Screen
        name="users"
        options={{ title: '我的' }}
      />
    </Tabs>
  );
}

const style = StyleSheet.create({
  logo: {
    width: 130,
    height: 30,
  },
  headerLeft: {
    marginLeft: 15,
  },
  headerRight: {
    marginRight: 15,
  }
});

9-5、配置 TabBar 的图标

默认TabBar上没有图标,显示了一个倒三角形。需要加个配置,打开app/(tabs)/_layout.js,增加一个TabBar图标组件:

/**
 * TabBar 图标组件
 * @param props
 */
function TabBarIcon(props) {
  return <SimpleLineIcons size={25} {...props} />;
}

然后要将这个图标组件,配置到每一个Tabs.Screen里:

export default function TabLayout() {
  return (
    <Tabs
      //...
    >
      <Tabs.Screen
        name="index"
        options={{
          title: '发现',
          tabBarIcon: ({ color }) => <TabBarIcon name="compass" color={color} />,
        }}
      />
      <Tabs.Screen
        name="videos"
        options={{
          title: '视频课程',
          tabBarIcon: ({ color }) => <TabBarIcon name="camrecorder" color={color} />,
        }}
      />
      <Tabs.Screen
        name="users"
        options={{
          title: '我的',
          tabBarIcon: ({ color }) => <TabBarIcon name="user" color={color} />,
        }}
      />
    </Tabs>
  );
}

9-6、配置 TabBar 的样式

默认是显示了个蓝色的图标,不是很好看。继续在screenOptions里加上颜色配置:

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        //...
        tabBarActiveTintColor: '#1f99b0', // 设置 TabBar 选中项的颜色
      }}
    >
      //...
    </Tabs>
  );
}

9-7、Android 水波纹问题

在Android上,点击Tabbar后,发现出现了奇怪的水波纹特效,这个效果是Android系统自己搞出来的,无论在模拟器还是真机上都存在。
对于这种效果,我们也可以通过配置去掉它。继续加上这一段tabBarButton的配置:

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        //...
        // Android 取消水波纹效果
        tabBarButton: (props) => (
          <TouchableOpacity
            {...props}
            activeOpacity={1}
            style={[props.style, { backgroundColor: 'transparent' }]}
          />
        ),
      }}
    >
      //...
    </Tabs>
  );
}

10、配置模态页(Modal)

在App里,经常还会看到一种从下向上弹出的页面,这个就叫做模态页

10-1、新增模态页

在 app 目录下新增一个目录和文件,例如teachers/[id].js,里面就放一些基础信息:

import { View, Text, StyleSheet } from 'react-native';

export default function Teacher() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>这里是老师详情页!</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 40,
    fontWeight: 'bold',
    color: '#e29447',
  },
});

10-2、配置模态页

  • 接着打开app/_layout.js,里面增加个模态页的配置。
  • 与之前其他的配置一样,但是增加了presentation属性,并设置成了modal。
export default function Layout() {
  return (
    <Stack
      //...
    >
      //...

      {/* Modal */}
      <Stack.Screen
        name="teachers/[id]"
        options={{
          presentation: 'modal',
          title: '老师详情',
        }}
      />

      //...
    </Stack>
  );
}

10-3、打开模态页

  • 再打开项目的首页app/(tabs)/index.js,我们增加一个链接,点击后弹出模态页。
  • 可以明显的看到,从下向上弹出的动画效果。
  • 而且可以看到模态页,是压在首页的上面的。
export default function Index() {
  return (
    <View style={styles.container}>
      //...

      <Link style={styles.link} href="/teachers/1">
        打开教师页(Modal)
      </Link>
    </View>
  );
}

10-4、模态页的关闭按钮

自定义一个关闭按钮好了。打开app/_layout.js文件,增加一个关闭按钮组件:

import { Stack, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { TouchableOpacity, View } from 'react-native';

/**
 * 模态页关闭按钮组件
 */
function CloseButton() {
  const router = useRouter();

  return (
    <View style={{ width: 30 }}>
      <TouchableOpacity onPress={() => router.dismiss()}>
        <MaterialCommunityIcons name="close" size={30} color="#1f99b0" />
      </TouchableOpacity>
    </View>
  );
}
  • 然后自定义一个关闭按钮组件,点击后,用router.dismiss关闭模态页
  • 这里其实用router.back也可以关闭模态页,但back的意思是返回。
  • 在一些包含多个屏幕的模态栈,使用back很容易出错。所以更推荐用dismiss,来专门来处理模态页的关闭。

然后将这个自定义关闭按钮,用headerLeft配置到模态页的左侧:

export default function Layout() {
  return (
    <Stack
      //...
    >
      //...

      {/* Modal */}
      <Stack.Screen
        name="teachers/[id]"
        options={{
          presentation: 'modal',
          title: '老师详情',
          headerLeft: () => <CloseButton />,
        }}
      />

      //...
    </Stack>
  );
}

10-5、全屏模态页

还有一种模态页,也是从下向上弹出,但是是全屏的。可以将模态配置这里改为fullScreenModal

<Stack.Screen
  name="modal"
  options={{
    presentation: 'fullScreenModal', // 全屏模态
  // ...
  }}
/>

10-6、Android 的模态页

用Andorid的同学,这样配置了以后,会发现并没有这种从下向上弹出的效果,依然还是左右切换的。
这是由于Andorid系统自身机制不同导致的,不过我们也可以加点配置,实现个类似的效果,配置里加上animation

<Stack.Screen
  name="modal"
  options={{
    presentation: 'modal',
    animation: 'slide_from_bottom', // 从底部滑入
  // ...
  }}
/>

在Android上弹出的模态页面只能全屏显示,无法实现和iOS一样的堆叠效果。

已经是最后一篇了!