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。通过 onResponderTerminationRequest 属性可以让 View 放弃处理事件的权利,将滚动事件交给 FlatList来处理。

 <View
      onStartShouldSetResponder={() => true}
      onResponderTerminationRequest={() => true}   <---- here
      onResponderGrant={_handlerClick}>
    {/* some code */}
</View>

代码

MyTiktok

参考文献

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

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
当您使用 React Native 编写一个简单的列表时,可以按照以下步骤进行操作: 1. 创建一个新的 React Native 项目,并确保您已经安装和配置了 React Native 开发环境。 2. 在项目中创建一个新的组件,例如 `ListPage.js`。 3. 在 `ListPage.js` 文件中,引入 ReactReact Native 相关的库和组件: ```javascript import React from 'react'; import { View, Text, FlatList } from 'react-native'; ``` 4. 创建一个函数式组件 `ListPage`,并在其中定义一个数据数组作为列表的数据源: ```javascript const ListPage = () => { const data = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, { id: 3, name: 'Item 3' }, // 添加更多的数据项... ]; return ( <View> <FlatList data={data} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => ( <Text>{item.name}</Text> )} /> </View> ); }; export default ListPage; ``` 5. 在主入口文件中(如 `App.js`),引入并使用 `ListPage` 组件: ```javascript import React from 'react'; import { View } from 'react-native'; import ListPage from './ListPage'; const App = () => { return ( <View style={{ flex: 1 }}> <ListPage /> </View> ); }; export default App; ``` 在这个示例中,我们使用了 `FlatList` 组件来展示列表数据。我们定义了一个简单的数据数组,然后在 `renderItem` 函数中渲染每个数据项。您可以根据实际需求修改数据源和渲染的列表项样式。 记得在终端中运行 `npm install` 安装所需的依赖,然后使用 `npx react-native run-android`(或 `npx react-native run-ios`)来启动应用程序并查看列表

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值