美文网首页ReactNativeReact Native学习Android
react-native山寨美团下拉菜单实现

react-native山寨美团下拉菜单实现

作者: 追梦3000 | 来源:发表于2017-01-03 00:47 被阅读3348次

    山寨美团下拉菜单实现目标

    山寨美团下拉菜单主要实现以下几个功能:
    1、在下拉的时候有动画过度效果
    2、�下拉菜单出现后点击菜单项,菜单项可选择,并触发对应的事件
    3、下拉菜单中的项目可以配置

    效果图

    Untitled.gif

    具体实现

    1、整体的结构

    下拉菜单项必须覆盖内容,并在下拉菜单出现的时候仍然可以点击上面的返回按钮,所以不能使用Modal组件

    所以这里采用的思路为

    <View>
      <MenuItems />
      {this.props.renderContent()}
      <MenuContent style={{position:'absolute'}} />
    </View>
    
    

    采用代理的方式绘制页面的主题,
    这样MenuContent部分可以遮挡住页面的内容,在View之外的部分在菜单出现之后,仍然可以交互。

    2、打钩图标的制作:

    这里采用了svg文件中的路径转ART的方法�,参考在React Native中使用ART

    具体方法为去http://www.iconsvg.com/下载一个svg文件,打开应该类似这种

    Paste_Image.png
    const Check = ()=>{
      return (
         <Surface
            width={18}
            height={12}
            >
            <Group scale={0.03}>
                <Shape
                    fill={COLOR_HIGH}
                    d={`M494,52c-13-13-33-13-46,0L176,324L62,211c-13-13-33-13-46,0s-13,33,0,46l137,136c6,6,15,10,23,10s17-4,23-10L494,99
          C507,86,507,65,494,52z`}
                />
            </Group>
          </Surface>
      );
    }
    

    3、下拉动画的实现
    参考【React Native开发】React Native进阶之Animated动画库详解-基础篇(64)

    这里分成两个部分

    • 背景颜色渐变
      这里使用Animated.timing(
      this.state.fadeInOpacity,
      {
      toValue: value,
      duration : 250,
      }
      创建一个动画
    • 下拉菜单逐渐出现
      Animated.timing(
      this.state.height[index],
      {
      toValue: height,
      duration : 250
      }
      );
    • 两个动画必须联动
     Animated.parallel([this.createAnimation(index, this.state.maxHeight[index]), this.createFade(1)]).start(); 
    

    4、具体代码

    /**
     * Sample React Native App
     * https://github.com/facebook/react-native
     * @flow
     */
    
    import React, { Component } from 'react';
    import {
      StyleSheet,
      Text,
      TextInput,
      View,
      Animated,
      TouchableWithoutFeedback,
      ScrollView,
      TouchableOpacity,
      TouchableHighlight,
      PixelRatio,
      Dimensions,
      Easing,
      Modal
    } from 'react-native';
    
    
    const SCREEN_WIDTH = Dimensions.get('window').width;
    const SCREEN_HEIGHT = Dimensions.get('window').height;
    
    
    class Form extends Component{
    
    
      componentDidMount() {
    
        /*
        Api.bind(this,"someapi",(props)=>{
          return parseInt(props.params.id);
        });*/
        
      }
    
      render(){
        if(Array.isArray(this.props.children)){
          for(var i=0; i < this.props.children.length; ++i){
            console.log(this.props.children[i].props.ref)
          }
        }else{
           console.log(this.props.children.ref)
        }
        
    
        return <View style={styles.container}>{this.props.children}</View>;
      }
    
    }
    
    import {
        ART
    } from 'react-native'
    
    const {Surface, Shape, Path, Group} = ART;
    
    const T_WIDTH = 7;
    const T_HEIGHT = 4;
    
    
    const COLOR_HIGH = '#00bea9';
    const COLOR_NORMAL = '#6c6c6c';
    
    class Triangle extends React.Component{
    
        render(){
    
            var path;
            var fill;
            if(this.props.selected){
              fill = COLOR_HIGH;
              path = new Path()
                .moveTo(T_WIDTH/2, 0)
                .lineTo(0, T_HEIGHT)
                .lineTo(T_WIDTH, T_HEIGHT)
                .close();
            }else{
              fill = COLOR_NORMAL;
              path = new Path()
                .moveTo(0, 0)
                .lineTo(T_WIDTH, 0)
                .lineTo(T_WIDTH/2, T_HEIGHT)
                .close();
            }
    
            return(
                <Surface width={T_WIDTH} height={T_HEIGHT}>
                    <Shape d={path} stroke="#00000000" fill={fill} strokeWidth={0} />
                </Surface>
            )
        }
    }
    
    const TopMenuItem = (props)=>{
      const onPress=()=>{
        props.onSelect(props.index);
      }
      return (
        <TouchableWithoutFeedback onPress={onPress}>
          <View style={styles.item}>
            <Text style={props.selected?styles.menuTextHigh:styles.menuText}>{props.label}</Text>
            <Triangle selected={props.selected} />
          </View>
        </TouchableWithoutFeedback>
      );
    };
    
    
    const Check = ()=>{
      return (
         <Surface
            width={18}
            height={12}
            >
            <Group scale={0.03}>
                <Shape
                    fill={COLOR_HIGH}
                    d={`M494,52c-13-13-33-13-46,0L176,324L62,211c-13-13-33-13-46,0s-13,33,0,46l137,136c6,6,15,10,23,10s17-4,23-10L494,99
          C507,86,507,65,494,52z`}
                />
            </Group>
          </Surface>
      );
    }
    
    
    const Subtitle = (props)=>{
      let textStyle = props.selected ? 
        [styles.tableItemText, styles.highlight, styles.marginHigh] : 
        [styles.tableItemText, styles.margin];
    
      let rightTextStyle = props.selected ? [styles.tableItemText, styles.highlight] : styles.tableItemText;
    
      let onPress = ()=>{
        props.onSelectMenu(props.index, props.subindex, props.data);
      }
    
      return (
        <TouchableHighlight onPress={onPress} underlayColor="#f5f5f5">
          <View style={styles.tableItem}>
            <View style={styles.row}>
              {props.selected && <Check />}
              <Text style={textStyle}>{props.data.title}</Text>
            </View>
            <Text style={rightTextStyle}>{props.data.subtitle}</Text>
          </View>
        </TouchableHighlight>
      );
    };
    
    const Title = (props)=>{
       let textStyle = props.selected ? 
        [styles.tableItemText, styles.highlight, styles.marginHigh] : 
        [styles.tableItemText, styles.margin];
    
      let rightTextStyle = props.selected ? [styles.tableItemText, styles.highlight] : styles.tableItemText;
    
    
      let onPress = ()=>{
        props.onSelectMenu(props.index, props.subindex, props.data);
      }
    
      return (
        <TouchableHighlight onPress={onPress} underlayColor="#f5f5f5">
          <View style={styles.titleItem}>
            {props.selected && <Check />}
            <Text style={textStyle}>{props.data.title}</Text>
          </View>
        </TouchableHighlight>
      );
    };
    
    /**
     * 使用方法:
     *
     *
     *
     * <TopMenu config={[  
     * {type:'single',label:'初始显示', selectedIndex:0, data:[ "label1","label2"  ]}
     *   
     * ]} />
     * 
     */
    
    
    
    const MAX_HEIGHT = 11 * 43;
    
    export default class TopMenu extends Component {
    
     
      constructor(props){
        super(props);
        let array = props.config;
        let top = [];
        let maxHeight = [];
        let subselected = [];
        let height = [];
        //最大高度
        var max = parseInt((SCREEN_HEIGHT - 80) * 0.8 / 43);
    
    
        for(let i=0, c=array.length; i < c; ++i ){
          let item = array[i];
          top[i] = item.data[item.selectedIndex].title;
          maxHeight[i] = Math.min(item.data.length, max) * 43;
          subselected[i] = item.selectedIndex;
          height[i] = new Animated.Value(0);
        }
    
    
        //分析数据
        this.state = {
          top : top,
          maxHeight : maxHeight,
          subselected: subselected,
          height: height,
          fadeInOpacity : new Animated.Value(0),
          selectedIndex : null
        };
    
        ///数据
    
      }
    
    
      componentDidMount() {
      
      }
    
      createAnimation=(index, height)=>{
        return Animated.timing(                 
            this.state.height[index],               
            {
              toValue: height,                 
              duration : 250
            }
          );
      }
    
      createFade=(value)=>{
        return Animated.timing(                 
            this.state.fadeInOpacity,               
            {
              toValue: value,                 
              duration : 250, 
            }
          );
      }
    
    
      onSelect=(index)=>{
        if(index===this.state.selectedIndex){
          //消失
          this.hide(index);
        }else{
          this.setState({selectedIndex:index, current: index});
          this.onShow(index);
        }
      }
    
      hide=(index, subselected)=>{
        let opts = {selectedIndex:null, current:index};
        if(subselected!==undefined){
          this.state.subselected[index]= subselected;
          this.state.top[index] = this.props.config[index].data[subselected].title;
          opts = {selectedIndex:null, current:index, subselected: this.state.subselected.concat() };
        }
        this.setState(opts);
        this.onHide(index);
      }
    
    
      onShow=(index)=>{
        
        Animated.parallel([this.createAnimation(index, this.state.maxHeight[index]), this.createFade(1)]).start(); 
      }
    
    
      onHide=(index)=>{
        //其他的设置为0
        for(let i=0, c = this.state.height.length; i < c; ++i){
          if(index!=i){
            this.state.height[i].setValue(0);
          }
        }
         Animated.parallel([this.createAnimation(index, 0), this.createFade(0)]).start();
        
      }
    
      onSelectMenu = (index, subindex, data)=>{
        this.hide(index, subindex);
        this.props.onSelectMenu && this.props.onSelectMenu(index, subindex, data);
      }
    
    
      renderList=(d, index)=>{
        let subselected = this.state.subselected[index];
        let Comp = null;
        if(d.type=='title'){
          Comp = Title;
        }else{
          Comp = Subtitle;
        }
    
        let enabled = this.state.selectedIndex ==index || this.state.current == index;
    
        return (
          <Animated.View key={index} pointerEvents={enabled ? 'auto':'none'} style={[styles.content, {opacity: enabled ? 1 : 0, height: this.state.height[index]}]}>
            <ScrollView style={styles.scroll}>
            {d.data.map((data, subindex)=>{
              return <Comp 
                      onSelectMenu={this.onSelectMenu} 
                      index={index} 
                      subindex={subindex} 
                      data={data} 
                      selected={subselected == subindex} 
                      key={subindex} />
            })}
            </ScrollView>
          </Animated.View>
        );
      }
    
      render() {
        let list = null;
        if(this.state.selectedIndex !== null){
          list = this.props.config[this.state.selectedIndex].data;
        }
        console.log(list);
        return (
          <View style={{flex:1}}>
            <View style={styles.topMenu}>
            {this.state.top.map((t, index)=>{
              return <TopMenuItem 
                key={index}
                index={index} 
                onSelect={this.onSelect} 
                label={t} 
                selected={this.state.selectedIndex === index} />
            })}
            </View>
            {this.props.renderContent()}
            <View style={styles.bgContainer} pointerEvents={this.state.selectedIndex !== null ? "auto" : "none"}>
              <Animated.View style={[styles.bg, {opacity:this.state.fadeInOpacity}]} />
              {this.props.config.map((d, index)=>{
                return this.renderList(d, index);
              })}
            </View>
          </View>
        );
      }
    }
    
    const LINE = 1/PixelRatio.get();
    
    const styles = StyleSheet.create({
    
      scroll:{flex:1, backgroundColor:'#fff'},
      bgContainer:{position:'absolute', top:40, width:SCREEN_WIDTH, height:SCREEN_HEIGHT},
      bg:{flex:1, backgroundColor:'rgba(50,50,50,0.2)'},
      content:{ 
        position:'absolute',
        width:SCREEN_WIDTH
      },
    
      highlight:{
        color:COLOR_HIGH
      },
    
      marginHigh:{marginLeft:10},
      margin:{marginLeft:28},
    
    
    
      titleItem:{
        height:43,
        alignItems:'center',
        paddingLeft:10,
        paddingRight:10,
        borderBottomWidth:LINE,
        borderBottomColor:'#eee',
        flexDirection:'row',
      },
    
      tableItem:{
        height:43,
        alignItems:'center',
        paddingLeft:10,
        paddingRight:10,
        borderBottomWidth:LINE,
        borderBottomColor:'#eee',
        flexDirection:'row',
        justifyContent:'space-between'
      },
      tableItemText:{fontWeight:'300', fontSize:14 },
      row:{
        flexDirection:'row'
      },
    
      item:{
        flex:1,
        flexDirection:'row',
        alignItems:'center',
        justifyContent:'center',
      },
      menuTextHigh:{
        marginRight:3,
        fontSize:13,
        color:COLOR_HIGH
      },
      menuText:{
        marginRight:3,
        fontSize:13,
        color:COLOR_NORMAL
      },
      topMenu:{
        flexDirection:'row',
        height:40,
        borderTopWidth:LINE,
        borderTopColor:'#bdbdbd',
        borderBottomWidth:1,
        borderBottomColor:'#f2f2f2'
      },
    
    });
    
    
    
    

    5、使用方式

    import React, { Component } from 'react';
    import {
      AppRegistry,
      StyleSheet,
      Text,
      TextInput,
      View,
      Animated,
      TouchableWithoutFeedback,
      ScrollView,
      TouchableOpacity,
      TouchableHighlight,
      PixelRatio,
      Dimensions,
      Alert
    } from 'react-native';
    import TopMenu from '../widget/TopMenu'
    import TitleBar from '../widget/TitleBar'
    const SCREEN_WIDTH = Dimensions.get('window').width;
    
    
    const CONFIG = [
      {
        type:'subtitle',
        selectedIndex:1,
        data:[
          {title:'全部', subtitle:'1200'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
          {title:'自助餐', subtitle:'300'},
        ]
      },
      {
        type:'title',
        selectedIndex:0,
        data:[{
          title:'智能排序'
        }, {
          title:'离我最近'
        }, {
          title:'好评优先'
        }, {
          title:'人气最高'
        }]
      }
    ];
    
    export default class TopMenuExample extends Component {
    
      constructor(props){
        super(props);
        this.state = {
          data:{}
        };
      }
    
    
      componentDidMount() {
      
      }
    
      onPress=()=>{
        Alert.alert('yes');
      }
    
      renderContent=()=>{
    
        return (
          <TouchableOpacity onPress={this.onPress}>
            <Text>index:{this.state.index} subindex:{this.state.subindex} title:{this.state.data.title}</Text>
          </TouchableOpacity>
        );
      }
    
      onSelectMenu=(index, subindex, data)=>{
        this.setState({index, subindex, data});
      }
    
      render() {
    
        return (
          <View style={styles.container} ref="MAIN">
            <TitleBar nav={this.props.nav} title="山寨美团菜单" />
            <TopMenu config={CONFIG} onSelectMenu={this.onSelectMenu} renderContent={this.renderContent} />
          </View>
        );
      }
    }
    
    const styles = StyleSheet.create({
    
      container: {
        backgroundColor:'#fff',
        flex: 1,
      },
      
    });
    
    

    代码暂时上传到这里
    链接:http://pan.baidu.com/s/1eSELKKM 密码:h89j
    注意ios运行的时候需要修改一下ip地址

    image.png

    相关文章

      网友评论

      • sky007z:你好,能共享下源码吗,754930324@qq.com谢谢
      • 狂野的骚猪:亲...有Demo么?2415516134@qq.com...谢谢!
      • d08fb27f2fbe:你好,你之前发的网盘已经过期了。能麻烦你再发一遍源码给我吗?谢谢。
      • MrJiang_a01b:您好。。博主,,可以添加二级菜单吗
      • 鹏鹏哥哥_:最近要用到这样的控件,可以发我一个吗?414826982@qq.com 谢谢
      • mutphy:按照这样写,下拉组件后面放列表筛选结果,由于下拉是绝对定位,但列表又放在下拉后面的,导致列表层级高于下拉列表,下拉不能覆盖列表的内容,这个该怎么解决呢?
      • ouxiaojie:求完整代码,530221248@qq.com 非常感谢
        ouxiaojie:@雪亮出品 谢谢~
        追梦3000:代码暂时上传到这里
        链接:http://pan.baidu.com/s/1eSELKKM 密码:h89j
        注意ios运行的时候需要修改一下ip地址
      • abfd8c9d79c3:我也想要一份完整代碼
        謝謝額啦
      • 玉思盈蝶:亲...有Demo么?1299625033@qq.com...谢谢!
        玉思盈蝶:@雪亮出品 好的..谢谢亲了
        追梦3000:代码暂时上传到这里
        链接:http://pan.baidu.com/s/1eSELKKM 密码:h89j
        注意ios运行的时候需要修改一下ip地址
      • c49042b87c82:最近在折腾下拉菜单,头好痛,请问有github地址吗?或则可以发我邮箱,1028900373@qq.com,万分感谢!
        追梦3000:代码暂时上传到这里
        链接:http://pan.baidu.com/s/1eSELKKM 密码:h89j
        注意ios运行的时候需要修改一下ip地址
      • cd0c13afd056:有项目完整代码吗,照着写有报错。

      本文标题:react-native山寨美团下拉菜单实现

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