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閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容