You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native FlatList点击单个音频所有滑块同步移动问题排查

问题描述

以下是加载滑块的代码,你也可以在ClassFlatListRender.js中找到这段代码。预期逻辑为:当FlatList传递的元素id等于选中id时,对应音频滑块才会生效,但实际触发任意一个音频条目时,所有列表的滑块都会同步移动。

{id == this.state.selectedOptionId ?
  (<View style={[styles.viewBar, { flexDirection: 'row', }]}>
      <View style={[styles.viewBarPlay, { width: playWidthMessage }]} />
    </View>)
    : 
<View><Text>no slider<Text></View>}
代码实现

代码结构如下:有一个包含所有逻辑和状态的容器组件,容器内嵌套FlatList组件,FlatList内部渲染自定义展示型列表。

Container 
Custom list component that includes the FlatList component and the renderItem method
invokes classFlatList component (presentational, stateless)

容器组件内引入该组件的代码如下:

<CustomList
  items={this.state.data} 
 />

CustomList组件代码:

export class CustomList extends Component {
  render() {
      const renderItem = ({ item }) => {
          return (
              <ClassFlatListRender
                  message={item.message}
                  id={item.id}
                  user={item.user}
                  timestamp={item.timestamp}
                  heights={item.heights}
                  url={item.url}
                  audioLength={item.audioLength}
                  type={item.type}
              />
          )
      }
      let { items } = this.props
      return (
          <View style={styles.container}>
              < FlatList
                  style={styles.container}
                  data={items.slice().sort((a, b) => b.timestamp - a.timestamp)}
                  renderItem={renderItem}
                  inverted={true}
                  extraData={items}
                  keyExtractor={(items) => `${items.timestamp}`}
              />
          </View>
      )
  }
} 

ClassFlatListRender.js代码:

export class ClassFlatListRender extends React.Component {
  constructor() {
      super();
      this.state = {
          update: false,
          currentPositionSec: 0,
          currentDurationSec: 0,
          playTime: '00:00:00',
          duration: '00:00:00',
          id: '',
          tempId: '',
          selectedOptionId: '',
          setId: '',
      };
      this.audioRecorderPlayer = new AudioRecorderPlayer();
      this.audioRecorderPlayer.setSubscriptionDuration(0.09); // optional. Default is 0.1
  }
 
  onStartPlay = async (id, url) => {
      console.log('onStartPlay');
      console.log("arvinf", url)
      const msg = await this.audioRecorderPlayer.startPlayer(url);
      console.log("msg", msg)
      const volume = await this.audioRecorderPlayer.setVolume(1.0);
      console.log(`file: ${msg}`, `volume: ${volume}`);
      this.audioRecorderPlayer.addPlayBackListener((e) => {
          this.setState({
              currentPositionSec: e.currentPosition,
              currentDurationSec: e.duration,
          });
      });
  };
  onPausePlay = async () => {
      await this.audioRecorderPlayer.pausePlayer();
  };
  renderElement = (id, audioLength) => {
      if (this.state.selectedOptionId == id) {
          return (
              <Fcon name={this.state.selectedOptionId == id ? 'play' : 'pasue'} size={24} color={'white'} />
          )
      }
      if (this.state.selectedOptionId !== id || this.state.currentPositionSec == this.state.duration) {
          return (
              <Fcon name={'play'} size={24} color={'white'} />
          )
      }
  }
  checkFunction = async (id, url) => {
      console.log("id: before", id)
      await this.setState({
          selectedOptionId: id,
          setId: id
      })
      console.log('id:after', this.state.selectedOptionId)
      id == this.state.setId ? this.onStartPlay(id, url) : this.onPausePlay()
  }
  render() {
      let { id, user, url, heights, message, audioLength, type } = this.props;
      let playWidthMessage =
          (this.state.currentPositionSec / this.state.currentDurationSec) *
          (screenWidth - 300);
      if (!playWidthMessage) {
          playWidthMessage = 0;
      }
      return (
          <View id={id}>
              {
              url.endsWith(".jpg") ?
                  < View key={id} style={[user == 1 ? styles.receiver : styles.sender, { paddingHorizontal: 14, paddingVertical: 8, borderRadius: heights > 50 ? 20 : 50 }]}>
                      <View style={{ alignItems: 'center', }}>
                          {console.log("address", url)}
                          <Image
                              resizeMode="contain"
                              style={{ width: 200, height: 200 }}
                              source={{ uri: url, width: 200, height: 200 }}
                          />
                      </View>
                  </View> : null
              }
              {
                  url.endsWith(".mp3") ?
                      (
                          <View key={this.props.id} style={[user == 1 ? styles.receiver : styles.sender, { paddingHorizontal: 14, paddingVertical: 8, borderRadius: heights > 50 ? 20 : 50 }]}>
                              <View style={[styles.viewPlayer, { backgroundColor: user == 1 ? darkgrey : '#0096FF', alignSelf: 'center', borderRadius: 50, flexDirection: 'row', justifyContent: "center", alignItems: 'center' }]}>
                                  <TouchableOpacity activeOpacity={0.5} onPress={() => {
                                      this.checkFunction(id, url)
                                  }} style={{ borderRadius: 50, width: 36, height: 36, justifyContent: 'center', alignItems: 'center' }}>
                                      <View>
                                          {this.renderElement(id, url, audioLength)}
                                      </View>
                                  </TouchableOpacity>
                                  <View >
                                      <TouchableOpacity
                                          style={[styles.viewBarWrapper, { width: screenWidth - 300 }]}
                                          onPress={this.onStatusPress}
                                      >
                                          {id == this.state.setId ?
                                              (<View style={[styles.viewBar, { flexDirection: 'row', }]}>
                                                  <View style={[styles.viewBarPlay, { width: playWidthMessage }]} />
                                              </View>
                                              )
                                              : <View><Text>ddd</Text></View>}
                                      </TouchableOpacity>
                                  </View>
                                  <Text style={[styles.txtRecordCounter, { color: 'white' }]}>{audioLength}</Text>
                              </View>
                          </View >) : null
              }
              {
                  type == "txt" ?
                      <View key={id} >
                          <TouchableOpacity onPress={async () => await this.setState({ tempId: id })} style={[styles.receiver, { borderRadius: heights > 50 ? 20 : 50, backgroundColor: this.state.tempId == id ? 'pink' : 'yellow' }]}>
                              <Text style={styles.receiverText}>{message}</Text>
                          </TouchableOpacity>
                      </View> : null
              }
          </View >
      )
  }
}
修复方案

根因定位

  • FlatList key提取逻辑错误:当前keyExtractor使用timestamp作为唯一标识,若存在相同时间戳的消息会导致组件复用混乱,即使没有重复时间戳,列表滚动时组件复用也会携带旧的播放状态
  • 播放状态存储层级错误:播放选中ID、播放进度等全局状态被分散存储在每个ClassFlatListRender子组件内部,没有统一管控,加上AudioRecorderPlayer底层是全局单例,播放回调会触发所有子组件的状态更新
  • 播放监听器没有清理:每次播放新增的addPlayBackListener没有在组件卸载、暂停、切换播放条目时移除,会触发冗余的状态更新
  • AudioRecorderPlayer实例重复创建:每个子组件都新建一个播放器实例,无法做到全局唯一的播放控制

修复步骤

  1. 修正FlatList的key提取逻辑,改用消息唯一ID作为key:
// CustomList组件内FlatList配置修改
keyExtractor={(item) => `${item.id}`}
  1. 播放状态提升到最外层容器组件统一管理,全局只保留一个AudioRecorderPlayer实例:
// 容器组件state新增字段
this.state = {
  // 原有其他state
  selectedPlayId: null,
  currentPositionSec: 0,
  currentDurationSec: 0
}
// 播放、暂停、进度监听逻辑统一放到容器组件实现,通过props传递给子组件调用

同时修改CustomList组件的extraData配置,新增播放相关状态作为依赖,确保播放状态变化时FlatList能触发重新渲染:

<FlatList
  // 其他原有配置不变
  extraData={[items, this.props.selectedPlayId, this.props.currentPositionSec]}
/>
  1. 改造ClassFlatListRender为纯展示组件,删除内部播放相关state和播放器实例,所有播放状态从props获取,滑块宽度计算逻辑调整为:
const playWidthMessage = this.props.id === this.props.selectedPlayId 
  ? (this.props.currentPositionSec / this.props.currentDurationSec) * (screenWidth - 300) 
  : 0;
  1. 添加播放监听器的清理逻辑,在暂停播放、切换播放条目、容器组件卸载时调用removePlayBackListener移除旧的监听器,避免冗余回调。

内容的提问来源于stack exchange,提问作者Rvind Jitta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 18:45:04