React Native 仿微信通讯录右侧字母快捷操作区

其实也不能说仿微信,大部分通讯录都一样,先来看两张效果图

第一张是微信、第二张是我写的

简单介绍一下功能点

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值