React Native 仿微信通訊錄右側字母快捷操作區
阿新 • • 發佈:2019-02-04
其實也不能說仿微信,大部分通訊錄都一樣,先來看兩張效果圖
第一張是微信、第二張是我寫的
簡單介紹一下功能點
1、好友排序
2、點選右側快捷操作區,相應跳轉到所屬通訊錄區域
3、ScrollView 在滑動過程中自動匹配右側字母分類,同時作出UI更新
詳細講講
好友資訊資料結構
let typeList = [
{ nickname: '曾泰', picture: '', letter: 'Z' },
{ nickname: '狄仁傑', picture: '', letter: 'D' },
{ nickname: '李元芳', picture: '', letter: 'L' },
......
]
右側操作欄資料結構
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、好友排序
這個是基本操作,沒什麼可說的,為了功能的完整性,純當做記錄吧
/** 對列表資訊進行排序 放在saga中進行處理 */
sortFriend () {
this.friendListSource = typeList.sort((a, b) => { return a.letter.localeCompare(b.letter) })
}
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 })
})
})
}
接下來,我們就開始定量計算了
/** 右側通訊錄 */
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 })
}
介面操作呢????
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>
3、ScrollView 在滑動過程中自動匹配右側字母分類,同時作出UI更新
<ScrollView
ref='scroll'
/** onScroll 回撥頻率 */
scrollEventThrottle={2}
/* 滑動監聽 */
onScroll={(e) => this.isScrollOn(e)}
>
其中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 })
}
}
})
}
}
由於我們的精度比較高,所以事件呼叫的頻率也比較高,所以我們對處理狀態和上一次的處理結果進行了儲存,下一次呼叫時會進行比對,如果條件一致,就不再處理,這樣處理頻率就會相當可控,效果還是比較不錯的,分塊講解可能不是很直觀,我就把全部程式碼貼出來,大家可以從全域性參考一下,歡迎提出建議
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>
)
}
}