视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
React Native仿美团下拉菜单的实例代码
2020-11-27 22:33:13 责编:小采
文档

本文介绍了React Native仿美团下拉菜单的实例代码,最近也在学习React Native,顺便分享给大家

在很多产品中都会涉及到下拉菜单选择功能,用的最好的当属美团了,其效果如下:

要实现上面的效果,在原生中比较好做,直接使用PopWindow组件即可。如果使用React Native开发上面的效果,需要注意几个问题:

1、 在下拉的时候有动画过度效果;

2、下拉菜单出现后点击菜单项,菜单项可选择,并触发对应的事件;

3、下拉菜单中的项目可以配置;

要实现弹框效果,我们马上回想到使用Model组件,而要绘制打钩图标和下拉三角,我们首先想到使用ART实现,当然选择使用图标也是可以的。例如使用ART绘制对勾的代码如下:

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>
 );
}

下拉动画的实现上,需要使用Animated。例如,背景颜色变化需要使用Animated.timing。

 this.state.fadeInOpacity, 
 {
 toValue: value, 
 duration : 250, 
 }

运行效果:

本示例设计三个文件:导航栏FoodActionBar.js,下拉弹框TopMenu.js和文件主类FoodView.js。

FoodActionBar.js

/**
 * https://github.com//react-native
 * @flow 首页的标题栏
 */

import React, {Component} from 'react';
import {Platform, View, Dimensions, Text, StyleSheet, TouchableOpacity, Image} from 'react-native';
import px2dp from '../util/Utils'

const isIOS = Platform.OS == "ios"
const {width, height} = Dimensions.get('window')
const headH = px2dp(isIOS ?  : 44)

export default class FoodActionBar extends Component {

 constructor(props) {
 super(props);
 this.state = {
 showPop: false,
 }
 }


 renderHeader() {
 return (
 <View style={styles.headerStyle}>
 <TouchableOpacity style={styles.action} >
 <Image style={styles.scanIcon}/>
 </TouchableOpacity>
 <TouchableOpacity style={styles.searchBar}>
 <Image source={require('../images/ic_search.png')} style={styles.iconStyle}/>
 <Text style={{fontSize: 13, color: "#666", marginLeft: 5}}>输入商家名、品类和商圈</Text>
 </TouchableOpacity>
 <TouchableOpacity style={styles.action} onPress={() => { this.setState({ showPop: !this.state.showPop }) }}>
 <Image style={styles.scanIcon}
 source={require('../images/icon_address.png')}/>
 </TouchableOpacity>
 </View>
 )
 }

 render() {
 return (
 <View>
 {this.renderHeader()}
 </View>
 );
 }
}

const styles = StyleSheet.create({
 headerStyle: {
 backgroundColor: "#ffffff",
 height: headH,
 paddingTop: px2dp(isIOS ? 20 : 0),
 flexDirection: 'row',
 alignItems: 'center',
 },
 searchBar: {
 flex:1,
 height: 30,
 borderRadius: 19,
 backgroundColor:'#e9e9e9',
 marginLeft: 10,
 flexDirection: 'row',
 justifyContent: 'flex-start',
 alignItems: 'center',
 alignSelf: 'center',
 paddingLeft: 10,
 },
 text: {
 fontSize: 16,
 color: '#ffffff',
 justifyContent: 'center',
 },
 iconStyle: {
 width: 22,
 height: 22,
 },
 action: {
 flexDirection: 'row',
 justifyContent: 'center',
 alignItems: 'center',
 marginLeft:10,
 marginRight:10
 },
 scanIcon: {
 width: 28,
 height: 28,
 alignItems: 'center',
 },
 scanText: {
 fontSize: 14,
 color: '#ffffff',
 justifyContent: 'center',
 alignItems: 'center',
 },
});

TopMenu.js

/**
 * Sample React Native App
 * https://github.com//react-native
 * @flow
 */

import React, {Component} from 'react';
import {
 AppRegistry,
 StyleSheet,
 Animated,
 ScrollView,
 Dimensions,
 PixelRatio,
 Text,
 TouchableWithoutFeedback,
 TouchableHighlight,
 ART,
 View
} from 'react-native';

const {Surface, Shape, Path, Group} = ART;

const {width, height} = Dimensions.get('window');

const T_WIDTH = 7;
const T_HEIGHT = 4;

const COLOR_HIGH = '#00bea9';
const COLOR_NORMAL = '#6c6c6c';

const LINE = 1 / PixelRatio.get();

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 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>
 );
};

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>
 );
}



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((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 styles = StyleSheet.create({

 scroll: {flex: 1, backgroundColor: '#fff'},
 bgContainer: {position: 'absolute', top: 40, width: width, height: height},
 bg: {flex: 1, backgroundColor: 'rgba(50,50,50,0.2)'},
 content: {
 position: 'absolute',
 width: 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'
 },

});

主类FoodView.js:

/**
 * Sample React Native App
 * https://github.com//react-native
 * @flow
 */

import React, {Component} from 'react';
import {
 AppRegistry,
 StyleSheet,
 TouchableOpacity,
 Dimensions,
 Text,
 View
} from 'react-native';
const {width, height} = Dimensions.get('window');

import FoodActionBar from "./pop/FoodActionBar";
import Separator from "./util/Separator";
import TopMenu from "./pop/TopMenu";


const CONFIG = [
 {
 type:'subtitle',
 selectedIndex:1,
 data:[
 {title:'全部', subtitle:'1200m'},
 {title:'自助餐', subtitle:'300m'},
 {title:'自助餐', subtitle:'200m'},
 {title:'自助餐', subtitle:'500m'},
 {title:'自助餐', subtitle:'800m'},
 {title:'自助餐', subtitle:'700m'},
 {title:'自助餐', subtitle:'900m'},
 ]
 },
 {
 type:'title',
 selectedIndex:0,
 data:[{
 title:'智能排序'
 }, {
 title:'离我最近'
 }, {
 title:'好评优先'
 }, {
 title:'人气最高'
 }]
 }
];


export default class FoodView extends Component {

 constructor(props){
 super(props);
 this.state = {
 data:{}
 };
 }

 renderContent=()=>{
 return (
 <TouchableOpacity >
 <Text style={styles.text}>index:{this.state.index} subindex:{this.state.subindex} title:{this.state.data.title}</Text>
 </TouchableOpacity>
 );
 // alert(this.state.data.title)
 };

 onSelectMenu=(index, subindex, data)=>{
 this.setState({index, subindex, data});
 };

 render() {
 return (
 <View style={styles.container}>
 <FoodActionBar/>
 <Separator/>
 <TopMenu style={styles.container} config={CONFIG} onSelectMenu={this.onSelectMenu} renderContent={this.renderContent}/>
 </View>
 );
 }
}

const styles = StyleSheet.create({
 container: {
 flex: 1,
 width:width,
 backgroundColor: '#F5FCFF',
 },
 text: {
 fontSize:20,
 marginTop:100,
 justifyContent: 'center',
 alignItems: 'center',

 },

});

下载本文
显示全文
专题