【REACT NATIVE 系列教程之十三】利用LISTVIEW與TEXTINPUT製作聊天/對話框&&獲取組件實例常用的兩種方式

本站文章均爲 李華明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 ,下面給出自動適應的樣式與修改:

先看效果圖:

11221

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
  },
});


以上一共做了這麼幾件事:

  1. 頂部添加一個標題

  2. 添加一個ListView

  3. 底部添加一個輸入框和發送按鈕

以上代碼需要講解的有幾點:

1. inputContentText 這個state中的變量用於記錄用戶在TextInput輸入的內容

2.  this.listHeight = 0; 獲取到ListHeight的高度

this.footerY = 0; 記錄ListView內容的最底部的Y位置。

(作用後續講)

3.  myRenderFooter(e){} 這裏是當ListView的 renderFooter 函數觸發時候調用的。(作用後續講)

4. pressSendBtn 是噹噹點擊發送按鈕後,調用我們的自定義函數。

先看下佈局後的效果圖(點擊查看動態效果):

user19118

二:下面我們實現點擊發送後,將用戶在輸入框內輸入的內容添加到我們的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的數據,因爲我們剛添加了一條數據

 效果圖如下(點擊查看動態效果):

user191218

三:讓新的數據永遠展示在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進行動畫滾動操作!

截此,我們的聊天、對話框完成,效果圖如下(點擊查看動態圖):

user324

   備註:每一行數據中Himi都定義了一個 isMe 的字段,這裏來表示說話是對方還是自己。

isMe = true :  頭像在右邊,說話底爲綠色。

    isMe =false : 頭像放左側,說話底爲白色。

    其實這裏Himi就是想做一些區分,模仿聊天的對話形式,所以加的變量。大家也可以各種自定義的啦~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章