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实例重复创建:每个子组件都新建一个播放器实例,无法做到全局唯一的播放控制
修复步骤
- 修正FlatList的key提取逻辑,改用消息唯一ID作为key:
// CustomList组件内FlatList配置修改 keyExtractor={(item) => `${item.id}`}
- 播放状态提升到最外层容器组件统一管理,全局只保留一个AudioRecorderPlayer实例:
// 容器组件state新增字段 this.state = { // 原有其他state selectedPlayId: null, currentPositionSec: 0, currentDurationSec: 0 } // 播放、暂停、进度监听逻辑统一放到容器组件实现,通过props传递给子组件调用
同时修改CustomList组件的extraData配置,新增播放相关状态作为依赖,确保播放状态变化时FlatList能触发重新渲染:
<FlatList // 其他原有配置不变 extraData={[items, this.props.selectedPlayId, this.props.currentPositionSec]} />
- 改造ClassFlatListRender为纯展示组件,删除内部播放相关state和播放器实例,所有播放状态从props获取,滑块宽度计算逻辑调整为:
const playWidthMessage = this.props.id === this.props.selectedPlayId ? (this.props.currentPositionSec / this.props.currentDurationSec) * (screenWidth - 300) : 0;
- 添加播放监听器的清理逻辑,在暂停播放、切换播放条目、容器组件卸载时调用
removePlayBackListener移除旧的监听器,避免冗余回调。
内容的提问来源于stack exchange,提问作者Rvind Jitta
相关产品推荐
相关产品推荐

