本站文章均爲 李華明Himi 原創,轉載務必在明顯處註明:
轉載自【黑米GameDev街區】 原文鏈接: http://www.himigame.com/react-native/2346.html
補充說明:
一:很多童鞋問,鍵盤調出來被擋住了,那麼下面給出三個解決方案:
1. 在render最外層包一個ScrollView,然後當鍵盤調出時,scrollTo即可實現。
2. 在底部添加一個可變化高度的view,根據鍵盤獲取、失去焦點時,進行處理實現
3. 使用插件:react-native-keyboard-spacer :https://github.com/Andr3wHur5t/react-native-keyboard-spacer
二:有的童鞋說對話框的背景沒有根據內容長短自適應,OK ,下面給出自動適應的樣式與修改:
先看效果圖:
1. 導入一個組件:Dimensions
2. 我們先將 renderEveryData 的函數改爲如下:
renderEveryData(eData) { var sWidth = Dimensions.get('window').width return ( <View style={eData.isMe==true?styles.everyRowRight:styles.everyRow}> <Image source={eData.isMe==true? null:require('./res/headIcon/ox1.png')} style={eData.isMe==true?null:styles.talkImg} /> <View style={{width:sWidth - 100}}> <View style={eData.isMe==true?styles.talkViewRight:styles.talkView}> <Text style={ eData.isMe==true?styles.talkTextRight:styles.talkText }> {eData.talkContent} </Text> </View> </View> <Image source={eData.isMe==true? require('./res/headIcon/ox2.png') :null} style={eData.isMe==true?styles.talkImgRight:null} /> </View> ); }
3. 用到的樣式如下:
everyRow:{ flexDirection:'row', alignItems: 'center' }, everyRowRight:{ flexDirection:'row', alignItems: 'center', justifyContent:'flex-end' }, talkView: { backgroundColor: 'white', padding: 10, borderRadius:5, marginLeft:5, marginRight:55, marginBottom:10, alignSelf:'flex-start', }, talkViewRight: { backgroundColor: '#90EE90', padding: 10, borderRadius:5, marginLeft:55, marginRight:5, marginBottom:10, alignSelf:'flex-end', }, talkText: { fontSize: 16, fontWeight: 'bold', }, talkTextRight: { fontSize: 16, fontWeight: 'bold', alignSelf:'flex-end', }, talkImg: { height: 40, width: 40, marginLeft:10, marginBottom:10 }, talkImgRight: { height: 40, width: 40, marginRight:10, marginBottom:10 },
width:sWidth – 100:這裏是來限定Text每一行的最大寬度。
sWidth:是獲取的屏幕寬。
因此通過修改這裏的值來指定你想要的每一行最大寬度吧。
——————————————–以上爲補充內容,下面是正文——————————————–
本篇Himi來利用ListView和TextInput這兩種組件實現對話、聊天框。
首先需要準備的有幾點:(組件的學習就不贅述了,簡單且官方有文檔)
1. 學習下 ListView:
官方示例:http://reactnative.cn/docs/0.27/tutorial.html#content
官方文檔:http://reactnative.cn/docs/0.27/listview.html#content
2. 學習下:TextInput:
官方文檔:http://reactnative.cn/docs/0.27/textinput.html#content
3. 獲取組件實例常用的兩種方式:
有時候,渲染出來的組件,我們需要拿到它的實例進行調用其函數等操作。假設有如下代碼段:
render() { return ( <Text>Himi</Text> ) }
如上,如果我們想要拿到這個Text組件的實例對象,有如下兩種形式:
第一種:
render() { return ( <Text>Himi</Text> ) }
使用時:this.refs._text ,通過this.refs進行獲取。
第二種:
render() { var _text; return ( <Text ref={(text) => { _text = text; }}> Himi </Text> ) }
使用時:_text ,直接用這個變量即可。
如上都有了一定了解時,那麼下面我們進行本篇的正題:
製作一個對話、聊天框,內容可滾動,且最新的消息永遠保持在最底部顯示!
一:首先我們先簡單佈局一個聊天場景,佈局+各種小組件的使用(代碼簡單,不多說):
import React, { Component } from 'react'; import { View, Text, TouchableHighlight, Image, PixelRatio, ListView, StyleSheet, TextInput, Alert, } from 'react-native'; var datas =[ { isMe:false, talkContent:'最近在學習React Native哦!', }, { isMe:true, talkContent:'聽說是個跨平臺開發原生App的開源引擎', }, { isMe:false, talkContent:'嗯啊,很不錯,可以嘗試下吧。過了這段時間繼續研究UE去了。唉~技術出身,就是放不下技術呀~', }, { isMe:false, talkContent:'感覺編不下去對話了呀......感覺編不下去對話了呀......感覺編不下去對話了呀......感覺編不下去對話了呀......', }, { isMe:true, talkContent:'無語!', }, { isMe:false, talkContent:'自說自話,好難!隨便補充點字數吧,嗯 就醬紫 :) ', }, { isMe:true, talkContent:'感覺編不下去對話了呀......感覺編不下去對話了呀..', }, { isMe:false, talkContent:'GG,思密達編不下去了!', }, ]; export default class FarmChildView extends React.Component { constructor(props) { super(props); this.state = { inputContentText:'', dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), }; this.listHeight = 0; this.footerY = 0; } componentDidMount() { this.setState({ dataSource: this.state.dataSource.cloneWithRows(datas) }); } renderEveryData(eData) { return ( <View style={{flexDirection:'row',alignItems: 'center'}}> <Image source={eData.isMe==true? null:require('./res/headIcon/ox1.png')} style={eData.isMe==true?null:styles.talkImg} /> <View style={eData.isMe==true?styles.talkViewRight:styles.talkView}> <Text style={ styles.talkText }> {eData.talkContent} </Text> </View> <Image source={eData.isMe==true? require('./res/headIcon/ox2.png') :null} style={eData.isMe==true?styles.talkImgRight:null} /> </View> ); } myRenderFooter(e){ } pressSendBtn(){ } render() { return ( <View style={ styles.container }> <View style={styles.topView}> <Text style={{fontSize:20,marginTop:15,color:'#f00'}}>Himi React Native 系列教程</Text> </View> <ListView ref='_listView' onLayout={(e)=>{this.listHeight = e.nativeEvent.layout.height;}} dataSource={this.state.dataSource} renderRow={this.renderEveryData.bind(this)} renderFooter={this.myRenderFooter.bind(this)} /> <View style={styles.bottomView}> <View style={styles.searchBox}> <TextInput ref='_textInput' onChangeText={(text) =>{this.state.inputContentText=text}} placeholder=' 請輸入對話內容' returnKeyType='done' style={styles.inputText} /> </View> <TouchableHighlight underlayColor={'#AAAAAA'} activeOpacity={0.5} onPress={this.pressSendBtn.bind(this)} > <View style={styles.sendBtn}> <Text style={ styles.bottomBtnText }> 發送 </Text> </View> </TouchableHighlight> </View> </View> ); } } var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#EEEEEE' }, topView:{ alignItems: 'center', backgroundColor: '#DDDDDD', height: 52, padding:5 }, bottomView:{ flexDirection: 'row', alignItems: 'center', backgroundColor: '#DDDDDD', height: 52, padding:5 }, sendBtn: { alignItems: 'center', backgroundColor: '#FF88C2', padding: 10, borderRadius:5, height:40, }, bottomBtnText: { flex: 1, fontSize: 18, fontWeight: 'bold', }, talkView: { flex: 1, alignItems: 'center', backgroundColor: 'white', flexDirection: 'row', padding: 10, borderRadius:5, marginLeft:5, marginRight:55, marginBottom:10 }, talkImg: { height: 40, width: 40, marginLeft:10, marginBottom:10 }, talkText: { flex: 1, fontSize: 16, fontWeight: 'bold', }, talkViewRight: { flex: 1, alignItems: 'center', backgroundColor: '#90EE90', flexDirection: 'row', justifyContent: 'flex-end', padding: 10, borderRadius:5, marginLeft:55, marginRight:5, marginBottom:10 }, talkImgRight: { height: 40, width: 40, marginRight:10, marginBottom:10 }, searchBox: { height: 40, flexDirection: 'row', flex:1, // 類似於android中的layout_weight,設置爲1即自動拉伸填充 borderRadius: 5, // 設置圓角邊 backgroundColor: 'white', alignItems: 'center', marginLeft:5, marginRight:5, marginTop:10, marginBottom:10, }, inputText: { flex:1, backgroundColor: 'transparent', fontSize: 20, marginLeft:5 }, });
以上一共做了這麼幾件事:
頂部添加一個標題
添加一個ListView
底部添加一個輸入框和發送按鈕
以上代碼需要講解的有幾點:
1. inputContentText 這個state中的變量用於記錄用戶在TextInput輸入的內容
2. this.listHeight = 0; 獲取到ListHeight的高度
this.footerY = 0; 記錄ListView內容的最底部的Y位置。
(作用後續講)
3. myRenderFooter(e){} 這裏是當ListView的 renderFooter 函數觸發時候調用的。(作用後續講)
4. pressSendBtn 是噹噹點擊發送按鈕後,調用我們的自定義函數。
先看下佈局後的效果圖(點擊查看動態效果):
二:下面我們實現點擊發送後,將用戶在輸入框內輸入的內容添加到我們的ListView上,並重繪!
主要處理邏輯,Himi已經設計好了,就是在 pressSendBtn 函數中處理即可,處理代碼段如下:
pressSendBtn(){ if(this.state.inputContentText.trim().length <= 0){ Alert.alert('提示', '輸入的內容不能爲空'); return; } datas.push({ isMe:false, talkContent:this.state.inputContentText, }); this.refs._textInput.clear(); this.setState({ inputContentText:'', dataSource: this.state.dataSource.cloneWithRows(datas) }) }
1. if( this.state.inputContentText.trim().length <= 0 )
inputContentText用來記錄用戶在輸入框輸入的內容,因此這裏我們先對內容是否爲空進行判定!
trim () 函數不多說了吧,去掉字符串首尾空格。純空格的內容也不允許發送~
2. datas.push
這裏是我們將新的數據添加到ListView中,其中文字內容就是我們記錄的用戶輸入的內容
3. this.refs._textInput.clear()
這裏就是我們一開始準備工作介紹的小3節,通過this.refs._textInput()來獲取我們定義的TextInput組件實例。
4. 最後我們調用了 this.setState函數來對其兩個變量進行修改:
inputContentText :把記錄用戶剛纔輸入在聊天框內的內容清空。
dataSource:更新ListView的數據,因爲我們剛添加了一條數據
效果圖如下(點擊查看動態效果):
三:讓新的數據永遠展示在ListView的底部,其實就是想要一個新數據添加後,自動從下滾上來的效果。
Himi在做這一步的時候考慮過幾種方式,下面介紹兩種比較容易理解實現的方式:
a) 通過計算每個ListView的每一行View的高度來計算出位置,然後與ListView的視圖高度進行對比,最後確定是否進行滾動操作(超出ListView的視圖才應該滾動)
b) 根據官方ListView提供的renderFooter函數來完成!
renderFooter:
官方解釋:“頁頭與頁腳會在每次渲染過程中都重新渲染(如果提供了這些屬性)。如果它們重繪的性能開銷很大,把他們包裝到一個StaticContainer或者其它恰當的結構中。頁腳會永遠在列表的最底部,而頁頭會在最頂部。”
粗糙的理解:每次繪製都會調用renderFooter這個繪製函數,而renderFooter就是繪製ListView最底部的位置。這裏不是ListView視圖最底部,而且ListView內容高度的最底部位置!!
因此我們通過ListView的renderFooter 繪製一個0高度的view,通過獲取其Y位置,其實就是獲取到了ListView內容高度底部的Y位置。
這裏我們來介紹b方案,簡單便捷。關於a方案,我想大家自己都很容易理解實現。
其實通過上面佈局這段代碼中,可以看到,Himi也已經對renderFooter的函數也綁到了自定義函數myRenderFooter上,所以我們只要在renderFooter中處理即可,如下代碼:
myRenderFooter(e){ return <View onLayout={(e)=> { this.footerY= e.nativeEvent.layout.y; if (this.listHeight && this.footerY &&this.footerY>this.listHeight) { var scrollDistance = this.listHeight - this.footerY; this.refs._listView.scrollTo({y:-scrollDistance}); } }}/> }
1. 首先我們先繪製一個0高度的view : return <View/>
2. 通過ListView的onLayout函數來獲取並執行我們的滾動等邏輯。
onLayout 函數官方說明:
“當組件掛載或者佈局變化的時候調用
參數爲:{nativeEvent: { layout: {x, y, width, height}}}
這個事件會在佈局計算完成後立即調用一次,不過收到此事件時新的佈局可能還沒有在屏幕上呈現,尤其是一個佈局動畫正在進行中的時候。”
3. this.footerY= e.nativeEvent.layout.y;
this.footerY 一開始說過了,用來記錄0高度view的相對於ListView所在底部的Y位置。
注:這裏Himi定義成this.footerY,原因是Himi也嘗試了其他方式實現聊天滾動,爲了方便使用。因此大家這裏也可以定義var臨時的即可。或者直接得到使用都無所謂啦~
4. if( this.listHeight && this.footerY &&this.footerY>this.listHeight )
this.listHeight:與第三步類似,Himi通過ListView的onLayout函數獲取到其高度記錄在此變量上。
這裏的判斷目的:當最新的內容高度大雨ListView視圖高度後,再開始執行滾動邏輯。
5. 最後的滾動邏輯代碼段:
var scrollDistance = this.listHeight – this.footerY;
this.refs._listView.scrollTo({y:-scrollDistance});
首先通過當前ListView的視圖高度-內容底部Y位置,獲取到相差的舉例 scrollDistance,這個距離就是我們需要ListView 滾動的舉例,且取反滾動!
最後 _listView 是我們ListView的組件實例,因爲ListView中也有ScrollView的特性,因此我們可以使用其:
scrollTo({x: 0, y: 0, animated: true})
對我們ListView進行動畫滾動操作!
截此,我們的聊天、對話框完成,效果圖如下(點擊查看動態圖):
備註:每一行數據中Himi都定義了一個 isMe 的字段,這裏來表示說話是對方還是自己。
isMe = true : 頭像在右邊,說話底爲綠色。
isMe =false : 頭像放左側,說話底爲白色。
其實這裏Himi就是想做一些區分,模仿聊天的對話形式,所以加的變量。大家也可以各種自定義的啦~