RN组件

1. RN 内置基础组件

打开 RN 的官网,可以看到官方文档中核心分为 4 个部分,分别是:

  • Guides(向导)
  • Components(内置组件)
  • API(接口文档)
  • Architecture(架构)

在上一章节《RN 快速入门》中,我们相当于将 Guides (向导)部分的内容已经完成了。本章节我们就着重来看 Components (内置组件)和 API(接口文档)。

在 RN 中,内置组件整体可以分为三大类:

  • 核心组件
  • Andriod 平台独有组件
  • iOS 平台独有组件

其中核心组件的数量是最多的。核心组件有一个特点,就是全平台通用。根据最终编译的目标平台,核心组件会被编译为对应平台的组件。如下图:

而核心组件又可以根据其用途分为以下几大类:

  • 基础组件
  • 容器组件
  • 列表组件
  • 功能组件

本小节我们就先来看一下第一类组件------基础组件。基础组件大致如下:

  • Image 组件
  • TextInput 组件
  • Button 组件
  • Switch 组件

Image 组件

Image 是一个图片展示组件,其作用类似于 AndriodImageView 或者 iOSUIImageViewImage 组件支持多种类型的图片显示,包括网络图片、静态资源、base64 图片格式。

要使用 Image 组件加载图片,只需要设置 source 属性即可,如果加载的是网络图片,还需要添加 uri 标识以及手动指定图像的尺寸。

目前,Image 组件支持的图片格式有 PNG、JPG、JPEG、BMP、GIF、WebP 以及 PSD 。不过,在默认情况下 Andriod 是不支持 GIFWebP 格式图片的,如果需要添加这两种图片格式的支持,需要在 android/app/build.gradle 文件中添加以下的依赖:

js 复制代码
dependencies {
  // If your app supports Android versions before Ice Cream Sandwich (API level 14)
  implementation 'com.facebook.fresco:animated-base-support:1.3.0'

  // For animated GIF support
  implementation 'com.facebook.fresco:animated-gif:2.5.0'

  // For WebP support, including animated WebP
  implementation 'com.facebook.fresco:animated-webp:2.5.0'
  implementation 'com.facebook.fresco:webpsupport:2.5.0'

  // For WebP support, without animations
  implementation 'com.facebook.fresco:webpsupport:2.5.0'
}

API 文档地址:reactnative.dev/docs/image

使用 Image 组件时,有一个常用的属性 resizeMode ,此属性用于控制当组件和图片尺寸不成比例时以何种方式调整图片的大小,对应的值有 5 种:

  • cover:在保持图片宽高比的前提下缩放图片,直到宽度和高度都大于等于容器视图的尺寸。
  • contain:在保持图片宽高比的前提下缩放图片,直到宽度和高度都小于等于容器视图的尺寸。
  • stretch:拉伸图片且不维持图片的宽高比,直到宽度和高度都刚好填满容器。
  • repeat:在维持原始尺寸的前提下,重复平铺图片直到填满容器。
  • center:居中且不拉伸的显示图片。

下面的示例演示了不同属性值之间视觉效果上的区别:

js 复制代码
import React, {Component} from 'react';
import {Platform, StyleSheet, Image, Text,View} from 'react-native';

export default class ImageResizeMode extends Component {

    render() {
        let imageSource=require("./assets/ok.png");

        return (
            <View style={styles.container}>
                <Image style={[styles.image,{resizeMode:'cover'}]}
                       source={imageSource}/>
                <Text style={styles.text}>cover</Text>

                <Image style={[styles.image,{resizeMode:'contain'}]}
                       source={imageSource}/>
                <Text style={styles.text}>contain</Text>

                <Image style={[styles.image,{resizeMode:'stretch'}]}
                       source={imageSource}/>
                <Text style={styles.text}>stretch</Text>

                <Image style={[styles.image,{resizeMode:'repeat'}]}
                       source={imageSource}/>
                <Text style={styles.text}>repeat</Text>

                <Image style={[styles.image,{resizeMode:'center'}]}
                       source={imageSource}/>
                <Text style={styles.text}>center</Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    image: {
        width: 140,
        height: 110,
        backgroundColor: 'red'
    },
    text: {
        justifyContent: 'center',
        fontSize:24
    }
});

TextInput 组件

TextInput 是一个输入框组件,用于将文本内容输入到 TextInput 组件上。作为一个高频使用的组件,TextInput 支持自动拼写、自动大小写切换、占位默认字符设置以及多种键盘设置功能。

js 复制代码
import React from "react";
import { View, StyleSheet, TextInput } from "react-native";

const UselessTextInput = () => {
  const [text, onChangeText] = React.useState("Useless Text");
  const [number, onChangeNumber] = React.useState(null);

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        onChangeText={onChangeText}
        placeholder="默认是字母键盘"
        value={text}
      />
      <TextInput
        style={styles.input}
        onChangeText={onChangeNumber}
        value={number}
        placeholder="使用数字键盘"
        keyboardType="numeric"
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "stretch",
    justifyContent: "center",
  },
  input: {
    height: 40,
    margin: 12,
    borderWidth: 1,
    padding: 10,
  },
});

export default UselessTextInput;

需要注意的是,TextInputAndriod 中默认有一个底边框且存在内边距。如果想让它看起来和 iOS 上的效果尽量一致,则需要将 padding 的值设置为 0

API 文档地址:reactnative.dev/docs/textin...

下面我们来看一个实际开发中使用到 TextInput 的案例------搜索框,代码如下:

js 复制代码
import React, { useState } from "react";
import { TextInput, StyleSheet, Text, View } from "react-native";

export default function SearchView() {
  const [text, setText] = useState("");
  const [show, isShow] = useState(false);

  function showOption(newVal) {
    console.log(newVal, "show");
    setText(newVal);
    isShow(true);
  }

  function hideOption(newVal) {
    console.log(newVal, "hide");
    setText(newVal);
    isShow(false);
  }

  return (
    <View style={styles.container}>
      <View style={styles.searchContainer}>
        <TextInput
          style={styles.inputStyle}
          returnKeyType="search"
          placeholder="请输入关键字"
          onChangeText={(val) => showOption(val)}
          value={text}
        />
        <View style={styles.btnStyle}>
          <Text style={styles.search} onPress={() => alert(text)}>
            搜索
          </Text>
        </View>
      </View>
      {show ? (
        <View style={[styles.resultStyle]}>
          <Text
            onPress={() => hideOption(text + "街")}
            style={styles.itemStyle}
            numberOfLines={1}
          >
            {text}街
          </Text>
          <Text
            onPress={() => hideOption(text + "道路")}
            style={styles.itemStyle}
            numberOfLines={1}
          >
            {text}道路
          </Text>
          <Text
            onPress={() => hideOption(80 + text + "车站")}
            style={styles.itemStyle}
            numberOfLines={1}
          >
            80{text}车站
          </Text>
        </View>
      ) : null}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#F5FCFF",
    paddingTop: 25,
  },
  searchContainer: {
    height: 45,
    flexDirection: "row",
  },
  inputStyle: {
    height: 45,
    flex: 1,
    marginTop: 20,
    borderWidth: 1,
    marginLeft: 10,
    paddingLeft: 5,
    borderColor: "#ccc",
    borderRadius: 5,
  },
  btnStyle: {
    width: 80,
    marginTop: 20,
    marginLeft: -5,
    marginRight: 10,
    borderBottomRightRadius: 5,
    borderTopRightRadius: 5,
    backgroundColor: "#23BEFF",
    height: 45,
    justifyContent: "center",
    alignItems: "center",
  },
  search: {
    color: "#fff",
    fontSize: 15,
    fontWeight: "bold",
  },
  resultStyle: {
    marginTop: 20,
    marginLeft: 10,
    marginRight: 10,
    height: 200,
    borderColor: "#ccc",
    borderTopRightRadius: 5,
    borderTopLeftRadius: 5,
  },
  itemStyle: {
    fontSize: 16,
    padding: 5,
    paddingTop: 10,
    paddingBottom: 10,
    borderWidth: 1,
    borderColor: "#ddd",
    borderTopWidth: 0,
  },
});

Button 组件

Button 是一个最基本的按钮组件,可以在跨平台上很好地呈现,支持最低级别的定制。

API 文档地址:reactnative.dev/docs/button

js 复制代码
import React from "react";
import {
  StyleSheet,
  Button,
  View,
  SafeAreaView,
  Text,
  Alert,
} from "react-native";

const Separator = () => <View style={styles.separator} />;

const App = () => (
  <SafeAreaView style={styles.container}>
    <View>
      <Text style={styles.title}>
        The title and onPress handler are required. It is recommended to set
        accessibilityLabel to help make your app usable by everyone.
      </Text>
      <Button
        title="Press me"
        onPress={() => Alert.alert("Simple Button pressed")}
      />
    </View>
    <Separator />
    <View>
      <Text style={styles.title}>
        Adjust the color in a way that looks standard on each platform. On iOS,
        the color prop controls the color of the text. On Android, the color
        adjusts the background color of the button.
      </Text>
      <Button
        title="Press me"
        color="#f194ff"
        onPress={() => Alert.alert("Button with adjusted color pressed")}
      />
    </View>
    <Separator />
    <View>
      <Text style={styles.title}>
        All interaction for the component are disabled.
      </Text>
      <Button
        title="Press me"
        disabled
        onPress={() => Alert.alert("Cannot press this one")}
      />
    </View>
    <Separator />
    <View>
      <Text style={styles.title}>
        This layout strategy lets the title define the width of the button.
      </Text>
      <View style={styles.fixToText}>
        <Button
          title="Left button"
          onPress={() => Alert.alert("Left button pressed")}
        />
        <Button
          title="Right button"
          onPress={() => Alert.alert("Right button pressed")}
        />
      </View>
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    marginHorizontal: 16,
  },
  title: {
    textAlign: "center",
    marginVertical: 8,
  },
  fixToText: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  separator: {
    marginVertical: 8,
    borderBottomColor: "#737373",
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
});

export default App;

Switch 组件

SwitchRN 提供的一个状态切换的组件,俗称开关组件,主要用来对开和关两个状态进行切换。

Switch 组件的用法比较简单,只需要给组件绑定 value 属性即可,这样它就是一个受控组件。如果需要改变组件的状态,则必须使用 onValueChange 方法来更新 value 的值。

API 文档地址:reactnative.dev/docs/switch

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

const App = () => {
  const [isEnabled, setIsEnabled] = useState(false);
  const toggleSwitch = () => setIsEnabled(previousState => !previousState);

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  }
});

export default App;

2. 容器组件

本小节我们来学习 RN 内置组件中的容器组件。容器组件大致如下:

  • View 组件
  • Text 组件
  • ScrollView 组件
  • Touchable 组件

View 组件

RN 中,View 容器组件支持 Flexbox 布局、样式、触摸事件处理和一些无障碍功能,它可以被放到其他容器组件里面,也可以包含任意多个子组件。

无论是 iOS 还是 AndriodView 组件都会直接对应平台的原生视图,其作用等同于 iOSUIView 或者 AndriodViewGroup

API 文档地址:reactnative.dev/docs/view

来看一个简单的示例:

js 复制代码
import React from "react";
import { View, StyleSheet } from "react-native";

const App = () => {
  return (
    <View
      style={{
        flexDirection: "row",
        padding: 20,
        flexWrap: "wrap",
        justifyContent: "space-between",
        borderWidth: 1,
      }}
    >
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
      <View style={styles.item} />
    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    width: 50,
    height: 50,
    borderWidth: 1,
    margin: 10,
  },
});

export default App;

Text 组件

RN 中,Text 是一个用来显示文本内容的组件,也是使用频率极高的组件,它支持文本和样式的嵌套以及触摸事件的处理。

js 复制代码
import React, { useState } from "react";
import { View, Text, StyleSheet } from "react-native";

const TextInANest = () => {
  const [titleText, setTitleText] = useState("Bird's Nest");
  const bodyText = "This is not really a bird nest.";

  const onPressTitle = () => {
    setTitleText("Bird's Nest [pressed]");
  };

  return (
    <View style={styles.container}>
      <Text style={styles.baseText}>
        {/* 除了继承 baseText 样式以外,有自己的样式 */}
        <Text style={styles.titleText} onPress={onPressTitle}>
          {titleText}
          {"\n"}
          {"\n"}
        </Text>
        {/* 继承 baseText 的样式 */}
        <Text numberOfLines={5}>{bodyText}</Text>
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  baseText: {
    fontSize: 30,
  },
  titleText: {
    fontSize: 20,
    fontWeight: "400",
  },
});

export default TextInANest;

从布局上讲,Text 组件没有类似于 CSS 行内元素这样的概念,所以单个 Text 组件也是独占一行 (因为它相当于网页中的 p 元素),但它属于 Flex 布局范畴,可以使用 flexDirection 属性设置行内并列的效果,例如:

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

const ViewBoxesWithColorAndText = () => {
  return (
    <View style={{ flex: 1, justifyContent: "center" }}>
      <View>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>1</Text>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>2</Text>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>3</Text>
      </View>
      <View style={{ flexDirection: "row" }}>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>1</Text>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>2</Text>
        <Text style={{ fontSize: 40, borderWidth: 1 }}>3</Text>
      </View>
    </View>
  );
};

export default ViewBoxesWithColorAndText;

Text 的嵌套主要是为了满足文本某些特定场景的需求。例如在一些信息展示类的场景中,通常需要将同一段落的部分文字的字号,颜色另外设置值,以达到视觉上的区分。

以前在 PC 端书写网页时,我们是通过嵌套 span 标签来处理此需求的,而在 RN 中则是使用 Text 的嵌套来实现。

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

const BoldAndBeautiful = () => {
  return (
    <View style={styles.container}>
      <Text>
        <Text style={{fontSize:28,color:'#999'}}>First part</Text>
        <Text>and</Text>
        <Text style={{fontSize:20,color:'red'}}>second part</Text>
      </Text>
      <View>
        <Text>First part and </Text>
        <Text>second part</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  baseText: {
    fontWeight: "bold",
  },
  innerText: {
    color: "red",
  },
});

export default BoldAndBeautiful;

不过 RN 中的 Text 嵌套写法也存在以下的问题

(1)被嵌套组件与位置相关的 style 样式几乎都不生效。

go 复制代码
import React from "react";
import { Text, View } from "react-native";

const BoldAndBeautiful = () => {
  return (
    <View style={{ marginTop: 20 }}>
      <Text style={{ fontSize: 28 }}>
        我是一段普通文字
        <Text style={{ paddingLeft: 10, borderWidth: 1 }}>左Padding 10</Text>
        <Text style={{ marginLeft: 10, borderWidth: 1 }}>左Margin 10</Text>
      </Text>
    </View>
  );
};

export default BoldAndBeautiful;

(2)内嵌 TextnumberOfLines 属性会失效。

go 复制代码
import React from "react";
import { Text, View } from "react-native";

const BoldAndBeautiful = () => {
  return (
    <View style={{ marginTop: 20 }}>
      <Text style={{ fontSize: 28, borderWidth: 1 }}>
        1.{" "}
        <Text numberOfLines={2} ellipsizeMode={"tail"}>
          我是一段普通文字我是一段普通文字我是一段普通文字我是一段普通文字我是一段普通文字
        </Text>
      </Text>
    </View>
  );
};

export default BoldAndBeautiful;

如果使用不同的 Text 组件设置不同的字号,那么对齐的方式仍然是使用 Flex 布局对齐。

例如垂直居中:

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

const App = () => {
  return (
    <View style={{ marginTop: 20 }}>
      <View
        style={{
          flexDirection: "row",
          marginTop: 10,
          borderWidth: 1,
          alignItems: "center",
        }}
      >
        <Text style={{ fontSize: 20 }}>我是文字</Text>
        <Text style={{ fontSize: 30 }}>我是大一点的文字</Text>
      </View>
      <View
        style={{
          flexDirection: "row",
          marginTop: 10,
          borderWidth: 1,
          alignItems: "flex-start",
        }}
      >
        <Text style={{ fontSize: 20 }}>我是文字</Text>
        <Text style={{ fontSize: 30 }}>我是大一点的文字</Text>
      </View>
    </View>
  );
};

export default App;

不过需要注意的是,由于字号大小不一,小字号文字的上边距会略小,例如将上例中 alignItems 值修改为 flex-start ,但是由于不同的字体大小可以明显的看到上边距是不同的。如果想要不同字体大小的文字边距相同,可以利用 padding 进行微调。

API 文档地址:reactnative.dev/docs/text

ScrollView 组件

ScrollView 是一个支持横向或竖向的滚动组件,几乎所有页面都会用到。

ScrollView 组件类似于 Web 中的 htmlbody 标签,浏览器中的页面之所以能上下滚动,就是因为 htmlbody 标签默认有一个 overflow-y: scroll 的属性,如果你把标签的属性设置为 overflow-y: hidden,页面就不能滚动了。

ReactNativeScrollView 组件在 Android 的底层实现用的是 ScrollViewHorizontalScrollView ,在 iOS 的底层实现用的是 UIScrollView

使用 ScrollView 组件时,必须要有一个确定的高度才能正常工作。如果不知道容器的准确高度,可以将 ScrollView 组件的样式设置为 {flex: 1},让其自动填充父容器的空余空间。

ScrollView 通常包裹在视图的外面,用于控制视图的滚动,并且很多时候我们并不直接给 ScrollView 设置固定高度或宽度,而是给其父组件设置固定高度或宽度。

后期我们会使用 ScrollView 组件来封装一个轮播图的自定义组件。

API 文档地址:reactnative.dev/docs/scroll...

Touchable 组件

RN 应用开发中,点击和触摸都是比较常见的交互行为,不过并不是所有的组件都支持点击事件。为了给这些不具备点击响应的组件绑定点击事件,RN 提供了 Touchable 系列组件。

正如前面所述,Touchable 系列组件并不是单指某一个组件,一共有 4 个,其中跨平台的有 3 个:

  • TouchableHighlight

    Touchable 系列组件中比较常用的一个,它是在 TouchableWithoutFeedback 的基础上添加了一些 UI 上的扩展,即当手指按下的时候,该视图的不透明度会降低,同时会看到视图变暗或者变亮,该标签可以添加 style 样式属性。

  • TouchableOpacity

    完全和 TouchableHighlight 相同,只是不可以修改颜色,只能修改透明度。

  • TouchableWithoutFeedback

    最基本的一个 Touchable 组件,只响应用户的点击事件,不会做任何 UI 上的改变,所以不用添加 style 样式属性,加了也没效果。

另外在 Android 平台上支持一个叫 TouchableNativeFeedback 的组件:

  • TouchableNativeFeedback :为了支持 Android 5.0 的触控反馈而新增的组件。该组件在 TouchableWithoutFeedback 所支持的属性的基础上增加了触摸的水波纹效果。可以通过 background 属性来自定义原生触摸操作反馈的背景。(仅限 Android 平台,IOS 平台使用会报错)

示例如下:

js 复制代码
import React, { useState } from "react";
import {
  View,
  StyleSheet,
  TouchableOpacity,
  TouchableNativeFeedback,
  TouchableHighlight,
  TouchableWithoutFeedback,
  Text,
} from "react-native";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <View style={styles.container}>
      <TouchableHighlight
        style={styles.touchableStyle}
        onPress={() => setCount(count + 1)}
      >
        <Text style={styles.txtStyle}>点击加1</Text>
      </TouchableHighlight>

      <TouchableOpacity
        style={styles.touchableStyle}
        onPress={() => setCount(count + 1)}
      >
        <Text style={styles.txtStyle}>点击加1</Text>
      </TouchableOpacity>

      <TouchableWithoutFeedback onPress={() => setCount(count + 1)}>
        <View style={styles.touchableStyle}>
          <Text style={styles.txtStyle}>点击加1</Text>
        </View>
      </TouchableWithoutFeedback>

      <TouchableNativeFeedback onPress={() => setCount(count + 1)}>
        <View style={styles.touchableStyle}>
          <Text style={styles.txtStyle}>点击加1</Text>
        </View>
      </TouchableNativeFeedback>

      <Text style={[styles.countText]}>{count !== 0 ? count : null}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  touchableStyle: {
    width: 300,
    height: 38,
    borderRadius: 5,
    alignSelf: "center",
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#06C1AE",
    marginTop: 20,
    marginBottom: 20,
  },
  txtStyle: {
    color: "#ffffff",
    textAlign: "center",
    fontSize: 18,
  },
  countText: {
    marginTop: 10,
    alignSelf: "center",
    fontSize: 38,
    color: "#06C1AE",
  },
});

3. Pressable 组件

通过前面的学习,我们已经知道在 RN 中提供了 ButtonTouchable 这两个交互组件来处理用户的点击操作。但是到了 RN 0.63 版本,官方又提供了新的交互组件:Pressable

新的交互组件在未来将替代目前可以进行交互的组件:Button, TouchableWithoutFeedback, TouchableHighlight, TouchableOpacity, TouchableNativeFeedback

新核心组件 Pressable ,可用于检测各种类型的交互。提供的 API 可以直接访问当前的交互状态,而不必在父组件中手动维护状态。它还可以使用各平台的所有功能,包括悬停,模糊,聚焦等。RN 希望开发者利用 Pressable 去设计组件,而不是使用带有默认效果的组件。如:TouchableOpacity

那么在这里,我们就要对这几代不同的交互组件做一个总结。

首先,开发者在开发时会用到点按组件,那么它的功能越简单开发者用起来就越轻松;但是与其相对的,应用最后开发出来是给用户使用的,对于用户来讲,则是希望功能越丰富就越能满足各种场景的需求。

那是让开发者简单易用好,还是用丰富的功能去满足用户,有没有两全其美之计?

实际上,RN 的点按组件经历了三个版本的迭代,才找到了两全其美的答案。等你了解了这个三个版本的迭代思路后,你就能很好明白优秀通用组件应该如何设计,才能同时在用户体验 UX 和开发者体验 DX 上找到平衡。

第一代 Touchable 组件

是的,你没有看错,Touchable 系列组件反而是在 RN 中所提供的第一代点按组件。

第一代点按组件想要解决的核心问题是,提过多种反馈风格。

一个体验好的点按组件,需要在用户点按后进行实时地反馈,通过视觉变化等形式,告诉用户点到了什么,现在的点击状态又是什么。

但不同的原生平台,有不同的风格,反馈样式也不同。Android 按钮点击后会有涟漪,iOS 按钮点击后会降低透明度或者加深背景色。RN 是跨平台的,那它应该如何支持多种平台的多种反馈风格呢?

第一代 Touchable 点按组件的设计思路是,提供多种原生平台的反馈风格给开发者自己选择。所以我们看到整个 Touchable 是一套组件,让开发者自己选择。

不过,对于开发者来讲,有经验的开发者可能知道如何进行选择,但新手却要花上很长时间,去了解不同组件之间的区别。所以说,Touchable 点按组件在提供多样性的功能支持的同时,也带来了额外的学习成本。

为了降低学习成本,RN 团队又开发了第二代点按组件------Button

第二代 Button 组件

第二代 Button 组件的实质是对 Touchable 组件的封装。在 Android 上是 TouchableNativeFeedback 组件,在 iOS 上是 TouchableOpacity 组件。

Button 组件的设计思想就是,别让开发者纠结选啥组件了,框架已经选好了,点按反馈的样式就和原生平台的自身风格保持统一就好了。

但是这仍然存在一个问题,那就是要让大多数开发者都选择同一个默认的 UI 样式真是太难了,萝卜白菜各有所爱。

另外,用户的审美也在慢慢地变化,涟漪风格也好,降低透明风格也好,背景高亮风格也好,或许几年后就不会再流行了。甚至连 Button 这个概念本身,都在慢慢地变化,现在的 App 中几乎只要是个图片或者文字都能点按,不再局限于只有四四方方的色块才能点按了。

第三代 Pressable 组件

第三代 Pressable 点按组件,不再是 Touchable 组件的封装,而是一个全新重构的点按组件,它的反馈效果可由开发者自行配置。

下面我们就来看一下 Pressable 组件的相关知识。

Pressable 是一个核心组件的封装,它可以检测到任意子组件的不同阶段的按压交互情况。

js 复制代码
<Pressable onPress={onPressFunction}>
  <Text>I'm pressable!</Text>
</Pressable>

在被 Pressable 包装的元素上:

  • onPressIn 在按压时被调用。
  • onPressOut 在按压动作结束后被调用。

在按下 onPressIn 后,将会出现如下两种情况的一种:

  1. 用户移开手指,依次触发 onPressOutonPress 事件。
  2. 按压持续 500 毫秒以上,触发 onLongPress 事件。(onPressOut 在移开手后依旧会触发。)

下面是针对这几个事件的演示示例:

js 复制代码
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

const App = () => {
  function onPressHandle() {
    console.log("onPressHandle");
  }

  function onPressOutHandle() {
    console.log("onPressOutHandle");
  }

  function onPressInHandle() {
    console.log("onPressInHandle");
  }

  function onLongPressHandle() {
    console.log("onLongPressHandle");
  }

  return (
    <View style={styles.container}>
      <Pressable
        onPress={onPressHandle}
        onPressIn={onPressInHandle}
        onPressOut={onPressOutHandle}
        onLongPress={onLongPressHandle}
      >
        <Text style={{textAlign: 'center'}}>Press Me</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

export default App;

在上面的示例中,当我们轻点按钮时,会依次触发 PressIn、Press、PressOut ,而如果按住不放,则是先触发 PressIn500ms 后触发 LongPress ,松开之后触发 PressOut

关于点按时的样式,也是可以自定义的。来看下面的示例:

js 复制代码
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

const App = () => {
  return (
    <View style={styles.container}>
      <Pressable
        style={({ pressed }) => {
          if (pressed) {
            return styles.pressdStyle;
          } else {
            return styles.unPressdStyle;
          }
        }}
      >
        {({ pressed }) => {
          // 根据是否点按返回不同的子组件
          if (pressed) {
            return (
              <Text
                style={{ textAlign: "center", color: "white", lineHeight: 100 }}
              >
                Pressd
              </Text>
            );
          } else {
            return (
              <Text style={{ textAlign: "center", color: "white" }}>
                Press Me
              </Text>
            );
          }
        }}
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
  pressdStyle: {
    backgroundColor: "rgb(210, 230, 255)",
    height: 100,
    lineHeight: "100",
  },
  unPressdStyle: {
    backgroundColor: "#ccc",
  },
});

export default App;

Pressable 组件有一个可触发区域 HitRect ,默认情况下,可触发区域 HitRect 就是盒模型中的不透明的可见区域。你可以通过修改 hitSlop 的值,直接扩大可触发区域。

例如:

js 复制代码
<Pressable
    hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
>
    ...
</Pressable>

在上面的示例中,我们增加了 Pressable 组件的可点击区域,并且明确指定了 4 个边各自扩充多少。在老点不中、老勾不中的场景中,你可以在不改变布局的前提下,设置 Pressable 组件的可触发区域 HitSlop ,让可点击区域多个 10 像素、20 像素,让用户的更容易点中。

另外,在 Pressable 组件中还有一个可保留区域 PressRect 的概念。

点按事件可保留区域的偏移量(Press Retention Offset )默认是 0 ,也就是说默认情况下可见区域就是可保留区域。你可以通过设置 pressRetentionOffset 属性,来扩大可保留区域 PressRect

举一个例子,当你在购物 App 点击购买按钮时,你已经点到购买按钮了,突然犹豫,开始进行心理博弈,想点又不想点。手指从按钮上挪开了,又挪了进去,然后又挪开了,如此反复。这时还要不要触发点击事件呢?要不要触发,其实是根据你手指松开的位置来判断的,如果你松手的位置在可保留区域内那就要触发,如果不是那就不触发。

最后我们把官网的示例看一下:

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

const App = () => {
  const [timesPressed, setTimesPressed] = useState(0);

  let textLog = "";
  if (timesPressed > 1) {
    textLog = timesPressed + "x onPress";
  } else if (timesPressed > 0) {
    textLog = "onPress";
  }

  return (
    <View style={styles.container}>
      <Pressable
        onPress={() => {
          setTimesPressed((current) => current + 1);
        }}
        style={({ pressed }) => [
          {
            backgroundColor: pressed ? "rgb(210, 230, 255)" : "white",
          },
          styles.wrapperCustom,
        ]}
      >
        {({ pressed }) => (
          <Text style={styles.text}>{pressed ? "Pressed!" : "Press Me"}</Text>
        )}
      </Pressable>
      <View style={styles.logBox}>
        <Text testID="pressable_press_console">{textLog}</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
  text: {
    fontSize: 16,
  },
  wrapperCustom: {
    borderRadius: 8,
    padding: 6,
  },
  logBox: {
    padding: 20,
    margin: 10,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: "#f0f0f0",
    backgroundColor: "#f9f9f9",
  },
});

export default App;

-EOF-

4. 列表组件

本小节我们来看一下 RN 中所提供的列表组件,主要包含:

  • FlatList
  • 下拉刷新
  • 上拉加载更多
  • SectionList

FlatList

FlatList 组件出现之前,RN 使用 ListView 组件来实现列表功能,不过在列表数据比较多的情况下,ListView 组件的性能并不是很好,所以在 0.43.0 版本中,RN 引入了 FlatList 组件。相比 ListView 组件,FlatList 组件适用于加载长列表数据,而且性能也更佳。

ListView 组件类似,FlatList 组件的使用也非常的简单,只需要给 FlatList 组件提供 datarenderItem 两个属性即可,如下所示:

js 复制代码
<FlatList
    data={[{key:"a"},{key:"b"}]}
    renderItem={({item})=><Text>{item.key}</Text>}
>

其中 data 表示数据源,一般为数组格式,renderItem 表示每行的绘制方法。除了 datarenderItem 两个必须属性外,FlatList 还支持诸如 ListHeaderComponentListFooterComponent 等属性,具体可以参阅官方文档:reactnative.dev/docs/flatli...

下面是一个使用 FlatList 渲染电影列表的示例:

首先定义了一个名为 MovieItemCell 的电影项目组件,用于渲染具体的电影项目,包含电影的标题、上映日期、评分、海报、导演、主演等信息。组件代码如下:

js 复制代码
import React from "react";
import {
  TouchableHighlight,
  View,
  Image,
  Text,
  StyleSheet,
} from "react-native";

export default function MovieItemCell(props) {
  const moveInfo = props.movie.item;
  let hasAverageScore = moveInfo.average != "0";
  return (
    <TouchableHighlight onPress={props.onPress}>
      <View style={styles.container}>
        <Image source={{ uri: moveInfo.movieImg }} style={styles.thumbnail} />
        <View style={styles.rightContainer}>
          <Text style={styles.title}>{moveInfo.title}</Text>
          <Text style={styles.year}>{moveInfo.year}</Text>
          {hasAverageScore ? (
            <View style={styles.horizontalView}>
              <Text style={styles.titleTag}>评分:</Text>
              <Text style={styles.score}>{moveInfo.average}</Text>
            </View>
          ) : (
            <View style={styles.horizontalView}>
              <Text style={styles.titleTag}>暂无评分</Text>
            </View>
          )}
          <View style={styles.horizontalView}>
            <Text style={styles.titleTag}>导演:</Text>
            <Text style={styles.name}>{moveInfo.directors}</Text>
          </View>
          <View style={styles.horizontalView}>
            <Text style={styles.titleTag}>主演:</Text>
            <Text style={styles.name}>
              {moveInfo.casts.length > 13
                ? moveInfo.casts.slice(0, 13) + "..."
                : moveInfo.casts}
            </Text>
          </View>
        </View>
      </View>
    </TouchableHighlight>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
    padding: 10,
    borderBottomWidth: 1,
    borderColor: "#e0e0e0",
  },
  thumbnail: {
    width: 110,
    height: 150,
    backgroundColor: "#f0f0f0",
  },
  rightContainer: {
    flex: 1,
    paddingLeft: 10,
    paddingTop: 5,
    paddingBottom: 5,
  },
  title: {
    fontSize: 16,
    fontWeight: "bold",
    color: "#333333",
    textAlign: "left",
  },
  year: {
    textAlign: "left",
    color: "#777777",
    marginTop: 10,
  },
  horizontalView: {
    flexDirection: "row",
    marginTop: 10,
  },
  titleTag: {
    color: "#666666",
  },
  score: {
    color: "#ff8800",
    fontWeight: "bold",
  },
  name: {
    color: "#333333",
    flex: 1,
  },
});

接下来,我们在 App.js 根组件中使用 FlatList 来做列表渲染,如下:

js 复制代码
import React, { useState, useEffect } from "react";
import {
  View,
  FlatList,
  Dimensions,
  Text,
  ActivityIndicator,
  StyleSheet,
  StatusBar,
  SafeAreaView,
} from "react-native";
import { queryMovies } from "./data/Service";
import MovieItemCell from "./view/MovieItemCell";

export const width = Dimensions.get("window").width;

export default function App() {
  // 初始化电影数据
  const data = queryMovies();
  // 初始化电影列表和加载状态
  const [movieList, setMovieList] = useState([]);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      setMovieList(data);
      setLoaded(true);
    }, 1000);
  }, []);

  // 渲染标题
  function renderTitle() {
    return (
      <View style={styles.bayStyle}>
        <Text style={styles.txtStyle}>电影列表</Text>
      </View>
    );
  }

  // 渲染加载条
  function renderLoad() {
    if (!loaded) {
      return (
        <View style={styles.container}>
          <ActivityIndicator animating={true} size="small" />
          <Text style={{ color: "#666666", paddingLeft: 10 }}>努力加载中</Text>
        </View>
      );
    }
  }

  function renderItem(item) {
    return (
      <MovieItemCell
        movie={item}
        onPress={() => {
          alert("点击电影:" + item.item.title);
        }}
      />
    );
  }

  // 渲染电影列表
  function renderList() {
    return (
      <FlatList
        data={movieList}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
      />
    );
  }

  return (
    <SafeAreaView style={styles.flex}>
      {/* 标题区域 */}
      {renderTitle()}
      {/* 加载条 */}
      {renderLoad()}
      {/* 列表区域 */}
      {renderList()}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  flex: {
    flex: 1,
    backgroundColor: "#268dcd",
  },
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
    flexDirection: "row",
  },
  bayStyle: {
    height: 48,
    width: width,
    justifyContent: "center",
    backgroundColor: "#268dcd",
  },
  txtStyle: {
    color: "#fff",
    textAlign: "center",
    fontSize: 18,
  },
});

其中,data 表示数据源,一般为数组格式,renderItem 表示每行的列表项。为了方便对列表单元视图进行复用,通常的做法是将列表单元视图独立出来,例如我们上面的 MovieItemCell 组件。

除此之外,FlatList 组件还有如下一些使用频率比较高的属性和方法:

  • itemkey :使用 FlatList 组件实现列表效果时,系统要求给每一行子组件设置一个 keykey 是列表项的唯一标识,目的是当某个子视图的数据发生改变时可以快速地重绘改变的子组件。一般,我们使用 FlatList 组件提供的 keyExtractor 属性来达到此效果。
js 复制代码
<FlatList
    ...
    keyExtractor={(item) => item.id}
/>
  • 分割线 seperatorFlatList 组件本身的分割线并不是很明显,如果要实现分割线,主要有两种策略:设置 borderBottom 或者 ItemSeperatorComponent 属性。如果只是一条简单的分割线,在 Item 组件里面添加 borderBottom 相关属性即可。
js 复制代码
<View style={{borderTopWidth: 0, borderBottomWidth: 1}}>
    ...
</View>

需要注意的是,使用 borderBottom 实现分割线时,列表顶部和底部的组件是不需要绘制的。

当然,更简单的方式是使用 ItemSeparatorComponent 属性,具体使用方式可以参阅官方文档:reactnative.dev/docs/flatli...

下拉刷新

下拉刷新是一个常见的需求,当用户已经处于列表的最顶端,此时继续往下拉动页面的话,就会有一个数据刷新的操作。

FlatList 中,提供了下拉刷新的功能,我们只需要设置 onRefreshrefreshing 这两个属性值即可。

  • onRefresh:下拉刷新操作触发时要进行的动作,对应是一个函数
  • refreshing:是否显示下拉刷新的等待图标,对应一个布尔值

下面来看一个具体的示例。代码片段如下:

js 复制代码
// 渲染电影列表
function renderList() {
    return (
      <FlatList
        data={movieList}
        renderItem={renderItem}
        keyExtractor={(item) =>
          item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1)
        }
        onRefresh={beginHeaderRefresh}
        refreshing={isHeaderRefreshing}
      />
    );
}

在上面的代码中,当用户下拉刷新时,触发 onRefresh 所对应的 beginHeaderRefresh 函数,此函数对应的操作如下:

js 复制代码
// 下拉刷新
function beginHeaderRefresh() {
    setIsHeaderRefreshing(true);
    // 模拟刷新了两条数据
    const newMovie = randomRefreshMovies();
    const data = [...movieList];
    data.unshift(newMovie[0], newMovie[1]);
    setTimeout(() => {
      setMovieList(data);
      setIsHeaderRefreshing(false)
    }, 1000);
}

首先我们将 isHeaderRefreshing 设置为 true ,以便出现下拉等待图标,之后调用 randomRefreshMovies 方法随机获取两条电影数据,之后模拟异步场景在一秒钟后更新 movieList 并且关闭 isHeaderRefreshing

其中 randomRefreshMovies 是从其他文件导入的,代码如下:

js 复制代码
// 随机刷新两部电影
export function randomRefreshMovies() {
  let randomStartIndex = Math.floor(Math.random() * (moviesData.length - 2));
  return moviesData.slice(randomStartIndex, randomStartIndex + 2);
}

至此,一个模拟的下拉刷新效果就做完了,每次下拉都会随机刷新两部电影在最前面。

上拉加载更多

上拉加载也是列表中一个常见的操作,上拉加载其实质就是以前 PC 端的分页效果。因为数据量过多,所以一般我们不会一次性加载所有的数据,此时就会进行一个分页的显示。而在移动端,分页显示变成了上拉加载的形式,当用户到达列表底部时,自动获取下一页的数据,并且拼接到原有数据的后面。

这里我们会用到两个属性,分别是:

  • onEndReached:上拉加载操作触发时要进行的动作,对应是一个函数
  • onEndReachedThreshold :表示距离底部多远时触发 onEndReached

下面来看一个具体的示例。代码片段如下:

js 复制代码
// 渲染电影列表
function renderList() {
    return (
      <FlatList
        data={movieList}
        renderItem={renderItem}
        keyExtractor={(item) =>
          item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1)
        }
        onRefresh={beginHeaderRefresh}
        refreshing={isHeaderRefreshing}
        onEndReached={beginFooterRefresh}
        onEndReachedThreshold={0.1} // 这里取值0.1,可以根据实际情况调整,取值尽量小
      />
    );
}

首先,在 FlatList 中添加了两个属性,onEndReached 对应 beginFooterRefresh 函数,表示触底时要进行的操作,onEndReachedThreshold 为阀值,这里我们设置的 0.1

beginFooterRefresh 函数对应内容如下:

js 复制代码
// 上拉加载
function beginFooterRefresh() {
    setIsFooterLoad(true);
    if (currentPage < totalPage) {
      currentPage++;
      const newMovie = queryMovies(currentPage, 10);
      const data = [...movieList];
      data.push(...newMovie);
      setTimeout(() => {
        setMovieList(data);
        setIsFooterLoad(false);
      }, 1000);
    }
}

onEndReached 对应的 beginFooterRefresh 函数中,我们首先设置 isFooterLoad 值为 true,这样就能显示下拉加载的等待画面,对应的代码如下:

js 复制代码
function renderFooterLoad() {
    if (isFooterLoad) {
      return (
        <View style={styles.footerStyle}>
          <ActivityIndicator animating={true} size="small" />
          <Text style={{ color: "#666", paddingLeft: 10 }}>努力加载中</Text>
        </View>
      );
    }
}

return (
    <SafeAreaView style={styles.flex}>
      {/* 标题区域 */}
      {renderTitle()}
      {/* 加载条 */}
      {renderLoad()}
      {/* 列表区域 */}
      {renderList()}
      {/* 根据 isFooterLoad 的值决定是否渲染下拉加载的等待画面 */}
      {renderFooterLoad()}
    </SafeAreaView>
);

之后仍然是在 setTimeout 中调用 queryMovies 函数来模拟异步请求,拿到数据后拼接到原来的 movieList 后面,并且关闭下拉加载的等待画面。

至此,一个模拟的上拉加载效果就做完了,每次上拉的时候都会加载 10 条新的电影数据在后面。

SectionList

FlatList 一样,SectionList 组件也是由 VirtualizedList 组件扩展来的。不同于 FlatList 组件,SectionList 组件主要用于开发列表分组、吸顶悬浮等功能。

SectionList 组件的使用方法也非常简单,只需要提供 renderItemrenderSectionHeadersections 等必要的属性即可。

js 复制代码
<SectionList
    renderItem={({item})=> <ListItem title={item.title}/>}
    renderSectionHeader={({section})=><Header title={section.key}/>}
    sections={[
        {data:[...],title:...},
        {data:[...],title:...},
        {data:[...],title:...},
    ]}
/>

常用的属性如下:

  • keyExtractor :和 FlatList 组件一样,表示项目的唯一标识
  • renderSectionHeader :用来渲染每个 section 的头部视图
  • renderItem :用来渲染每一个 section 中的每一个列表项视图
  • sections :用来渲染视图的数据源,类似于 FlatList 中的 data 属性
  • stickySectionHeadersEnabled :当 section 把它前一个 section 的可视区推离屏幕时,这个 sectionheader 是否粘连在屏幕顶端

有关 SectionList 组件更多的属性,可以参阅官方文档:reactnative.dev/docs/sectio...

下面我们来看一个 SectionList 组件的具体示例:

js 复制代码
import React, { useState, useEffect } from "react";
import {
  View,
  Dimensions,
  Text,
  ActivityIndicator,
  StyleSheet,
  SafeAreaView,
  SectionList,
} from "react-native";
import { queryMovies } from "./data/Service";
import MovieItemCell from "./view/MovieItemCell";

export const width = Dimensions.get("window").width;
export const height = Dimensions.get("window").height;

export default function App() {
  // 初始化电影数据
  const displayingMovies = queryMovies(1, 10);
  const incomingMovies = queryMovies(2, 10);

  // 初始化电影列表和加载状态
  const [sectionData, setSectionData] = useState([]);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      setSectionData([
        { title: "正在上映", data: displayingMovies },
        { title: "即将上映", data: incomingMovies },
      ]);
      setLoaded(true);
    }, 1000);
  }, []);

  // 渲染标题
  function renderTitle() {
    return (
      <View style={styles.barStyle}>
        <Text style={styles.txtStyle}>电影列表</Text>
      </View>
    );
  }

  // 渲染加载条
  function renderLoad() {
    if (!loaded) {
      return (
        <View style={styles.container}>
          <ActivityIndicator animating={true} size="small" />
          <Text style={{ color: "#666666", paddingLeft: 10 }}>努力加载中</Text>
        </View>
      );
    }
  }

  function renderItem({item}) {
    return (
      <MovieItemCell
        movie={item}
        onPress={() => {
          alert("点击电影:" + item.title);
        }}
      />
    );
  }

  function renderSectionHeader({ section }) {
    return (
      <View style={styles.sectionHeader}>
        <Text style={styles.sectionTitle}>{section.title}</Text>
      </View>
    );
  }

  // 渲染电影列表
  function renderList() {
    return (
      <SectionList
        keyExtractor={(item) => item.id}
        renderSectionHeader={renderSectionHeader}
        renderItem={renderItem}
        sections={sectionData}
        stickySectionHeadersEnabled={true}
      />
    );
  }

  return (
    <SafeAreaView style={styles.flex}>
      {/* 标题区域 */}
      {renderTitle()}
      {/* 加载条 */}
      {renderLoad()}
      {/* 列表区域 */}
      {renderList()}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  flex: {
    flex: 1,
    backgroundColor: "#fff",
  },
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
    flexDirection: "row",
  },
  loadingView: {
    flex: 1,
    height: height,
    backgroundColor: "#F5FCFF",
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    padding: 10,
  },
  barStyle: {
    height: 48,
    width: width,
    justifyContent: "center",
    backgroundColor: "#fff",
  },
  txtStyle: {
    color: "#000",
    textAlign: "center",
    fontSize: 18,
  },
  sectionHeader: {
    padding: 10,
    backgroundColor: "#268dcd",
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: "bold",
    color: "#fff",
  },
});

-EOF-

5. 功能组件

最后,我们来看一下 RN 中内置的功能组件,这些组件会在用户浏览页面时给予用户不同程度的反馈,也是开发中不可或缺的重要组件。

主要如下:

  • ActivityIndicator
  • KeyboardAvoidingComponent
  • Modal
  • RefreshControl
  • StatusBar

ActivityIndicator

ActivityIndicator 组件常用于发送请求时所显示的等待圆圈,两个常见的属性 sizecolor 分别用于设置等待圆圈的尺寸和颜色。

js 复制代码
import React from "react";
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";

const App = () => (
  <View style={[styles.container, styles.horizontal]}>
    <ActivityIndicator />
    <ActivityIndicator size="large" />
    <ActivityIndicator size="small" color="#0000ff" />
    <ActivityIndicator size="large" color="#00ff00" />
  </View>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center"
  },
  horizontal: {
    flexDirection: "row",
    justifyContent: "space-around",
    padding: 10
  }
});

export default App;

官方 API 文档地址:reactnative.dev/docs/activi...

KeyboardAvoidingComponent

我们在开发的时候,经常会遇到手机上弹出的键盘常常会挡住当前的视图,所以该组件的功能就是解决这个常见问题的,它可以自动根据手机上键盘的位置,调整自身的 position 或底部的 padding,以避免被遮挡。

常用属性:

  • behavior 该参数的可选值为:height、position、padding,来定义其自适应的方式

  • contentContainerStyle 如果设定 behavior 值为 position ,则会生成一个 View 作为内容容器。此属性用于指定此内容容器的样式。

  • keyboardVerticalOffset 视图离屏幕顶部有一定距离时,利用这个属性来补偿修正这段距离(键盘在竖直方向上的偏移量)

用法:

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

<KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
  ... 在这里放置需要根据键盘调整位置的组件 ...
</KeyboardAvoidingView>

下面我们首先来看一个会遮挡输入框的示例:

js 复制代码
import React from "react";
import { View, TextInput, Image, StyleSheet, Dimensions } from "react-native";
import logo from "./assets/logo.png";

const window = Dimensions.get("window");

const IMAGE_HEIGHT = window.width / 2;

const App = () => {
  return (
    <View style={styles.container}>
      <Image source={logo} style={styles.logo} />
      <TextInput placeholder="Email" style={styles.input} />
      <TextInput placeholder="Username" style={styles.input} />
      <TextInput placeholder="Password" style={styles.input} />
      <TextInput placeholder="Confirm Password" style={styles.input} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: "#4c69a5",
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  input: {
    height: 50,
    backgroundColor: "#fff",
    marginHorizontal: 10,
    marginVertical: 5,
    width: window.width - 30,
    paddingLeft: 10,
  },
  logo: {
    height: IMAGE_HEIGHT,
    resizeMode: "contain",
    marginBottom: 20,
    padding: 10,
    marginTop: 20,
  },
  register: {
    marginBottom: 20,
    width: window.width - 100,
    alignItems: "center",
    justifyContent: "center",
    height: 50,
    backgroundColor: "#ffae",
  },
});

export default App;

接下来我们需要做的,就是使用 KeyboardAvoidingView 替换 View ,然后给它加一个 behaviorprop

js 复制代码
<KeyboardAvoidingView 
  style={styles.container} 
  behavior="padding"
  keyboardVerticalOffset={-150}
>
  ...
</KeyboardAvoidingView>

在上面的代码中,我们使用 KeyboardAvoidingView 替换了最外层的 View ,并设置 behavior 属性的值为 paddingkeyboardVerticalOffset 属性也就是用户屏幕顶部和原生视图之间的距离设置为了 -150,从而避免了键盘遮挡输入框。

Modal 组件用来显示一个弹出框,弹出框常用于用户点击了某一个按钮后弹出一段提示信息。

下面是官方所提供的一个关于 Modal 组件的基本示例:

js 复制代码
import React, { useState } from "react";
import { Alert, Modal, StyleSheet, Text, Pressable, View } from "react-native";

const App = () => {
  const [modalVisible, setModalVisible] = useState(false);
  return (
    <View style={styles.centeredView}>
      <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => {
          Alert.alert("Modal has been closed.");
          setModalVisible(!modalVisible);
        }}
      >
        <View style={styles.centeredView}>
          <View style={styles.modalView}>
            <Text style={styles.modalText}>Hello World!</Text>
            <Pressable
              style={[styles.button, styles.buttonClose]}
              onPress={() => setModalVisible(!modalVisible)}
            >
              <Text style={styles.textStyle}>Hide Modal</Text>
            </Pressable>
          </View>
        </View>
      </Modal>
      <Pressable
        style={[styles.button, styles.buttonOpen]}
        onPress={() => setModalVisible(true)}
      >
        <Text style={styles.textStyle}>Show Modal</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  centeredView: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    marginTop: 22
  },
  modalView: {
    margin: 20,
    backgroundColor: "white",
    borderRadius: 20,
    padding: 35,
    alignItems: "center",
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5
  },
  button: {
    borderRadius: 20,
    padding: 10,
    elevation: 2
  },
  buttonOpen: {
    backgroundColor: "#F194FF",
  },
  buttonClose: {
    backgroundColor: "#2196F3",
  },
  textStyle: {
    color: "white",
    fontWeight: "bold",
    textAlign: "center"
  },
  modalText: {
    marginBottom: 15,
    textAlign: "center"
  }
});

export default App;

官方 API 文档地址:reactnative.dev/docs/modal

RefreshControl

该组件在 ScrollViewListView 中用于添加拉动刷新功能。当 ScrollViewscrollY: 0 时,向下滑动会触发 onRefresh 事件。

下面是官方所提供的一个关于 RefreshControl 组件的基本示例:

js 复制代码
import React from 'react';
import { RefreshControl, SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native';

const wait = (timeout) => {
  return new Promise(resolve => setTimeout(resolve, timeout));
}

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

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

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView
        contentContainerStyle={styles.scrollView}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
      >
        <Text>Pull down to see RefreshControl indicator</Text>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
    backgroundColor: 'pink',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;

官方 API 文档地址:reactnative.dev/docs/refres...

StatusBar

StatusBar 是用来控制应用程序状态栏的组件。 状态栏是显示当前时间、Wi-Fi 和蜂窝网络信息、电池电量和/或其他状态图标的区域,通常位于屏幕顶部。

下面是官方所提供的一个关于 StatusBar 组件的基本示例:

js 复制代码
import React, { useState } from 'react';
import { Button, Platform, SafeAreaView, StatusBar, StyleSheet, Text, View } from 'react-native';

const STYLES = ['default', 'dark-content', 'light-content'];
const TRANSITIONS = ['fade', 'slide', 'none'];

const App = () => {
  const [hidden, setHidden] = useState(false);
  const [statusBarStyle, setStatusBarStyle] = useState(STYLES[0]);
  const [statusBarTransition, setStatusBarTransition] = useState(TRANSITIONS[0]);

  const changeStatusBarVisibility = () => setHidden(!hidden);

  const changeStatusBarStyle = () => {
    const styleId = STYLES.indexOf(statusBarStyle) + 1;
    if (styleId === STYLES.length) {
      setStatusBarStyle(STYLES[0]);
    } else {
      setStatusBarStyle(STYLES[styleId]);
    }
  };

  const changeStatusBarTransition = () => {
    const transition = TRANSITIONS.indexOf(statusBarTransition) + 1;
    if (transition === TRANSITIONS.length) {
      setStatusBarTransition(TRANSITIONS[0]);
    } else {
      setStatusBarTransition(TRANSITIONS[transition]);
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar
        animated={true}
        backgroundColor="#61dafb"
        barStyle={statusBarStyle}
        showHideTransition={statusBarTransition}
        hidden={hidden} />
      <Text style={styles.textStyle}>
        StatusBar Visibility:{'\n'}
        {hidden ? 'Hidden' : 'Visible'}
      </Text>
      <Text style={styles.textStyle}>
        StatusBar Style:{'\n'}
        {statusBarStyle}
      </Text>
      {Platform.OS === 'ios' ? (
        <Text style={styles.textStyle}>
          StatusBar Transition:{'\n'}
          {statusBarTransition}
        </Text>
      ) : null}
      <View style={styles.buttonsContainer}>
        <Button
          title="Toggle StatusBar"
          onPress={changeStatusBarVisibility} />
        <Button
          title="Change StatusBar Style"
          onPress={changeStatusBarStyle} />
        {Platform.OS === 'ios' ? (
          <Button
            title="Change StatusBar Transition"
            onPress={changeStatusBarTransition} />
        ) : null}
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#ECF0F1'
  },
  buttonsContainer: {
    padding: 10
  },
  textStyle: {
    textAlign: 'center',
    marginBottom: 8
  }
});

export default App;

官方 API 文档地址:reactnative.dev/docs/status...

6. 第三方组件库

通过前面的学习,我们已经将 RN 官方所提供的内置组件快速过了一遍,但是官方所提供的组件往往是比较基础且局限的,此时我们就可以使用一些第三方组件库来提高我们的开发效率。

本小节我们就一起来看一下 RN 常见的第三方组件库有哪些,并且以 NativeBase 为例做一个第三方组件库的使用示例。

本小节包含以下内容:

  • RN 常见第三方组件库
  • NativeBase 使用示例

RN 常见第三方组件库

1. NativeBase 组件库

NativeBase 是一个广受欢迎的 UI 组件库,为 RN 提供了数十个跨平台组件。在使用 NativeBase 时,你可以使用任意开箱即用的第三方原生库,而这个项目本身也拥有一个丰富的生态系统,从有用的入门套件到可定制的主题模板。

NativeBase 官网地址:nativebase.io/

2. Ant Design Mobile RN 组件库

Ant Design Mobile RN 是由蚂蚁金服推出的 RN 组件库,如果是 React 的开发者都会对 React 的常用组件库 Ant Design 有所耳闻,而 Ant Design Mobile RN 则是蚂蚁金服在 RN 方向的延伸。

特点如下:

  • UI 样式高度可配置,拓展性更强,轻松适应各类产品风格
  • 基于 React Native 的 iOS / Android / Web 多平台支持,组件丰富、能全面覆盖各类场景 (antd-mobile)
  • 提供 "组件按需加载" / "Web 页面高清显示" / "SVG Icon" 等优化方案,一体式开发
  • 使用 TypeScript 开发,提供类型定义文件,支持类型及属性智能提示,方便业务开发
  • 全面兼容 react

Ant Design Mobile RN 官网地址:rn.mobile.ant.design/index-cn

3. React Native Elements 组件库

React Native Elements 是一个高度可定制的跨平台 UI 工具包,完全用 Javascript 构建。该库的作者声称"React Native Elements 的想法更多的是关于组件结构而不是设计,这意味着在使用某些元素时可以减少样板代码,但可以完全控制它们的设计",这对于开发新手和经验丰富的老手来说都很有吸引力。

React Native Elements 官网地址:reactnativeelements.com/

4. React Native Material 组件库

React Native Material UI 是一组高度可定制的 UI 组件,实现了谷歌的 Material Design 。请注意,这个库使用了一个名为 uiThemeJS 对象,这个对象在上下文间传递,以实现最大化的定制化能力。

React Native Material 官网地址:www.react-native-material.com/

5. Nachos UI 组件库

Nachos UI 是一个 RN 组件库,提供了 30 多个可定制的组件,这些组件也可以通过 react-native-webWeb 上运行。它通过了快照测试,支持格式化和 yarn,提供了热火的设计和全局主题管理器。

Nachos UI 官网地址:avocode.com/nachos-ui

6. React Native Paper 组件库

React Native Paper 是一个跨平台的 UI 组件库,它遵循 Material Design 指南,提供了全局主题支持和可选的 babel 插件,用以减少捆绑包大小。

React Native Paper 官网地址:callstack.github.io/react-nativ...

NativeBase 使用示例

上面罗列了很多 RN 的第三方组件库,但并不是说每一个我们都需要去学习,在开发时选择一个自己用的惯的来使用即可。

这里我们以第一个 NativeBase 为例来演示如何使用第三方组件库。

要使用第三方组件库,首先第一步需要进行安装。

官方提供了安装指南:docs.nativebase.io/installatio...

可以看到,在安装指南中,官方根据开发者不同形式搭建的 RN 项目,提供了对应的安装方式。

由于我们目前的 RN 项目是使用 expo 搭建的,因此选择对应的安装指南。

上面分为了"新项目"和"已有项目",选择已有项目,然后根据指南输入下面的指令:

js 复制代码
npm install native-base
expo install react-native-svg
expo install react-native-safe-area-context

注:安装过程中可能会涉及到科学上网,请自行解决网络问题

当然,你也可以选择基于 NativeBase 组件库创建一个全新的项目,命令为:

js 复制代码
expo init my-app --template @native-base/expo-template

具体的操作如下图所示:

最后,我们可以测试一下组件库是否安装成功。

下面的代码是创建一个全新的 NativeBase 项目时,根组件 App.js 所对应的代码:

js 复制代码
import React from "react";
import {
  Text,
  Link,
  HStack,
  Center,
  Heading,
  Switch,
  useColorMode,
  NativeBaseProvider,
  extendTheme,
  VStack,
  Button,
  Box,
} from "native-base";
import NativeBaseIcon from "./components/NativeBaseIcon";

// Define the config
const config = {
  useSystemColorMode: false,
  initialColorMode: "dark",
};

// extend the theme
export const theme = extendTheme({ config });

export default function App() {
  return (
    <NativeBaseProvider>
      <Center
        _dark={{ bg: "blueGray.900" }}
        _light={{ bg: "blueGray.50" }}
        px={4}
        flex={1}
      >
        <VStack space={5} alignItems="center">
          <NativeBaseIcon />
          <Heading size="lg">Welcome to NativeBase</Heading>
          <HStack space={2} alignItems="center">
            <Text>Edit</Text>
            <Box
              _web={{
                _text: {
                  fontFamily: "monospace",
                  fontSize: "sm",
                },
              }}
              px={2}
              py={1}
              _dark={{ bg: "blueGray.800" }}
              _light={{ bg: "blueGray.200" }}
            >
              App.js
            </Box>
            <Text>and save to reload.</Text>
          </HStack>
          <Button onPress={() => console.log("hello world")}>Click Me</Button>
          <Link href="https://docs.nativebase.io" isExternal>
            <Text color="primary.500" underline fontSize={"xl"}>
              Learn NativeBase
            </Text>
          </Link>
          <ToggleDarkMode />
        </VStack>
      </Center>
    </NativeBaseProvider>
  );
}

// Color Switch Component
function ToggleDarkMode() {
  const { colorMode, toggleColorMode } = useColorMode();
  return (
    <HStack space={2} alignItems="center">
      <Text>Dark</Text>
      <Switch
        isChecked={colorMode === "light"}
        onToggle={toggleColorMode}
        aria-label={
          colorMode === "light" ? "switch to dark mode" : "switch to light mode"
        }
      />
      <Text>Light</Text>
    </HStack>
  );
}

在根组件中引入了 NativeBaseIcon 组件,该组件位于 components 目录下面:

js 复制代码
import React from "react";
import { Icon } from "native-base";
import { G, Path } from "react-native-svg";
const NativeBaseIcon = () => {
  return (
    <Icon size="220px" viewBox="0 0 602.339 681.729">
      <G
        id="Group_403"
        data-name="Group 403"
        transform="translate(14575 1918.542)"
      >
        <Path
          id="Path_1"
          data-name="Path 1"
          d="M488.722,0A45.161,45.161,0,0,1,527.83,22.576L675.676,278.584a45.162,45.162,0,0,1,0,45.171L527.83,579.763a45.162,45.162,0,0,1-39.108,22.576H193.008A45.162,45.162,0,0,1,153.9,579.763L6.053,323.755a45.162,45.162,0,0,1,0-45.171L153.9,22.576A45.162,45.162,0,0,1,193.008,0Z"
          transform="translate(-13972.661 -1918.542) rotate(90)"
          fill="#356290"
        />
        <Path
          id="Path_252"
          data-name="Path 252"
          d="M401.1,0A60.816,60.816,0,0,1,453.77,30.405L567.2,226.844a60.816,60.816,0,0,1,0,60.82L453.77,484.1A60.816,60.816,0,0,1,401.1,514.509H174.241A60.816,60.816,0,0,1,121.575,484.1L8.149,287.665a60.816,60.816,0,0,1,0-60.82L121.575,30.405A60.816,60.816,0,0,1,174.241,0Z"
          transform="translate(-14016.576 -1865.281) rotate(90)"
          fill="#1784b2"
        />
        <Path
          id="Path_251"
          data-name="Path 251"
          d="M345.81,0a36.573,36.573,0,0,1,31.674,18.288L480.566,196.856a36.573,36.573,0,0,1,0,36.569L377.484,411.993a36.573,36.573,0,0,1-31.674,18.288H139.655a36.572,36.572,0,0,1-31.674-18.288L4.9,233.425a36.573,36.573,0,0,1,0-36.569L107.981,18.288A36.573,36.573,0,0,1,139.655,0Z"
          transform="translate(-14058.69 -1820.41) rotate(90)"
          fill="#50bfc3"
        />
        <Path
          id="_x3C__x2F__x3E_"
          d="M187.066,335.455V297.993l-65.272-21.949,65.272-22.523V216.059L81,259.5v32.521Zm38.726,29.3L286.123,174H256.7l-60.33,190.759Zm72.052-29.3,106.066-43.783V259.267L297.844,216.059V254.44l59.3,23.328-59.3,19.421Z"
          transform="translate(-14516.286 -1846.988)"
          fill="#fff"
        />
      </G>
    </Icon>
  );
};

export default NativeBaseIcon;

效果如下图所示:

至此,我们就成功了引入了 NativeBase 组件库,之后要使用该组件库中的某一个组件,只需要按照文档说明引入然后使用即可。

相关推荐
老码沉思录1 天前
React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
javascript·react native·react.js
老码沉思录1 天前
React Native 全栈开发实战班 :数据管理与状态之React Hooks 基础
javascript·react native·react.js
老码沉思录1 天前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
老码沉思录1 天前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
老码沉思录2 天前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录2 天前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
老码沉思录2 天前
React Native 全栈开发实战班 - 核心组件与导航
javascript·react native·react.js
老码沉思录2 天前
React Native 全栈开发实战班 - 导航栈定制
javascript·react native·react.js
堕落年代5 天前
React Native使用axios会不会有问题
javascript·react native·react.js