react native 抖音視頻列表頁

實現效果

需要實現的效果主要有兩個,一個是上下翻頁效果,還有就是點讚的動畫。

效果

列表頁上下翻頁實現

播放視頻使用 react-native-video 庫。列表使用 FlatList,每個 item 佔用一整屏,配合 pagingEnabled 屬性可以翻頁效果。
通過 onViewableItemsChanged 來控制只有當前的頁面才播放視頻。

const ShortVideoPage = () => {
  const [currentItem, setCurrentItem] = useState(0);
  const [data, setData] = useState<ItemData[]>([]);

  const _onViewableItemsChanged = useCallback(({ viewableItems }) => {
    // 這個方法爲了讓state對應當前呈現在頁面上的item的播放器的state
    // 也就是隻會有一個播放器播放,而不會每個item都播放
    // 可以理解爲,只要不是當前再頁面上的item 它的狀態就應該暫停
    // 只有100%呈現再頁面上的item(只會有一個)它的播放器是播放狀態
    if (viewableItems.length === 1) {
      setCurrentItem(viewableItems[0].index);
    }
  }, []);

  useEffect(() => {
    const mockData = [];
    for (let i = 0; i < 100; i++) {
      mockData.push({ id: i, pause: false });
    }
    setData(mockData);
  }, []);

  return (
    <View style={{ flex: 1 }}>
      <StatusBar
        backgroundColor="transparent"
        translucent
      />
      <FlatList<ItemData>
        onMoveShouldSetResponder={() => true}
        data={data}
        renderItem={({ item, index }) => (
          <ShortVideoItem
            paused={index !== currentItem}
            id={item.id}
          />
        )}
        pagingEnabled={true}
        getItemLayout={(item, index) => {
          return { length: HEIGHT, offset: HEIGHT * index, index };
        }}
        onViewableItemsChanged={_onViewableItemsChanged}
        keyExtractor={(item, index) => index.toString()}
        viewabilityConfig={{
          viewAreaCoveragePercentThreshold: 80, // item滑動80%部分纔會到下一個
        }}
      />
    </View>
  );
};

點贊效果

單次點擊的時候切換暫停/播放狀態,連續多次點擊每次在點擊位置出現一個愛心,隨機旋轉一個角度,愛心先放大再變透明消失。

愛心動畫實現

const AnimatedHeartView = React.memo(
  (props: AnimatedHeartProps) => {
    // [-25, 25]隨機一個角度
    const rotateAngle = `${Math.round(Math.random() * 50 - 25)}deg`;
    const animValue = React.useRef(new Animated.Value(0)).current;

    React.useEffect(() => {
      Animated.sequence([
        Animated.spring(animValue, {
          toValue: 1,
          useNativeDriver: true,
          bounciness: 5,
        }),
        Animated.timing(animValue, {
          toValue: 2,
          useNativeDriver: true,
        }),
      ]).start(() => {
        props.onAnimFinished();
      });
    }, [animValue, props]);

    return (
      <Animated.Image
        style={{
          position: 'absolute',
          width: 108,
          height: 126,
          top: props.y - 100,
          left: props.x - 54,
          opacity: animValue.interpolate({
            inputRange: [0, 1, 2],
            outputRange: [1, 1, 0],
          }),
          transform: [
            {
              scale: animValue.interpolate({
                inputRange: [0, 1, 2],
                outputRange: [1.5, 1.0, 2],
              }),
            },
            {
              rotate: rotateAngle,
            },
          ],
        }}
        source={require('./img/heart.webp')}
      />
    );
  },
  () => true,
);

連續點贊判定

監聽手勢,記錄每次點擊時間 lastClickTime,設置 CLICK_THRESHOLD 連續兩次點擊事件間隔小於 CLICK_THRESHOLD 視爲連續點擊,在點擊位置創建愛心,添加到 heartList,否則視爲單次點擊,暫停播放。

const ShortVideoItem = React.memo((props: ShortVideoItemProps) => {
  const [paused, setPaused] = React.useState(props.paused);
  const [data, setData] = React.useState<VideoData>();
  const [heartList, setHeartList] = React.useState<HeartData[]>([]);
  const lastClickTime = React.useRef(0); // 記錄上次點擊時間
  const pauseHandler = React.useRef<number>();

  useEffect(() => {
    setTimeout(() => {
      setData({
        video: TEST_VIDEO,
        hasFavor: false,
      });
    });
  }, []);

  useEffect(() => {
    setPaused(props.paused);
  }, [props.paused]);

  const _addHeartView = React.useCallback(heartViewData => {
    setHeartList(list => [...list, heartViewData]);
  }, []);

  const _removeHeartView = React.useCallback(index => {
    setHeartList(list => list.filter((item, i) => index !== i));
  }, []);

  const _favor = React.useCallback(
    (hasFavor, canCancelFavor = true) => {
      if (!hasFavor || canCancelFavor) {
        setData(preValue => (preValue ? { ...preValue, hasFavor: !hasFavor } : preValue));
      }
    }, [],
  );

  const _handlerClick = React.useCallback(
    (event: GestureResponderEvent) => {
      const { pageX, pageY } = event.nativeEvent;
      const heartViewData = {
        x: pageX,
        y: pageY - 60,
        key: new Date().getTime().toString(),
      };
      const currentTime = new Date().getTime();
      // 連續點擊
      if (currentTime - lastClickTime.current < CLICK_THRESHOLD) {
        pauseHandler.current && clearTimeout(pauseHandler.current);
        _addHeartView(heartViewData);
        if (data && !data.hasFavor) {
          _favor(false, false);
        }
      } else {
        pauseHandler.current = setTimeout(() => {
          setPaused(preValue => !preValue);
        }, CLICK_THRESHOLD);
      }

      lastClickTime.current = currentTime;
    }, [_addHeartView, _favor, data],
  );

  return <View
    onStartShouldSetResponder={() => true}
    onResponderGrant={_handlerClick}
    style={{ height: HEIGHT }}
  >
    {
      data
        ? <Video source={{ uri: data?.video }}
          style={styles.backgroundVideo}
          paused={paused}
          resizeMode={'contain'}
          repeat
        />
        : null
    }
    {
      heartList.map(({ x, y, key }, index) => {
        return (
          <AnimatedHeartView
            x={x}
            y={y}
            key={key}
            onAnimFinished={() => _removeHeartView(index)}
          />
        );
      })
    }
    <View style={{ justifyContent: 'flex-end', paddingHorizontal: 22, flex: 1 }}>
      <View style={{
        backgroundColor: '#000',
        opacity: 0.8,
        height: 32,
        borderRadius: 16,
        alignItems: 'center',
        justifyContent: 'center',
        marginRight: 'auto',
        paddingHorizontal: 8,
      }}>
        <Text
          style={{ fontSize: 14, color: '#FFF' }}
        >
          短視頻招募了
        </Text>
      </View>
      <View
        style={{ height: 1, marginTop: 12, backgroundColor: '#FFF' }}
      />
      <Text
        style={{
          marginTop: 12,
          color: '#FFF',
          fontSize: 16,
          fontWeight: 'bold',
        }}
        numberOfLines={1}
      >
        5㎡長條形衛生間如何設計乾溼分離?
      </Text>
      <Text
        style={{
          marginTop: 8,
          color: '#FFF',
          opacity: 0.6,
          fontSize: 12,
        }}
        numberOfLines={2}
      >
        家裏只有一個衛生間,一定要這樣裝!顏值比五星酒店衛生間還高級,衛生間,一定要這樣裝!顏值比衛生間,一定要這樣裝!
      </Text>
      <View style={{
        flexDirection: 'row',
        marginTop: 18,
        marginBottom: 20,
        alignItems: 'center',
      }}>
        <View
          style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: '#FFF' }}
        />
        <Text style={{ color: '#FFF', fontSize: 14, marginLeft: 4 }}>
          造作設計工作坊
        </Text>
      </View>
    </View>
    <View style={{
      position: 'absolute',
      right: 20,
      bottom: 165,
    }}>
      <Image
        style={styles.icon}
        source={data?.hasFavor ? require('./img/love-f.png') : require('./img/love.png')}
      />
      <Text style={styles.countNumber}>1.2w</Text>
      <Image
        style={styles.icon}
        source={require('./img/collect.png')}
      />
      <Text style={styles.countNumber}>1.2w</Text>
      <Image
        style={styles.icon}
        source={require('./img/comment.png')}
      />
      <Text style={styles.countNumber}>1.2w</Text>
    </View>
    {
      paused
        ? <Image
          style={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            width: 40,
            height: 40,
            marginLeft: -20,
            marginTop: -20,
          }}
          source={require('./img/play.webp')}
        />
        : null
    }
  </View>;
}, (preValue, nextValue) => preValue.id === nextValue.id && preValue.paused === nextValue.paused);

手勢衝突

通過 GestureResponder 攔截點擊事件之後會造成 FlatList 滾動事件失效,所以需要攔截滾動事件交給 FlatList。

<FlatList<ItemData>
    onMoveShouldSetResponder={() => true}
/>

代碼

MyTiktok

參考文獻

https://blog.csdn.net/qq_38356174/article/details/96439456
https://juejin.im/post/5b504823e51d451953125799

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章