12-7 RN之弹幕功能

169 阅读3分钟

在本节中,我们将实现弹幕功能。当点击弹幕按钮时,多个弹幕从右往左移动,模拟真实的弹幕效果。

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 创建弹幕动画并避免重叠,同时加入了弹幕的动态添加、删除和轨道管理。下一节我们将实现底部标签导航器并完成播放控制的功能。