React Native 入门指南: 构建 UI 的必备核心组件

引言

React Native 提供了许多内置的 核心组件, 相当于 Web 开发中的基础的 HTML 标签组件。 和 Web 开发类似后续开发中我们都是基于语言所提供的核心组件来绘制页面或者封装复杂的可复用的组件。

本文将按照 官方文档 的分类, 对 React Native 所提供的一些基础组件进行展开学习。

一、基本组件

用于构建 UI 的最基本的组件....

1.1 容器组件: View

可以对标为 HTML 中的 div 标签, View 是构建 UI 的最基本组件, 它就是一个容器, 支持带有 flexbox、样式、一些触摸处理和辅助功能控件的布局。视图直接映射到 React Native 运行的任何平台上的原生视图。

Viewdiv 一样, 可以有 0 到多个任何类型的子项

js 复制代码
import { View } from 'react-native';

export default function HomeScreen() {
  return (
    <View>
      <View/>
      <View/>
    </View>
  );
}

Viewdiv 一样, 默认情况下该组件独占一行(块级元素)

js 复制代码
import { View } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ padding: 100 }}>
      <View style={{ width: 50, height: 50, backgroundColor: 'red' }} />
      <View style={{ width: 50, height: 50, backgroundColor: 'blue' }} />
    </View>
  );
}

View 有且只支持 flex 布局, 直接设置相关属性即可, 无需手动设置为 flex 布局(display: flex;)

js 复制代码
import { View } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ height: 100, flexDirection: 'row', margin: 10, marginTop: 100 }}>
      <View style={{ backgroundColor: 'blue', flex: 0.2 }} />
      <View style={{ backgroundColor: 'red', flex: 0.4 }} />
    </View>
  );
}

View 所支持的 props 熟悉还是挺多的, 具体的可以查看 官方文档, 常用的就是绑定事件、以及设置属性

js 复制代码
import { View } from 'react-native';

export default function HomeScreen() {
  return (
    <View
      onTouchMove={() => { console.log(111) }}
      style={{ height: 100, width: 100, backgroundColor: 'blue' }}
    />
  );
}

1.2 文本组件: Text

不同于 Web 开发, 在 React Native 中所有文本都必须使用 Text 进行包裹, 否则是展示不出来的!

js 复制代码
import { View, Text } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ padding: 100 }}>
     <Text style={{ fontSize: 20 }}>可以正常展示</Text>
     不能展示
    </View>
  );
}

Text 组件同样支持设置样式、嵌套和触摸处理。

js 复制代码
import { View, Text } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ padding: 100 }} >
      <Text style={{ fontSize: 20 }} onPress={() => {console.log(1)}}>
        可以正常展示
        {'\n'}
        <Text>支持嵌套</Text>
      </Text>
    </View>
  );
}

对于嵌套的 Text 组件, 组件间的样式也是相互继承的

js 复制代码
import { View, Text } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ padding: 100 }} >
      <Text style={{ fontSize: 20, color: 'red' }}>
        可以正常展示
        {'\n'}
        <Text>支持嵌套</Text>
      </Text>
    </View>
  );
}

Text 组件不能使用 Flexbox 布局, Text 中的所有内容, 都是使用文本布局。这意味着 Text 内的元素不再是矩形, 而是在看到行尾时换行(内联)

js 复制代码
import { View, Text } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ margin: 100 }}>
      <Text style={{ flexDirection: 'column', fontSize: 20 }}>
        <Text>第一段文本</Text>
        <Text>第二段文本</Text>
      </Text>
    </View>
  );
}

样式继承问题: 在 Web 上, 为整个文档设置字体系列和大小的常用方法是利用继承的 CSS 属性, 为最外层元素我们可以设置通用的一些样式。如下代码, 为根元素设置了字体样式, 之后文档中的所有元素都将继承此样式, 除非它们或其父元素之一指定了新规则。

js 复制代码
html {
  font-family: 'lucida grande', tahoma, verdana, arial, sans-serif;
  font-size: 11px;
  color: #141823;
}

然而在 React Native 中, 我们要求更加严格: 你必须将所有文本节点包装在 Text 组件中。不能在 View 下直接拥有文本节点, 而且也有且只能为 Text 组件设置字体相关的样式。

所以这里我们是无法在 View 上为整个子树设置默认字体, 与此同时 fontFamily 也只接受单个字体名称, 这与 CSS 中的 font-family 不同。

所以在开发应用程序中要想为文字设置默认的、一致的字体样式, 推荐方法是封装一个通用的组件 MyAppText, 并在您的应用程序中使用此组件。当然我们还可以使用此组件创建更具体的组件, 例如用于其他类型的文本组件 MyAppHeaderText

js 复制代码
const MyAppText  = ({ children, fontSize = 20 }) => (
  <Text style={{ fontSize, fontFamily: 'tahoma' }}>
    {children}
  </Text> 
)

const MyAppHeaderText  = ({ children }) => (
  <MyAppText fontSize={30}>
    {children}
  </MyAppText> 
)

export default function HomeScreen() {
  return (
    <View style={{ margin: 50, marginTop: 100 }}>
      <Text >
        <MyAppHeaderText>MyAppHeaderText</MyAppHeaderText>
        {'\n'}
        <MyAppText>MyAppText</MyAppText>
      </Text>
    </View>
  );
}

当然 React Native 仍然具有样式继承的, 但仅限于文本子树。如下代码, 第二部分将同时为粗体、大号字体和红色。

js 复制代码
import { View, Text } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ margin: 50, marginTop: 100 }}>
      <Text style={{ fontWeight: 'bold', fontSize: 40 }}>
        I am bold
        <Text style={{ color: 'red' }}>and red</Text>
      </Text>
    </View>
  );
}

1.3 图片组件: Image

img 标签, 在 React Native 中如果我们需要展示图片资源, 则需要使用 Image 组件。

使用方法如下:

  1. 使用 source 来指定图片资源
  2. 注意: 对于网络和数据图像, 我们需要手动指定图像的尺寸, 否则不能展示
js 复制代码
import { View, Image } from 'react-native';
import logo from '@/assets/images/react-logo.png';

export default function HomeScreen() {
  return (
    <View style={{ margin: 50, marginTop: 100 }}>
      <Image source={logo} />
      <Image
        style={{ width: 50, height: 50 }}
        source={{
          uri: 'https://reactnative.dev/img/tiny_logo.png',
        }}
      />
      <Image
        style={{ width: 50, height: 50 }}
        source={{ uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==' }}
      />
    </View>
  );
}

1.4 文本输入组件: TextInput

inputReact Native 中可使用 TextInput 来处理用户的输入。

如下代码是一个最基本使用例子, 如果你使用过 React 相信应该是很容易理解的! 代码中使用了 TextInput 组件来获取用户的输入, 并订阅 onChangeText 事件以读取用户输入。

js 复制代码
import { useState } from 'react';
import { View, TextInput } from 'react-native';

export default function HomeScreen() {
  const [text, setText] = useState();
  return (
    <View style={{ margin: 50, marginTop: 100 }}>
      <TextInput
        style={{ borderWidth: 1, height: 40, padding: 10 }}
        onChangeText={(text) => {
          console.log(text)
          setText(text)
        }}
        value={text}
      />
    </View>
  );
}

对于多行文本, 在 Web 中我们可以使用 textarea, React Native 则沿用 TextInput 通过 multiline 属性来区分, 同时在 AndroidnumberOfLines 可限制文本区域输入框要展示的行数, 当然在 IOS 中则只能通过样式来实现, 比如: heightminHeightmaxHeight

js 复制代码
<TextInput
  multiline
  numberOfLines={4} // 针对 Android
  style={{ borderWidth: 1, padding: 10, height: 80 }}
/>

同样的, 在 React Native 也是可以使用 Ref 获取到原生组件实例, 在实例上同样是有 focusblur 等方法

补充: 如果在 IOS 模拟器中无法输入内容, 记得开启下如下配置:

1.5 滚动视图组件: ScrollView

不同于 WebReact Native 中, 常规的容器 View 是不支持滚动的

js 复制代码
import { View, Text, ScrollView } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ marginTop: 100, margin: 50, padding: 20, height: 200, backgroundColor: 'pink' }}>
      <Text style={{ fontSize: 30 }}>
        Keep in mind that ScrollViews must have a bounded height in order to work, since they contain unbounded-height children into a bounded container (via a scroll interaction). In order to bound the height of a ScrollView, either set the height of the view directly (discouraged) or make sure all parent views have bounded height. Forgetting to transfer down the view stack can lead to errors here, which the element inspector makes quick to debug.
      </Text>
    </View>
  );
}

React Native 中, 如果需要一个支持滚动的容器, 需要使用专门的组件 ScrollView

diff 复制代码
import { View, Text, ScrollView } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ marginTop: 100, margin: 50, padding: 20, height: 200, backgroundColor: 'pink' }}>
+     <ScrollView>
        <Text style={{ fontSize: 30 }}>
          Keep in mind that ScrollViews must have a bounded height in order to work, since they contain unbounded-height children into a bounded container (via a scroll interaction). In order to bound the height of a ScrollView, either set the height of the view directly (discouraged) or make sure all parent views have bounded height. Forgetting to transfer down the view stack can lead to errors here, which the element inspector makes quick to debug.
        </Text>
+     </ScrollView>
    </View>
  );
}

默认情况下 ScrollView 的高度是自适应的, 会根据父容器进行自适应! 如上代码所示, 我只是给父容器 View 设置了高度, ScrollView 是自动撑满父容器的。当然除此之外, 我们也是可以直接为 ScrollView 通过 Style 来限制高度的, 比如 heightmaxHeightminHeight

js 复制代码
<ScrollView style={{ height: 300 }}>
  {/* 内容 */}
</ScrollView>

<ScrollView style={{ minHeight: 300, maxHeight: 500 }}>
  {/* 内容 */}
</ScrollView>

ScrollViewWeb 中的 div 一样, 会一次渲染容器内部的所有子组件, 这样的话就会有一个性能缺点。想象一下, 假设我们有一个长列表, 全部展示的话可能有几百屏幕的内容。如果一次为所有内容创建组件和视图(可是其中大部分甚至可能未显示在用户界面中) 如此将导致渲染速度变慢和内存使用量增加。其实就是我们 Web 中常说的虚拟滚动场景, 对于 Web 段我们需要自己去实现虚拟滚动去弥补这个缺陷, 但是在 React Native 中是有提供基础的组件 FlatList, 可以帮我们实现虚拟滚动的效果的。当然这里我们不对 FlatList 进行展开, 我们后续会讲到。

1.6 样式表: StyleSheet

下面是几种常见样式写法:

  1. 内联, 直接在组件内部进行编码
js 复制代码
<View style={{ height: 100, width: 100, backgroundColor: 'red' }}>
</View>
  1. 组件外部引用, 将对象抽离到组件外部
js 复制代码
const style = { 
  box: {
    height: 100, 
    width: 100, 
    backgroundColor: 'red',
  }
}

export default function HomeScreen() {
  return (
    <View style={style.box}></View>
  );
}
  1. 使用官方提供的 StyleSheet
js 复制代码
const style = StyleSheet.create({ 
  box: {
    height: 100, 
    width: 100, 
    backgroundColor: 'red',
  }
})

export default function HomeScreen() {
  return (
    <View style={style.box}></View>
  );
}

那么上面几种方式又有什么区别呢?

  1. 首先说下内联写法, 这种写法其实在 Web 开发中也是经常被用到的, 其最主要的缺点就是, 内联写法导致每次组件重新渲染时, 都会创建新的样式对象, 可能导致不必要的性能开销; 当然好处就是可以根据 state 或者 props 进行动态计算样式
  2. 第二种方式只是做了简单的抽离, 只是相对内联的写法稍微好点, 缺点就是无法根据 state 或者 props 进行动态计算样式, 同时在编写样式时 TS 的作用就没了, 没法进行智能提示
  3. 第三种方式则是推荐的写法, 该写法 React Native 做了很多优化: StyleSheet.create 会在 JS 端进行预编译样式, 并分配一个 ID, 而 React Native原生端 其实是引用这些 ID, 而不是解析去 JS 对象(因为已经在声明时预编译好了), 同时这样的话也方便进行一些样式的拆分、复用。可以减少 JS 线程的计算量, 提高渲染效率; 如下代码所示 styles.box 只会在应用初始化时创建一次, 然后 React Native 在内部存储这个样式, 并分配一个 ID(例如 1) 来引用它, 而不是每次都解析整个对象。
js 复制代码
const style = StyleSheet.create({ 
  box: {
    height: 100, 
    width: 100, 
    backgroundColor: 'red',
  }
})

StyleSheet.create 有效优点:

  1. 通过将 样式JSX分离, 可以使代码更易于理解、同时也更方便样式复用
  2. 在大多数 IDE 中, 使用 StyleSheet.create() 将提供静态类型检查和建议, 以帮助您编写有效的样式

StyleSheet.compose(style1, style2): 样式复用(合并)样式, 可将 style2style1 中的样式进行合并, 如果有相同的样式规则, 则 style2 中的样式将会覆盖 style1 中的样式

js 复制代码
const page = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
  },
  text: { },
});

const lists = StyleSheet.create({
  listContainer: {
    flex: 1,
    backgroundColor: '#61dafb',
  },
  listItem: {},
});

const style = StyleSheet.compose(page.container, lists.listContainer);

为了做到更好的性能同时避免样式被修改, 保证样式的一致性, 提高安全性, 默认情况下 StyleSheet.createStyleSheet.compose 生成的样式规则对象, 是一个冻结对象(immutable), 也就是说我们是无法直接操作其中的样式规则的。

js 复制代码
const page = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
  },
  text: { },
});

const lists = StyleSheet.create({
  listContainer: {
    flex: 1,
    backgroundColor: '#61dafb',
  },
  listItem: {},
});

const style = StyleSheet.compose(page.container, lists.listContainer);

console.log(Object.isFrozen(style?.[0])) // true
console.log(Object.isFrozen(page.container)) // true

而你如果想要拿到一个普通的样式规则, 然后基于此进行操作的话。可以使用 StyleSheet.flatten 来实现

js 复制代码
const page = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
  },
  text: { },
});

const lists = StyleSheet.create({
  listContainer: {
    flex: 1,
    backgroundColor: '#61dafb',
  },
  listItem: {},
});

const style = StyleSheet.flatten([page.container, lists.listContainer]);

console.log(style); // { flex: 1, padding: 24, backgroundColor: '#61dafb' }
console.log(Object.isFrozen(style)); // false

二、用户界面

一些可供用户交互的组件...

2.1 按钮组件: Button

不同于 WebReact Native 中是无法直接为 View 容器组件绑定按压(点击)事件的

React Native 提供了一个最基本的按钮组件, 支持最低程度的自定义。如下代码所示, 则是最基本的一个实现, 其中 titleonPress 是最要属性:

  • title: 按钮要显示的文本内容
  • onPress: 按压事件回调函数
js 复制代码
import { View, Button } from 'react-native';

export default function HomeScreen() {
  return (
    <View style={{ margin: 100 }}>
      <Button
        title="Learn More"
        onPress={() => console.log('按下了')}
      />
    </View>
  );
}

官方提供的按钮组件, 只能实现一个最基本的按钮样式。如果需要定制更复杂的样式, 或者需要一整个容器都允许接收按压事件的话。则需要使用 Pressable 来进行构建自己的按钮。

js 复制代码
// 最基本
<Pressable onPress={() => {}}>
  <Text>I'm pressable!</Text>
</Pressable>


// 允许包裹任何内容
<Pressable onPress={() => console.log('按下了')}>
  <View style={{ width: 100, height: 100, backgroundColor: 'pink', marginBottom: 50 }} >
  </View>
  <Text>我也是可以按压的</Text>
</Pressable>

丰富的事件, 可实现更友好的交互效果:

  • 当用户按压时会先触发 onPressIn 事件
  • 当用户按压行为停止时会触发 onPressOut 事件
  • 在上面逻辑不变的情况下, 如果用户按压时间超过 500 毫秒, 还会触发 onLongPress 事件

来看一个复杂的交互: 按压时, 调整容器的样式, 按压结束则恢复

js 复制代码
import { useState } from 'react';
import { View, Text, Pressable } from 'react-native';

export default function HomeScreen() {
  const [isPressing, setIsPressing] = useState(false)

  return (
    <View style={{ margin: 100 }}>
      <Pressable 
        onPressIn={setIsPressing.bind(null, true)}
        onPressOut={setIsPressing.bind(null, false)}>
        <View 
          style={{ 
            width: 100, 
            height: 100, 
            backgroundColor: isPressing ? 'pink' : 'red',
          }} 
        />
      </Pressable>
    </View>
  );
}

2.2 开关组件: Switch

官方提供的一个受控的 Switch 组件, 需要注意的是该组件需要 onValueChangevalue 属性配合使用, 如果 value 属性未及时更新, 组件将继续呈现所提供的 value 属性(默认 false)

js 复制代码
import { useState } from 'react';
import { View, Switch } from 'react-native';

export default function HomeScreen() {
  const [value, setValue] = useState(false)
  const handleValueChange = () => setValue(pre => !pre)

  return (
    <View style={{ margin: 100 }}>
      <Switch
        value={value}
        onValueChange={handleValueChange}
        // trackColor={{false: '#767577', true: '#81b0ff'}}
        // thumbColor={isEnabled ? '#f5dd4b' : '#f4f3f4'}
        // ios_backgroundColor="#3e3e3e"
      />
    </View>
  );
}

三、列表视图

与更通用的 ScrollView 不同, 官方还提供了两个更为强大的列表组件, 主要适用于长列表。实现了类似虚拟滚动的效果, 在渲染大列表数据时只会渲染当前屏幕上的元素, 从而提高应用性能。

3.1 扁平列表组件: FlatList

专门用于渲染扁平列表的高性能组件, 如上文所述, 该组件自带 「虚拟滚动」 效果, 对于长列表只会渲染当前屏幕用户可见内容。从而极大的提升性能。

如下代码是一个基本的 Demo, 不难想象, 组件 FlatListdatarenderItem 一个提供列表数据, 一个则是负责渲染列表项。

js 复制代码
import { View, Text, FlatList } from 'react-native';

const DATA = Array.from({ length: 100 }, (_, index) => (
  {
    id: `bd7acbea-c1b1-46c2-aed5-3ad53abb28ba-${index}`,
    title: `Item_${index}`,
  }
))

const Item = ({ title }) => (
  <View style={{ margin: 5, padding: 5, backgroundColor: 'pink' }}>
    <Text style={{ fontSize: 20 }}>{title}</Text>
  </View>
)

export default function HomeScreen() {
  return (
    <View style={{ margin: 100, height: 200 }}>
      <FlatList
        data={DATA}
        renderItem={({ item }) => <Item title={item.title} />}
      />
    </View>
  );
}

如下代码 keyExtractor 用于设置每个列表项的 key 值。默认列表项先尝试使用 item.key, 如果不存在则尝试使用 item.id, 实在都没有则回退到使用索引

js 复制代码
<FlatList
  data={DATA}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Item title={item.title} />}
/>

当然 FlatList 功能还是很强大的, 这里就不一一展开了, 具体可实现的功能如下:

  1. 完全跨平台, 无需考虑兼容问题
  2. 支持水平模式
  3. 可配置的可见性回调: 其实就是可监听列表项, 进入或移出视口事件
  4. 支持设置标题、页脚、分隔符
  5. 支持下拉刷新
  6. 支持滚动加载
  7. 支持 ScrollToIndex: 即允许滚动到指定某条数据
  8. 支持多列展示

补充: 其实 FlatList 是基于虚拟列表组件 VirtualizedList 进行封装出来的, 大部分场景我们直接使用 FlatList 即可, 当然你的场景比较特殊, 那么可以尝试使用更为灵活、底层的 VirtualizedList 组件

3.2 分区列表组件: SectionList

SectionList 组件可用于实现如下分组列表, 同 FlatList 一样该组件底层也是使用 VirtualizedList 组件所以也是支持虚拟滚动效果的, 对于长列表也只会渲染用户可见内容。

用法基本和 FlatList 类似, sections 设置数据源, renderItem 渲染列表项, renderSectionHeader 则设置分组头部

js 复制代码
import { View, Text, SectionList } from 'react-native';

const DATA = Array.from({ length: 100 }, (_, index) => (
  {
    title: `Group_${index}`,
    id: `bd7acbea-c1b1-46c2-aed5-3ad53abb28ba-${index}`,
    data: ['French Fries', 'Onion Rings', 'Fried Shrimps'],
  }
))

export default function HomeScreen() {
  return (
    <View style={{ margin: 100, height: 200 }}>
      <SectionList
        sections={DATA}
        renderItem={({item}) => (
          <View style={{ backgroundColor: '#FED6D6', margin: 5, padding: 5 }}>
            <Text style={{ fontSize: 20 }}>{item}</Text>
          </View>
        )}
        renderSectionHeader={({ section: { title } }) => (
          <View style={{ backgroundColor: '#E0E0E0', margin: 5, padding: 5 }} >
            <Text style={{ fontSize: 30 }}>{title}</Text>
          </View>
        )}
      />
    </View>
  );
}

默认情况下 SectionList 组件是自带吸顶效果的, 如下图所示:

可通过 stickySectionHeadersEnabled 属性进行关闭

diff 复制代码
<SectionList
+ stickySectionHeadersEnabled={false}
  ....
>
</SectionList>

当然 SectionList 功能还是很强大的, 这里就不一一展开了, 具体可实现的功能如下:

  1. 完全跨平台, 无需考虑兼容问题
  2. 可配置的可见性回调: 其实就是可监听列表项, 进入或移出视口事件
  3. 支持设置标题、页脚、分组标题、可设置分组间分隔符、列表项之间的分隔符
  4. 支持下拉刷新
  5. 支持滚动加载
  6. 支持异构数据和多样化的项渲染

补充: 其实 SectionList 是基于虚拟列表组件 VirtualizedList 进行封装出来的, 大部分场景我们直接使用 SectionList 即可, 当然你的场景比较特殊, 那么可以尝试使用更为灵活、底层的 VirtualizedList 组件

3.3 虚拟列表组件: VirtualizedList

VirtualizedListReact Native 提供的一个高性能长列表组件, 它是 FlatListSectionList 的底层实现。相比 ScrollView 它能够高效渲染超大数据量的列表, 避免性能问题。如果项目远离用户可见区域, 则以低优先级 (在任何正在运行的交互之后) 逐步渲染项目, 否则以高优先级渲染项目, 以最大限度地减少看到空白区域的可能性。

大部分情况我们更推荐使用, 更方便的 FlatListSectionList 组件, 这些组件也有更好的文档。一般来说, 只有当您需要比 FlatList 提供的更大的灵活性时才应该使用它。

如下代码是 VirtualizedList 的一个基本用例:

js 复制代码
import { View, Text, VirtualizedList } from 'react-native';

const getItem = (data, index) => ({
  id: Math.random().toString(12).substring(0),
  title: `Item ${index + 1}`,
});

const getItemCount = (data) => 50;

const Item = ({ title }) => (
  <View style={{ backgroundColor: 'pink', margin: 5, padding: 5 }}>
    <Text style={{ fontSize: 20 }}>{title}</Text>
  </View>
);

export default function HomeScreen() {
  return (
    <View style={{ margin: 100, height: 200 }}>
      <VirtualizedList
        getItem={getItem}
        initialNumToRender={4}
        getItemCount={getItemCount}
        renderItem={({item}) => <Item title={item.title} />}
      />
    </View>
  );
}

四、Android 定制化组件 & API

简单介绍下 React Native 中专门针对 Android 设计的几个组件或 API

4.1 应用退出 API: BackHandler

BackHandlerReact Native 提供的 API, 专门用于监听 Android 设备的物理返回键(IOS 因为没有物理返回键, 所以不适用)

那么该 API 适用于哪些场景呢?

  1. 监听 Android 物理返回键 (比如在 App 里按返回键)
  2. 自定义返回逻辑 (比如二次确认退出)
  3. 拦截默认行为 (比如在某些页面阻止返回桌面)

如下代码, 是一个简单的 Demo:

js 复制代码
useEffect(() => {
  // 事件处理函数
  const handleBackHandler = () => {
    Alert.alert('Hold on!', 'Are you sure you want to go back?', [
      {
        text: 'Cancel',
        onPress: () => null,
        style: 'cancel',
      },
      { text: 'YES', onPress: () => BackHandler.exitApp() }, // 手动退出应用
    ]);

    return true;
  };

  // 注册事件
  const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackHandler );

  // 移除事件
  return () => backHandler.remove();
}, []);

注意事项:

  1. 事件订阅按相反的顺序调用。即, 后注册的订阅优先被调用
  2. 同时如果当前执行的事件返回 true 那么后续事件则不再执行, 其实就表示退出应用了。自然后面的事件也就没必要执行了。
  3. 如果订阅的事件都没有返回 true 或者未注册任何订阅,它将以编程方式调用默认的后退按钮功能来退出应用程序。

4.2 抽屉组件: DrawerLayoutAndroid

缘由: 在原生 Android 上, 是提供了一个原生的组件 DrawerLayout 用于实现抽屉效果, 因为是系统级的 UI 组件, 在性能以及用户体验上肯定是最优的。然而在 IOS 上并没有相对应的一个组件。 所以在 React Native 中则专门针对 Android 提供了 DrawerLayoutAndroid 组件, 最终打包出来的产物其实调用的也是原生的 DrawerLayout 组件。然而除非只需要开发 Android 不考虑 IOS 否则我们还是建议使用 react-navigationDrawerNavigator 来实现抽屉导航。尽量减少差异化处理。减少代码的复杂度、维护成本。

如下代码是一个简单 Demo:

js 复制代码
const drawerRef = useRef(null); // 引用 DrawerLayoutAndroid 组件

<DrawerLayoutAndroid
  ref={drawerRef}
  drawerWidth={250} // 侧边栏宽度
  drawerPosition="left" // 侧边栏位置(left / right)
  renderNavigationView={() => (
    <View>
      <Text>这里是侧边栏内容</Text>
      <Button title="关闭抽屉" onPress={drawerRef.current.closeDrawer} />
    </View>
  )}>
 <View>
    <Text>📌 主界面</Text>
    <Button title="打开抽屉" onPress={drawerRef.current.openDrawer} />
  </View>
</DrawerLayoutAndroid>

4.3 权限处理 API: PermissionsAndroid

安卓中, 对于常规的权限需求我们只需要在 AndroidManifest.xml 配置好即可, 用户在安装应用程序时会默认授予。但是对于一些重要(危险)的权限, 需要给出弹层提示用户。这里则需要特定的 API 进行处理。在 React Native 中则提供了 PermissionsAndroid API 供我们使用。

如下代码是一个简单 Demo:

js 复制代码
import { Button, PermissionsAndroid } from 'react-native';

const requestCameraPermission = async () => {
  try {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.CAMERA, // 指定权限
      {
        title: 'Cool Photo App Camera Permission',
        message: 'Cool Photo App needs access to your camera  so you can take awesome pictures.' ,
        buttonNeutral: 'Ask Me Later',
        buttonNegative: 'Cancel',
        buttonPositive: 'OK',
      },
    );

    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      console.log('You can use the camera');
    } else {
      console.log('Camera permission denied');
    }
  } catch (err) {
    console.warn(err);
  }
};

<Button title="request permissions" onPress={requestCameraPermission} />

4.4 消息提示组件: ToastAndroid

DrawerLayoutAndroid, 原生 Android 提供了一个系统级的 UI 组件 Toast 组件, 它可以在屏幕底部弹出一个短暂的消息, 但是又不会阻塞用户操作。然而在 IOS 中并没有类似的组件。 所以 React Native 则专门针对 Android 提供了 ToastAndroid 组件。最终编译后在 Android 中则映射为 Toast 组件。同样的, 如果你的应用不止 Android 还需要考虑 IOS, 这里还是建议使用 Alertreact-native-toast-message

如下代码是一个简单 Demo:

js 复制代码
const showToastWithGravity = () => {
  ToastAndroid.showWithGravity(
    'All Your Base Are Belong To Us',
    ToastAndroid.SHORT,
    ToastAndroid.CENTER,
  );
};

<Button title="Toggle Toast With Gravity" onPress={showToastWithGravity}/>

五、IOS 定制化组件 & API

简单介绍下 React Native 中专门针对 IOS 设计的几个组件或 API

5.1 操作弹层 API: ActionSheetIOS

一个专属于 IOS 的操作弹出框组件, 在原生 IOS 中, ActionSheet 是系统自带的 UI 组件, 通常用于 弹出操作菜单React Native 直接封装了这个原生组件, 因此提供了 ActionSheetIOS 供开发者使用。如果你的应用需要考虑 Android 则推荐使用 AlertModalreact-native-actions-sheet 来实现类似效果。

如下代码是一个简单 Demo:

js 复制代码
import { ActionSheetIOS, Button } from 'react-native';

const onPress = () => ActionSheetIOS.showActionSheetWithOptions(
  {
    options: ['Cancel', 'Generate number', 'Reset'],
    destructiveButtonIndex: 2,
    cancelButtonIndex: 0,
    userInterfaceStyle: 'dark',
  },
  buttonIndex => {
    if (buttonIndex === 0) {
      // cancel action
    } else if (buttonIndex === 1) {
      setResult(String(Math.floor(Math.random() * 100) + 1));
    } else if (buttonIndex === 2) {
      setResult('🔮');
    }
  },
);

<Button onPress={onPress} title="Show Action Sheet" />

六、其他

6.1 活动指示器组件: ActivityIndicator

其实就是 Web 开发中常见的 Loading 组件

如下代码所示, 使用起来还是比较简单的:

js 复制代码
import { ActivityIndicator } from 'react-native';

<ActivityIndicator />
<ActivityIndicator size="large" />
<ActivityIndicator size="small" color="#0000ff" />
<ActivityIndicator size="large" color="#00ff00" />

6.2 对话框组件: Alert

用于实现弹出对话框(弹窗/警告框), 可选择提供一组按钮列表, 点击任何按钮都会触发相应的 onPress 回调函数并关闭弹层。默认情况下, 唯一的按钮将是「确定」按钮。

上图 👆🏻 效果, 核心代码如下:

js 复制代码
import { Button, Alert } from 'react-native';

const createTwoButtonAlert = () => Alert.alert('Alert Title', 'My Alert Msg', [
  {
    text: 'Cancel',
    style: 'cancel',
    onPress: () => console.log('Cancel Pressed'),
  },
  { text: 'OK', onPress: () => console.log('OK Pressed') },
]);

const createThreeButtonAlert = () => Alert.alert('Alert Title', 'My Alert Msg', [
  {
    text: 'Ask me later',
    onPress: () => console.log('Ask me later pressed'),
  },
  {
    text: 'Cancel',
    style: 'cancel',
    onPress: () => console.log('Cancel Pressed'),
  },
  { text: 'OK', onPress: () => console.log('OK Pressed') },
]);

<Button title="双按钮" onPress={createTwoButtonAlert} />
<Button title="三按钮" onPress={createThreeButtonAlert} />

这是一个适用于 AndroidIOSAPI, 但是该组件在这两个平台还是存在差异的:

  • 是否允许输入: 在 IOS 上支持在弹层上, 展示输入框, 并允许用户进行输入, 但是 Android 则不行
  • 按钮数量: 在 IOS 上, 您可以指定任意数量的按钮。但是在 Android 上最多可以指定三个按钮
  • 样式、交互: 在 UI 以及交互上也有所不同

如何选择?

  1. 仅仅需要简单的确认对话框, 又比较住在原生体验(IOS/Android 各自风格) 则选择 Alert
  2. 如需要自定义 UI、注重跨平台一致的弹窗样式、需要输入框(Android 也支持) 则推荐使用 react-native-dialog 进行定制

6.3 动画组件以及相关 API: Animated

不同于 WebReact Native 中是无法使用 CSS 来实现动画效果的, 只能通过 JS 或者官方提供的 Animated 组件以及相关 API 来实现高性能的动画。

Animated 提供了一些封装好的组件: Animated.ViewAnimated.TextAnimated.ImageAnimated.ScrollViewAnimated.FlatList....

创建动画的核心工作流程是创建一个 Animated.Value, 将其连接到动画组件的一个或多个样式属性, 然后使用 Animated.timing() 通过动画驱动更新。

如下, 是一个简单的 Demo:

  1. 使用官方提供的 useAnimatedValue 来创建一个 Animated.Value
  2. 通过事件调用 Animated.timing 来驱动 Animated.Value 值过渡更新
  3. 使用 Animated.timing 来动态设置视图(样式)
js 复制代码
import { View, Button, Text, Animated, useAnimatedValue } from 'react-native';

export default function HomeScreen() {
  const fadeAnim = useAnimatedValue(0);

  const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 5000,
      useNativeDriver: true,
    }).start();
  };

  const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeAnim, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View style={{ margin: 100, height: 200 }}>
      <Animated.View
        style={{ 
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
          opacity: fadeAnim, // 动画
        }}>
        <Text style={{ fontSize: 20 }}> Fading View! </Text>
      </Animated.View>
      <View 
        style={{
          flexBasis: 100,
          marginVertical: 16,
          justifyContent: 'space-evenly',
        }}>
        <Button title="Fade In View" onPress={fadeIn} />
        <Button title="Fade Out View" onPress={fadeOut} />
      </View>
    </View>
  );
}

当然不管在哪里, 动画都是一个复杂的主题, 这里我们就不进行展开, 后面可以单独开一篇来说明!

6.4 屏幕尺寸管理 API: Dimensions

Dimensions 相关 APIReact Native 提供的 屏幕尺寸管理工具, 主要是用于获取设备的屏幕宽高。它通常用于适配不同屏幕尺寸的 UI 布局。

如下代码所示, 视图和控制台将展示 Dimensionswindowscreen 的所有内容:

js 复制代码
import { View, Text, Dimensions } from 'react-native';

export default function HomeScreen() {
  const windowDimensions = Dimensions.get('window');
  const screenDimensions = Dimensions.get('screen');
  console.log('screenDimensions', screenDimensions);
  console.log('windowDimensions', windowDimensions);

  return (
    <View style={{ marginTop: 100 }}>
      <Text style={{ fontSize: 20, padding: 20, lineHeight: 30 }}>
        <Text >Window Dimensions {'\n'}</Text>
        {Object.entries(windowDimensions).map(([key, value]) => (
          <Text>{key} - {value}{'\n'}</Text>
        ))}
        {'\n'}
        <Text >Screen Dimensions {'\n'}</Text>
        {Object.entries(screenDimensions).map(([key, value]) => (
          <Text>{key} - {value}{'\n'}</Text>
        ))} 
      </Text>
    </View>
  );
}

那么问题来了, windowscreen 有啥区别呢?

API 作用
Dimensions.get('window') 应用窗口的宽高(不包含状态栏 & 导航栏)
Dimensions.get('screen') 整个屏幕的宽高(包含状态栏 & 导航栏)

虽然屏幕的尺寸在组件加载前就可以立即获取, 但它们可能会发生变化(例如由于设备旋转、可折叠设备等), 因此任何依赖于这些常量的渲染逻辑或样式都应尝试在每次渲染时都调用相关函数去实时获取, 而不是缓存该值(例如, 使用内联样式而不是在 StyleSheet 中设置值)。甚至我们可能需要监听屏幕尺寸变化, 来动态渲染视图, 这里同样需要通过 Dimensions 来实现:

js 复制代码
const [dimensions, setDimensions] = useState({
  window: Dimensions.get('window'),
  screen: Dimensions.get('screen'),
})

useEffect(() => {
  // 监听屏幕尺寸的变更
  const subscription = Dimensions.addEventListener(
    'change',
    ({ window, screen }) => {
      setDimensions({ window, screen });
    },
  );

  return () => subscription?.remove();
});

当然我们实际上可能并不需要这么麻烦, 官方其实给我们提供了一个特别好用的 hookuseWindowDimensionshook 帮我们作了封装, 它会在窗口大小变化时自动更新结果。

js 复制代码
const window = useWindowDimensions()
console.log(window); // { fontScale: 1, height: 874, scale: 3, width: 402 }

当然需要注意的是官方并没有提供对应的 screen 相关的 hook, 为什么没有 useScreenDimensions 下面是 GPT 的一个解释

所以如果我们需要监听 screen 的变更, 那只能手动调用 Dimensions 来进行监听了...

6.5 键盘遮挡处理组件: KeyboardAvoidingView

在移动端, 当用户点击输入框, 弹出键盘时经常会出现键盘遮挡输入框的问题, 尤其是在 IOS 上。而如果不加以处理的话, 用户很容易就看不到输入框输入的内容。

而针对上诉问题, React Native 官方则专门提供了 KeyboardAvoidingView 组件来处理该问题。

下面代码是一个简单的 Demo:

js 复制代码
import React from 'react';
import { KeyboardAvoidingView, TextInput, Platform } from 'react-native';

const KeyboardAvoidingComponent = () => {
  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      style={{ margin: 50 }}>
      <TextInput placeholder="Username" />
    </KeyboardAvoidingView>
  );
};

export default KeyboardAvoidingComponent;

上文 behavior 属性则用于规定如何对键盘的存在做出合理的布局调整。

behavior 作用
height 减少 View 高度(适用于 Android)
padding 整体向上移动(适用于 IOS)
position 使用绝对定位移动(较少使用)

推荐写法:

js 复制代码
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}

6.6 深链接 API: Linking

首先我们需要先了解下何谓深链接(Deep Linking), 该链接可用于在 App 之间进行跳转。类似于网页的超链接, 不同的是它是用于移动应用。

深链接 VS 普通链接

| 对比 | 普通链接(网页) | 深链接(App 内部) | | --- | --- | | 格式 | https://example.com/page | myapp://page | | 作用 | 打开网页 | 直接跳转 App 内部页面 | | 适用场景 | 浏览器、社交平台 | App 之间导航、广告推广 |

深链接的工作原理: 深链接本质上就是一个自定义 URL Scheme, 当用户点击 myapp://profile/123, 系统会尝试检查是否安装了 App

  • 如果已安装 --> 直接打开 App 并跳转到 profile/123 页面
  • 如果未安装 --> 可能会显示错误(可配合 Universal Links 引导用户下载 App)
    内置 URL 方案: 如简介中所述, 每个平台上都存在一些用于核心功能的 URL 方案。以下是非详尽的列表, 但涵盖了最常用的方案。当然除了下列几个常见的方案, 我们也是可以自定义 URL Scheme

| Scheme 方案 | Description 描述 | iOS | Android | | --- | --- | --- | | mailto | 打开邮件应用程序, 例如: mailto: [email protected] | [x] | [x] | | tel | 打开电话应用程序, 例如: tel: +123456789 | [x] | [x] | | sms | 打开短信应用程序, 例如: sms: +123456789 | [x] | [x] | | https / http | 打开网页浏览器应用程序, 例如: https ://expo.io | [x] | [x] |

下面是在 React Native 中处理深层链接的方式:

js 复制代码
// 1. 如果该应用程序已打开, 则可以使用 Linking.addEventListener 监听到跳转过来的深链接
Linking.addEventListener('url', event => {
  console.log(`检测到深链接: ${event.url}`);
});

// 2. 如果应用程序尚未打开, 则可以使用 Linking.getInitialURL() 获取到跳转过来的深链接
const initialUrl = await Linking.getInitialURL();

下面是在 React Native 中从一个 App 内跳转深链接的方式:

js 复制代码
const handlePress = useCallback(async () => {
  // 检查是否支持自定义 URL 方案的链接
  const supported = await Linking.canOpenURL(url);

  if (supported) {
    // by some browser in the mobile
    await Linking.openURL(url);
  } else {
    Alert.alert(`Don't know how to open this URL: ${url}`);
  }
}, [url]);

当用户缺少需要的一些系统权限设置时, 也可通过 Linking 引导用户进去设置中心进行配置:

js 复制代码
await Linking.openSettings();

当然在安卓中我们还可以使用 Linking.sendIntent() 调用 Intent 进行系统级的 API, 比如:

  • 发送短信(不跳转到短信 App)
  • 拨打电话(不弹出拨号界面)
  • 打开 WiFi、蓝牙、位置设置等系统界面
  • 与其他 App 交互(如打开特定页面)
js 复制代码
const action = "android.settings.APP_NOTIFICATION_SETTINGS";
const extras = [
  {
    key: 'android.provider.extra.APP_PACKAGE',
    value: 'com.facebook.katana',
  },
]

try {
  await Linking.sendIntent(action, extras);
} catch (e) {
  Alert.alert(e.message);
}

React Native 中, 官方就给我们提供了现成的弹窗组件。

如下代码是一个简单的 Demo:

  1. transparent: 该属性决定了模态框整体背景是否是透明的, 设置为 true 则模态框背景透明(仅遮罩部分), 否则, 模态框背景不透明(默认白色背景, 覆盖整个屏幕)
  2. animationType: 该属性控制模态框的打开和关闭时的动画效果, 该属性有三个可选值, slide(从屏幕底部进出)、 fade(透明度渐变进出)、none(默认, 没有动画过渡)
  3. visible: 该属性则控制模态框的显示/隐藏
js 复制代码
import { useState } from 'react';
import { Button, Modal, Text, View } from 'react-native';

const App = () => {
  const [modalVisible, setModalVisible] = useState(false);
  return (
      <View style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
      }}>
        <Modal
          transparent
          animationType="slide"
          visible={modalVisible}>
          {/* 弹窗外层, 占满整个屏幕 */}
          <View 
            style={{
              flex: 1,
              alignItems: 'center',
              justifyContent: 'center',
            }}>
            <View style={{ backgroundColor: 'white' }}>
              <Text>Hello World!</Text>
              <Button title='Hide Modal' onPress={() => setModalVisible(!modalVisible)} />
            </View>
          </View>
        </Modal>
        <Button title='Show Modal' onPress={setModalVisible.bind(null, true)} />
      </View>
  );
};

export default App;

6.8 像素比 API: PixelRatio

PixelRatio 用于处理屏幕像素密度(DPR, 设备像素比)相关的事情。主要作用其实就用来获取设备的像素密度信息 , 以及做一些 像素与 dp 单位 的换算, 解决不同屏幕上 UI 元素尺寸不一致的问题。

  1. PixelRatio.get() 获取当前设备的像素比
js 复制代码
import { PixelRatio, View } from 'react-native';
PixelRatio.get() // 3
  1. PixelRatio.getPixelSizeForLayoutSize(layoutSize) 将布局尺寸(dp)转换为像素尺寸(px)
js 复制代码
// 根据设备像素比, 来获取正确大小的图像
const image = getImage({
  width: PixelRatio.getPixelSizeForLayoutSize(200), // 200 * 3 px
  height: PixelRatio.getPixelSizeForLayoutSize(100), // 100 * 3 px
});

<Image source={image} style={{ width: 200, height: 100 }} />;
  1. PixelRatio.roundToNearestPixel(layoutSize) 将布局尺寸(dp)四舍五入为整数像素尺寸(px)。例如, 在 PixelRatio3 的设备上, PixelRatio.roundToNearestPixel(8.4) 会返回 8.33, 而 8.33pd 则等于 (8.33 * 3) = 25 像素。
js 复制代码
PixelRatio.roundToNearestPixel(3) // 3db   =>    3 * 3px
PixelRatio.roundToNearestPixel(8.4) // 8.3db    =>   25px
PixelRatio.getPixelSizeForLayoutSize(8.3) // 25 
  1. PixelRatio.getFontScale() 获取设备字体的缩放比例, 在 AndroidIOS 都是可以在设置中设置字体大小。
js 复制代码
PixelRatio.getFontScale() // 1

6.9 刷新控制组件: RefreshControl

该组件通常配合 ScrollViewListView 等组件一起使用, 主要是用于给列表添加下拉刷新的功能。当 ScrollView 等组件位于 scrollY: 0 时继续向下滑动 RefreshControl 组件将会触发 onRefresh 事件。

细节不展开了, 直接看代码...

js 复制代码
import React from 'react';
import { ScrollView, View, RefreshControl } from 'react-native';
import {SafeAreaView, SafeAreaProvider} from 'react-native-safe-area-context';

const data = Array.from({ length: 20 }).fill(1)

const App = () => {
  const [refreshing, setRefreshing] = React.useState(false);

  const onRefresh = React.useCallback(() => {
    setRefreshing(true);
    setTimeout(() => setRefreshing(false), 2000);
  }, []);

  return (
    <SafeAreaProvider>
      <SafeAreaView >
        <ScrollView 
          refreshControl={
            <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
          }
          style={{ height: 600, backgroundColor: 'pink' }}>
          {data.map(() => <View style={{ height: 50, margin: 10, backgroundColor: 'red' }} />)}
        </ScrollView>
      </SafeAreaView>
    </SafeAreaProvider>
  );
};

6.10 状态栏组件: StatusBar

StatusBar 就是用来管理手机系统顶部的那条 信号/时间/电池 状态栏区域的样式。你可以控制它:

props 属性 描述 Android IOS
backgroundColor 设置状态栏背景色 [x] [ ]
backgroundColor 设置状态栏背景色 [x] [ ]
barStyle 设置状态栏文本的颜色, 可选值有 default(默认, IOS 为深色, Android 为浅色)light-content(浅色)、dark-content(深色) [x] [x]
hidden 是否将状态栏隐藏 [x] [x]
animated 状态栏属性变化之间的过渡是否应以动画形式呈现 [x] [x]
translucent 状态栏是否半透明 [x] [ ]
showHideTransition 使用 hidden 属性显示和隐藏状态栏时的过渡效果。可选值有 defaultlight-contentdark-content [ ] [x]
js 复制代码
<StatusBar
  animated={true} 
  backgroundColor="#61dafb"
  barStyle={statusBarStyle}
  showHideTransition={statusBarTransition}
  hidden={hidden}
/>

七、参考

相关推荐
2501_915373883 分钟前
Vue.js 入门教程
前端·javascript·vue.js
WindrunnerMax4 分钟前
从零实现富文本编辑器#3-基于Delta的线性数据结构模型
前端·javascript·github
前端李白5 分钟前
🛫历经一个月,免费图片压缩工具站上线了!
前端·后端
掘金安东尼7 分钟前
🧭 前端周刊第410期(2025年4月14日–20日)
前端·面试·github
夜羽rancho15 分钟前
二分查找,其实就这些了
前端·算法
Face16 分钟前
JavaScript基础
前端·javascript
宇宙的有趣17 分钟前
Codegen 加速开发:从数据结构到模版代码
前端
Zan17 分钟前
使用 mcp-use 轻松打造连接 LLM 和 MCP 的 Typescript 工具
前端·后端·typescript
xianshenglu18 分钟前
我的Angular总结:建议使用FormGroup.controls 来访问子表单控件
前端·angular.js
天天扭码18 分钟前
一分钟解决 | 高频面试算法题——接雨水(双指针最优解)
前端·算法·面试