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

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

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

推荐阅读更多精彩内容