React Native Popup
KidLau 人气:0React Native 官方提供了 Modal
组件,但 Modal
是属于全屏的弹出层,当 Modal
显示时,操作区域只有 Modal
里的元素,而且焦点会被 Modal
劫持。虽然移动端不常见,但有些场景还是希望可以用轻量级一点的 Popup
。
在 React Native 里,元素的层级是不可以被穿透的,子元素无论如何都不能遮挡父元素。所以选择了在顶层添加 Popup
,设置绝对定位,显示时根据指定元素来动态调整 Popup
的位置的方案。
具体实现
Popup
会有显示或隐藏两种状态,使用一个 state
来控制。
const Component = () => { const [visible, setVisible] = useState(false); return ( <> {visible && <></>} </> ); };
Popup
的 属于视图类组件,UI 结构包括:
- 一个作为容器的
View
,由于 iOS 有刘海,所以在 iOS 上需要使用SafeAreaView
来避免被刘海遮挡。同时添加一个点击事件监听当点击时关闭Popup
。 - 一个指向目标对象的三角形。
- 一个包裹内容的
View
。
由于 Popup
的位置和内容是动态的,所以需要两个 state
存储相关数据。
- 一个存储位置相关的 CSS。
- 一个存储动态内容。
const Component = ({ style, ...other }) => { const [visible, setVisible] = useState(false); const [popupStyle, setPopupStyle] = useState({}); const [content, setContent] = useState(null); const onPress = useCallback(() => { setVisible(false); }, []); return ( <> {visible && createElement( Platform.OS === 'ios' ? SafeAreaView : View, { style: { ...styles.popup, ...popupStyle, }, }, <TouchableOpacity onPress={onPress}> <View style={styles.triangle} /> <View style={{ ...styles.content, ...style }} {...other}> {content} </View> </TouchableOpacity>, )} </> ); }; const styles = StyleSheet.create({ popup: { position: 'absolute', zIndex: 99, shadowColor: '#333', shadowOpacity: 0.12, shadowOffset: { width: 2 }, borderRadius: 4, }, triangle: { width: 0, height: 0, marginLeft: 12, borderLeftWidth: 8, borderLeftColor: 'transparent', borderRightWidth: 8, borderRightColor: 'transparent', borderBottomWidth: 8, borderBottomColor: 'white', }, content: { backgroundColor: 'white', }, });
因为是全局的 Popup
,所以选择了一个全局变量来提供 Popup
相关的操作方法。
如果全局
Popup
不适用,可以改成在需要时插入Popup
并使用ref
来提供操作方法。
目标元素,动态内容和一些相关的可选配置都是在调用 show
方法时通过参数传入的,
useEffect(() => { global.$popup = { show: (triggerRef, render, options = {}) => { const { x: offsetX = 0, y: offsetY = 0 } = options.offset || {}; triggerRef.current.measure((x, y, width, height, left, top) => { setPopupStyle({ top: top + height + offsetY, left: left + offsetX, }); setContent(render()); setVisible(true); }); }, hide: () => { setVisible(false); }, }; }, []);
完整代码
import React, { createElement, forwardRef, useState, useEffect, useCallback, } from 'react'; import PropTypes from 'prop-types'; import { View, SafeAreaView, Platform, TouchableOpacity, StyleSheet, } from 'react-native'; const Component = ({ style, ...other }, ref) => { const [visible, setVisible] = useState(false); const [popupStyle, setPopupStyle] = useState({}); const [content, setContent] = useState(null); const onPress = useCallback(() => { setVisible(false); }, []); useEffect(() => { global.$popup = { show: (triggerRef, render, options = {}) => { const { x: offsetX = 0, y: offsetY = 0 } = options.offset || {}; triggerRef.current.measure((x, y, width, height, left, top) => { setPopupStyle({ top: top + height + offsetY, left: left + offsetX, }); setContent(render()); setVisible(true); }); }, hide: () => { setVisible(false); }, }; }, []); return ( <> {visible && createElement( Platform.OS === 'ios' ? SafeAreaView : View, { style: { ...styles.popup, ...popupStyle, }, }, <TouchableOpacity onPress={onPress}> <View style={styles.triangle} /> <View style={{ ...styles.content, ...style }} {...other}> {content} </View> </TouchableOpacity>, )} </> ); }; Component.displayName = 'Popup'; Component.prototype = {}; const styles = StyleSheet.create({ popup: { position: 'absolute', zIndex: 99, shadowColor: '#333', shadowOpacity: 0.12, shadowOffset: { width: 2 }, borderRadius: 4, }, triangle: { width: 0, height: 0, marginLeft: 12, borderLeftWidth: 8, borderLeftColor: 'transparent', borderRightWidth: 8, borderRightColor: 'transparent', borderBottomWidth: 8, borderBottomColor: 'white', }, content: { backgroundColor: 'white', }, }); export default forwardRef(Component);
使用方法
在入口文件页面内容的末尾插入
Popup
元素。// App.jsx import Popup from './Popup'; const App = () => { return ( <> ... <Popup /> </> ); };
使用全局变量控制。
// 显示 $popup.show(); // 隐藏 $popup.hide();
加载全部内容