React Native (三): 自定义视图

React Native (一):基础
React Native (二):StatusBar 、 NavigationBar 与 TabBar
React Native (三):自定义视图
React Native (四):加载新闻列表
React Native (五):上下拉刷新加载
React Native (六):加载所有分类与详情页

这次我们要做的仿 新闻头条 的首页的顶部标签列表,不要在意新闻内容。

1.请求数据

首先做顶部的目录视图,首先我们先获取数据:

Home.js 中加入方法:

componentDidMount() {
        let url = 'http://api.iapple123.com/newscategory/list/index.html?clientid=1114283782&v=1.1'
        fetch(url, {
            method: 'GET',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
        })
            .then((res) => {
                res.json()
                    .then((json) =>{
                        LOG('GET SUCCESS =>',url, json)

                    })
                    .catch((e) => {
                        LOG('GET ERROR then =>',url,e)

                    })
            })
            .catch((error) => {
                LOG('GET ERROR=>',url, '==>',error)
            })
    }

componentDidMount()是在此页面加载完成后由系统调用。

用到的 LOG 需要在 setup.js 添加全局方法 :

global.LOG = (...args) => {

    if(__DEV__){
        // debug模式
        console.log('/------------------------------\\');
        console.log(...args);
        console.log('\\------------------------------/');
        return args[args.length - 1];
    }else{
        // release模式
    }

};

完整的生命周期可以看这个 文档

我们使用 fetch 进行请求数据,你也可以用 这里 的方法进行请求数据。

注意在 iOS 中需要去 Xcode 打开 ATS

2.自定义视图

Home 文件夹内创建 SegmentedView.js

先定义一个基础的 View

import React from 'react'

import {
    View,
    StyleSheet,
    Dimensions
} from 'react-native'
const {width, height} = Dimensions.get('window')

export default class SegmentedView extends React.Component {
    render() {
        const { style } = this.props
        return (
            <View style={[styles.view, style]}>
              
            </View>
        )
    }
}


const styles = StyleSheet.create({
    view: {
        height: 50,
        width: width,
        backgroundColor: 'white',
    }
})

这里的 const {width, height} = Dimensions.get('window') 是获取到的屏幕的宽和高。

然后在 Home.js 加入 SegmentedView:

import SegmentedView from './SegmentedView'

    render() {
        return (
            <View style={styles.view}>
                <NavigationBar
                    title="首页"
                    unLeftImage={true}
                />

                <SegmentedView
                    style={{height: 30}}
                />


            </View>
        )
    }

SegmentedViewconst { style } = this.props 获取到的就是这里设置的 style={height: 30}

<View style={[styles.view, style]}> 这样设置样式,数组中的每一个样式都会覆盖它前面的样式,不过只会覆盖有的 key-value,比如这里 style={height: 30} ,它只会覆盖掉前面的 height ,最终的样式为 :

{
    height: 30,
    width: width,
    backgroundColor: 'white',
}
    

3.传数据

请求到的数据需要传给 SegmentedView 来创建视图,我们在 Home.js 加入构造,现在的 Home.js 是这样的:

import React from 'react'

import {
    View,
    StyleSheet
} from 'react-native'

import NavigationBar from '../Custom/NavBarCommon'
import SegmentedView from './SegmentedView'

export default class Home extends React.Component {

    // 构造
      constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            list: null
        };
      }

    componentDidMount() {
        let url = 'http://api.iapple123.com/newscategory/list/index.html?clientid=1114283782&v=1.1'
        fetch(url, {
            method: 'GET',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
        })
            .then((res) => {
                res.json()
                    .then((json) =>{
                        LOG('GET SUCCESS =>',url, json)

                        this.setState({
                            list: json.CategoryList
                        })
                    })
                    .catch((e) => {
                        LOG('GET ERROR then =>',url,e)
                    })
            })
            .catch((error) => {
                LOG('GET ERROR=>',url, '==>',error)
            })
    }

    render() {
        return (
            <View style={styles.view}>
                <NavigationBar
                    title="首页"
                    unLeftImage={true}
                />
                <SegmentedView
                    list={this.state.list}
                    style={{height: 30}}
                />
            </View>
        )
    }
}

const styles = StyleSheet.create({
    view: {
        flex:1,
        backgroundColor: 'white'
    }
})

再数据请求完成后调用 setState() ,系统会收集需要更改的地方然后刷新页面,所以这个方法永远是异步的。

现在请求完数据后就会把数组传给 SegmentedView 了。

再看 SegmentedView ,我们需要用一个 ScrollView 来放置这些标签:

import React from 'react'

import {
    View,
    StyleSheet,
    Text,
    TouchableOpacity,
    Dimensions,
    ScrollView
} from 'react-native'

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


// 一 屏最大数量, 为了可以居中请设置为 奇数
const maxItem = 7

export default class SegmentedView extends React.Component {

    // 构造
      constructor(props) {
        super(props);
       // 初始状态
        this.state = {
            itemHeight: 50,
        };

          if (props.style && props.style.height > 0) {
              this.state = {
                  ...this.state,
                  itemHeight: props.style.height,  //如果在使用的地方设置了高度,那么保存起来方便使用
              };
          }
          this._getItems = this._getItems.bind(this)
      }

    _getItems() {
        const { list } = this.props  //获取到 传入的数组

        if (!list || list.length == 0) return []

       // 计算每个标签的宽度
        let itemWidth = width / list.length

        if (list.length > maxItem) {
            itemWidth = width / maxItem
        }

        let items = []
        for (let index in list) {
            let dic = list[index]
            items.push(
                <View
                    key={index}
                    style={{height: this.state.itemHeight, width: itemWidth, alignItems: 'center', justifyContent:'center',backgroundColor:'#EEEEEE'}}
                >
                    {/* justifyContent: 主轴居中, alignItems: 次轴居中 */}

                    <Text>{dic.NameCN}</Text>
                </View>
            )
        }

        return items
    }

    render() {
      const { style } = this.props

      return (
            <View style={[styles.view, style]}>
                <ScrollView
                    style={styles.scrollView}
                    horizontal={true} //横向显示
                    showsHorizontalScrollIndicator={false} //隐藏横向滑动条
                >
                    {this._getItems()}
                </ScrollView>
            </View>
        )
    }
}


const styles = StyleSheet.create({
    view: {
        height: 50,
        width: width,
        backgroundColor: 'white',
    },

    scrollView: {
        flex:1,
        backgroundColor: '#EEEEEE',
    }
})

4.使标签可选并改变偏移量

现在运行已经可以显示出标签列表了,我们还需要能点击,有选中和未选中状态,所以我们把数组中添加的视图封装一下:


class Item extends React.Component {
    render() {

        const {itemHeight, itemWidth, dic} = this.props

        return (
            <TouchableOpacity
                style={{height: itemHeight, width: itemWidth, alignItems: 'center', justifyContent:'center',backgroundColor:'#EEEEEE'}}
            >
                {/* justifyContent: 主轴居中, alignItems: 次轴居中 */}

                <Text>{dic.NameCN}</Text>
            </TouchableOpacity>
        )
    }
}

我们需要可以点击,所以把 View 换成了 TouchableOpacity,记得在顶部导入。

然后修改数组的 push 方法


items.push(
    <Item
        key={index}
        itemHeight={this.state.itemHeight}
        itemWidth={itemWidth}
        dic={dic}   
    />
)

现在运行已经可以点击了,接下来设置选中和未选中样式,在 Item 内加入:


constructor(props) {
    super(props);
    // 初始状态
    this.state = {
        isSelect: false
    };
}

Text 加入样式:

<Text style={{color: this.state.isSelect ? 'red' : 'black'}}>{dic.NameCN}</Text>

TouchableOpacity 加入点击事件:

<TouchableOpacity
    style={{height: itemHeight, width: itemWidth, alignItems: 'center', justifyContent:'center',backgroundColor:'#EEEEEE'}}
    onPress={() => {
        this.setState({
            isSelect: true
        })
    }}
>

现在标签已经可以进行点击,点击后变红,我们需要处理点击后让上一个选中的变为未选中,我们给 Item 加一个方法:

_unSelect() {
    this.setState({
        isSelect: false
    })
}

我们还需要接收一个回调函数: onPress

const {itemHeight, itemWidth, dic, onPress} = this.props
    
 <TouchableOpacity
    style={{height: itemHeight, width: itemWidth, alignItems: 'center', justifyContent:'center',backgroundColor:'#EEEEEE'}}
    onPress={() => {
        onPress && onPress()
        this.setState({
            isSelect: true
        })
    }}
>

现在去 items.push 加入 onPress ,我们还需要一个状态 selectItem 来记录选中的标签:


// 初始状态
this.state = {
    itemHeight: 50,
    selectItem: null,
};
<Item
    ref={index}  //设置 ref 以供获取自己
    key={index}
    itemHeight={this.state.itemHeight}
    itemWidth={itemWidth}
    dic={dic}
    onPress={() => {
        this.state.selectItem && this.state.selectItem._unSelect() //让已经选中的标签变为未选中
        this.state.selectItem = this.refs[index]  //获取到点击的标签
    }}
/>

现在运行,就可以选中的时候取消上一个标签的选中状态了,但是我们需要默认选中第一个标签。

我们给 Item 加一个属性 isSelect

<Item
    ref={index}  //设置 ref 以供获取自己
    key={index}
    isSelect={index == 0}
    itemHeight={this.state.itemHeight}
    itemWidth={itemWidth}
    dic={dic}
    onPress={() => {
        this.state.selectItem && this.state.selectItem._unSelect() //让已经选中的标签变为未选中
        this.state.selectItem = this.refs[index]  //获取到点击的标签
    }}
/>

修改 Item :

 constructor(props) {
    super(props);
    // 初始状态
    this.state = {
        isSelect: props.isSelect
    };
  }
      

现在运行发现第一项已经默认选中,但是点击别的标签,发现第一项并没有变成未选中,这是因为 this.state.selectItem 初始值为 null,那我们需要把第一项标签赋值给它。

由于只有在视图加载或更新完成才能通过 refs 获取到某个视图,所以我们需要一个定时器去触发选中方法。

Itemconstructor() 加入定时器:

 constructor(props) {
    super(props);
    // 初始状态
    this.state = {
        isSelect: props.isSelect
    };
    
    this.timer = setTimeout(
          () => 
              props.isSelect && props.onPress && props.onPress() //100ms 后调用选中操作
          ,
          100
        ); 
 }
      

搞定,最后我们还需要点击靠后的标签可以自动居中,我们需要操作 ScrollView 的偏移量,给 ScrollView 设置 ref='ScrollView'


<ScrollView
    ref="ScrollView"
    style={styles.scrollView}
    horizontal={true}
    showsHorizontalScrollIndicator={false}
>

然后去 items.push 加入偏移量的设置:

<Item
    ref={index}
    key={index}
    isSelect={index == 0}
    itemHeight={this.state.itemHeight}
    itemWidth={itemWidth}
    dic={dic}
    onPress={() => {
        this.state.selectItem && this.state.selectItem._unSelect()
        this.state.selectItem = this.refs[index]

        if (list.length > maxItem) {
            let meiosis = parseInt(maxItem / 2)
            this.refs.ScrollView.scrollTo({x: (index - meiosis < 0 ? 0 : index - meiosis > list.length - maxItem ? list.length - maxItem : index - meiosis ) * itemWidth, y: 0, animated: true})
        }
    }}
/>

现在的效果:

effect
effect

项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容