在本节中,我们将实现弹幕功能。当点击弹幕按钮时,多个弹幕从右往左移动,模拟真实的弹幕效果。
1. 创建弹幕组件
首先,我们创建 Barrage 组件,用于显示弹幕。弹幕从右往左移动,使用 Animated.View 实现。
import React from 'react';
class Barrage extends React.Component {
translateX = new Animated.Value(viewportWidth);
componentDidMount() {
Animated.timing(this.translateX, {
toValue: 0,
duration: 6000,
useNativeDriver: true,
}).start();
}
render() {
return (
<Animated.View style={{ transform: [{ translateX: this.translateX }] }}>
<Text>我是弹幕</Text>
</Animated.View>
);
}
}
export default Barrage;
在 Detail 组件中使用 Barrage 组件:
<Barrage />
2. 动态添加弹幕
为了模拟真实场景,我们需要每秒向弹幕组件传递新的弹幕。定义一个弹幕数据数组,并使用 setInterval 动态添加数据。
const data: string[] = [
'最灵繁的人也看不见自己的背脊',
'朝闻道,夕死可矣',
'阅读是人类进步的阶梯',
// 更多弹幕数据...
];
interface Message {
id: number;
title: string;
}
class Detail extends Component<IProps, IState> {
state = { data: [] };
componentDidMount() {
this.addBarrage();
}
addBarrage = () => {
this.interval = setInterval(() => {
const { barrage } = this.state;
if (barrage) {
const id = Date.now();
const title = getText();
const newData = [{ title, id }];
this.setState({ data: newData });
}
}, 1000);
};
getText = () => {
const index = randomIndex(data.length);
return data[index];
};
}
3. 弹幕渲染
在 Barrage 组件中渲染每条弹幕数据,并确保每次更新时合并新的弹幕和已有弹幕。
interface IProps {
data: Message[];
}
interface IState {
data: Message[];
list: Message[];
}
class Barrage extends PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
data: props.data,
list: props.data,
};
}
static getDerivedStateFromProps(nextProps: IProps, prevState: IState) {
const { data } = nextProps;
if (data !== prevState.data) {
return {
data,
list: prevState.list.concat(data),
};
}
return null;
}
render() {
const { list } = this.state;
return (
<View style={styles.container}>
{list && list.map(this.renderItem)}
</View>
);
}
renderItem = (item: Message) => {
return <Item data={item} />;
};
}
4. 创建 Item 组件
Item 组件用于渲染单条弹幕,并使用 Animated 实现移动效果。
class Item extends React.PureComponent {
translateX = new Animated.Value(0);
componentDidMount() {
Animated.timing(this.translateX, {
toValue: viewportWidth,
duration: 6000,
useNativeDriver: true,
}).start();
}
render() {
const { data } = this.props;
return (
<Animated.View
style={{
position: 'absolute',
height: 20,
transform: [
{
translateX: this.translateX.interpolate({
inputRange: [0, 10],
outputRange: [viewportWidth, 0],
}),
},
],
}}
>
<Text>{data.title}</Text>
</Animated.View>
);
}
}
export default Item;
5. 删除已移动的弹幕
为了避免内存泄漏,每条弹幕动画完成后需要删除,从 list 中移除该弹幕。
componentDidMount() {
const { outside, data } = this.props;
Animated.timing(this.translateX, {
toValue: viewportWidth,
duration: 6000,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
outside(data);
}
});
}
outside = (data: IBarrage) => {
const { list } = this.state;
const newList = list.concat();
if (newList.length > 0) {
const { trackIndex } = data;
if (newList[trackIndex]) {
newList[trackIndex] = newList[trackIndex].filter(item => item.id !== data.id);
this.setState({
list: newList,
});
}
}
};
6. 控制弹幕轨道
为了避免弹幕重叠,我们使用二维数组表示多个轨道。每个轨道存储一个或多个弹幕。每次添加新弹幕时,检查当前轨道是否可用。
interface IState {
data: Message[];
list: Message[][];
}
static getDerivedStateFromProps(nextProps: IProps, prevState: IState) {
const { data, maxTrack } = nextProps;
if (data !== prevState.data) {
return {
data,
list: addBarrage(data, maxTrack, prevState.list),
};
}
return null;
}
const addBarrage = (data: Message[], maxTrack: number, barrages: IBarrage[][]) => {
for (let i = 0; i < data.length; i++) {
const message = data[i];
const trackIndex = getTrackIndex(barrages, maxTrack);
if (trackIndex < 0) continue;
if (!barrages[trackIndex]) {
barrages[trackIndex] = [];
}
const barrage = { ...message, trackIndex };
barrages[trackIndex].push(barrage);
}
return barrages;
};
const getTrackIndex = (barrages: IBarrage[][], maxTrack: number) => {
for (let i = 0; i < maxTrack; i++) {
if (!barrages[i] || barrages[i].length === 0) {
return i;
}
const lastItem = barrages[i][barrages[i].length - 1];
if (lastItem.isFree) {
return i;
}
}
return -1;
};
7. 计算 top 值并避免重叠
为每个弹幕计算一个随机的 top 值,确保每个弹幕出现在不同的轨道上。
<Animated.View
style={{
position: 'absolute',
top: data.trackIndex * 30,
}}
>
<Text>{data.title}</Text>
</Animated.View>
8. 监听动画进度
当弹幕移动到一定距离时,更新其 isFree 属性,允许在轨道上添加新的弹幕。
addListener(({ value }) => {
if (value > 0.3) {
data.isFree = true;
}
});
总结
在本节中,我们实现了弹幕功能,通过 Animated 创建弹幕动画并避免重叠,同时加入了弹幕的动态添加、删除和轨道管理。下一节我们将实现底部标签导航器并完成播放控制的功能。