不同于我们的 tab 图标,我们用来展示的每一个用户的图片都来自一个外部的源. 这不是问题,事实上展示它们要比展示静态资源更加简单.
我们只是向 Image 组件传递一个对象,而不是向它传入一个需要的图片. 这个对象会有一个属性—— url,它会指向我们想要加载的图片.
当我们将用户信息作为一个叫做person的属性进行传递时,我们可以通过 this.props.person.picture 访问到图片的 URL。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> </View> ) } }); // omitted code var styles = StyleSheet.create({ ... person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personImage: { flex: 1, height: 200 }, ... }); module.exports = facemashTab;
这里也还有一些必要的样式 - 重新设置图片的大小难以置信的简单. 类似的 CSS 属性,比如 background-size,可以在 React Native 中被应用到图片之上, 而这里我们智慧在上面放一个 height,而图片会据此对尺寸进行重新设置.
现在我们可以将剩下的用户信息添加进去了。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </View> ) } }); // omitted code var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, personImage: { flex: 1, height: 200 }, personName: { fontSize: 18, flex: 1, paddingLeft: 5 }, personScore: { flex: 0.25, alignItems: 'center' }, personScoreHeader: { color: 'rgba( 0, 0, 0, 0.3 )', fontSize: 10, fontWeight: 'bold' }, personScoreValue: { color: 'rgba( 0, 0, 0, 0.6 )', fontSize: 16 }, won: { color: '#93C26D' }, lost: { color: '#DD4B39' } }); module.exports = facemashTab;
你可以从分支二检出到目前这儿为止的代码。
现在我们已经让用户显示了出来,可以着手加入点击时间来让用户选择出谁比较热门了。
React Native 为我们提供了 TouchableHighlight 组件. 它能让我们的View组件正常的响应触摸. 当它被触摸时,被封装视图的透明度降低了. 这就让我们的组件“感官上”是可以触摸的了.
我们准备用这个东西封装个人信息部分. 将来我们可能想要创建它来让用户可以在上面点击,从而看到更多有关那个人的图片.
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </TouchableHighlight> </View> ) } }); // omitted code
当你重新载入我们所做的修改并且在用户信息上点击,会发现它起作用了 - 但看起来有点糟糕. 这是因为我们还没有在视图上设置一个背景颜色,其意义是让整个组件变暗.
// omitted code var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { backgroundColor: '#fff', borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, ... }); // omitted code
现在当你在信息盒子上点击时,它就能正确工作了!
TouchableHighlight 为我们提供了 TouchableWithoutFeedback 也有的一个相同的事件。TouchableWithoutFeedback 不应该被使用,因为所有被触摸的东西都应该提供某些类型的视觉上可见的反馈。
这样我们就可以利用 onPress - 它会在用户已经释放了触摸,但是还没有被打断 (比如还在让他们的手指在可触摸的区域移动)时被调用。
我们需要向下将一个属性传递到我们的 Person 组件,当其被触摸到时。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ActivityIndicatorIOS, TouchableHighlight } = React; var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight onPress={ this.props.onPress }> ... </TouchableHighlight> </View> ) } }); var facemashTab = React.createClass({ ..., onPersonPress: function() { this.setState({ currentIndex: this.state.currentIndex + 1 }); }, ..., render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { var { list, currentIndex } = this.state; var record = list[currentIndex]; var people = record.users.map(person => <Person person={ person } onPress={ this.onPersonPress } />); contents = ( <View style={ styles.content }> { people } </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } }); // omitted code
如你所见,在你的主TAB组件里面现在有了一个 onPersonPress 属性. 然后我们就可以将这个传到 Person 组件那儿, 而它们会在 TouchableHighlight 区域被触摸时调用到它. 而后我们可以增加索引,视图就会用新的人物集合来进行重新渲染.
这是对 facemash 的 tab 所做的最后修改. 如果你希望走得更远,下面是一些好主意
当选取了一个人物时可以去请求一个 REST 的端点
检查是否已经到达了列表的尽头,显示一条消息
让用户可以在照片上面点击来看更多的照片
你可以在分支三上面检出 facemash 的 tab 的最终代码。
我们现在将注意力转移到消息tab上了。这一功能有点像 iMessage - 它是有关用户的一个可滚动列表,在其中一项上面点击将会导航至一个针对那个用户的聊天视图。
幸运的是, React Native 给了我们 ListView 组件,它能让我们拥有一个简单的,(使用了ScrollView的)可滚动列表,而且能高效的显示出列表(只对发生变化的行进行重新渲染,并限制了每次事件循环渲染的行只有一个)。
为了使用一个 ListView, 我们需要有一个数据源. 数据元让我们可以拥有一个定制的函数来检查一行是不是发生了变化 (可以想到它类似于 toshouldComponentUpdate) ,我们可以把JSON数据放到它里面去. 数据源存在于我们的状态对象之中。
在 thetabs/folder 下创建一个名为 Messages.js 的新文件
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image } = React; var messagesTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } }); module.exports = messagesTab;
你同时需要编辑 editindex.ios.jsto 来指向新创建的组件 -
// omitted code var MessagesTab = require('./tabs/Messages'); var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, changeTab(tabName) { StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0); this.setState({ selectedTab: tabName }); }, render: function() { return ( <TabBarIOS> ... <TabBarIOS.Item title="Messages" icon={ require('image!messages') } onPress={ () => this.changeTab('messages') } selected={ this.state.selectedTab === 'messages' }> <MessagesTab /> </TabBarIOS.Item> ... </TabBarIOS> ); } }); // omitted code
根据我上面所说的,我们列表视图需要的一个数据源。这个可以通过 viaListView.DataSource 访问。我们将会在 ourgetInitialState 赋初值。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView } = React; var messagesTab = React.createClass({ getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } }); module.exports = messagesTab;
现在我们已经获取到数据源,需要从服务器获取一些数据,知道我们跟谁在通信。我已经包含在一个端点配置中,在 thestep-threebranch 显得更短。
// omitted code var messagesTab = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); // omitted code
这个会获取到我们的数据,然后更新我们的数据源。这些处理完之后,无论什么时候我们的数据源更新了,我们的视图也会更新的。
现在我们来看看 ListViewcomponent。简单的说, 组件需要两个属性 -dataSource(我们已经有了) 。andrenderRow.renderRow 是一个需要返回一个 React 元素的函数。在数据源中每一行都会调用一次,然后作为参数为每一行传递合适的数据。
// omitted code var messagesTab = React.createClass({ ..., renderRow: function (){ return ( <View> <Text>Row goes here!</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
我们现在每一行都可以显示正确的数据。每个项的对象都作为第一个参数传递给 ourrenderRowfunction。
// omitted code var messagesTab = React.createClass({ ..., renderRow: function (person){ return ( <View> <Text>{ person.user.firstName } { person.user.lastName }</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
我们继续我们的步骤,在这里添加其他的信息,比如图片和最新接收到的信息。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio } = React; function prettyTime(timestamp) { var createdDate = new Date(timestamp); var distance = Math.round( ( +new Date() - timestamp ) / 60000 ); var hours = ('0' + createdDate.getHours()).slice(-2); var minutes = ('0' + createdDate.getMinutes()).slice(-2); var month = ('0' + (createdDate.getMonth() + 1)).slice(-2); var date = ('0' + createdDate.getDate()).slice(-2); var year = createdDate.getFullYear(); var string; if (distance < 1440) { string = [hours, minutes].join(':'); } else if (distance < 2879) { string = 'Yesterday'; } else { string = [date, month, year].join('/'); } return string; } var messagesTab = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, row: { flex: 1, alignItems: 'center', backgroundColor: 'white', flexDirection: 'row', padding: 10 }, textContainer: { flex: 1 }, cellImage: { height: 60, borderRadius: 30, marginRight: 10, width: 60 }, time: { position: 'absolute', top: 0, right: 0, fontSize: 12, color: '#cccccc' }, name: { flex: 1, fontSize: 16, fontWeight: 'bold', marginBottom: 2 }, lastMessage: { color: '#999999', fontSize: 12 }, cellBorder: { backgroundColor: 'rgba(0, 0, 0, 0.1)', height: 1 / PixelRatio.get(), marginLeft: 4 } }); module.exports = messagesTab;
Looking good!
你会注意到我们在样式表中使用了一个叫做 PixelRatio 的东西. 用这个我们就可以得到能够拿来在设备上显示的最细的线. 一般,我们会用 1px 作为最细的边框,但是 React Native 中没有 px 的概念。
现在我们可以添加代码来处理在用户项上面的触摸了。我们将使用 NavigatorIOS 组件 - 你会在诸如 iMessage 和 Notes 这样的应用上看到这个东西. 它能让我们获得视图之间的回退功能,顶端的导航条也会如此。
实际上我们准备创建一个新的 React 组件来装这个导航。这是因为组件需要对一个初始的 React 组件进行渲染。
我们会将 messagesTab 组件改称做 messageList,并创建另外一个叫做 messagesTab 的组件
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS } = React; // omitted code var messageList = React.createClass({ ..., render: function(){ return ( <View style={ > <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); var messagesTab = React.createClass({ render: function() { return ( <NavigatorIOS style={ styles.container } initialRoute={ { title: 'Messages', component: messageList } } /> ); } }); // omitted code
看看已经变得更专业了哦。就像我们在 facemash 的 Tab 中所做的,我们现在可以向行中添加触摸时高亮效果( TouchableHighlight) 了。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS, TouchableHighlight } = React; // omitted code var messageList = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </TouchableHighlight> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
重新加载然后,你会收到一个错误。这是因为我们传了两个子组件到 TouchableHighlight,但它只能很好的拿一个来进行显示。不要担心啦,我们还可以把这俩子组件封装到另外一个 View 组件中来解决问题啊。
// omitted code var messageList = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... }); // omitted code
现在,当我们在一行上面触摸时,就会收到我们预期的效果了。等等 - 我们的底部边框看起来怪怪的!这是因为我们使用的是 rgba 值。整个视图的背景颜色正在发生变化,这意味着我们的边框随后会变得更暗。不要担心,我们可以给它一个十六进制值的。
var styles = StyleSheet.create({ ..., cellBorder: { backgroundColor: '#F2F2F2', height: 1 / PixelRatio.get(), marginLeft: 4 } });
如上所述的代码你可以在分支四中看到。
现在我们已经让主列表有了样式,可以来处理用户触摸时导航发生的变化了。
NavigatorIOS 让我们可以在想要改变当前的路由时,“按下”到组件的里面去。为此,我们需要子组件以及 messagList 中能有某些方式能访问到 NavigatorIOS 实体。幸运的是,这个已经以叫做 navigator 的属性传入了。
让我们向 TouchableHighlight 组件加入一个 onPress 事件吧.
// omitted code var messageList = React.createClass({ ..., openChat: function (user){ }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> ... </TouchableHighlight> </View> ); }, ... }); // omitted code
现在我们需要一个 React 组件来传递到 navigator。请在 tabs/ 文件夹中创建一个叫做 MessageView.js 的新文件。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View } = React; var messageView = React.createClass({ render: function(){ return ( <View style={ styles.container }> <Text>Message view!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, }); module.exports = messageView;
我们可以将这个包含到 Messages.js 文件中,并将它放到 navigator 那儿去。
// omitted code var messageList = React.createClass({ ..., openChat: function (user){ this.props.navigator.push({ title: `${user.firstName} ${user.lastName}`, component: MessageView, passProps: { user } }); }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... }); // omitted code
从这儿你可以看到我们将下一个路由压入了 navigator。在这儿我们可以设置下一个路由的标题 (用户的姓名),渲染什么组件 (我们新创建的 MessageView) 以及传入什么属性。这让我们可以访问我们在 MessageView 组件中需要的任何东西 (我们准备传入用户对象)。
这对于我们聊天列表中的每一个都会起作用,不管其数量多还是少。
不过,文本会被我们新的标题条切段。解决这个问题,主要在第一个 View 组件上放一个内边距(padding)就可以了。
我们现在已经有一个用户属性被传进来了,同样可以对其进行展示!
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View } = React; var messageView = React.createClass({ render: function(){ var user = this.props.user; return ( <View style={ styles.container }> <Text>Chat with { user.firstName } { user.lastName }</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', paddingTop: 64 }, }); module.exports = messageView;
你可以在 step-five
分支检测出上面的代码。
如果你想在这个tab上做更多的事情,下面是一些好主意
当MessageView载入时获取那个用户的聊天信息
尝试实现下拉刷新 - 提示:使用ListView上的renderHeader属性
在用户的姓名右侧添加一个“设置(Settings)" 按钮
请自由地在设置 tab 中加入一些功能。尝试使用其他的组件,比如 DatePickerIOS 遗迹 TextInput,来做一些通用的设置 (DOB,name,等等)。
希望这里的讨论能触及一些同你在你的 React Native 应用程序中通常会用到有所不同的组件。如有任何疑问,可以在推特 @rynclark 上联系我.
评论删除后,数据将无法恢复
评论(29)
引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“javadeveloper”的评论
用在什么产品上了,介绍介绍。引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“javadeveloper”的评论
用在什么产品上了,介绍介绍。引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“工头叫我去搬砖”的评论
怪不得我觉得淘宝app那么难用呢,与他的公司地位完全不符啊引用来自“1棵拼搏的寂静草”的评论
最近刚发布的版本才是react,之前的不是引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“工头叫我去搬砖”的评论
怪不得我觉得淘宝app那么难用呢,与他的公司地位完全不符啊引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“kut”的评论
这货目测现在还是一玩具,上不了生产,哪个团队用都要承担项目失败的巨大风险。引用来自“1棵拼搏的寂静草”的评论
天猫,淘宝用的引用来自“javadeveloper”的评论
用在什么产品上了,介绍介绍。