引言:本文是我研究react-native时写的一个简单的demo,代码里有详细的注释,好废话不多说,直接上代码。
1.项目目录
2.index.android.js
/** * index.android.js 入口文件 * Sample React Native App * https://github.com/facebook/react-native * @flow */ // icon={require("image!book")} // icon={require("image!movie")} import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, StatusBar } from 'react-native'; // 导入导航器 var Navigation = require("./android_views/common/navigation"); // 导入BookList var BookList = require("./android_views/book/book_list"); // 导入MovieList var MovieList = require("./android_views/movie/movie_list"); // tab组件 import TabNavigator from 'react-native-tab-navigator'; // 隐藏状态栏 StatusBar.setHidden(true); // TabNavigator管理两个模块:图书、电影 var DoubanProject = React.createClass({ getInitialState: function() { return { selectedTab: "图书" }; }, render: function() { return ( <TabNavigator> <TabNavigator.Item // 标题 title="图书" // 设置选中的位置 selected={this.state.selectedTab=="图书"} // 点击Event onPress={() => { this.setState({ selectedTab:"图书" }) }} //图标 renderIcon={() => <Image style={styles.icon} source={require("./res/images/book.png")} />} //选中时图标 renderSelectedIcon={() => <Image style={[styles.icon,{tintColor:'#2f8fe6'}]} source={require("./res/images/book.png")} />}> <Navigation component={BookList}/> </TabNavigator.Item> <TabNavigator.Item // 标题 title="电影" // 设置选中的位置 selected={this.state.selectedTab=="电影"} // 点击Event onPress={() => { this.setState({ selectedTab:"电影" }) }} //图标 renderIcon={() => <Image style={styles.icon} source={require("./res/images/movie.png")} />} //选中时图标 renderSelectedIcon={() => <Image style={[styles.icon,{tintColor:'#2f8fe6'}]} source={require("./res/images/movie.png")} />}> <Navigation component={MovieList}/> </TabNavigator.Item> </TabNavigator> ) } }); const styles = StyleSheet.create({ icon: { 22, height: 22 } }); AppRegistry.registerComponent('DoubanProject', () => DoubanProject);
3.service.js
/* 1.接口 API 基于豆瓣开放API的图书、电影 */ var BaseURL = "https://api.douban.com/v2/"; var Douban_APIS = { /* 图书搜索 image 图书缩略图 title 图书名称 publisher 出版社 author 作者 price 价格 pages 图书总页数 */ book_search: BaseURL + "book/search", /* 图书详情 image 图书缩略图 title 图书名称 publisher 出版社 author 作者 price 价格 pages 图书总页数 summary 图书简介 author_intro 作者简介 */ book_detail_id: BaseURL + "book/", /* 电影搜索 images.medium 电影图像 title 电影名称 casts 电影演员 数据需要再处理 rating.average 电影评分 year 电影上映时间 genres 电影标签 alt 电影详情url */ movie_search: BaseURL + "movie/search", } // 导出 module.exports = Douban_APIS;
4.util.js
/* 2.定义工具类 实现功能:定义多个属性,在项目中会使用的一些功能。包括:获取屏幕尺寸、loading组件、GET请求方法 包含组件: 外部引用: GET请求方法需要从外部引入url、请求成功的回调方法、请求失败的回调方法。 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Dimensions, // 用于获取设备屏幕的宽高 ActivityIndicator // loading组件 } from 'react-native'; // 定义对象,将提供的功能作为属性存放 var Util = { // 屏幕尺寸 windowSize: { Dimensions.get("window").width, height: Dimensions.get("window").height }, // 基于fetch的get方法 只负责下载数据,下载后的处理操作在回调方法中实现 // successCallback 数据下载成功的回调方法,在组件中实现 // failCallback 数据下载失败的回调方法,在组件中实现 getRequest: function(url, successCallback, failCallback) { fetch(url) .then((response) => response.json()) .then((responseData) => successCallback(responseData)) .catch((error) => failCallback(error)); }, // loading效果 loading: <ActivityIndicator style={{marginTop:200}} /> } // 导出 module.exports = Util;
5.searchBar.js
/* 3.实现功能:封装搜索栏组件,包括文本输入框和搜索按钮 包含组件: 外部传入: 输入框和按钮的属性设置由外部引入。例如:placeholder、onPress、onChangeText 使用...this.props将外部传入的属性设置给TextInput和TouchableOpacity 注意:指定高度、边框颜色、边框线框 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, TextInput, TouchableOpacity } from 'react-native'; // 定义组件 var SearchBar = React.createClass({ render: function() { return ( <View style={styles.container}> <View style={styles.inputContainer}> <TextInput style={styles.input} {...this.props} /> </View> <TouchableOpacity style={styles.btn} {...this.props}> <Text style={styles.search}>搜索</Text> </TouchableOpacity> </View> ); } }); var styles = StyleSheet.create({ container: { flexDirection: "row", justifyContent: "flex-end", alignItems: "center", height: 44, marginTop: 10 }, inputContainer: { flex: 1, marginLeft: 5 }, input: { flex: 1, height: 44, borderWidth: 1, borderRadius: 4, borderColor: "#CCC", paddingLeft: 5 }, btn: { 55, height: 44, marginLeft: 5, marginRight: 5, backgroundColor: "#23BEFF", borderRadius: 4, justifyContent: "center", alignItems: "center" }, search: { flex: 1, color: "#fff", fontSize: 15, fontWeight: "bold", textAlign: "center", lineHeight: 34 } }); module.exports = SearchBar;
6.left_icon.js
/* 4.实现功能:封装返回按钮图标,不使用图片 包含组件: 外部传入: */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View } from 'react-native'; // 定义组件 返回图标 var Icon = React.createClass({ render: function() { return ( <View> <View style={styles.go}></View> </View> ); } }); var styles = StyleSheet.create({ go: { 15, height: 15, borderLeftWidth: 2, borderBottomWidth: 2, borderColor: "#fff", marginLeft: 10, transform: [{rotate: "45deg"}] // 将一个矩形框旋转了45度 } }); module.exports = Icon;
7.header.js
/* 5.实现功能:封装header,在头部展示标题和返回按钮 包含组件: 外部传入: navigator 点击返回按钮返回上一级页面 initObj(backName、barTitle) 返回按钮的名称、标题 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, TouchableOpacity } from 'react-native'; // 导入左侧按钮 var Icon = require("./left_icon"); var Header = React.createClass({ render: function() { // 获取obj对象,包括:backName(按钮名称)、barTitle var headerContent = this.props.initObj; return ( <View style={styles.header}> <TouchableOpacity style={styles.left_btn} onPress={this._pop}> <Icon /> <Text style={styles.btn_next}>{headerContent.backName}</Text> </TouchableOpacity> <View style={styles.title_container}> <Text style={styles.title} numberOfLines={1}>{headerContent.barTitle}</Text> </View> </View> ); }, // 返回按钮事件处理方法 _pop: function() { this.props.navigator.pop(); } }); var styles = StyleSheet.create({ header: { height: 44, backgroundColor: "#3497FF", flexDirection: "row", justifyContent: "center", alignItems: "center" }, left_btn: { flexDirection: "row", justifyContent: "center", alignItems: "center" }, btn_text: { color: "#fff", fontSize: 17, fontWeight: "bold" }, title_container: { flex: 1, justifyContent: "center", alignItems: "center" }, title: { color: "#fff", fontSize: 18, fontWeight: "bold", lineHeight: 18, 200 } }); module.exports = Header;
8.navigation.js
/* 6.实现功能:封装导航器初始化设置 包含组件:Navigator 外部传入: component 需要展示的页面组件 route对象 必须添加component属性; 如果需要传值可以添加passProps属性 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View } from 'react-native'; // npm install react-native-deprecated-custom-components --save import CustomerComponents, { Navigator } from 'react-native-deprecated-custom-components'; var Navigation = React.createClass({ render: function() { // 创建route对象,约定格式 var rootRoute = { component: this.props.component, passProps: { } }; return ( <Navigator initialRoute={rootRoute} configureScene={() => {return Navigator.SceneConfigs.PushFromRight}} renderScene={(route,navigator) => { var Component = route.component; return ( <View style={{flex:1}}> <Component navigator={navigator} route={route} {...route.passProps}/> </View> ); }}/> ); } }); module.exports = Navigation;
9.customWebView.js
/* 7.实现功能:封装WebView,根据传入的url展示网页信息 包含组件:Header、WebView 外部传入: 给Header设置:navigator、initObj(backName、title) 给WebView设置:source(url) */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, WebView } from 'react-native'; var Header = require("./header"); var CustomWebView = React.createClass({ render: function() { return ( <View style={{backgroundColor:"white",flex:1}}> <Header navigator={this.props.navigator} initObj={{ backName:this.props.backName, barTitle:this.props.title }}/> <WebView startInLoadingState={true} contentInset={{top:-44,bottom:-120}} source={{uri:this.props.url}}/> </View> ); } }); module.exports = CustomWebView;
10.book_item.js
/* 8.图书列表 item 实现功能:展示图书信息,点击item进入图书详情页面 包含组件:基本组件 外部传入: book 图书对象 onPress事件处理方法 通过...this.props绑定,需要设置参数,即图书id 需要使用的字段: image 图书缩略图 title 图书名称 publisher 出版社 author 作者 price 价格 pages 图书总页数 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; var BookItem = React.createClass({ render: function() { var book = this.props.book; return ( <TouchableOpacity style={styles.item} {...this.props}> {/* 图书图像 */} <View style={styles.imageContainer}> <Image style={styles.image} source={{uri:book.image}}/> </View> {/* 图书信息 */} <View style={styles.contentContainer}> <View style={styles.textContainer}> <Text numberOfLines={1}>{book.title}</Text> </View> <View style={styles.textContainer}> <Text style={styles.publisher_author} numberOfLines={1}>{book.publisher}</Text> </View> <View style={styles.textContainer}> <Text style={styles.publisher_author} numberOfLines={1}>{book.author}</Text> </View> <View style={{flexDirection:"row",flex:1,alignItems:"center"}}> <Text style={styles.price}>{book.price}</Text> <Text style={styles.pages}>{book.pages}页</Text> </View> </View> </TouchableOpacity> ) } }); var styles = StyleSheet.create({ item: { flexDirection: "row", height: 120, padding: 10 }, imageContainer: { justifyContent: "center", alignItems: "center" }, image: { 80, height: 100 }, contentContainer: { flex: 1, marginLeft: 15 }, textContainer: { flex: 1, justifyContent: "center" }, publisher_author: { color: "#A3A3A3", fontSize: 13 }, price: { color: "#2BB2A3", fontSize: 16 }, pages: { marginLeft: 10, color: "#A7A0A0" } }); module.exports = BookItem;
11.book_list.js
/* 9.图书列表模块:搜索栏、图书列表 图书列表的内容:通过调用图书搜索接口获得多条图书数据 图书列表Item是单独封装的 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, TouchableOpacity, ListView, ScrollView } from 'react-native'; // 从common模块导入内容 var Util = require("./../common/util"); var SearchBar = require("./../common/searchBar"); var ServiceURL = require("./../common/service"); var BookItem = require("./book_item"); var BookDetail = require("./book_detail"); var BookList = React.createClass({ getInitialState: function() { var ds = new ListView.DataSource({ rowHasChanged: (oldRow, newRow) => oldRow !== newRow }); return { // dataSource dataSource: ds, // 网络请求状态标识 show: false, // 搜索关键字 // 作用:1.搜索接口需要设置搜索内容 2.点击搜索按钮时,修改关键字内容,重新请求数据,重新渲染 keywords: "react" }; }, getData: function() { // 开启loading,每次搜索时都需要重新下载显示数据 this.setState({ show: false }); // 请求数据 var that = this; var url = ServiceURL.book_search + "?count=20&q=" + this.state.keywords; Util.getRequest(url, function(data) { // 请求成功回调函数 /* 如果没有相关书籍,使用alert提示 https://api.douban.com/v2/book/search?count=20&q=react {"count":0,"start":0,"total":0,"books":[]} */ if (!data.books || data.books.length == 0) { return alert("未查询到相关书籍"); } // 设置下载状态和数据源 var ds = new ListView.DataSource({ rowHasChanged: (oldRow, newRow) => oldRow !== newRow }); that.setState({ show: true, dataSource: ds.cloneWithRows(data.books) }); }, function(error) { // 请求失败回调函数 alert(error); }) }, // TextInput的onChangeText事件处理方法 _changeText: function(text) { this.setState({ keywords: text }); }, _searchPress: function() { this.getData(); }, _showDetail:function(bookID){ var detailRoute = { component: BookDetail, passProps:{ bookID: bookID } } this.props.navigator.push(detailRoute); }, // 布局 render: function() { return ( <ScrollView> <SearchBar placeholder="请输入图书的名称" onPress={this._searchPress} onChangeText={this._changeText}/> { // 请求数据时显示loading,数据请求成功后显示ListView this.state.show ? <ListView dataSource={this.state.dataSource} initialListSize={10} renderRow={this._renderRow} renderSeparator={this._renderSeparator}/> : Util.loading } </ScrollView> ); }, componentDidMount: function() { // 请求数据 this.getData(); }, _renderRow: function(book) { return <BookItem book={book} onPress={this._showDetail.bind(this,book.id)}/> }, // 分割线 _renderSeparator: function(sectionID: number, rowID: number) { var style = { height: 1, backgroundColor: "#CCCCCC" } return <View style={style} key={sectionID+rowID} /> } }); var styles = StyleSheet.create({ // }); module.exports = BookList;
12.book_detail.js
/* 10.图书详情 实现功能:展示图书详情,包括:图书信息、图书简介、作者简介 包含组件:基本组件、BookItem(图书信息使用BookItem展示) 外部传入: 需要使用的字段: image 图书缩略图 title 图书名称 publisher 出版社 author 作者 price 价格 pages 图书总页数 summary 图书简介 author_intro 作者简介 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, ScrollView } from 'react-native'; // 引入 var ServiceURL = require("./../common/service"); var Util = require("./../common/util"); var Header = require("./../common/header"); var BookItem = require("./book_item"); // 创建组件类 var BookDetail = React.createClass({ getInitialState:function() { return { bookData:null // 图书对象详情信息 } }, getData:function(){ // 获取图书信息 var that = this; var url = ServiceURL.book_detail_id + this.props.bookID; Util.getRequest(url,function(data){ that.setState({ bookData:data }); },function(error){ alert(error); }); }, render:function(){ return ( <ScrollView style={styles.container}> { this.state.bookData ? <View> <Header initObj={{backName:"图书",barTitle:this.state.bookData.title}} navigator={this.props.navigator}/> <BookItem book={this.state.bookData}/> <View> <Text style={styles.title}>图书简介</Text> <Text style={styles.text}>{this.state.bookData.summary}</Text> </View> <View style={{marginTop:10}}> <Text style={styles.title}>作者简介</Text> <Text style={styles.text}>{this.state.bookData.author_intro}</Text> </View> <View style={{height:55}}></View> </View> : Util.loading } </ScrollView> ); }, // 组件挂载以后,进行网络请求 componentDidMount:function(){ // 请求图书详情 this.getData(); } }); var styles = StyleSheet.create({ container:{ flex:1, backgroundColor:"white" }, title:{ fontSize:16, marginTop:10, marginLeft:10, marginBottom:10, fontWeight:"bold" }, text:{ marginLeft:10, marginRight:10, color:"#000D22" } }); module.exports = BookDetail;
13.movie_item.js
/* 11.电影列表item 实现功能:展示电影信息,点击item进入电影详情页面 包含组件:基本组件 外部传入: movie 电影对象 onPress 通过...this.props绑定,需要设置参数:电影名称、电影详情页面url 需要使用的字段: images.medium 电影图像 title 电影名称 casts 电影演员 数据需要再处理 rating.average 电影评分 year 电影上映时间 genres 电影标签 alt 电影详情url */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; var MovieItem = React.createClass({ render:function() { var movie = this.props.movie; // 提取演员姓名 // 原始数据结构:数组元素是描述演员的对象,对象中包含演员名字 // 需要遍历数组,把每个演员的名字存在一个新的数组中 var actors = []; for(var i in movie.casts){ actors.push(movie.casts[i].name); } return ( <TouchableOpacity style={styles.item} {...this.props}> <View style={styles.imageContainer}> <Image style={styles.image} resizeMode="contain" source={{uri:movie.images.medium}}/> </View> <View style={styles.contentContainer}> <View style={styles.textContainer}> <Text style={styles.text} numberOfLines={1}>名称:{movie.title}</Text> </View> <View style={styles.textContainer}> <Text style={styles.text} numberOfLines={1}>演员:{actors}</Text> </View> <View style={styles.textContainer}> <Text style={styles.text} numberOfLines={1}>评分:{movie.rating.average}</Text> </View> <View style={styles.textContainer}> <Text style={styles.text} numberOfLines={1}>时间:{movie.year}</Text> </View> <View style={styles.textContainer}> <Text style={styles.text} numberOfLines={1}>标签{movie.genres}</Text> </View> </View> </TouchableOpacity> ); } }); var styles = StyleSheet.create({ item:{ flexDirection:"row", height:120, padding:10 }, imageContainer:{ justifyContent:"center", alignItems:"center" }, image:{ 80, height:110 }, contentContainer:{ flex:1, marginLeft:15 }, textContainer:{ flex:1, justifyContent:"center" }, text:{ color:"black" } }); module.exports = MovieItem;
14.movie_list.js
/* 12.电影列表模块:搜索框、电影列表 电影列表的内容:通过调用电影搜索接口获得多条电影数据 电影列表Item是单独封装的 */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, TouchableOpacity, ListView, ScrollView } from 'react-native'; var SearchBar = require("./../common/searchBar"); var Util = require("./../common/util"); var ServiceURL = require("./../common/service"); var MovieItem = require("./movie_item"); var MovieWebView = require("./../common/customWebView"); var MovieList = React.createClass({ getInitialState:function() { var ds = new ListView.DataSource({ rowHasChanged:(oldRow,newRow) => oldRow!==newRow }); return { dataSource: ds, show: false, keywords:"哈利波特" }; }, _changeText:function(text){ this.setState({ keywords:text }); }, _searchPress:function(){ this.getData(); }, _showDetail:function(title,url){ var detailRoute = { component:MovieWebView, passProps:{ backName:"电影", title:title, url:url } }; // 推出 this.props.navigator.push(detailRoute); }, getData:function(){ this.setState({ show:false }); var that = this; var url = ServiceURL.movie_search + "?count=20&q=" + this.state.keywords; /* https://api.douban.com/v2/movie/search?count=20&q=哈利波特 {"count":0,"start":0,"total":0,"books":[]} */ Util.getRequest(url,function(data){ if(!data.subjects||data.subjects.length==0){ return alert("未找到相关电影"); } var ds = new ListView.DataSource({ rowHasChanged:(oldRow,newRow) => oldRow!==newRow }); var movies = data.subjects; that.setState({ show:true, dataSource:ds.cloneWithRows(movies) }); },function(error){ alert(error); }); }, render:function(){ return ( <ScrollView> <SearchBar placeholder="请输入电影的名称" onPress={this._searchPress} onChangeText={this._changeText}/> { this.state.show ? <ListView dataSource={this.state.dataSource} initialListSize={10} renderRow={this._renderRow} renderSeparator={this._renderSeparator}/> : Util.loading } </ScrollView> ); }, componentDidMount:function(){ // 请求数据 this.getData(); }, _renderRow:function(movie){ return <MovieItem movie={movie} onPress={this._showDetail.bind(this, movie.title, movie.alt)}/>; }, // 分割线 _renderSeparator:function(sectionID:number,rowID:number){ var style = { height: 1, backgroundColor:"#CCCCCC" }; return <View style={style} key={sectionID+rowID}></View> } }); var styles = StyleSheet.create({ }); module.exports = MovieList;
15.效果图