1. 程式人生 > >React Native 仿微信通訊錄右側字母快捷操作區

React Native 仿微信通訊錄右側字母快捷操作區

其實也不能說仿微信,大部分通訊錄都一樣,先來看兩張效果圖

第一張是微信、第二張是我寫的

簡單介紹一下功能點

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