豆瓣电影案例

项目创建与目录结构设计

项目源码:https://github.com/fanxiao168/DoubanMovie

使用create-react-native-app projectName命令创建一个项目,然后在项目中创建src文件夹作为源代码目录,里面创建config目录存放常量与配置数据,创建components目录存放项目组件,组件划分为公共组件与页面组件两种。

- src
    - config
        - api.js: 项目所需接口
    - components
        - common
            - Banner.js: 头部轮播图
            - Navbar.js:底部导航
        - page
            - Home.js:首页
            - find
                - Find.js:发现页
            - profile
                - Profile.js:个人页
            - book
                - BookList.js:书籍列表
                - BookDetail.js:书籍详情
            - music
                - MusicList.js:音乐列表
                - MusicDetail.js:音乐详情
            - movie
                - MovieList.js:电影列表
                - MovieDetail.js:电影详情

配置文件

API配置文件

  • 创建 src/config/api.js 文件,配置项目所属的接口,将来接口变化修改这里即可
  • 豆瓣接口文档
const dbDomain = 'http://api.douban.com/v2';

export default {

    // 书籍
    book: {},

    // 音乐
    music: {},

    // 电影
    movie: {
        // 正在热映
        hot: `${dbDomain}/movie/in_theaters`,
        // 即将上映
        soon: `${dbDomain}/movie/coming_soon`,
        // top250
        top: `${dbDomain}/movie/top250`,
        // 详情
        detail: `${dbDomain}/movie/subject/` // 后面需要加电影:id路径参数
    }
    
}

公共Banner与NavBar

Banner

  • 安装 yarn add react-native-swiper 插件
  • 删除源码中fontFamily样式,这个字体在原生Android中默认未安装,使用时会报错
  • 创建 src/components/common/Banner.js 头部轮播图组件
import React, { Component } from 'react';
const Dimensions = require('Dimensions');
const screenSize = Dimensions.get("window");
import {
    StyleSheet,
    View,
    Text,
    Button,
    Image,
    TouchableHighlight
} from 'react-native';

import Swiper from 'react-native-swiper';

export default class Banner extends Component {

    constructor(props) {
        super(props);
        this.state = {
            list: [
                {id: 1, uri: 'http://img0.imgtn.bdimg.com/it/u=1292571282,473977860&fm=27&gp=0.jpg'},
                {id: 2, uri: 'http://img1.imgtn.bdimg.com/it/u=2345964138,2156090285&fm=27&gp=0.jpg'},
                {id: 3, uri: 'http://img3.imgtn.bdimg.com/it/u=3367888536,3273349933&fm=27&gp=0.jpg'},
            ]
        };
    }

    // 进入详情页
    _pushDetail(id) {
        console.log(`进入${id}详情页`);
    }

    // 列表渲染
    _getList() {
        return (
            this.state.list.map((item, i) => {
                return (
                    <View style={styles.item} key={`key${i}`}>
                        <TouchableHighlight
                            activeOpacity={0.75}
                            underlayColor="#c1c1c1"
                            onPress={this._pushDetail.bind(this, item.id)}>
                            <Image
                                style={styles.itemImg} 
                                source={{uri: item.uri}}>
                            </Image>
                        </TouchableHighlight>
                    </View>
                )
            })
        )
    }

    // Banner高度
    _getHeight() {
        return this.props.height || 200;
    }

    render() {
        return (
            <View style={[styles.wrapper, {height: this._getHeight()}]}>
                <Swiper showsButtons={true} autoplay={true}>
                    {/* 列表渲染 */}
                    { this._getList() }
                </Swiper>
            </View>
        )
    }
}

const styles = StyleSheet.create({
    wrapper: {
        width: '100%',
    },
    item: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    },
    itemImg: {
        width: '100%',
        height: '100%'
    }
});

NavBar

  • 创建 src/components/common/Banner.js 底部导航组件
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    TouchableNativeFeedback
} from 'react-native';

export default function(props) {
    return (
        <View style={[styles.wrapper, props.style]}>
            <View style={styles.item}>
                <Text style={styles.itemText}>首页</Text>
            </View>

            <View style={styles.item}>
                <Text style={styles.itemText}>发现</Text>
            </View>
            <View style={styles.item}>
                <Text style={styles.itemText}>我</Text>
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    wrapper: {
        position: 'absolute',
        bottom: 0,
        left: 0,
        width: '100%',
        height: 40,
        backgroundColor: 'skyblue',
        flexDirection: 'row',
    },
    item: {
        flex: 1,
        height: '100%',
        justifyContent: 'center',
        alignItems: 'center',
    },
    itemText: {
        color: '#fff',
        fontWeight: 'bold',
        fontSize: 16,
    }
});

Home页

  • Home是应用的首页,由头部Banner轮播图、底部NavBar导航、中部九宫格构成。
  • 首先导入写好的 Banner 与 Navbar 组件,然后实现中部九宫格布局。
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    TouchableNativeFeedback
} from 'react-native';

import { Actions } from 'react-native-router-flux';
import Banner from '../common/Banner';
import NavBar from '../common/NavBar';

export default class Home extends Component {
    render() {
        return (
            <View style={styles.wrapper}>
                <Banner></Banner>
                <View style={styles.apps}>
                    <View style={styles.appsRow}>
                        {/* 1 三个touchable组件,只有这个效果组件不影响结构与样式,其他两个影响 */}
                        <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.book}>
                            <View style={[styles.appsRowItem, {backgroundColor: 'blue'}]}>
                                <Text style={styles.appsRowItemText}>书籍</Text>
                            </View>
                        </TouchableNativeFeedback>
                        {/* 2 */}
                        <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.music}>
                            <View style={[styles.appsRowItem, {backgroundColor: 'yellow'}]}>
                                <Text style={styles.appsRowItemText}>音乐</Text>
                            </View>
                        </TouchableNativeFeedback>
                        {/* 3 */}
                        <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.movie}>
                            <View style={[styles.appsRowItem, {backgroundColor: 'green'}]}>
                                <Text style={styles.appsRowItemText}>电影</Text>
                            </View>
                        </TouchableNativeFeedback>
                    </View>
                    <View style={styles.appsRow}>
                        <View style={[styles.appsRowItem, {backgroundColor: 'pink'}]}>
                            <Text style={styles.appsRowItemText}>同城</Text>
                        </View>
                        <View style={[styles.appsRowItem, {backgroundColor: 'skyblue'}]}>
                            <Text style={styles.appsRowItemText}>广播</Text>
                        </View>
                        <View style={[styles.appsRowItem, {backgroundColor: 'orange'}]}>
                            <Text style={styles.appsRowItemText}>相册</Text>
                        </View>
                    </View>
                    <View style={styles.appsRow}>
                        <View style={[styles.appsRowItem, {backgroundColor: 'cyan'}]}>
                            <Text style={styles.appsRowItemText}>论坛</Text>
                        </View>
                        <View style={[styles.appsRowItem, {backgroundColor: 'purple'}]}>
                            <Text style={styles.appsRowItemText}>线上活动</Text>
                        </View>
                        <View style={[styles.appsRowItem, {backgroundColor: 'hotpink'}]}>
                            <Text style={styles.appsRowItemText}>线下活动</Text>
                        </View>
                    </View>
                </View>
                <NavBar></NavBar>
            </View>
        )
    }
}

const styles = StyleSheet.create({
    wrapper: {
        flex: 1,
        width: '100%',
        position: 'relative',
    },
    apps: {
        height: 450
    },
    appsRow: {
        flex: 1,
        flexDirection: 'row',
    },
    appsRowItem: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    appsRowItemText: {
        fontSize: 26
    }
})

底部导航顶级入口页

  • 在 NavBar 中设定了三个顶级入口,先把他们都创建好,下一步就实现他们之间的路由切换
  • 创建 src/components/page/find/Find.js 发现页面组件
  • 创建 src/components/page/profile/Profile.js 个人中心页面组件
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    TouchableNativeFeedback
} from 'react-native';
import { Actions } from 'react-native-router-flux';

import NavBar from '../../common/NavBar';

export default class Find extends Component {

    render() {
        return (
            // 为了让底部导航定位而relative
            <View style={{flex: 1, position: 'relative'}}>
                <Text>发现</Text>
                <Text>发现</Text>
                <Text>发现</Text>
                <Text>发现</Text>
                <Text>发现</Text>
                <NavBar></NavBar>
            </View>
        )
    }

}
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    TouchableNativeFeedback
} from 'react-native';
import { Actions } from 'react-native-router-flux';

import NavBar from '../../common/NavBar';

export default class Profile extends Component {

    render() {
        return (
            // 为了让底部导航定位而relative
            <View style={{flex: 1, position: 'relative'}}>
                <Text>我的</Text>
                <Text>我的</Text>
                <Text>我的</Text>
                <Text>我的</Text>
                <Text>我的</Text>
                <NavBar></NavBar>
            </View>
        )
    }

}

集成react-native-router-flux

路由配置

修改根目录下的 App.js ,在这里进行路由配置,先配置底部导航对应的三个页面进行路由测试。

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Router, Stack, Scene } from 'react-native-router-flux';

import Home from './src/components/page/Home';
import Find from './src/components/page/find/Find';
import Profile from './src/components/page/profile/Profile';

export default class App extends React.Component {
  render() {
    return (
      <Router>
        {/* stack可以对scene进行分组,stack可以包含子stack */}
        <Stack key="root">
          {/* scene用来配置页面路由信息,key与component属性是必须的,其中key值而且必须唯一,通过key可进行页面的切换 */}
          <Scene hideNavBar key="home" component={Home}></Scene>
          <Scene hideNavBar key="find" component={Find}></Scene>
          <Scene hideNavBar key="profile" component={Profile}></Scene>
        </Stack>
      </Router>
    );
  }
}

Navbar修改

  • 导入 TouchableNativeFeedback 组件包装底部导航项,并绑定点击事件
  • 导入 Actions 对象实现路由跳转
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    TouchableNativeFeedback
} from 'react-native';
import { Actions } from 'react-native-router-flux';

export default class NavBar extends Component {

    render() {
        return (
            <View style={styles.wrapper}>
                {/* 通过Actions[配置的路由key]方法实现页面跳转 */}
                <TouchableNativeFeedback onPress={Actions.home}>
                    <View style={styles.item}>
                        <Text style={styles.itemText}>首页</Text>
                    </View>
                </TouchableNativeFeedback>
                <TouchableNativeFeedback onPress={Actions.find}>
                    <View style={styles.item}>
                        <Text style={styles.itemText}>发现</Text>
                    </View>
                </TouchableNativeFeedback>
                <TouchableNativeFeedback onPress={Actions.profile}>
                    <View style={styles.item}>
                        <Text style={styles.itemText}>我</Text>
                    </View>
                </TouchableNativeFeedback>
            </View>
        )
    }

}

const styles = StyleSheet.create({
    wrapper: {
        position: 'absolute',
        bottom: 0,
        left: 0,
        width: '100%',
        height: 50,
        backgroundColor: 'skyblue',
        flexDirection: 'row'
    },
    item: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    itemText: {
        color: '#fff',
        fontWeight: 'bold',
        fontSize: 16,
    }
})

九宫格对应页面路由配置

创建组件

  • 创建 src/components/page/book/BookList.js
  • 创建 src/components/page/book/BookDetail.js
  • 创建 src/components/page/music/MusicList.js
  • 创建 src/components/page/music/MusicDetail.js
  • 创建 src/components/page/movie/MovieList.js
  • 创建 src/components/page/movie/MovieDetail.js

路由配置

  • 修改 App.js 根组件, 导入新模块组件,并进行路由配置
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Router, Stack, Scene } from 'react-native-router-flux';

import Home from './src/components/page/Home';
import Find from './src/components/page/find/Find';
import Profile from './src/components/page/profile/Profile';

import BookList from './src/components/page/book/BookList';
import BookDetail from './src/components/page/book/BookDetail';

import MusicList from './src/components/page/music/MusicList';
import MusicDetail from './src/components/page/music/MusicDetail';

import MovieList from './src/components/page/movie/MovieList';
import MovieDetail from './src/components/page/movie/MovieDetail';

export default class App extends React.Component {
  render() {
    return (
      <Router>
        {/* stack可以对scene进行分组,stack可以包含子stack */}
        <Stack key="root">

          {/* scene用来配置页面信息的,其中key属性是必须的,而且必须唯一,需要通过key进行页面切换 */}
          <Scene key="home" component={Home} hideNavBar></Scene>
          <Scene key="find" component={Find} hideNavBar></Scene>
          <Scene key="profile" component={Profile} hideNavBar></Scene>

          {/* 书籍路由配置 */}
          <Stack key="book" title="书籍">
            {/* scene有一个initial属性,用来指定该stack下的组件入口,入口组件可以通过Stack的key进入 */}
            {/* 将来组件的入口变化了,修改initial属性就可 */}
            <Scene initial hideNavBar key="bookList" component={BookList}></Scene>
            <Scene hideNavBar key="bookDetail" component={BookDetail}></Scene>
          </Stack>

          {/* 音乐路由配置 */}
          <Stack key="music" title="音乐">
            <Scene initial title="列表" key="musicList" component={MusicList}></Scene>
            <Scene title="详情" key="musicDetail" component={MusicDetail}></Scene>
          </Stack>
          
          {/* 电影路由配置 */}
          <Stack key="movie" hideNavBar backButtonTintColor={"skyblue"} navigationBarStyle={styles.navigationBarStyle} titleStyle={styles.navigationBarTitleStyle}>
            {/* stack拥有一个所有子组件的公共title,每个scene还拥有自己独立的子title */}
            {/* 你可以根据需要统一使用stack-title,或者隐藏stacktitle,使用独立子title */}
            {/* 或者都使用,或者都不使用 */}
            <Scene title="电影" hideTabBar={false} hideNavBar={false} key="movieList" component={MovieList}></Scene>
            <Scene title="电影详情" hideNavBar={false} leftButtonIconStyle={{color: 'blue'}} key="movieDetail" component={MovieDetail}></Scene>
          </Stack>
        </Stack>
      </Router>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  navigationBarStyle: {
    backgroundColor: 'skyblue',
    height: 30
  },
  navigationBarTitleStyle: {
    color: 'hotpink',
  }
});

九宫格元素事件绑定

修改 Home 组件,使用Touchable组件包装里面的项,并绑定路由跳转方法

<View style={styles.appsRow}>
    {/* 1 三个touchable组件,只有这个效果组件不影响结构与样式,其他两个影响 */}
    <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.book}>
        <View style={[styles.appsRowItem, {backgroundColor: 'blue'}]}>
            <Text style={styles.appsRowItemText}>书籍</Text>
        </View>
    </TouchableNativeFeedback>
    {/* 2 */}
    <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.music}>
        <View style={[styles.appsRowItem, {backgroundColor: 'yellow'}]}>
            <Text style={styles.appsRowItemText}>音乐</Text>
        </View>
    </TouchableNativeFeedback>
    {/* 3 */}
    <TouchableNativeFeedback activeOpacity={0.8} onPress={Actions.movie}>
        <View style={[styles.appsRowItem, {backgroundColor: 'green'}]}>
            <Text style={styles.appsRowItemText}>电影</Text>
        </View>
    </TouchableNativeFeedback>
</View>

电影列表

  • 修改 src/components/movie/movieList.js
  • 使用 fetch 获取电影列表数据进行渲染, 获取完毕后隐藏loading
  • 导入 ActivityIndicator 组件, 并使用 isLoaded 状态控制 loading 的显示隐藏
  • 导入 TouchableNativeFeedback 组件, 用于添加点击事件与效果
  • 导入 Actions 对象, 用于页面跳转
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    Image,
    TouchableNativeFeedback,
    ActivityIndicator,
    FlatList
} from 'react-native';
import { Actions } from 'react-native-router-flux';

import api from '../../../config/api';

export default class MovieList extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isLoaded: false,
            list: []
        }
    }

    // 加载电影列表数据,url使用配置对象的属性即可
    componentWillMount() {
        fetch(api.movie.hot)
            .then(res => res.json())
            .then(data => {

                // 数据拿到后,修改loading状态,存储数据列表
                this.setState({
                    isLoaded: true,
                    list: data.subjects
                })
            })
    }

    // 获取loading
    _getLoading() {
        return (
            <ActivityIndicator size="large" color="hotpink"></ActivityIndicator>
        )
    }

    // 获取电影列表
    _getList() {
        return (
            // 懒加载列表组件,用它有两个原因,1、不用的话超出屏幕的内容看不到,2、性能高
            <FlatList
                showsVerticalScrollIndicator={false}
                data={ this.state.list }
                renderItem={
                    // e.index为下标,e.item为数组中的单个元素
                    (e) => {
                       return (
                            // 路由导航时可以传递参数,但是传参要调用方法,
                            // 为了防止页面一上来就被调用,造成页面自动跳转,所以我们这里需要把调用函数包裹一层
                            <TouchableNativeFeedback key={`key${e.index}`} onPress={ ()=>Actions.movieDetail({id: e.item.id}) }>
                                <View style={styles.item}>
                                    <Image source={{uri: e.item.images.large}} style={styles.itemImg}></Image>
                                    <View style={styles.itemContent}>
                                        <Text style={styles.itemContentT}>名称:{e.item.original_title}</Text> 
                                        <Text style={styles.itemContentT}>年份:{e.item.year}</Text> 
                                        <Text style={styles.itemContentT}>类型:{e.item.genres[0]}</Text>
                                        <Text style={styles.itemContentT}>口碑:{e.item.rating.average}</Text>
                                    </View>
                                </View>
                            </TouchableNativeFeedback>
                       )
                    }
                }
            />
        )
    }

    render() {
        return (
            <View style={styles.wrapper}>
                {
                    this.state.isLoaded? this._getList(): this._getLoading()
                }
            </View>
        )
    }

}

const styles = StyleSheet.create({
    wrapper: {
        paddingHorizontal: 40,
        paddingVertical: 10,
    },
    item: {
        flexDirection: 'row',
        marginBottom: 20
    },
    itemImg: {
        width: 130,
        height: 180
    },
    itemContent: {
        marginLeft: 20,
        justifyContent: 'space-around',
    },
    itemContentT: {
        color: 'hotpink',
        fontSize: 18,
        fontWeight: 'bold',
    }
})

电影详情

  • 修改 src/components/movie/movieDetail.js
  • 使用 fetch 获取电影列表数据进行渲染, 获取完毕后隐藏loading
  • 导入 ActivityIndicator 组件, 并使用 isLoaded 状态控制 loading 的显示隐藏
import React, { Component } from 'react';
import {
    StyleSheet,
    View,
    Text,
    Image,
    TouchableNativeFeedback,
    ActivityIndicator,
    FlatList
} from 'react-native';

import api from '../../../config/api';

export default class MovieDetail extends Component {

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

    // 请求详情数据
    componentWillMount() {
        // 调用接口,别忘了传参
        fetch(api.movie.detail + this.props.id)
            .then(res => res.json())
            .then(data => {
                this.setState({
                    isLoaded: true,
                    data: data
                })
            });
        
    }    

    // 获取loading
    _getLoading() {
        return (
            <ActivityIndicator size="large" color="hotpink"></ActivityIndicator>
        )
    }

    // 获取电影详情
    _getData() {
        return (
            <View style={styles.wrapper}>
                <Image source={{uri: this.state.data.images.large}} style={styles.img}></Image>
                <Text>电影名称:{this.state.data.original_title}</Text>
                <Text>上映时间:{this.state.data.year}</Text>
                <Text>用户口碑:{this.state.data.rating.average}</Text>
                <Text>所属国家:{this.state.data.countries[0]}</Text>
                <Text>
                    <Text>所属类型:</Text>
                    {
                        this.state.data.genres.map((v, i) => {
                            return (
                                <Text key={`key${i}`}>{v}</Text>
                            )
                        })
                    }
                </Text>
                <Text>
                    <Text>演员列表:</Text>
                    {
                        this.state.data.casts.map((v, i) => {
                            return (
                                // 最后一个数值没有、号
                                (this.state.data.casts.length - 1) == i
                                ?
                                <Text key={`key${i}`}>{v.name}</Text>
                                :
                                <Text key={`key${i}`}>{v.name + '、'}</Text>
                            )
                        })
                    }
                </Text>
                <Text>
                    <Text>导演列表:</Text>
                    {
                        this.state.data.directors.map((v, i) => {
                            return (
                                // 最后一个数值没有、号
                                (this.state.data.directors.length - 1) == i
                                ?
                                <Text key={`key${i}`}>{v.name}</Text>
                                :
                                <Text key={`key${i}`}>{v.name + '、'}</Text>
                            )
                        })
                    }
                </Text>
                <Text>故事摘要:{this.state.data.summary}</Text>
            </View>
        )
    }

    render() {
        return (
            <View>
                {
                    this.state.isLoaded? this._getData(): this._getLoading()
                }
            </View>
        )
    }

}

const styles = StyleSheet.create({
    wrapper: {
        paddingHorizontal: 40,
        paddingVertical: 10,
        alignItems: 'center',
    },
    img: {
        width: 285,
        height: 400
    }
})
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值