• npm i --save react-native-popover 此第三方组件是用老语法写的,直接运行如果报错的话 从node_modules文件件里面找个这个组件把里面的Popover.js的代码替换成如下代码: /** * Created by xiaowuzai on 2018/...

    先放图:
    这里写图片描述

    首先下载第三方组件

    npm i --save react-native-popover

    此第三方组件是用老语法写的,直接运行如果报错的话 从node_modules文件件里面找个这个组件把里面的Popover.js的代码替换成如下代码:

    /**
     * Created by xiaowuzai on 2018/3/29.
     */
    import React, { Component } from 'react';
    import PropTypes from "prop-types";
    import {
        StyleSheet,
        Dimensions,
        Animated,
        Text,
        TouchableWithoutFeedback,
        View,
        Easing
    } from 'react-native';
    
    var noop = () => {};
    
    var {height: SCREEN_HEIGHT, width: SCREEN_WIDTH} = Dimensions.get('window');
    var DEFAULT_ARROW_SIZE = new Size(10, 5);
    
    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    
    function Size(width, height) {
        this.width = width;
        this.height = height;
    }
    
    function Rect(x, y, width, height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    
    export default class Popover extends Component{
        constructor(props){
            super(props)
            this.state={
                contentSize: {},
                anchorPoint: {},
                popoverOrigin: {},
                placement: 'auto',
                isTransitioning: false,
                scale: new Animated.Value(0),
                translate: new Animated.ValueXY(),
                fade: new Animated.Value(0)
            }
            this.computeAutoGeometry=this.computeAutoGeometry.bind(this)
            this.computeGeometry=this.computeGeometry.bind(this)
            this.measureContent=this.measureContent.bind(this)
            this.computeTopGeometry=this.computeTopGeometry.bind(this)
            this.computeBottomGeometry=this.computeBottomGeometry.bind(this)
            this.computeLeftGeometry=this.computeLeftGeometry.bind(this)
            this.computeRightGeometry=this.computeRightGeometry.bind(this)
            this.getArrowSize=this.getArrowSize.bind(this)
            this.getArrowColorStyle=this.getArrowColorStyle.bind(this)
            this.getArrowRotation=this.getArrowRotation.bind(this)
            this.getArrowDynamicStyle=this.getArrowDynamicStyle.bind(this)
            this.getTranslateOrigin=this.getTranslateOrigin.bind(this)
            this._startAnimation=this._startAnimation.bind(this)
            this._startDefaultAnimation=this._startDefaultAnimation.bind(this)
            this._getDefaultAnimatedStyles=this._getDefaultAnimatedStyles.bind(this)
            this._getExtendedStyles=this._getExtendedStyles.bind(this)
        }
    
        measureContent(x) {
            var {width, height} = x.nativeEvent.layout;
            var contentSize = {width, height};
            var geom = this.computeGeometry({contentSize});
    
            var isAwaitingShow = this.state.isAwaitingShow;
            this.setState(Object.assign(geom,
                {contentSize, isAwaitingShow: undefined}), () => {
                // Once state is set, call the showHandler so it can access all the geometry
                // from the state
                isAwaitingShow && this._startAnimation({show: true});
            });
        }
        computeGeometry({contentSize, placement}) {
            placement = placement || this.props.placement;
    
            var options = {
                displayArea: this.props.displayArea,
                fromRect: this.props.fromRect,
                arrowSize: this.getArrowSize(placement),
                contentSize,
            }
    
            switch (placement) {
                case 'top':
                    return this.computeTopGeometry(options);
                case 'bottom':
                    return this.computeBottomGeometry(options);
                case 'left':
                    return this.computeLeftGeometry(options);
                case 'right':
                    return this.computeRightGeometry(options);
                default:
                    return this.computeAutoGeometry(options);
            }
        }
        computeTopGeometry({displayArea, fromRect, contentSize, arrowSize}) {
            var popoverOrigin = new Point(
                Math.min(displayArea.x + displayArea.width - contentSize.width,
                    Math.max(displayArea.x, fromRect.x + (fromRect.width - contentSize.width) / 2)),
                fromRect.y - contentSize.height - arrowSize.height);
            var anchorPoint = new Point(fromRect.x + fromRect.width / 2.0, fromRect.y);
    
            return {
                popoverOrigin,
                anchorPoint,
                placement: 'top',
            }
        }
        computeBottomGeometry({displayArea, fromRect, contentSize, arrowSize}) {
            var popoverOrigin = new Point(
                Math.min(displayArea.x + displayArea.width - contentSize.width,
                    Math.max(displayArea.x, fromRect.x + (fromRect.width - contentSize.width) / 2)),
                fromRect.y + fromRect.height + arrowSize.height);
            var anchorPoint = new Point(fromRect.x + fromRect.width / 2.0, fromRect.y + fromRect.height);
    
            return {
                popoverOrigin,
                anchorPoint,
                placement: 'bottom',
            }
        }
        computeLeftGeometry({displayArea, fromRect, contentSize, arrowSize}) {
            var popoverOrigin = new Point(fromRect.x - contentSize.width - arrowSize.width,
                Math.min(displayArea.y + displayArea.height - contentSize.height,
                    Math.max(displayArea.y, fromRect.y + (fromRect.height - contentSize.height) / 2)));
            var anchorPoint = new Point(fromRect.x, fromRect.y + fromRect.height / 2.0);
    
            return {
                popoverOrigin,
                anchorPoint,
                placement: 'left',
            }
        }
        computeRightGeometry({displayArea, fromRect, contentSize, arrowSize}) {
            var popoverOrigin = new Point(fromRect.x + fromRect.width + arrowSize.width,
                Math.min(displayArea.y + displayArea.height - contentSize.height,
                    Math.max(displayArea.y, fromRect.y + (fromRect.height - contentSize.height) / 2)));
            var anchorPoint = new Point(fromRect.x + fromRect.width, fromRect.y + fromRect.height / 2.0);
    
            return {
                popoverOrigin,
                anchorPoint,
                placement: 'right',
            }
        }
        computeAutoGeometry({displayArea, contentSize}) {
            var placementsToTry = ['left', 'right', 'bottom', 'top'];
    
            for (var i = 0; i < placementsToTry.length; i++) {
                var placement = placementsToTry[i];
                var geom = this.computeGeometry({contentSize: contentSize, placement: placement});
                var {popoverOrigin} = geom;
    
                if (popoverOrigin.x >= displayArea.x
                    && popoverOrigin.x <= displayArea.x + displayArea.width - contentSize.width
                    && popoverOrigin.y >= displayArea.y
                    && popoverOrigin.y <= displayArea.y + displayArea.height - contentSize.height) {
                    break;
                }
            }
    
            return geom;
        }
        getArrowSize(placement) {
            var size = this.props.arrowSize;
            switch(placement) {
                case 'left':
                case 'right':
                    return new Size(size.height, size.width);
                default:
                    return size;
            }
        }
        getArrowColorStyle(color) {
            return { borderTopColor: color };
        }
        getArrowRotation(placement) {
            switch (placement) {
                case 'bottom':
                    return '180deg';
                case 'left':
                    return '-90deg';
                case 'right':
                    return '90deg';
                default:
                    return '0deg';
            }
        }
        getArrowDynamicStyle() {
            var {anchorPoint, popoverOrigin} = this.state;
            var arrowSize = this.props.arrowSize;
            var width = arrowSize.width + 2;
            var height = arrowSize.height * 2 + 2;
    
            return {
                left: anchorPoint.x - popoverOrigin.x - width / 2,
                top: anchorPoint.y - popoverOrigin.y - height / 2,
                width: width,
                height: height,
                borderTopWidth: height / 2,
                borderRightWidth: width / 2,
                borderBottomWidth: height / 2,
                borderLeftWidth: width / 2,
            }
        }
        getTranslateOrigin() {
            var {contentSize, popoverOrigin, anchorPoint} = this.state;
            var popoverCenter = new Point(popoverOrigin.x + contentSize.width / 2,
                popoverOrigin.y + contentSize.height / 2);
            return new Point(anchorPoint.x - popoverCenter.x, anchorPoint.y - popoverCenter.y);
        }
        componentWillReceiveProps(nextProps) {
            var willBeVisible = nextProps.isVisible;
            var {
                isVisible,
            } = this.props;
    
            if (willBeVisible !== isVisible) {
                if (willBeVisible) {
                    // We want to start the show animation only when contentSize is known
                    // so that we can have some logic depending on the geometry
                    this.setState({contentSize: {}, isAwaitingShow: true});
                } else {
                    this._startAnimation({show: false});
                }
            }
        }
        _startAnimation({show}) {
            var handler = this.props.startCustomAnimation || this._startDefaultAnimation;
            handler({show, doneCallback: () => this.setState({isTransitioning: false})});
            this.setState({isTransitioning: true});
        }
        _startDefaultAnimation({show, doneCallback}) {
            var animDuration = 300;
            var values = this.state;
            var translateOrigin = this.getTranslateOrigin();
    
            if (show) {
                values.translate.setValue(translateOrigin);
            }
    
            var commonConfig = {
                duration: animDuration,
                easing: show ? Easing.out(Easing.back()) : Easing.inOut(Easing.quad),
            }
    
            Animated.parallel([
                Animated.timing(values.fade, {
                    toValue: show ? 1 : 0,
                    ...commonConfig,
                }),
                Animated.timing(values.translate, {
                    toValue: show ? new Point(0, 0) : translateOrigin,
                    ...commonConfig,
                }),
                Animated.timing(values.scale, {
                    toValue: show ? 1 : 0,
                    ...commonConfig,
                })
            ]).start(doneCallback);
        }
        _getDefaultAnimatedStyles() {
            // If there's a custom animation handler,
            // we don't return the default animated styles
            if (typeof this.props.startCustomAnimation !== 'undefined') {
                return null;
            }
    
            var animatedValues = this.state;
    
            return {
                backgroundStyle: {
                    opacity: animatedValues.fade,
                },
                arrowStyle: {
                    transform: [
                        {
                            scale: animatedValues.scale.interpolate({
                                inputRange: [0, 1],
                                outputRange: [0, 1],
                                extrapolate: 'clamp',
                            }),
                        }
                    ],
                },
                contentStyle: {
                    transform: [
                        {translateX: animatedValues.translate.x},
                        {translateY: animatedValues.translate.y},
                        {scale: animatedValues.scale},
                    ],
                }
            };
        }
        _getExtendedStyles() {
            var background = [];
            var popover = [];
            var arrow = [];
            var content = [];
    
            [this._getDefaultAnimatedStyles(), this.props].forEach((source) => {
                if (source) {
                    background.push(source.backgroundStyle);
                    popover.push(source.popoverStyle);
                    arrow.push(source.arrowStyle);
                    content.push(source.contentStyle);
                }
            });
    
            return {
                background,
                popover,
                arrow,
                content,
            }
        }
        render() {
            if (!this.props.isVisible && !this.state.isTransitioning) {
                return null;
            }
    
            var {popoverOrigin, placement} = this.state;
            var extendedStyles = this._getExtendedStyles();
            var contentStyle = [styles.content, ...extendedStyles.content];
            var arrowColor = StyleSheet.flatten(contentStyle).backgroundColor;
            var arrowColorStyle = this.getArrowColorStyle(arrowColor);
            var arrowDynamicStyle = this.getArrowDynamicStyle();
            var contentSizeAvailable = this.state.contentSize.width;
    
            // Special case, force the arrow rotation even if it was overriden
            var arrowStyle = [styles.arrow, arrowDynamicStyle, arrowColorStyle, ...extendedStyles.arrow];
            var arrowTransform = (StyleSheet.flatten(arrowStyle).transform || []).slice(0);
            arrowTransform.unshift({rotate: this.getArrowRotation(placement)});
            arrowStyle = [...arrowStyle, {transform: arrowTransform}];
    
            return (
                <TouchableWithoutFeedback onPress={this.props.onClose}>
                    <View style={[styles.container, contentSizeAvailable && styles.containerVisible ]}>
                        <Animated.View style={[styles.background, ...extendedStyles.background]}/>
                        <Animated.View style={[styles.popover, {
                            top: popoverOrigin.y,
                            left: popoverOrigin.x,
                        }, ...extendedStyles.popover]}>
                            <Animated.View style={arrowStyle}/>
                            <Animated.View ref='content' onLayout={this.measureContent} style={contentStyle}>
                                {this.props.children}
                            </Animated.View>
                        </Animated.View>
                    </View>
                </TouchableWithoutFeedback>
            );
        }
    };
    Popover.defaultProps={
        isVisible: false,
        displayArea: new Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
        arrowSize: DEFAULT_ARROW_SIZE,
        placement: 'auto',
        onClose: noop
    }
    
    var styles = StyleSheet.create({
        container: {
            opacity: 0,
            top: 0,
            bottom: 0,
            left: 0,
            right: 0,
            position: 'absolute',
            backgroundColor: 'transparent',
        },
        containerVisible: {
            opacity: 1,
        },
        background: {
            top: 0,
            bottom: 0,
            left: 0,
            right: 0,
            position: 'absolute',
            //backgroundColor: 'rgba(0,0,0,0.5)',
        },
        popover: {
            backgroundColor: 'transparent',
            position: 'absolute',
            shadowColor: 'black',
            shadowOffset: {width: 0, height: 2},
            shadowRadius: 2,
            shadowOpacity: 0.8,
        },
        content: {
            borderRadius: 3,
            padding: 6,
            backgroundColor: '#fff',
        },
        arrow: {
            position: 'absolute',
            borderTopColor: 'transparent',
            borderRightColor: 'transparent',
            borderBottomColor: 'transparent',
            borderLeftColor: 'transparent',
        },
    });
    

    下面是demo代码

    /**
     * Sample React Native App
     * https://github.com/facebook/react-native
     * @flow
     */
    
    import React, {Component} from 'react';
    import Popover from "react-native-popover"
    import {
        Platform,
        StyleSheet,
        Text,
        View,
        Image,
        TouchableOpacity,
        TouchableHighlight,
    
    } from 'react-native';
    import Toast, {DURATION} from 'react-native-easy-toast';
    
    const instructions = Platform.select({
        ios: 'Press Cmd+R to reload,\n' +
        'Cmd+D or shake for dev menu',
        android: 'Double tap R on your keyboard to reload,\n' +
        'Shake or press menu button for dev menu',
    });
    const spinnerTextArray = ['关羽', '张飞', '马超', '黄忠', "赵云"]
    type Props = {};
    export default class App extends Component<Props> {
    
        constructor(props) {
            super(props);
            this.state = {
                //下拉列表是否可见
                isVisible: false,
                //下拉列表大小范围
                spinnerRect: {},
            }
        }
    
        //显示下拉列表
        showSpinner() {
            this.refs.spinner.measure((ox, oy, width, height, px, py) => {
                this.setState({
                    isVisible: true,
                    spinnerRect: {x: px, y: py, width: width, height: height}
                });
            });
        }
    
        //隐藏下拉列表
        closeSpinner() {
            this.setState({
                isVisible: false
            });
        }
    
        //下拉列表每一行点击事件
        onItemClick(spinnerItem) {
            this.closeSpinner();
            this.toast.show(spinnerItem, DURATION.LENGTH_SHORT);
        }
    
        render() {
            return <View style={{flex: 1, alignItems: 'center'}}>
                <TouchableOpacity
                    ref='spinner'
                    style={{flexDirection: 'row', alignItems: 'center', marginTop: 10}}
                    underlayColor='transparent'
                    onPress={() => this.showSpinner()}>
                    <Text>
                        点击可以弹出下拉菜单
                    </Text>
                    <Image source={require('./imgs/ic_tiaozhuan_down.png')}/>
                </TouchableOpacity>
                <Popover
                    //设置可见性
                    isVisible={this.state.isVisible}
                    //设置下拉位置
                    fromRect={this.state.spinnerRect}
                    placement="bottom"
                    //点击下拉框外范围关闭下拉框
                    onClose={() => this.closeSpinner()}
                    //设置内容样式
                    contentStyle={{opacity: 0.82, backgroundColor: '#343434'}}
                    style={{backgroundColor: 'red'}}>
                    <View style={{alignItems: 'center'}}>
                        {spinnerTextArray.map((result, i, arr) => {
                            return <TouchableHighlight key={i} onPress={() => this.onItemClick(arr[i])}
                                                       underlayColor='transparent'>
                                <Text
                                    style={{fontSize: 18, color: 'white', padding: 8, fontWeight: '400'}}>
                                    {arr[i]}
                                </Text>
                            </TouchableHighlight>
                        })
                        }
                    </View>
                </Popover>
                <Toast ref={toast => {
                    this.toast = toast
                }}/>
            </View>
        }
    }
    
    

    此篇文章仅做备份,方便以后使用

    展开全文
  • React Native中的Popover框架实现的效果就类似于Android原生的Spinner或PopWindow实现的下拉框效果, 开源框架地址:https://www.npmjs.com/package/react-native-popover import React, {Component} from "react";...
      React Native中的Popover框架实现的效果就类似于Android原生的Spinner或PopWindow实现的下拉框效果,
     开源框架地址:https://www.npmjs.com/package/react-native-popover
    
    import React, {Component} from "react";
    import {
        StyleSheet,
        View,
        TouchableOpacity,
        TouchableHighlight,
        Text,
        Image
    } from "react-native";
    import Popover from './Popover'
    import Toast, {DURATION} from 'react-native-easy-toast';
    var spinnerTextArray = ['深圳南山', '深圳宝安', '深圳罗湖', '深圳福田']
    //Popover开源地址:https://www.npmjs.com/package/react-native-popover
    export default class PopoverDemo extends Component {
        constructor(props) {
            super(props);
            this.state = {
                //下拉列表是否可见
                isVisible: false,
                //下拉列表大小范围
                spinnerRect: {},
            }
        }
    
        //显示下拉列表
        showSpinner() {
            this.refs.spinner.measure((ox, oy, width, height, px, py) => {
                this.setState({
                    isVisible: true,
                    spinnerRect: {x: px, y: py, width: width, height: height}
                });
            });
        }
    
        //隐藏下拉列表
        closeSpinner() {
            this.setState({
                isVisible: false
            });
        }
    
        //下拉列表每一行点击事件
        onItemClick(spinnerItem) {
            this.closeSpinner();
            this.toast.show(spinnerItem, DURATION.LENGTH_SHORT);
        }
    
        //TouchableOpacity用于封装视图,使其可以正确响应触摸操作
        //ref使用参考http://blog.csdn.net/jiangbo_phd/article/details/51758148
        render() {
            return <View style={{flex:1,alignItems:'center'}}>
                <TouchableOpacity
                    ref='spinner'
                    style={{flexDirection:'row',alignItems:'center',marginTop:10}}
                    underlayColor='transparent'
                    onPress={()=>this.showSpinner()}>
                    <Text>
                        点击可以弹出下拉菜单
                    </Text>
                    <Image source={require('./image/down.png')}/>
                </TouchableOpacity>
                <Popover
                    //设置可见性
                    isVisible={this.state.isVisible}
                    //设置下拉位置
                    fromRect={this.state.spinnerRect}
                    placement="bottom"
                    //点击下拉框外范围关闭下拉框
                    onClose={()=>this.closeSpinner()}
                    //设置内容样式
                    contentStyle={{opacity:0.82,backgroundColor:'#343434'}}
                    style={{backgroundColor: 'red'}}>
                    <View style={{alignItems: 'center'}}>
                        {spinnerTextArray.map((result, i, arr) => {
                            return <TouchableHighlight key={i} onPress={()=>this.onItemClick(arr[i])}
                                                       underlayColor='transparent'>
                                <Text
                                    style={{fontSize: 18,color:'white', padding: 8, fontWeight: '400'}}>
                                    {arr[i]}
                                </Text>
                            </TouchableHighlight>
                        })
                        }
                    </View>
                </Popover>
                <Toast ref={toast=>{
                           this.toast=toast
                        }}/>
            </View>
        }
    }
    展开全文
  • 趋势页面的导航条是有个popover的控件 所以不是简单的text 需要改造NavigationBar需要改造NavigationBar.js //渲染顶部title renderTitle = () => { let view = (this.props.title.length != 0

    父控件 更新子控件 通过属性传值

    现在写趋势页面. 其实趋势页面和最热页面都差不多.

    趋势页面的导航条是有个popover的控件 所以不是简单的text 需要改造NavigationBar

    需要改造NavigationBar.js

     //渲染顶部title
        renderTitle = () => {
            let view = (this.props.title.length != 0) ? (
                <Text style={styles.title}>{this.props.title}</Text>) : this.props.titleView;
            
            return <View style={styles.titleWrapper}>
                {view}
            </View>
        }
    
        render() {
            return <View style={styles.container}>
                <View style={styles.container}>
                    <StatusBar hidden={false} barStyle="light-content"/>
                </View>
                {/*顶部导航栏*/}
                <View style={styles.navBar}>
                    <View style={styles.leftBtnStyle}>
                        {this.props.leftButton}
                    </View>
                    {this.renderTitle()}
                    <View style={styles.rightBar}>
                        {this.props.rightButton}
                    </View>
    
                </View>
            </View>
        }
    

    如果传递过来的title有文本 那么久返回文本 否则 就返回一个传递过来的节点 element.

    趋势页面主要代码

    
        renderTitle = () => {
            return <TouchableOpacity
                activeOpacity={0.5}>
                <View style={{flexDirection: 'row', alignItems: 'center', justifyContent: 'center'}}>
                    <Text style={{color: '#FFF', fontsize: 16}}>趋势</Text>
                    <Image source={require('../../res/images/ic_spinner_triangle.png')}
                           style={{width: 12, height: 12, marginLeft: 5}}/>
                </View>
            </TouchableOpacity>
        }
    
    
        render() {
            return <View style={styles.container}>
                <NavigationBar
                    titleView={this.renderTitle()}
                ></NavigationBar>
    
            </View>
        }
    

    效果

    mark

    拷贝最热页面代码

    直接照搬 我这里图省事

    //TODU

    在这里出现一个问题 数据都是异步获取的. 比如 tab上面的 语言是用asyncStorage异步获取的

    那么就必须有个默认的语言列表 但是listview 是同步执行的 setState后 界面没有刷新,

    先跳过这个问题

    这里照搬之后请求的链接不能一样 使用GitHubTrending

    https://github.com/trending 趋势页面

    https://github.com/crazycodeboy/GitHubTrending github 地址

    安装
    npm install GitHubTrending --save

    import GitHubTrending from ‘GitHubTrending’; 引入

    请求连接的时候用这个请求

     //加载数据
        loadData = () => {
            this.setState({isLoading: true});
            new GitHubTrending().fetchTrending(`https://github.com/trending/${this.props.tabLabel}?since=daily`)
                .then(value => {
                    //更新dataSource
                    this.setState({
                        dataSource: this.state.dataSource.cloneWithRows(value),
                        isLoading: false,
                    });
                }).catch((error) => {
                console.error(error);
            }).done();
    

    列表的item 可以复用 但是 有些区别 那么我把原来的item 修改下. 一个是作为趋势页面的item 一个是作为最热页面的item. 另外一个就是作为趋势的item

    mark

    都是些重复的东西了

    趋势顶部点击popover

    地址
    https://github.com/jeanregisser/react-native-popover

    但是不能直接安装 把popover.js下载下来 拷贝到项目中来用

    import Popover from ‘…/compoent/Popover’

    拷贝到compoent包里面 引入使用

     showPopover = () => {
            console.log(this.refs);
            this.refs.button.measure((ox, oy, width, height, px, py) => {
                this.setState({
                    isVisible: true,
                    buttonRect: {x: px, y: py, width: width, height: height}
                });
            });
        }
    
        closePopover = () => {
            this.setState({isVisible: false});
        }
        
        
         render() {
            return <View style={styles.container}>
                <NavigationBar
                    titleView={this.renderTitle()}
                ></NavigationBar>
                <ScrollableTabView
                    tabBarBackgroundColor="#63B8FF"
                    tabBarActiveTextColor="#FFF"
                    tabBarInactiveTextColor="#F5FFFA"
                    tabBarUnderlineStyle={{backgroundColor: "#E7E7E7", height: 2}}>
                    {
                        this.state.languages.map((item, i) => {
                            // console.log(item);
                            return (item.checked == undefined || item.checked ?
                                <TrendingTab {...this.props} key={`tab${i}`} tabLabel={item.name}/> : null)
                        })
                    }
                </ScrollableTabView>
                <Popover
                    isVisible={this.state.isVisible}
                    fromRect={this.state.buttonRect}
                    onClose={this.closePopover}>
                    <Text>I'm the content of this popover!</Text>
                </Popover>
            </View>
        }
    

    自己写的代码很少 直接照着官方文档一顿拷贝就行…

    看下效果

    mark

    展开全文
  • 一个很棒的React Native弹窗控件,支持iOS和Android
  • 最近的开发中要用到很多的各式各样的组件。但是发现ant design mobile(后面简称ANTDM)里很多的资源。于是就分析一下,学习学习。 ANTDM直接使用了typescript,没有用...Popover组件在: | |--components | |--p...

    最近的开发中要用到很多的各式各样的组件。但是发现ant design mobile(后面简称ANTDM)里很多的资源。于是就分析一下,学习学习。

    ANTDM直接使用了typescript,没有用ES2015,不过这不会是障碍,反而是学习typescript的一个好机会。基本上可以学的开源项目里比这个好的也不多。

    目录结构

    Popover组件在:

    |
    |--components
      |
      |--popover

    我们要分析的组件全部都在components这个目录下。

    在这个目录里还包含tests, demostyle。里面分别存放测试代码、实例和样式。其他的文件包括*[component name]_native.tsx[component name].txs以及对应的index.native.tsxindex.tsx,方便外部引入组件。

    计算点击组件的位置

    这个是最核心的问题了!

    实现React Native的弹出菜单,需要达到在界面上的某个可点击组件上点击了之后,就可以在被点击的组件紧挨着的下方出现一个菜单(其他的计算,比如弹出菜单在左下、右下,左上,右上的位置计算暂时不提)。

    用户点击了哪个组件(按钮),哪个按钮的下面就出现一个菜单(View)。这就需要确定点击组件的位置。

    我们看一下index.native.tsx这个文件。文件里基本上没几行代码,直接看render方法里返回的是MenuContext等。也就是,这个文件没实现什么pop over需要的什么东西。都在import里了:

    import Menu, { MenuContext, MenuOptions, MenuOption, MenuTrigger }from 'react-native-menu';

    所以ANTDM的源码分析到此为止。

    我们要跳到react-native-menu。我们分析代码的方式就是无限递归,一直找到实现功能的代码为止。那么我们就可以分析react-native-menu了。

    react-native-menu

    这个项目的写法也是很不同。用的是比较老的ES5的React版本。github地址在这里

    这个项目里很多的文件,各位可以后面慢慢看。我们来看makeMenuContext.js

    在这个项目里,除了index.js之外都是叫做makeXXX.js。里面都是HOC的实现方式。而且更加Trick的是HOC的前两个参数是ReactReactNative

    回到makeMenuContext.js,在openMenu()这个方法里就有实现的方式。这就是我们寻找代码递归跳出的地方。我们来看一下实现方式:

    openMenu(name) {
      const handle = ReactNative.findNodeHandle(this._menus[name].ref);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._menus[name].measurements = { x, y, w, h, px, py };
    
        this.setState({
          openedMenu: name,
          menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements),
          backdropWidth: this._ownMeasurements.w
        });
    
        this._activeMenuHooks = this._menus[name];
        this._activeMenuHooks && this._activeMenuHooks.didOpen();
      });
    },

    这里使用了UIManager,来自:

      const {
        UIManager,
        TouchableWithoutFeedback,
        ScrollView,
        View,
        BackHandler
      } = ReactNative

    用现代一点的写法的话就是:import { UIManager } from 'react-native';

    使用的时候是这么用的:

      const handle = ReactNative.findNodeHandle(this._menus[name].ref);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        // x, y, width, height, pageX, pageY
      });

    measure()方法的回调里得到的就是该组件对于Screen的位置。还有其他的measureXXX()方法在这里可以看到。

    measure得到的x,y,w,h,px,py是这个组件的左上角坐标(x,y)和宽、高。在这个measure方法里得到的px和py与这个组件的左上角坐标值一样。

    注意:measure的时候,只有在原生视图完成绘制之后才会返回值。

    所以,如果要快点得到一个组件在screen上的坐标值的话,那么可以这样:

    <View onLayout={this.onLayout}>
      
    </View>
    
    // onLayout
    onLayout() {
      const handle = ReactNative.findNodeHandle(this.refs.Container);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._ownMeasurements = {x, y, w, h, px, py};
      });
    }

    所以,在弹出菜单的组件上使用onLayoutprops得到它的位置。

    注意

    they(measureXXX方法) are not available on composite components that aren't directly backed by a native view.

    大意是,如果组合组件的最外层不是一个原生view的话,measureXXX()方法是没法用的!!

    那么measure方法的第一个参数,也就是measure的目标组件如何获得呢?代码在这里:const handle = ReactNative.findNodeHandle(this._menus[name].ref);。在findNodeHandle()方法的参数是组件的ref。那么,通过组件的ref可以得到组件的handle。在通过这个handle就可以来measure组件,得到这个组件的位置、宽高等数据。

    到这里我们就知道如何来算出触发组件的位置了。但是,这个直接使用UIManager的方法太复杂了。

    基本上,组件可以直接调用measure方法。我们来简单的实现一下这个弹出菜单的功能。

    Reimplement

    不管单词对错了。总之是重写一次。简化版的!为了篇幅足够长,我就把代码都贴出来了。哈哈~

    /**
     * Created by Uncle Charlie, 2018/03/01
     * @flow
     */
    
    import React from 'react';
    import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
    
    type Prop = {
      text: ?string,
      onPress: (e?: any) => void,
      styles?: { button: any, text: any },
    };
    
    export default class Button extends React.Component<Prop, {}> {
      static defaultProps = {
        text: 'Show Menu',
      };
    
      handlePress = () => {
        const { onPress } = this.props;
    
        if (!this.container) {
          console.error('container view is empty');
          return;
        }
    
        this.container.measure((x, y, w, h, px, py) => {
          console.log('===>measure', { x, y, w, h, px, py });
          onPress && onPress({ left: x, top: y + h });
        });
      };
    
      onLayout = () => {};
    
      render() {
        const { text, styles } = this.props;
        const wrapper =
          styles && styles.wrapper ? styles.wrapper : innerStyles.wrapper;
        return (
          <View
            style={wrapper}
            onLayout={this.onLayout}
            ref={container => (this.container = container)}
          >
            <TouchableOpacity onPress={this.handlePress}>
              <View>
                <Text>{text}</Text>
              </View>
            </TouchableOpacity>
          </View>
        );
      }
    }
    
    const innerStyles = StyleSheet.create({
      wrapper: {
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: 'green',
      },
    });

    这个简化版的实现思路就是:

    1. 点击按钮(TouchableOpacity)的时候measure按钮组件
    2. 把measure出来的按钮组件的位置作为参数发送给父组件
    3. 父组件在计算后的位置显示menu

    measure

    在measure组件之前,首先要获得这个组件的ref。

      render() {
        // ...
        return (
          <View ref={container => (this.container = container)}
          >
          // ...
          </View>
        );
      }

    得到的ref就是this.container

      handlePress = () => {
        const { onPress } = this.props;
    
        if (!this.container) {
          console.error('container view is empty');
          return;
        }
    
        this.container.measure((x, y, w, h, px, py) => {
          console.log('===>measure', { x, y, w, h, px, py });
          onPress && onPress({ left: x, top: y + h });
        });
      };

    在点击按钮之后开始measure。直接在获得的ref上调用measure方法就可以:this.container.measure。获得measure的结果之后,调用props传过来的方法onPress把需要用到的数据传过去。

    绘制Menu

    renderMenu = () => {
        const { top, left, open } = this.state;
        if (!open) {
          return null;
        }
    
        return (
          <View
            style={{
              position: 'absolute',
              left,
              top,
              width: 100,
              height: 200,
              backgroundColor: 'rgba(52, 52, 52, 0.8)',
            }}
          >
            <Text>Menu</Text>
          </View>
        );
      };

    我们要View显示在一个特定的位置的时候,需要在style里设置位置模式为position: 'absolute',也就是启用绝对定位。

    上面的left、和top就是菜单的具体位置。宽、高暂时hard code了(简化版。。。)。

    这样就一个popover,超级简化版的,就完成了。全部的代码在这里

    最后

    我们在前文中说道过一个更好的获得触发组件的位置的方式,onLayout。这个方法是空的。各位可以试着完成这个方法,或者全部完成这个popover组件作为练习。

    展开全文
  • 1.Popover组件怎么用 使用组件的同时(悬停触发) 又触发接口 获取后台数据 展现在气泡卡片上 (只显示唯一的气泡卡片,目前代码会出现一组气泡卡片 试了很多乱七八糟的办法 不会用 求救) ``` onVisibleChangeABox...
  • React tooltip的封装

    2019-12-02 16:20:09
    前端页面中的tooltip一直是被广泛应用的功能,tooltip本身的dom结构万变不离其宗,最大的问题可能就是那个气泡框下面的三角形的渲染,但是对于老手来说,解决这种小问题和吃饭喝水一样简单。 // ant中tooltip的使用...

    前端页面中的tooltip一直是被广泛应用的功能,tooltip本身的dom结构万变不离其宗,最大的问题可能就是那个气泡框下面的三角形的渲染,但是对于老手来说,解决这种小问题和吃饭喝水一样简单。

    // ant中tooltip的使用方法
    import { Tooltip, Button } from 'antd';
    
    ReactDOM.render(
      <div>
        <Tooltip placement="topLeft" title="Prompt Text">
          <Button>Align edge / 边缘对齐</Button>
        </Tooltip>
        <Tooltip placement="topLeft" title="Prompt Text" arrowPointAtCenter>
          <Button>Arrow points to center / 箭头指向中心</Button>
        </Tooltip>
      </div>,
      mountNode,
    );
    

    自从我开始使用react和antd之后,我一直很好奇antd里面的tooltip是怎么封装出来的,后来的工作中因为机缘巧合的关系,一直都没时间仔细研究一下,今天稍微花了点时间,实现和antd里面差不多的使用效果。

    其实实现tooltip并不是什么很困难的事情,困难的是如何将其优雅地封装起来,如同ant中的一样,使用Tooltip模块将需要触发的实体包裹起来,对于使用者来说是最直观,最方便的使用方法。而且ant的tooltip的实现中,并不会侵入页面的原始结构。

    技术难点:

    1. 触发事件的注入

    想要在react中实现,不入侵页面的结构的tooltip,就必须要将触发事件直接注入到props.children中,而不是在外面再包一层dom,再包一层dom会影响到css样式的编写。所以需要用到Rreact.cloneElement的Api

    return (
        <>
          {React.Children.map(props.children, (child: any) => {
            return React.cloneElement(child, {
              className: child.props.className + ' tooltip-wrap-content',
              onMouseEnter: mouseIn,
              onMouseLeave: mouseOut
            });
          })}
        </>
      );
    

    这个api可以在渲染子节点的时候主动注入自己想要传递给子节点的参数,这样就可以将触发事件注入进去了。当然不能直接覆盖子节点的事件,需要做一下代理,这里是简单写了。

    2. tooltip的结构

    这个其实是小问题,tooltip的结构可以分为两个问题,一个是tooltip应该渲染到哪里,一个是tooltip本身的dom结构的渲染。

    在React v16中有一个createPortal的Api,可以将react节点渲染至任意dom(包括react节点)下,用这个方法的话,我们就可以避免将tooltip渲染到实体的附近了,能最大程度避免污染到原始结构。

    由于采用了触发实体与tooltip渲染位置分离的结构,所以就不能直接用绝对定位来确定tooltip在窗口里的position了,需要使用fixed的定位方式。同时计算触发实体距离窗口边界的距离,直接根据窗口定位。虽然不是很有意义,但是还是将如何计算实体距离窗口边界代码贴出来好了,毕竟也曾经困扰过我一段时间:

    function getOffset(dom) {
        let parent: any = dom;
        let left: number = 0;
        let top: number = 0;
        while (parent) {
          left += parent.offsetLeft;
          top += parent.offsetTop;
          parent = parent.offsetParent;
        }
        return {
          left, top, 
          width: dom.offsetWidth,
          height: dom.offsetHeight
        };
      }
    

    只要搞清楚offsetParent,offsetLeft,offsetTop,offsetWidth,offsetHeight代表的意义就不难看懂上面的代码了。

    展开全文
  • 注意:未经允许不可私自转载,违者必究 React Native官方文档:https://reactnative.cn/docs/getting-started/ ...在写自定义Toast弹窗之前我们要先创建一个React Native第二视图层。 创建教程:https:...
  • 用对话框的话显得大材小用,毕竟只有一个输入框,而Popover气泡卡片正好可以满足需求而且占用面积小。因为里面添加了自定义的确认、取消按钮,所以需要手动控制气泡框的显隐,那么问题来了。当给定一个值来控制显隐...
  • 1.https://github.com/aroth/react-native-uploader 文件上传2.https://github.com/oblador/react-native-animatable 动画3.https://github.com/oblador/react-native-vector-icons 图标4....
  • react native中怎么将view固定在屏幕底部不被keyboard顶起
  • 这个是因为react最新版本抛弃使用了createClass这个函数,这个也是...var Popover = React.createClass({ propTypes: { isVisible: PropTypes.bool, onClose: PropTypes.func, }, ...
  • react Portal实现传送门(可以把组件挂载到任意节点上) 流程图 react Portal Portals 提供了一个最好的在父组件包含的DOM结构层级外的DOM节点渲染组件的方法 ReactDOM.createPortal(child,container); 第一个参数...
  • element之popover弹出框

    2020-05-19 17:16:22
    项目中遇到这种问题,一个页面有多种popover,如图,我想要一个背景是蓝色的popover弹出框和背景是白色的弹出框 当你改变其中一个popover时,其他的都别改变了,这是因为你改的popover的样式是全局的,且它是独立于...
  • RN中解决键盘问题 ...感觉整个人都不好了,react-native-tab-navigator,是一个很常用的第三方库,在android上选择一个TextInput,然后会发现在首次渲染的时候,tab navigator这个组件是没问题的,...
  • 引言 UI 组件中有很多弹出式组件,常见的如 Dialog,Tooltip 以及 Select 等。这些组件都有一个特点,它们的弹出层通常不是渲染在当前的 DOM 树中,...我们都知道 React App 的顶层某个地方肯定有这么一行代码:Rea...
  • 代码并非原创,忘了从哪里找到的,如有人知道,可博客留言。 ...https://codepen.io/Anyicheng2015/pen/jaayMw首先,你要对React有点了解。...class ContextMenu extends React.Component { constructor(props) {
  • React Native: 原生Popover菜单
  • 前言 在重构的路上,总能写点什么东西出来 ...用到的react 16.6特性 lazy, Suspense来实现子组件的懒加载 memo让函数式组件有PureComponent的特性(浅比较) flexbox来布局 效果图 代码实现 in...
  • react阻止事件冒泡

    2020-03-17 16:46:38
    class OuterClickExample extends React.Component { constructor(props) { super(props); this.state = { isOpen: false }; this.toggleContainer = React.createRef(); this.o...
1 2 3 4 5 ... 20
收藏数 642
精华内容 256