其实也不能说仿微信,大部分通讯录都一样,先来看两张效果图
第一张是微信、第二张是我写的
简单介绍一下功能点
1、好友排序
2、点击右侧快捷操作区,相应跳转到所属通讯录区域
3、ScrollView 在滑动过程中自动匹配右侧字母分类,同时作出UI更新
详细讲讲
好友信息数据结构
let typeList = [
{ nickname: '曾泰', picture: '', letter: 'Z' },
{ nickname: '狄仁杰', picture: '', letter: 'D' },
{ nickname: '李元芳', picture: '', letter: 'L' },
......
]
- 1
- 2
- 3
- 4
- 5
- 6
右侧操作栏数据结构
let addressAllList = [
{ title: 'A', number: 0, scrollHeight: 0 },
{ title: 'B', number: 0, scrollHeight: 0 },
{ title: 'C', number: 0, scrollHeight: 0 },
{ title: 'D', number: 0, scrollHeight: 0 },
{ title: 'E', number: 0, scrollHeight: 0 },
{ title: 'F', number: 0, scrollHeight: 0 },
......
】
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
1、好友排序
这个是基本操作,没什么可说的,为了功能的完整性,纯当做记录吧
/** 对列表信息进行排序 放在saga中进行处理 */
sortFriend () {
this.friendListSource = typeList.sort((a, b) => { return a.letter.localeCompare(b.letter) })
}
- 1
- 2
- 3
- 4
2、点击右侧快捷操作区,相应跳转到所属通讯录区域
原理是这样的,通过计算获得,每个字母区间所包含的好友个数,相应的就能得到该字幕区间的高度Height值,如果有了ScrollView的y值和每一行的高度值,那么我们就能精确的算出点击每个字母我们需要跳转到的绝对位置,嗯
但前提是,我们需要精确的得到这两个值
componentDidMount () {
/** 获取列表组件高度 */
const that = this
setTimeout(function () {
that.refs.friendArea.measure((x, y, width, height, left, top) => {
console.log('好友列表从高度' + y + '开始渲染***************')
that.setState({ y })
})
that.refs.friendItem.measure((x, y, width, height, left, top) => {
console.log('列表Item高度为' + height + '***************')
that.rowHeight = height
that.setState({ rowHeight: height })
})
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
接下来,我们就开始定量计算了
/** 右侧通讯录 */
sortAddress () {
/**
* 计算每层个数
*/
let tempList = addressAllList
typeList.map((item) => {
addressAllList.map((element, index) => {
if (element.title === item.letter) {
let { number } = tempList[index]
// console.log('出现一个相同项' + item.letter)
tempList.splice(index, 1, { ...tempList[index], number: number + 1 })
}
})
})
/**
* 计算每层y
*/
tempList.map((item, index) => {
let change = {}
if (index === 0) {
change = { ...item, scrollHeight: this.state.y }
} else {
const { scrollHeight, number } = this.addressListSource[index - 1]
change = { ...item, scrollHeight: scrollHeight + number * this.state.rowHeight }
}
this.addressListSource.push(change)
})
// console.log(this.addressListSource)
this.setState({ addressList: this.addressListSource })
}
- 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
界面操作呢????
this.refs.scroll.scrollTo({ y: scrollHeight, animated: true })} 就好了
<View style={{ position: 'absolute', top: y, right: px2dp(6) }}>
{ addressList.map((item, index) => {
const { title, number, scrollHeight } = item
return (number !== 0 &&
<TouchableWithoutFeedback onPress={() => this.refs.scroll.scrollTo({ y: scrollHeight, animated: true })} key={index}>
<View style={{ width: px2dp(40), height: px2dp(40), borderRadius: px2dp(20), margin: px2dp(4), backgroundColor: onNumber !== index ? Colors.C8 : Colors.CB, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ textAlign: 'center', fontSize: px2dp(24), color: onNumber !== index ? Colors.C3 : Colors.C8 }}>{title}</Text>
</View>
</TouchableWithoutFeedback>
)
})}
</View>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3、ScrollView 在滑动过程中自动匹配右侧字母分类,同时作出UI更新
<ScrollView
ref='scroll'
/** onScroll 回调频率 */
scrollEventThrottle={2}
/* 滑动监听 */
onScroll={(e) => this.isScrollOn(e)}
>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
其中scrollEventThrottle是滑动的帧数反馈,number类型,代表多少帧调用一次 onScroll 事件,如果对位置变化比较敏感,建议设置的小一点的值,当number === 200 ,表示只调用一次onScroll事件,咱们这里给一个2,位置的实时精度是比较高的
/** 是否滑动到当前的层 */
isScrollOn (e) {
const y = e.nativeEvent.contentOffset.y
/** 重复区间与异常值判断 */
if ((!(this.area.min && this.area.max && (y >= this.area.min && y < this.area.max)) && !(this.area.min && !this.area.max && y >= this.area.min) && !(y < this.state.y)) || (!this.area.min && !this.area.max)) {
console.log('分层处理频率**********************************')
let addressListSource = this.addressListSource
addressListSource.map((item, index) => {
if (index <= addressListSource.length - 2) {
if (y >= item.scrollHeight && y < addressListSource[index + 1].scrollHeight) {
this.area = { min: item.scrollHeight, max: addressListSource[index + 1].scrollHeight }
this.setState({ onNumber: index })
}
} else {
if (y >= item.scrollHeight) {
this.area = { min: item.scrollHeight, max: null }
this.setState({ onNumber: index })
}
}
})
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
由于我们的精度比较高,所以事件调用的频率也比较高,所以我们对处理状态和上一次的处理结果进行了保存,下一次调用时会进行比对,如果条件一致,就不再处理,这样处理频率就会相当可控,效果还是比较不错的,分块讲解可能不是很直观,我就把全部代码贴出来,大家可以从全局参考一下,欢迎提出建议
class MyPatientScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: '患者',
...ApplicationStyles.defaultHeaderStyle
});
constructor (props) {
super(props)
this.state = {
refreshing: false,
searchText: '',
isFocused: false,
addressList: [],
friendList: [],
y: 0,
rowHeight: 0,
onNumber: 0
}
/** functions */
/** object */
this.addressListSource = []
this.friendListSource = []
this.area = { min: null, max: null }
}
componentDidMount () {
/** 获取列表组件高度 */
const that = this
setTimeout(function () {
that.refs.friendArea.measure((x, y, width, height, left, top) => {
console.log('好友列表从高度' + y + '开始渲染***************')
that.setState({ y })
})
that.refs.friendItem.measure((x, y, width, height, left, top) => {
console.log('列表Item高度为' + height + '***************')
that.rowHeight = height
that.setState({ rowHeight: height })
})
})
}
componentWillReceiveProps (nextProps) {
if (!this.props.isFocused && nextProps.isFocused) {
this.onFocus()
}
if (this.props.isFocused && !nextProps.isFocused) {
this.onBlur()
}
}
componentWillUnmount () {
}
onFocus () {
this.props.toggleTabBarAction(true)
/** 对列表信息进行排序 放在saga中进行处理 */
// this.sortFriend()
/** 右侧通讯录 */
this.sortAddress()
}
onBlur () {}
/** 对列表信息进行排序 放在saga中进行处理 */
sortFriend () {
this.friendListSource = typeList.sort((a, b) => { return a.letter.localeCompare(b.letter) })
}
/** 右侧通讯录 */
sortAddress () {
/** ************************* 右侧通讯录 **********************************/
/**
* 计算每层个数
*/
let tempList = addressAllList
typeList.map((item) => {
addressAllList.map((element, index) => {
if (element.title === item.letter) {
let { number } = tempList[index]
// console.log('出现一个相同项' + item.letter)
tempList.splice(index, 1, { ...tempList[index], number: number + 1 })
}
})
})
// console.log(tempList)
/**
* 计算每层y
*/
tempList.map((item, index) => {
let change = {}
if (index === 0) {
change = { ...item, scrollHeight: this.state.y }
} else {
const { scrollHeight, number } = this.addressListSource[index - 1]
change = { ...item, scrollHeight: scrollHeight + number * this.state.rowHeight }
}
this.addressListSource.push(change)
})
// console.log(this.addressListSource)
this.setState({ addressList: this.addressListSource })
}
/** 是否滑动到当前的层 */
isScrollOn (e) {
const y = e.nativeEvent.contentOffset.y
/** 重复区间与异常值判断 */
if ((!(this.area.min && this.area.max && (y >= this.area.min && y < this.area.max)) && !(this.area.min && !this.area.max && y >= this.area.min) && !(y < this.state.y)) || (!this.area.min && !this.area.max)) {
console.log('分层处理频率**********************************')
console.log(y)
console.log(this.area)
let addressListSource = this.addressListSource
addressListSource.map((item, index) => {
if (index <= addressListSource.length - 2) {
if (y >= item.scrollHeight && y < addressListSource[index + 1].scrollHeight) {
this.area = { min: item.scrollHeight, max: addressListSource[index + 1].scrollHeight }
this.setState({ onNumber: index })
}
} else {
if (y >= item.scrollHeight) {
this.area = { min: item.scrollHeight, max: null }
this.setState({ onNumber: index })
}
}
})
}
}
_contentViewScroll (e) {
var offsetY = e.nativeEvent.contentOffset.y // 滑动距离
var contentSizeHeight = e.nativeEvent.contentSize.height // scrollView contentSize高度
var oriageScrollHeight = e.nativeEvent.layoutMeasurement.height // scrollView高度
if (offsetY + oriageScrollHeight >= contentSizeHeight) {
console.log('即将加载新数据********************')
}
}
onRefresh () {
const that = this
this.setState({ refreshing: true })
setTimeout(function () {
that.setState({ refreshing: false })
}, 1200)
}
render () {
const { navigation } = this.props
const { refreshing, searchText, isFocused, addressList, y, onNumber } = this.state
return (
<View style={{ flex: 1, backgroundColor: Colors.C8 }}>
<ScrollView
ref='scroll'
automaticallyAdjustContentInsets={false}
style={{ }}
/** onScroll 回调频率 */
scrollEventThrottle={2}
onScroll={(e) => this.isScrollOn(e)}
onMomentumScrollEnd={this._contentViewScroll.bind(this)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={this.onRefresh.bind(this)}
tintColor='#999'
title='刷新请稍候...'
titleColor='#999'
colors={['#999', '#999', '#999']}
progressBackgroundColor='#fff'
/>
}
>
<View style={{ backgroundColor: Colors.C8, padding: px2dp(30) }}>
<View style={{ borderWidth: 1, borderColor: Colors.C7, borderRadius: px2dp(10), paddingLeft: isFocused || searchText ? px2dp(70) : px2dp(0), justifyContent: 'center' }}>
<TextInput ref={(e) => { if (e) this._textInput = e }} value={searchText} onChangeText={(text) => this.setState({ searchText: text })} style={{ height: px2dp(60), fontSize: px2dp(24) }} placeholder={isFocused && !searchText ? '搜索' : ''} />
{ isFocused || searchText
? <View style={{ position: 'absolute', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', marginLeft: px2dp(20) }}>
<Icon name='search' size={px2dp(40)} color={Colors.C6} />
</View>
: <TouchableWithoutFeedback onPress={() => { this._textInput.focus(); this.setState({ isFocused: true }) }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: px2dp(60), marginTop: -px2dp(60) }}>
<Icon name='search' size={px2dp(40)} color={Colors.C6} />
<Text style={{ fontSize: px2dp(24), lineHeight: px2dp(34), color: Colors.C5, marginLeft: px2dp(10) }}>搜索</Text>
</View>
</TouchableWithoutFeedback>
}
</View>
<TouchableOpacity onPress={() => navigation.navigate('newPatientList')}>
<View style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', marginTop: px2dp(20) }}>
<Icon name='profile' size={px2dp(80)} color={Colors.CB} />
<Text style={{ fontSize: px2dp(24), lineHeight: px2dp(34), color: Colors.C3, marginLeft: px2dp(20) }}>新的患者</Text>
</View>
</TouchableOpacity>
<View style={{ }} ref='friendArea'>
{
undefined !== typeList && typeList.length > 0 && typeList.map((item, index) => {
const { nickname, picture } = item
let checked = true
if (searchText && nickname.indexOf(searchText) === -1) return false
return (checked &&
<TouchableOpacity onPress={() => navigation.navigate('patientInfo')} key={index} ref='friendItem'>
<View style={{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', paddingTop: px2dp(20), paddingBottom: px2dp(20), borderBottomWidth: 1, borderBottomColor: Colors.C7 }}>
<Image source={require('../../Images/PersonalCenter/avatar.png')} style={{ width: px2dp(80), height: px2dp(80), borderRadius: px2dp(40) }} />
<Text style={{ fontSize: px2dp(24), lineHeight: px2dp(34), color: Colors.C3, marginLeft: px2dp(20) }}>{nickname}</Text>
</View>
</TouchableOpacity>
)
})
}
</View>
</View>
</ScrollView>
<View style={{ position: 'absolute', top: y, right: px2dp(6) }}>
{ addressList.map((item, index) => {
const { title, number, scrollHeight } = item
return (number !== 0 &&
<TouchableWithoutFeedback onPress={() => this.refs.scroll.scrollTo({ y: scrollHeight, animated: true })} key={index}>
<View style={{ width: px2dp(40), height: px2dp(40), borderRadius: px2dp(20), margin: px2dp(4), backgroundColor: onNumber !== index ? Colors.C8 : Colors.CB, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ textAlign: 'center', fontSize: px2dp(24), color: onNumber !== index ? Colors.C3 : Colors.C8 }}>{title}</Text>
</View>
</TouchableWithoutFeedback>
)
})}
</View>
</View>
)
}
}
转载http://blog.csdn.net/qq_28978893/article/details/78503106