React Native 进行 Modal 的封装使用

作者: 这真不是玩笑 | 来源:发表于2018-04-04 16:27 被阅读325次

    1.背景

    在文章例子中的 RN(以下用 RN 表示 React Native )版本是 0.55.0。在项目的开发中,会遇到很多全屏弹窗的使用需求,而 RN 官方也提供了一个这样的组件 Modal。但是在实际使用中,在 ios 端如果显示一个 Modal 的时候去打开一个新的 Modal 将无法打开。同时在一个页面里面存在两个以上 Modal 控件的时候,打开第三个 Modal 的时候页面会卡主。

    2.使用 RootSiblings 封装自己的 Modal

    解决上面的问题,可以采用的一种方法就是使用一个全屏的 View 去代替使用 RN 的 Modal 控件,但是有个弊端就是这个 View 的父布局需要是全屏的并且需要考虑这个 View 的布局位置必须在最外层,否则会被其他的控件挡住。这里有其他的方法去解决这个问题,就是采用 RootSiblings 去封装自己的 Modal。

    2.1 RootSiblings 使用介绍

    这里说的 RootSiblings 指的是第三方库 react-native-root-siblings ,实现原理就是重写了系统的 AppRegistry.registerComponent 方法,当我们通过这个方法注册根组件的时候,替换根组件为我们自己的实现的包装类。包装类中监听了目标通知 siblings.update,接收到通知就将通知传入的组件视图添加到包装类顶层,然后进行刷新显示。这样来看就可以解决上面使用 View 实现全屏弹窗的问题。

    2.2 使用 RootSiblings 封装自己的 Modal 控件

    在这里创建 ModalView 组件类进行封装使用。对于 Android 平台下,我们依旧使用 RN 的 Modal 组件来实现全屏弹窗效果。而对于 ios 平台,我们就是使用今天的重点 RootSiblings 来进行封装使用。ModalView 完整代码如下

    /**
     * 作者:请叫我百米冲刺 on 2018/4/3 下午4:24
     * 邮箱:mail@hezhilin.cc
     *
     * 因为ios端同时只能存在一个 Modal,并且显示第三个 Modal 界面的时候有奇怪的 bug
     *
     * 为了兼容 ios 的使用,这里需要封装一个 ModalView
     *
     * Android 依旧使用 React Native Modal 来进行实现
     * ios 的话采用 RootSiblings 配合进行使用
     *
     * 同时采用与 React Native Modal 相同的API
     */
    'use strict';
    import React, {Component} from "react";
    import {Modal, Animated, Platform, Easing, StyleSheet, Dimensions} from "react-native";
    import RootSiblings from 'react-native-root-siblings';
    
    const {height} = Dimensions.get('window')
    var isAndroid = Platform.OS == 'android'
    
    export default class ModalView extends Component {
    
        constructor(props) {
            super(props);
            this.state = {
                visible: false, //给android式的modal进行使用的
                animationSlide: new Animated.Value(0),
                animationFade: new Animated.Value(0)
            };
            //ios也可以指定使用android的实现方式
            if (this.props.useAndroid) {
                isAndroid = true
            }
        }
    
        render() {
            this.RootSiblings && this.RootSiblings.update(this.renderIos())
            return isAndroid ? this.renderAndroid() : null
        }
    
        renderAndroid = () => {
            return (
                <Modal {...this.props}
                       transparent={true}
                       visible={this.state.visible}
                       onRequestClose={() => {
                           if (this.props.onRequestClose) {
                               this.props.onRequestClose()
                           } else {
                               this.disMiss()
                           }
                       }}>
                    {this.props.children}
                </Modal>
            )
        }
    
        renderIos = () => {
            return (
                <Animated.View style={[styles.root,
                    {opacity: this.state.animationFade},
                    {
                        transform: [{
                            translateY: this.state.animationSlide.interpolate({
                                inputRange: [0, 1],
                                outputRange: [height, 0]
                            }),
                        }]
                    }]}>
                    {this.props.children}
                </Animated.View>
            );
        }
    
        show = (callback) => {
            if (this.isShow()) {
                return
            }
            if (isAndroid) {
                this.setState({visible: true}, () => callback && callback())
            } else {
                this.RootSiblings = new RootSiblings(this.renderIos(), () => {
                    if (this.props.animationType == 'fade') {
                        this.animationFadeIn(callback)
                    } else if (this.props.animationType == 'slide') {
                        this.animationSlideIn(callback)
                    } else {
                        this.animationNoneIn(callback)
                    }
                });
            }
        }
    
        disMiss = (callback) => {
            if (!this.isShow()) {
                return
            }
            if (isAndroid) {
                this.setState({visible: false}, () => callback && callback())
            } else {
                if (this.props.animationType == 'fade') {
                    this.animationFadeOut(callback)
                } else if (this.props.animationType == 'slide') {
                    this.animationSlideOut(callback)
                } else {
                    this.animationNoneOut(callback)
                }
            }
        }
    
        isShow = () => {
            if (isAndroid) {
                return this.state.visible
            } else {
                return this.RootSiblings ? true : false
            }
        }
    
        animationNoneIn = (callback) => {
            this.state.animationSlide.setValue(1)
            this.state.animationFade.setValue(1)
            callback && callback()
        }
    
        animationNoneOut = (callback) => {
            this.animationCallback(callback);
        }
    
        animationSlideIn = (callback) => {
            this.setState({visible: true}, () => {
                this.state.animationSlide.setValue(0)
                this.state.animationFade.setValue(1)
                Animated.timing(this.state.animationSlide, {
                    easing: Easing.linear(),
                    duration: 300,
                    toValue: 1,
                }).start(() => callback && callback());
            })
        }
    
        animationSlideOut = (callback) => {
            this.state.animationSlide.setValue(1)
            this.state.animationFade.setValue(1)
            Animated.timing(this.state.animationSlide, {
                easing: Easing.linear(),
                duration: 300,
                toValue: 0,
            }).start(() => this.animationCallback(callback));
        }
    
        animationFadeIn = (callback) => {
            this.setState({visible: true}, () => {
                this.state.animationSlide.setValue(1)
                this.state.animationFade.setValue(0)
                Animated.timing(this.state.animationFade, {
                    easing: Easing.linear(),
                    duration: 300,
                    toValue: 1,
                }).start(() => callback && callback());
            })
        }
    
        animationFadeOut = (callback) => {
            this.state.animationSlide.setValue(1)
            this.state.animationFade.setValue(1)
            Animated.timing(this.state.animationFade, {
                easing: Easing.linear(),
                duration: 300,
                toValue: 0,
            }).start(() => this.animationCallback(callback));
        }
    
        animationCallback = (callback) => {
            this.RootSiblings && this.RootSiblings.destroy(() => {
                callback && callback()
                this.RootSiblings = undefined
            })
        }
    }
    
    const styles = StyleSheet.create({
        root: {
            position: 'absolute',
            left: 0,
            top: 0,
            right: 0,
            bottom: 0
        }
    })
    

    在这里我们让改 ModalView 使用 RN 的 Modal 相同的 Api,对于 ios 部分,在这里我们添加了三种启动动画模式,类似于 RN Modal 的 slide,fade动画,以及无动画 none。动画实现使用的是 Animated 和 Easing ,具体 Animated 的使用方法可以参照文档 React Native 动画

    2.3 ModalView 实现步骤分析

    在 ModalView 调用 show 方法的时候,这里要创建 RootSiblings 并将需要全屏显示的组件添加到 RootSiblings 之中,同时根据动画展示类型去调用相关的 Animated 动画

    renderIos = () => {
       return (
          <Animated.View style={[styles.root,
              {opacity: this.state.animationFade},
              {
                 transform: [{
                   translateY: this.state.animationSlide.interpolate({
                    inputRange: [0, 1],
                    outputRange: [height, 0]
                    }),
                  }]
              }]}>
              {this.props.children}
         </Animated.View>
        );
     }
    this.RootSiblings = new RootSiblings(this.renderIos(), () => {
          if (this.props.animationType == 'fade') {
                this.animationFadeIn(callback)
           } else if (this.props.animationType == 'slide') {
                this.animationSlideIn(callback)
           } else {
               this.animationNoneIn(callback)
          }
    });
    

    在 ModalView 调用 disMiss 方法的时候,要去销毁这么这个 RootSiblings 组件

    animationCallback = (callback) => {
            this.RootSiblings && this.RootSiblings.destroy(() => {
                callback && callback()
                this.RootSiblings = undefined
            })
        }
    

    同时在这里要注意的是,在 RN 的界面更新之中,通过 state 去进行组件 UI 界面的更新,最终会反馈到 render() 方法中。由于 ModalView 的实现方式是通过 RootSiblings 把显示的组件直接添加到根组件的。无法通过 state 去更新界面 UI ,所以在这里我们需要主动调用 update 方法去进行界面的更新。

    render() {
        this.RootSiblings && this.RootSiblings.update(this.renderIos())
        return isAndroid ? this.renderAndroid() : null
    }
    

    假如需要展示的弹窗页面没有使用 state 进行界面 UI 的更新,那么就不需要调用 RootSiblings.update 方法。

    2.4 ModalView 简单展示

    ios.gif
    android.gif

    动图上展示了带动画的弹窗展示以及多个 Modal 进行展示

    3.最后

    要使用上面的 ModalView ,只需要导入第三方库 react-native-root-siblings ,然后把 ModalView 拷贝到你的项目中就可以了。最后附上上面的 demo 的地址:https://github.com/hzl123456/ModalViewDemo

    相关文章

      网友评论

      • 米奇小林:刚 遇到这个问题,不过 让原生处理掉了,facebook 封装的也太垃圾了

      本文标题:React Native 进行 Modal 的封装使用

      本文链接:https://www.haomeiwen.com/subject/mwekhftx.html