补充说明:
一:很多童鞋问,键盘调出来被挡住了,那么下面给出三个解决方案:
1. 在render最外层包一个ScrollView,然后当键盘调出时,scrollTo即可实现。
2. 在底部添加一个可变化高度的view,根据键盘获取、失去焦点时,进行处理实现
3. 使用插件:react-native-keyboard-spacer :https://github.com/Andr3wHur5t/react-native-keyboard-spacer
二:有的童鞋说对话框的背景没有根据内容长短自适应,OK ,下面给出自动适应的样式与修改:
先看效果图:
1. 导入一个组件:Dimensions
2. 我们先将 renderEveryData 的函数改为如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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. 用到的样式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
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. 获取组件实例常用的两种方式:
有时候,渲染出来的组件,我们需要拿到它的实例进行调用其函数等操作。假设有如下代码段:
1
2
3
4
5
|
render() { return (
<Text>Himi</Text>
)
} |
如上,如果我们想要拿到这个Text组件的实例对象,有如下两种形式:
第一种:
1
2
3
4
5
|
render() { return (
<Text>Himi</Text>
)
} |
使用时:this.refs._text ,通过this.refs进行获取。
第二种:
1
2
3
4
5
6
7
8
|
render() { var _text;
return (
<Text ref={(text) => { _text = text; }}>
Himi
</Text>
)
} |
使用时:_text ,直接用这个变量即可。
如上都有了一定了解时,那么下面我们进行本篇的正题:
制作一个对话、聊天框,内容可滚动,且最新的消息永远保持在最底部显示!
一:首先我们先简单布局一个聊天场景,布局+各种小组件的使用(代码简单,不多说):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
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 函数中处理即可,处理代码段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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中处理即可,如下代码:
1
2
3
4
5
6
7
8
9
10
|
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就是想做一些区分,模仿聊天的对话形式,所以加的变量。大家也可以各种自定义的啦~