RN自定义组件

309 阅读6分钟

自定义组件案例 1:弹框组件

在使用 RN 进行跨平台应用开发时,官方提供的组件往往是有限的,并且很多组件并不是多平台通用的,有些只针对特别的平台。此时,要想在应用开发上保持页面样式的一致性,除了直接选择第三方开源库以外,另一个有效的手段就是自定义组件。

本小节我们来自定义一个弹框组件。

首先我们简单复习一下 ES6 中导入导出模块的相关知识。

// 组件导出
export default class App extends Component{
    ...
}

// 组件导入
import App from './App'

除了组件外,变量和常量也支持导入和导出。

// 变量和常量导出
var name = "张三";
const age = "18";
export {name, age}

// 变量和常量的导入
import {name, age} from "./App"

方法的导入以及导出,和变量、常量的导入导出类似。

// 方法导出
export function sum(a, b){
    return a + b
}

// 方法导入
import {sum} from "./Util"

另外,我们还会使用到 propTypes。通常,通用组件需要使用自定义属性的方式接收外界传入的值,如果是必须要传入的值,可以使用 isRequired 关键字。例如:

static propTypes = {
    title: PropTypes.string.PropTypes.func.isRequired,
    content: PropTypes.string,
}

需要注意的是,由于 propTypes15.0.0 版本中已经被移除掉了,所以在 15.5.0 以及之后的版本中,需要使用新的方式引入。

import PropTypes from 'prop-types';

好了,前置内容介绍完毕后,接下来我们就来封装第一个自定义组件——弹框组件。

image-20220620132110129

上图是封装好之后的效果,可以看到,整个弹框由 4 个部分组成,分别是图片、标题、内容、确认按钮以及关闭按钮。其中图片、标题、内容、确认按钮内容都是应该在使用组件时传递进去的。

完整的封装组件代码如下:

import React, { Component } from "react";
import PropTypes from "prop-types";
import {
  View,
  Text,
  TouchableOpacity,
  Image,
  StyleSheet,
  ImageBackground,
  Dimensions,
} from "react-native";

const { width } = Dimensions.get("window");

export default class FreeDialog extends Component {
  static propTypes = {
    isShow: PropTypes.bool.isRequired,
    title: PropTypes.string,
    content: PropTypes.string,
    buttonContent: PropTypes.string,
    closeDialog: PropTypes.func.isRequired,
    imageSource: PropTypes.string.isRequired,
  };

  closeDialogHandle() {
    this.props.closeDialog();
  }

  render() {
    if (!this.props.isShow) {
      return null;
    } else {
      return (
        <View style={styles.containerBg}>
          <View style={[styles.dialogBg]}>
            <Image source={this.props.imageSource} style={styles.logoStyle} />
            <Text style={styles.titleStyle}>{this.props.title}</Text>
            <Text style={styles.contentStyle}>{this.props.content}</Text>
            <TouchableOpacity>
              <ImageBackground
                resizeMode="stretch"
                source={require("../images/commen_btn.png")}
                style={styles.buttonStyle}
              >
                <Text style={styles.btnContentStyle}>
                  {this.props.buttonContent}
                </Text>
              </ImageBackground>
            </TouchableOpacity>
          </View>

          <TouchableOpacity
            style={styles.btnCloseStyle}
            onPress={this.closeDialogHandle.bind(this)}
          >
            <Image
              source={require("../images/ic_close.png")}
              style={{ height: 38, width: 38 }}
            />
          </TouchableOpacity>
        </View>
      );
    }
  }
}

const styles = StyleSheet.create({
  containerBg: {
    backgroundColor: "rgba(0, 0, 0, 0.7)",
    position: "absolute",
    left: 0,
    right: 0,
    bottom: 0,
    top: 0,
    overflow: "hidden",
    justifyContent: "center",
    alignItems: "center",
  },
  dialogBg: {
    width: width - 100,
    backgroundColor: "#ffffff",
    borderRadius: 10,
    overflow: "hidden",
    alignItems: "center",
  },
  logoStyle: {
    height: ((width - 100) * 258) / 400,
    width: width - 100,
  },
  titleStyle: {
    marginTop: 14,
    color: "#333333",
    fontSize: 18,
    fontWeight: "600",
  },
  contentStyle: {
    marginTop: 5,
    color: "#333333",
    fontSize: 14,
    fontWeight: "400",
  },
  buttonStyle: {
    height: ((width - 135) * 88) / 480,
    width: width - 180,
    marginTop: 36,
    marginBottom: 22,
    alignItems: "center",
    justifyContent: "center",
  },
  btnContentStyle: {
    fontSize: 16,
    color: "white",
    textAlign: "center",
    fontWeight: "600",
  },
  btnCloseStyle: {
    padding: 10,
    marginTop: 33,
    alignItems: "center",
  },
});

App.js 根组件中使用测试:

import React, { PureComponent } from "react";
import {
  TouchableOpacity,
  StyleSheet,
  Text,
  View,
  Dimensions,
} from "react-native";
import FreeDialog from "./components/FreeDialog";

const { width } = Dimensions.get("window");

export default class DialogPage extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      isShowDialog: false,
    };
  }

  renderDialog() {
    return (
      <FreeDialog
        isShow={this.state.isShowDialog}
        closeDialog={this.closeDialog.bind(this)}
        title={"年终大促"}
        content={"您有新的新年礼品请查收!"}
        buttonContent={"新年礼品请查收"}
        imageSource={require("./images/dialog_bg.png")}
      />
    );
  }

  showDialog() {
    this.setState({
      isShowDialog: true,
    });
  }

  closeDialog() {
    this.setState({
      isShowDialog: false,
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.btnContainer}
          onPress={this.showDialog.bind(this)}
        >
          <Text style={styles.textStyle}>免费咨询医生</Text>
        </TouchableOpacity>
        {this.renderDialog()}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
  },
  btnContainer: {
    marginTop: 15,
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: "#EE7942",
    height: 38,
    width: width - 100,
    borderRadius: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  textStyle: {
    color: "#ffffff",
    fontSize: 18,
  },
});

-EOF-

自定义组件案例 2:单选组件

RN 中,官方并没有提供单选组件,如果应用开发中涉及到单选功能,就需要开发者使用第三方开源库或者自己封装单选组件。

通常,一个正常的单选功能会包含若干个子选项,每个子选项的前面有一个标识勾选状态的圆环,当某个选项被选中时圆环会变成实心,表示选中状态。如下图所示:

image-20220620132355185 image-20220620132411214

要完成自定义单选功能,首先需要自定义一个单选按钮组件。通过分析可以发现,单选按钮的左边是图片,右边是描述文字,按钮的图片有选中和未选中两种状态。基于此,在开发的时候可以定义一个状态变量 selected 来记录按钮的选中状态,为 true 时表示选中,为 false 时表示未选中。

state = {
    selected: this.props.selected   // 状态由外部传入
};

为了保证自定义组件的通用性,除了状态是由外部传入的,JSX 中要渲染的图片和文字以及按钮的样式也需要由外部传入,因此可以定义如下一些必要的参数或属性供外部传入。

const {text, drawablePadding, style} = this.props;

当单选按钮接收到外部传入的属性后,就可以执行渲染操作了。同时,当改变按钮的选中状态之后,还需要将状态传递出去,此时就需要使用默认属性。自定义单选按钮的示例如下:

import React, { PureComponent } from "react";
import { View, Pressable, Text, Image, StyleSheet } from "react-native";
let selectedImage = require("../assets/radio_selted.png");
let unSelectedImage = require("../assets/radio_select.png");

export default class RadioButton extends PureComponent {
  static defaultProps = {
    selectedChanged: false,
    selectedTextColor: "#F83D2B",
    unSelectedTextColor: "#333333",
  };

  state = {
    selected: this.props.selected,
  };

  constructor(props) {
    super(props);
    this.selectedChanged = props.selectedChanged;
  }

  // 每个按钮的点击事件
  pressHandle() {
    this.selectedChanged(this.state.selected);
    this.setState({
      selected: !this.state.selected,
    });
  }

  render() {
    const { text, drawablePadding } = this.props;
    const { selected } = this.state;

    return (
      <Pressable onPress={this.pressHandle.bind(this)}>
        <View style={styles.radioStyle}>
          {/* 左边图片 */}
          <Image
            style={styles.image}
            source={selected ? selectedImage : unSelectedImage}
          />
          {/* 右边文字 */}
          <Text
            style={{
              color: selected
                ? this.props.selectedTextColor
                : this.props.unSelectedTextColor,
              marginLeft: drawablePadding,
              fontSize: 18,
            }}
          >
            {text}
          </Text>
        </View>
      </Pressable>
    );
  }

  setSelectedState(state) {
    this.setState({
      selected: state,
    });
  }
}

const styles = StyleSheet.create({
  radioStyle: {
    flexDirection: "row",
    alignItems: "center",
    marginLeft: 20,
    marginRight: 20,
    marginTop: 10,
    marginBottom: 10,
  },
  image: {
    width: 22,
    height: 22,
  },
  text: {
    flexDirection: "row",
    alignItems: "center",
  },
});

上面的代码完成了一个自定义单选按钮的功能,并不能真正的实现单选功能。因为一个正常的单选功能会包含若干子选项,而且只能有一个选项被选中。

因此,要完成自定义单选功能,还需要定义一个容器组件,这个容器组件会包含多个单选按钮,并且只有一个能选中。通过分析,自定义的单选组件的数据源、排列方向和默认选中项都需要由外界传入,因此自定义的单选组件至少需要提供如下一些属性供使用方传入。

const {data, orientation, defaultValue, drawablePadding} = this.props;

当然,除了上面的一些必要属性,开发者还可以根据实际需要自由定制。下面是自定义单选按钮的容器组件示例代码:

import React, { PureComponent } from "react";
import { View } from "react-native";
import RadioButton from "./RadioButton";

export default class RadioGroup extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      currentIndex: -1, // 当前选中的索引
      dataArray: [],
      itemChange: props.itemChange,
    };
  }

  render() {
    // 获取参数
    const { data, orientation, defaultValue, drawablePadding } =
      this.props;

    return (
      <View style={{ flexDirection: orientation }}>
        {data.map((radioData, index) => {
          return (
            <RadioButton
              index={index}
              selected={index === defaultValue ? true : false}
              key={index}
              ref={(radioButton) => this.state.dataArray.push(radioButton)}
              text={radioData.text}
              oritation={orientation}
              drawablePadding={drawablePadding}
              selectedChanged={() => {
                this.change(index);
              }}
            />
          );
        })}
      </View>
    );
  }

  change(index) {
    this.state.currentIndex = index;
    console.log(this.state.dataArray)
    this.state.dataArray.map((refer, index2) => {
      if (refer !== null) {
        refer.setSelectedState(index2 === this.state.currentIndex);
      }
    });
    this.state.itemChange(this.state.currentIndex);
  }
}

在上面的自定义单选按钮容器组件中,当某个子选项被选中后,会调用 change 方法改变选中按钮的状态,进而通知组件进行视图更新。

至此,自定义单选组件就算基本开发完成了。使用时,只需要根据要求传入必要的属性即可。

<RadioGroup
  orientation="row"
  data={data}
  defaultValue={0}
  drawablePadding={8}
  itemChange={(index) => {
    alert(index);
  }}
></RadioGroup>

下面是我们在根组件中进行测试的完整代码:

import React, { PureComponent } from "react";
import { View, Text, StyleSheet } from "react-native";
import RadioGroup from "./components/RadioGroup";

export default class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      data:[{ text: "个人" }, { text: "单位" }, { text: "其他" }]
    };
  }
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>发票抬头</Text>
        <RadioGroup
          orientation="column"
          data={this.state.data}
          defaultValue={0} //默认选中的值
          drawablePadding={10} //图片与文字的间距
          itemChange={(index) => {
            alert(index);
          }}
        ></RadioGroup>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    marginTop: 35,
  },
  title: {
    height : 30,
    fontSize: 20,
    borderColor: "black",
    marginLeft: 15,
    fontWeight: "bold",
  },
});

-EOF-