首页内容拆分
看效果图,首页由热门影院、豆瓣热门、热门影视等列表组成,每个列表又由头加横向滑动的 电影海报列表构成。
所以可以先把页面的电影海报、评分、列表头做成组件,然后在使用 ScrollView 将内容包裹即可构成首页。
<div align=center> <img src="https://img2018.cnblogs.com/blog/1312841/201907/1312841-20190705163146521-1101885873.png" /> </div>开发头部组件
头部组件结构简单,唯一需要注意的就是点击查看更多的时候需要跳转页面,所有需要一个自定义事件,供页面使用。
在 src 目录创建 itemsHeader.js,内容如下:
1import { Text, View, StyleSheet, TouchableWithoutFeedback } from 'react-native'; 2import PropTypes from 'prop-types'; 3import { px } from '../utils/device'; 4import Icon from 'react-native-vector-icons/AntDesign'; 5 6export default class ItemsHeader extends Component { 7 constructor(props) { 8 super(props); 9 } 10 static propTypes = { 11 title: PropTypes.string, 12 onPress: PropTypes.func 13 } 14 static defaultProps = {} 15 render() { 16 const { title, onPress } = this.props; 17 return ( 18 <View style={styles.header}> 19 <Text style={styles.title}>{title}</Text> 20 <TouchableWithoutFeedback onPress={() => onPress && onPress()}> 21 <View style={styles.getMore}> 22 <Text style={styles.moreText}>查看更多</Text> 23 <Icon name='right' size={px(30)} color='#00b600'></Icon> 24 </View> 25 </TouchableWithoutFeedback> 26 </View> 27 ) 28 } 29} 30 31const styles = StyleSheet.create({ 32 header: { 33 height: px(90), 34 width: px(750), 35 paddingLeft: px(30), 36 paddingRight: px(30), 37 backgroundColor: '#ffffff', 38 flexDirection: 'row', 39 alignItems: 'center', 40 justifyContent: 'space-between' 41 }, 42 title: { 43 fontSize: px(32), 44 color: '#333', 45 fontWeight: '600' 46 }, 47 getMore: { 48 flexDirection: 'row', 49 alignItems: 'center', 50 }, 51 moreText: { 52 fontSize: px(28), 53 marginLeft: px(30), 54 color: '#00b600', 55 marginRight: px(6) 56 } 57});
开发评分组件
评分组件需要考虑到星星大小、间距、颜色、数量,点击星星时改变星星的选中状态,并返回自定义事件 onPress 供调用者使用。
如果使用组件时调用了onPress,那么组件的值为可以改变,如果没有,那么组件应该为只读状态。
1import React, { Component } from 'react'; 2import { Text, View, StyleSheet } from 'react-native'; 3import PropTypes from 'prop-types'; 4import { px } from '../utils/device'; 5import Icon from 'react-native-vector-icons/AntDesign'; 6 7export default class Rate extends Component { 8 constructor(props) { 9 super(props); 10 this.state = { 11 value: this.props.value 12 } 13 } 14 componentWillReceiveProps(newProps) { 15 const { value } = newProps; 16 if (value !== this.state.value) { 17 this.setState({ 18 value 19 }); 20 } 21 } 22 static propTypes = {//如果使用组件时调用了onPress,那么组件默认为可以改变,如果没有,那么组件应该为只读 23 value: PropTypes.number, 24 size: PropTypes.number, 25 margin: PropTypes.number, 26 max: PropTypes.number, 27 color: PropTypes.string, 28 onPress: PropTypes.func 29 } 30 static defaultProps = { 31 value: 0, 32 size: 20, 33 margin: 5, 34 max: 5, 35 color: '#00b600' 36 } 37 bindClick = (index) => { 38 const { onPress } = this.props; 39 if (!onPress) { 40 return; 41 } 42 onPress(index + 1); 43 this.setState({ 44 value: index + 1 45 }) 46 } 47 render() { 48 const { size, margin, max, color, onPress } = this.props; 49 const { value } = this.state; 50 const defaultStars = [], activeStars = []; 51 for (let i = 0; i < max; i++) { 52 defaultStars.push(<Icon name='star' key={i} size={size} color='#ececec' onPress={() => this.bindClick(i)} style={{ marginRight: margin }}></Icon>) 53 } 54 for (let i = 0; i < value; i++) { 55 activeStars.push(<Icon name='star' key={i} size={size} color={color} onPress={() => this.bindClick(i)} style={{ marginRight: margin }}></Icon>) 56 } 57 // 选中状态的星星的宽度 58 const activeStarsWidth = (size + margin) * Math.floor(value) + size * (value - Math.floor(value)); 59 return ( 60 <View style={styles.rate}> 61 <View style={[styles.stars, styles.active, { width: activeStarsWidth }]}> 62 {activeStars.map(item => item)} 63 </View> 64 <View style={styles.stars}> 65 {defaultStars.map(item => item)} 66 </View> 67 </View> 68 ) 69 } 70} 71 72 73const styles = StyleSheet.create({ 74 rates: { 75 flexDirection: 'row', 76 position: 'relative' 77 }, 78 stars: { 79 flexDirection: 'row', 80 alignItems: 'center', 81 overflow: 'hidden', 82 flexGrow: 0 83 }, 84 active: { 85 position: 'absolute', 86 zIndex: 200, 87 left: 0, 88 top: 0 89 } 90});
开发电影海报组件
海报组件开发需要注意的是:
-
点击电影海报,跳转详情页面,跳转逻辑都是一样的,所以可以不用自定义事件的方式跳转,直接在组件里面调用 this.props.navigation.push 进行跳转。页面在 router 里注册后可以直接使用 this.props.navigation.push,但是组件不行。在组件中,想要使用 navigation 进行跳转,要么是使用自定义属性,将 navigation 传入组件,要么使用 react-navigation 提供的 withNavigation翻翻,withNavigation(component) 返回一个 render 函数,默认将 navigation 作出自定义属性传入组件。
-
有些海报图片背景纯白,和页面背景融合了,看不到边界,所以需要给他设置 border,由于 Image 组件不能设置 border,所以这里需要使用 ImageBackground 组件。
-
title 只能为一行,产出部分省略,需要加一个 numberOfLines={1} 的属性。
import React, { Component } from 'react'; import { Text, View, StyleSheet, ImageBackground, TouchableWithoutFeedback } from 'react-native'; import PropTypes from 'prop-types'; import { withNavigation } from 'react-navigation'; import { px } from '../utils/device'; import Rate from './rate';
class MoviesItem extends Component { constructor(props) { super(props); } static propTypes = { data: PropTypes.object } render() { const { data, navigation } = this.props; const { id, title, cover, rating, null_rating_reason } = data; return ( <TouchableWithoutFeedback onPress={() => navigation.push('Detail', { id })}> <View style={styles.page}> <ImageBackground source={{ uri: cover.url }} style={styles.img}></ImageBackground> <Text style={styles.title} numberOfLines={1}>{title}</Text> {rating ? ( <View style={styles.rate}> <Rate value={rating.value / 2} size={px(20)} margin={px(4)} /> <Text style={styles.rateText}>{rating.value.toFixed(1)}</Text> </View> ) : ( <Text style={styles.rate}>{null_rating_reason}</Text> )} </View> </TouchableWithoutFeedback> ) } }
export default withNavigation(MoviesItem);
const styles = StyleSheet.create({ page: { width: px(160) }, img: { width: px(160), height: px(224), overflow: 'hidden', borderRadius: px(8), borderWidth: 1, borderStyle: 'solid', borderColor: '#f8f8f8' }, title: { fontSize: px(28), fontWeight: '600', color: '#333', marginTop: px(12), lineHeight: px(40) }, rate: { flexDirection: 'row', alignItems: 'center' }, rateText: { fontSize: px(24), color: '#999', marginLeft: px(6) } });