React Native 进行 Modal 的封装使用

在使用 React Native(以下简称 RN ,使用版本为 0.59.5) 开发 App 的过程中,有许许多多使用到弹窗控件的场景,虽然 RN 自带了一个 Modal 控件,但是在使用过程中它有一些不太好的体验和问题。

  • Android 端的 Modal 控件无法全屏,也就是内容无法从状态栏处开始布局。
  • ios 端的 Modal 控件的层级太高,是基于 window 的,如果在 Modal 中打开一个新的 ViewController 界面的时候,将会被 Modal 控件给覆盖住,同时 ios 的 Modal 控件只能弹出一个。

针对上面所发现的问题,我们需要对 RN 的 Modal 控件整体做一个修改和封装,以便于在使用中可以应对各种不同样的业务场景。

Android FullScreenModal 的封装使用

针对第一个问题,查看了 RN Modal 组件在 Android 端的实现,发现它是对 Android Dialog 组件的一个封装调用,那么假如我能实现一个全屏展示的 Dialog,那么是不是在 RN 上也就可以实现全屏弹窗了。

Android 原生实现全屏 Dialog

FullScreenDialog 主要实现代码如下

public class FullScreenDialog extends Dialog {

    private boolean isDarkMode;
    private View rootView;

    public void setDarkMode(boolean isDarkMode) {
        this.isDarkMode = isDarkMode;
    }

    public FullScreenDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);
    }

    @Override
    public void setContentView(@NonNull View view) {
        super.setContentView(view);
        this.rootView = view;
    }

    @Override
    public void show() {
        super.show();
        StatusBarUtil.setTransparent(getWindow());
        if (isDarkMode) {
            StatusBarUtil.setDarkMode(getWindow());
        } else {
            StatusBarUtil.setLightMode(getWindow());
        }
        AndroidBug5497Workaround.assistView(rootView, getWindow());
    }
}

在这里主要起作用的是 StatusBarUtil.setTransparent(getWindow()); 方法,它的主要作用是将状态栏背景透明,并且让布局内容可以从 Android 状态栏开始。

   /**
     * 使状态栏透明,并且是从状态栏处开始布局
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static void transparentStatusBar(Window window) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            View decorView = window.getDecorView();
            int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            decorView.setSystemUiVisibility(option);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }

这里需要注意的是,该方法只有在 Android 4.4 以上才会有效果,不过如今已经是 9012 年了,主流 Android 用户使用的版本应该没有低于 Android 4.4 了吧。

封装给 RN 进行相关的调用

Android 原生部分实现

有了 FullScreenDialog ,下一步就是封装组件给 RN 进行调用了,这里主要的步骤就是参考 RN Modal 的 Android 端实现,然后替换其中的 Dialog 为 FullScreenDialog,最后封装给 RN 进行调用。

public class FullScreenModalManager extends ViewGroupManager<FullScreenModalView> {
    @Override
    public String getName() {
        return "RCTFullScreenModalHostView";
    }

    public enum Events {
        ON_SHOW("onFullScreenShow"),
        ON_REQUEST_CLOSE("onFullScreenRequstClose");
        private final String mName;

        Events(final String name) {
            mName = name;
        }

        @Override
        public String toString() {
            return mName;
        }
    }

    @Override
    @Nullable
    public Map getExportedCustomDirectEventTypeConstants() {
        MapBuilder.Builder builder = MapBuilder.builder();
        for (Events event : Events.values()) {
            builder.put(event.toString(), MapBuilder.of("registrationName", event.toString()));
        }
        return builder.build();
    }

    @Override
    protected FullScreenModalView createViewInstance(ThemedReactContext reactContext) {
        final FullScreenModalView view = new FullScreenModalView(reactContext);
        final RCTEventEmitter mEventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
        view.setOnRequestCloseListener(new FullScreenModalView.OnRequestCloseListener() {
            @Override
            public void onRequestClose(DialogInterface dialog) {
                mEventEmitter.receiveEvent(view.getId(), Events.ON_REQUEST_CLOSE.toString(), null);
            }
        });
        view.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialog) {
                mEventEmitter.receiveEvent(view.getId(), Events.ON_SHOW.toString(), null);
            }
        });
        return view;
    }

    @Override
    public LayoutShadowNode createShadowNodeInstance() {
        return new FullScreenModalHostShadowNode();
    }

    @Override
    public Class<? extends LayoutShadowNode> getShadowNodeClass() {
        return FullScreenModalHostShadowNode.class;
    }

    @Override
    public void onDropViewInstance(FullScreenModalView view) {
        super.onDropViewInstance(view);
        view.onDropInstance();
    }

    @ReactProp(name = "autoKeyboard")
    public void setAutoKeyboard(FullScreenModalView view, boolean autoKeyboard) {
        view.setAutoKeyboard(autoKeyboard);
    }

    @ReactProp(name = "isDarkMode")
    public void setDarkMode(FullScreenModalView view, boolean isDarkMode) {
        view.setDarkMode(isDarkMode);
    }

    @ReactProp(name = "animationType")
    public void setAnimationType(FullScreenModalView view, String animationType) {
        view.setAnimationType(animationType);
    }

    @ReactProp(name = "transparent")
    public void setTransparent(FullScreenModalView view, boolean transparent) {
        view.setTransparent(transparent);
    }

    @ReactProp(name = "hardwareAccelerated")
    public void setHardwareAccelerated(FullScreenModalView view, boolean hardwareAccelerated) {
        view.setHardwareAccelerated(hardwareAccelerated);
    }

    @Override
    protected void onAfterUpdateTransaction(FullScreenModalView view) {
        super.onAfterUpdateTransaction(view);
        view.showOrUpdate();
    }
}

在这里有几点需要注意的

  • 由于 RN Modal 已经存在了 onShow 和 onRequestClose 回调,这里不能再使用这两个命名,所以这里改成了 onFullScreenShow 和 onFullScreenRequstClose,但是在 js 端还是重新命名成 onShow 和 onRequestClose ,所以在使用过程中还是没有任何变化
  • 增加了 isDarkMode 属性,对应上面的状态栏字体的颜色
  • 增加了 autoKeyboard 属性,根据该属性判断是否需要自动弹起软件盘

其他的一些属性和用法也就跟 RN Modal 的一样了。

JS 部分实现

在 JS 部分,我们只需要 Android 的实现就好了,ios 还是沿用原来的 Modal 控件。这里参照 RN Modal 的 JS 端实现如下

import React, {Component} from "react";
import {requireNativeComponent, View}  from "react-native";

const FullScreenModal = requireNativeComponent('RCTFullScreenModalHostView', FullScreenModalView)
export default class FullScreenModalView extends Component {

    _shouldSetResponder = () => {
        return true;
    }

    render() {
        if (this.props.visible === false) {
            return null;
        }
        const containerStyles = {
            backgroundColor: this.props.transparent ? 'transparent' : 'white',
        };
        return (
            <FullScreenModal
                style={{position: 'absolute'}}  {...this.props}
                onStartShouldSetResponder={this._shouldSetResponder}
                onFullScreenShow={() => this.props.onShow && this.props.onShow()}
                onFullScreenRequstClose={() => this.props.onRequestClose && this.props.onRequestClose()}>
                <View style={[{position: 'absolute', left: 0, top: 0}, containerStyles]}>
                    {this.props.children}
                </View>
            </FullScreenModal>
        )
    }

}

使用 RootSiblings 封装 Modal

针对第二个问题,一种方法是通过 ios 原生去封装实现一个 Modal 控件,但是在 RN 的开发过程中,发现了一个第三方库 react-native-root-siblings , 它重写了系统的 AppRegistry.registerComponent 方法,当我们通过这个方法注册根组件的时候,替换根组件为我们自己的实现的包装类。包装类中监听了目标通知 siblings.update,接收到通知就将通知传入的组件视图添加到包装类顶层,然后进行刷新显示。通过 RootSiblings 也可以实现一个 Modal 组件,而且它的层级是在当前界面的最上层的。

实现界面 Render 相关

由于 RootSiblings 的实现是通过将组件添加到它注册到根节点中的,并不直接通过 Component 的 Render 进行布局,而 RN Modal 的显示隐藏是通过 visible 属性进行控制,所以在 componentWillReceiveProps(nextProps) 中根据 visible 进行相关的控制,部分实现代码如下

render() {
  if (this.props.visible) {
    this.RootSiblings && this.RootSiblings.update(this.renderRootSiblings());
  }
  return null;
}

componentWillReceiveProps(nextProps) {
    const { onShow, animationType, onDismiss } = this.props;
    const { visible } = nextProps;
    if (!this.RootSiblings && visible === true) { // 表示从没有到要显示了
      this.RootSiblings = new RootSiblings(this.renderRootSiblings(), () => {
        if (animationType === 'fade') {
          this._animationFadeIn(onShow);
        } else if (animationType === 'slide') {
          this._animationSlideIn(onShow);
        } else {
          this._animationNoneIn(onShow);
        }
      });
    } else if (this.RootSiblings && visible === false) { // 表示显示之后要隐藏了
      if (animationType === 'fade') {
        this._animationFadeOut(onDismiss);
      } else if (animationType === 'slide') {
        this._animationSlideOut(onDismiss);
      } else {
        this._animationNoneOut(onDismiss);
      }
   }
}

实现 Modal 展示动画相关

RN Modal 实现了三种动画模式,所以这里在通过 RootSiblings 实现 Modal 组件的时候也实现了这三种动画模式,这里借助的是 RN 提供的 Animated 和 Easing 进行相关的实现

  • 'none' 这种不必多说,直接进行展示,没有动画效果
  • 'fade' 淡入浅出动画,也就是透明度的一个变化,这里使用了 Easing.in 插值器使得效果更加平滑
  • 'slide' 幻灯片的滑入画出动画,这是是组件 Y 方向位置的一个变化,这里使用了 Easing.in 插值器使得效果更加平滑

完整的一个 使用 RootSiblings 封装 Modal 实现代码如下

import React, { Component } from 'react';
import {
  Animated, Easing, Dimensions, StyleSheet,
} from 'react-native';
import RootSiblings from 'react-native-root-siblings';

const { height } = Dimensions.get('window');
const animationShortTime = 250; // 动画时长为250ms

export default class ModalView extends Component {
  constructor(props) {
    super(props);
    this.state = {
      animationSlide: new Animated.Value(0),
      animationFade: new Animated.Value(0),
    };
  }

  render() {
    if (this.props.visible) {
      this.RootSiblings && this.RootSiblings.update(this.renderRootSiblings());
    }
    return null;
  }

  componentWillReceiveProps(nextProps) {
    const { onShow, animationType, onDismiss } = this.props;
    const { visible } = nextProps;
    if (!this.RootSiblings && visible === true) { // 表示从没有到要显示了
      this.RootSiblings = new RootSiblings(this.renderRootSiblings(), () => {
        if (animationType === 'fade') {
          this._animationFadeIn(onShow);
        } else if (animationType === 'slide') {
          this._animationSlideIn(onShow);
        } else {
          this._animationNoneIn(onShow);
        }
      });
    } else if (this.RootSiblings && visible === false) { // 表示显示之后要隐藏了
      if (animationType === 'fade') {
        this._animationFadeOut(onDismiss);
      } else if (animationType === 'slide') {
        this._animationSlideOut(onDismiss);
      } else {
        this._animationNoneOut(onDismiss);
      }
    }
  }

  renderRootSiblings = () => {
    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>
    );
  }

  _animationNoneIn = (callback) => {
    this.state.animationSlide.setValue(1);
    this.state.animationFade.setValue(1);
    callback && callback();
  }

  _animationNoneOut = (callback) => {
    this._animationCallback(callback);
  }

  _animationSlideIn = (callback) => {
    this.state.animationSlide.setValue(0);
    this.state.animationFade.setValue(1);
    Animated.timing(this.state.animationSlide, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 1,
    }).start(() => callback && callback());
  }

  _animationSlideOut = (callback) => {
    this.state.animationSlide.setValue(1);
    this.state.animationFade.setValue(1);
    Animated.timing(this.state.animationSlide, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 0,
    }).start(() => this._animationCallback(callback));
  }

  _animationFadeIn = (callback) => {
    this.state.animationSlide.setValue(1);
    this.state.animationFade.setValue(0);
    Animated.timing(this.state.animationFade, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 1,
    }).start(() => callback && callback());
  }

  _animationFadeOut = (callback) => {
    this.state.animationSlide.setValue(1);
    this.state.animationFade.setValue(1);
    Animated.timing(this.state.animationFade, {
      easing: Easing.in(),
      duration: animationShortTime,
      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,
  },
});

使用 View 封装 Modal

上面两种 Modal 的封装已经能够满足绝大部分业务场景的需求,但是如果在 Modal 中需要打开新的界面(不创建新的 ViewController 和 Acticity ),并且 Modal 不进行隐藏的话,比如使用 react-navigation 跳转页面,那么上面实现的 Modal 层级会太高了。所以这里通过 View 去实现类似的一个 Modal 控件。它的实现代码类似于上面的 RootSiblings 的实现。在 Render 中进行展示就好了,动画也可以使用上面的实现

 render() {
    return this._renderView()
 }

_renderView = () => {
    if (this.state.visible) {
      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>
      );
    } else {
      return null
    }
}

整体 Modal 控件的封装

相对于 RN Modal ,上面新增了三种 Modal 的实现,为了整合整体的使用,所以需要对它们进行一个整体的封装使用,通过制定 modalType 的方式,来指定我们需要它内部的实现,先上代码

/**
 * @Author: linhe
 * @Date: 2019-05-12 10:11
 *
 * 因为ios端同时只能存在一个Modal,并且Modal多次显示隐藏会有很奇怪的bug
 *
 * 为了兼容ios的使用,这里需要封装一个ModalView
 *
 * Android 依旧使用 React Native Modal 来进行实现
 * ios 的话采用 RootSiblings 配合进行使用
 *
 * 这个是因为有的modal里面还需要跳转到其他界面
 * 这个时候主要要将该View放到最外边的层级才可以
 *
 * modalType:1 //表示使用Modal进行实现
 *           2 //表示使用RootSiblings进行实现
 *           3 //表示使用View进行实现
 * 注意:默认情况下 Android 使用的是1,ios使用的是2
 *
 * 同时采用与 React Native Modal 相同的API
 */
'use strict';
import React, {Component} from "react";
import {Animated, BackHandler, Platform, Easing, StyleSheet, Dimensions, Modal} from "react-native";
import PropTypes from 'prop-types'
import RootSiblings from 'react-native-root-siblings';
import FullScreenModal from './FullScreenModal/FullScreenModal'

const {height} = Dimensions.get('window')
const animationShortTime = 250 //动画时长为250ms
const DEVICE_BACK_EVENT = 'hardwareBackPress';

export default class ModalView extends Component {

  static propTypes = {
    isDarkMode: PropTypes.bool, // false 表示白底黑字,true 表示黑底白字
    autoKeyboard: PropTypes.bool, // 未知原因的坑,modal中的edittext自动弹起键盘要设置这个参数为true
    useReactModal: PropTypes.bool, // 是否使用 RN Modal 进行实现
    modalType: PropTypes.number // modalType 类型,默认 android 为 1,ios 为 2
  };

  static defaultProps = {
    isDarkMode: false,
    autoKeyboard: false,
    useReactModal: false,
    modalType: (Platform.OS === 'android' ? 1 : 2) // 默认 android 为1,ios 为2
  };

  constructor(props) {
    super(props);
    this.state = {
      visible: false,
      animationSlide: new Animated.Value(0),
      animationFade: new Animated.Value(0)
    };
  }

  render() {
    const {modalType} = this.props
    if (modalType === 1) { //modal实现
      return this._renderModal()
    } else if (modalType === 2) { //RootSiblings实现
      this.RootSiblings && this.RootSiblings.update(this._renderRootSiblings())
      return null
    } else { //View的实现
      return this._renderView()
    }
  }

  _renderModal = () => {
    const ModalView = this.props.useReactModal ? Modal : FullScreenModal
    return (
      <ModalView
        transparent={true}
        {...this.props}
        visible={this.state.visible}
        onRequestClose={() => {
          if (this.props.onRequestClose) {
            this.props.onRequestClose()
          } else {
            this.disMiss()
          }
        }}>
        {this.props.children}
      </ModalView>
    )
  }

  _renderRootSiblings = () => {
    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>
    );
  }

  _renderView = () => {
    if (this.state.visible) {
      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>
      );
    } else {
      return null
    }
  }

  show = (callback) => {
    if (this.isShow()) {
      return
    }
    const {modalType, animationType} = this.props
    if (modalType === 1) { //modal
      this.setState({visible: true}, () => callback && callback())
    } else if (modalType === 2) { //RootSiblings
      this.RootSiblings = new RootSiblings(this._renderRootSiblings(), () => {
        if (animationType === 'fade') {
          this._animationFadeIn(callback)
        } else if (animationType === 'slide') {
          this._animationSlideIn(callback)
        } else {
          this._animationNoneIn(callback)
        }
      });
      // 这里需要监听 back 键
      this._addHandleBack()
    } else { //view
      if (animationType === 'fade') {
        this.setState({visible: true}, () => this._animationFadeIn(callback))
      } else if (animationType === 'slide') {
        this.setState({visible: true}, () => this._animationSlideIn(callback))
      } else {
        this.setState({visible: true}, () => this._animationNoneIn(callback))
      }
      // 这里需要监听 back 键
      this._addHandleBack()
    }
  }

  disMiss = (callback) => {
    if (!this.isShow()) {
      return
    }
    const {modalType, animationType} = this.props
    if (modalType === 1) { //modal
      this.setState({visible: false}, () => callback && callback())
    } else { //RootSiblings和View
      if (animationType === 'fade') {
        this._animationFadeOut(callback)
      } else if (animationType === 'slide') {
        this._animationSlideOut(callback)
      } else {
        this._animationNoneOut(callback)
      }
      // 移除 back 键的监听
      this._removeHandleBack()
    }
  }

  isShow = () => {
    const {modalType} = this.props
    if (modalType === 1 || modalType === 3) { //modal和view
      return this.state.visible
    } else { //RootSiblings
      return !!this.RootSiblings
    }
  }

  _addHandleBack = () => {
    if (Platform.OS === 'ios') {
      return
    }
    // 监听back键
    this.handleBack = BackHandler.addEventListener(DEVICE_BACK_EVENT, () => {
      const {onRequestClose} = this.props
      if (onRequestClose) {
        onRequestClose()
      } else {
        this.disMiss()
      }
      return true
    });
  }

  _removeHandleBack = () => {
    if (Platform.OS === 'ios') {
      return
    }
    this.handleBack && this.handleBack.remove()
  }

  _animationNoneIn = (callback) => {
    this.state.animationSlide.setValue(1)
    this.state.animationFade.setValue(1)
    callback && callback()
  }

  _animationNoneOut = (callback) => {
    this._animationCallback(callback);
  }

  _animationSlideIn = (callback) => {
    this.state.animationSlide.setValue(0)
    this.state.animationFade.setValue(1)
    Animated.timing(this.state.animationSlide, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 1,
    }).start(() => callback && callback());
  }

  _animationSlideOut = (callback) => {
    this.state.animationSlide.setValue(1)
    this.state.animationFade.setValue(1)
    Animated.timing(this.state.animationSlide, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 0,
    }).start(() => this._animationCallback(callback));
  }

  _animationFadeIn = (callback) => {
    this.state.animationSlide.setValue(1)
    this.state.animationFade.setValue(0)
    Animated.timing(this.state.animationFade, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 1,
    }).start(() => callback && callback());
  }

  _animationFadeOut = (callback) => {
    this.state.animationSlide.setValue(1)
    this.state.animationFade.setValue(1)
    Animated.timing(this.state.animationFade, {
      easing: Easing.in(),
      duration: animationShortTime,
      toValue: 0,
    }).start(() => this._animationCallback(callback));
  }

  _animationCallback = (callback) => {
    if (this.props.modalType === 2) {//RootSiblings
      this.RootSiblings && this.RootSiblings.destroy(() => {
        callback && callback()
        this.RootSiblings = undefined
      })
    } else { //view
      this.setState({visible: false}, () => callback && callback())
    }
  }
}

const styles = StyleSheet.create({
  root: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0,
  }
});

这里主要通过 useReactModal 和 modalType 两个属性来控制我们所需要的实现

  • modalType 表示 modal 内部实现方式,1 表示使用层级较高的实现,2 表示使用 RootSiblings 进行实现,3 表示使用 View 进行实现,当不进行指定的时候,默认 Android 为 1,ios 为 2
  • useReactModal 主要针对 Android 端并且 modalType 为 1 的时候使用,true 表示使用 RN Modal,false 表示使用 FullScreenModal ,默认为 false
  • 对外提供 show,disMiss,isShow 方法分别表示显示弹窗,隐藏弹窗和判断当前弹窗的状态,同时在 show 和 disMiss 方法调用的时候还添加了 callback 回调

其他

Android Back 键的注意

当 Modal 组件使用 RootSiblings 或者 View 实现的时候,它并没有处理一个 Android 的返回键,所以对于这两种实现的时候,要额外处理一个 Back 键的操作,这里借助了 RN BackHandler 这个了,如果 modalType 不为 1 的话,需要通过 BackHandler 去实现一个返回键的监听,然后通过 onRequestClose 属性进行返回

const DEVICE_BACK_EVENT = 'hardwareBackPress';

_addHandleBack = () => {
    if (Platform.OS === 'ios') {
      return
    }
    // 监听back键
    this.handleBack = BackHandler.addEventListener(DEVICE_BACK_EVENT, () => {
      const {onRequestClose} = this.props
      if (onRequestClose) {
        onRequestClose()
      } else {
        this.disMiss()
      }
      return true
    });
}

_removeHandleBack = () => {
    if (Platform.OS === 'ios') {
      return
    }
    this.handleBack && this.handleBack.remove()
}

View 封装 Modal 时候的注意

如果是 View 实现的 Modal 控件,那必须要注意它的一个层级,必须满足它所处于整个界面布局的最外层,否则它可能会被其他组件所挡住,同时它的最大的显示范围取决于它的父 View 的显示范围。

最后

当然在最后还是要附上实现效果图

ios.gif

android.gif

再一次附上 Demo 地址:https://github.com/hzl123456/ModalViewDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,326评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,228评论 1 304
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,979评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,489评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,894评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,900评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,075评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,803评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,565评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,778评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,255评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,582评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,254评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,151评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,952评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,035评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,839评论 2 277

推荐阅读更多精彩内容