Flatlist 滚动问题 Expo React Native

问题描述 投票:0回答:1

我正在使用 Firebase 后端创建一个 Expo React Native 项目。 我想向我的用户显示帖子,以便他们可以像 Instagram 卷轴一样垂直滚动。 所以我使用了来自react-native的Flatlist。
问题:我正在从我的 firebase 中获取平面列表的内容,它由一个视频和一些显示在帖子顶部的 Ui 交互元素组成。 但由于某种原因,当用户滚动时,我无法在用户屏幕中保留单个帖子。 每次发生滚动时,前一篇文章底部的某些部分在用户的视口中可见。而且这个重叠部分随着每一篇文章的发布而增加。 滚动浏览 5-6 个帖子后,2 个视频同时呈现在用户视口中。

当前的代码非常未经优化且混乱,但这一个问题正在造成糟糕的体验。

  • 这是我的代码的一些重要文件
  1. ScrollScreen.jsx - 这是我在堆栈导航器中渲染的屏幕。
  2. Scroller.jsx - 这是我的组件,位于上面的屏幕内,它正在渲染向用户显示的 Flatlist
  3. index.js - Mypost 组件,其中包含渲染每个帖子的视频和其他 UI 元素的代码。
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Scroller from '../components/Scroller';
import { getCurrentUserKeywords, getPost } from '../redux/actions/user';

const ScrollScreen = () => {
  const { posts } = useSelector(store => store.user);
  const [change, setChange] = useState(false);
  const [filteredPosts, setFilteredPosts] = useState([]);
  const dispatch = useDispatch();

  useEffect(() => {
    (
      async function () {
        dispatch(getPost());
      }
    )()
  }, [])

  useEffect(() => {
    console.log('change')
    setChange(prev => !prev);
  }, [posts])

  useEffect(() => {
    const filterPostsByKeywords = async () => {
      const keywords = await getCurrentUserKeywords();
      // Fetch the current user's keywords
      const filtered = posts.filter(post =>
        post.hashtags.some(hashtag => keywords.includes(hashtag))
        //  each post has a 'hashtags' array
      );
      setFilteredPosts(filtered);
      console.log(`Filtered posts length: ${filtered.length}`);
    };


    if (posts.length > 0) {
      filterPostsByKeywords();
    }
  }, [posts]);
  return (
    <View>
      <Scroller posts={filteredPosts} change={change} profile={false} />
    </View>
  )
}

export default ScrollScreen
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useRef, useState } from 'react';
import { Dimensions, FlatList, StyleSheet, View } from 'react-native';
import { useSelector } from 'react-redux';
import useMaterialNavBarHeight from '../hooks/useMaterialNavBarHeight';
import PostSingle from './post';

export default function Scroller({ posts: allPosts, change, profile, currentIndex }) {
    const [posts, setPosts] = useState(allPosts);
    const isScrollTab = useRef(true);
    const mediaRefs = useRef([]);
    const storeCellRef = useRef([]);
    const currentVideoRes = useRef(null);
    const navigation = useNavigation();

    const { change: pageChange } = useSelector(store => store.user);

    useEffect(() => {
        setPosts(allPosts);
    }, [allPosts, change]);

    useEffect(() => {
        console.log('mount');
    }, [pageChange]);

    useEffect(() => {
        const handleScreenChange = () => {
            if (currentVideoRes.current) currentVideoRes.current.stop();
            isScrollTab.current = false;
        };

        const handleFocus = () => {
            isScrollTab.current = true;
            if (currentVideoRes.current) currentVideoRes.current.play();
        };

        const unsubscribeBlur = navigation.addListener('blur', handleScreenChange);
        const unsubscribeFocus = navigation.addListener('focus', handleFocus);

        return () => {
            unsubscribeFocus();
            unsubscribeBlur();
        };
    }, [navigation]);

    const onViewableItemsChanged = useRef(({ changed }) => {
        changed.forEach(element => {
            const cell = mediaRefs.current[element.key];
            if (cell) {
                if (element.isViewable && isScrollTab.current) {
                    // Stop all videos
                    for (let index = 0; index < storeCellRef.current.length; index++) {
                        const cell = storeCellRef.current[index];
                        cell.stop();
                    }
                    // Clear the array
                    storeCellRef.current = [];
                    // Play the current video
                    cell.play();
                    currentVideoRes.current = cell;
                    storeCellRef.current.push(cell);
                } else {
                    cell.stop();
                    // Remove the cell from the array
                    storeCellRef.current = storeCellRef.current.filter(c => c !== cell);
                }
            }
        });
    });

    const feedItemHeight = Dimensions.get('window').height - useMaterialNavBarHeight(profile);

    const [followedUsers, setFollowedUsers] = useState(new Set());
    const renderItem = ({ item, index }) => {
        return (
            <View style={{ height: feedItemHeight, backgroundColor: 'black' }}>
                <PostSingle followedUsers={followedUsers}
                    setFollowedUsers={setFollowedUsers} item={item} key={index} ref={ref => (mediaRefs.current[item.id] = ref)} />
            </View>
        );
    };

    const handleEndReach = () => {
        setPosts(prevPosts => [...prevPosts, ...allPosts]);
    }

    return (
        <FlatList
            windowSize={4}
            data={posts}
            initialScrollIndex={currentIndex}
            getItemLayout={(data, index) => (
                { length: feedItemHeight, offset: feedItemHeight * index, index }
            )}
            renderItem={renderItem}
            initialNumToRender={1}
            maxToRenderPerBatch={2}
            removeClippedSubviews
            viewabilityConfig={{
                itemVisiblePercentThreshold: 50
            }}
            pagingEnabled
            decelerationRate={'normal'}
            onViewableItemsChanged={onViewableItemsChanged.current} /*
            to fix the audio leak bug
            */
            keyExtractor={item => item.id || String(item.index)}
            showsVerticalScrollIndicator={false}
            showsHorizontalScrollIndicator={false}
            onEndReached={handleEndReach}
        />
    );
}

const styles = StyleSheet.create({
    container: { flex: 1 }
});

import { ResizeMode, Video } from "expo-av";
import React, {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react";
import {
  ActivityIndicator,
  FlatList,
  Image,
  Linking,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
} from "react-native";
// import { useUser } from '../../hooks/useUser'
// import PostSingleOverlay from './overlay'
import { Feather } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { LinearGradient } from "expo-linear-gradient";
import { Avatar } from "react-native-paper";
import index from "uuid-random";
import { COLORS } from "../../constants";
import tw from "../../customtwrnc";
import {
  LikePost,
  checkFollow,
  checkLike,
  followUser,
  getUserById,
} from "../../redux/actions/user";
import CommentModel from "./CommentModel";
import styles from "./styles";

const renderItem = ({ item }) => (
  <TouchableOpacity>
    <View style={tw`mx-1`}>
      <Text style={tw`text-sm text-[${COLORS.secondary}] font-montserrat`}>
        {item}
      </Text>
    </View>
  </TouchableOpacity>
);

export const PostSingle = forwardRef(({ item, ...props }, parentRef) => {
  const video = React.useRef(null);
  const timeoutref = React.useRef(null);
  const [isLoading, setIsLoading] = React.useState(true);
  const [user, setUser] = useState(null);
  const [longPressTimer, setLongPressTimer] = useState(null);
  const [videoStop, setVideoStop] = useState(false);
  const [isLike, setIsLike] = useState(false);
  const [likesCount, setLikesCount] = useState(0);
  const [likeLoading, setLikeLoading] = useState(false);
  const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
  const bottomSheetModalRef = useRef(null);
  const navigation = useNavigation();
  const [mute, setMute] = useState(false);
  const [showPopup, setShowPopup] = useState(false);
  const [imageFailedToLoad, setImageFailedToLoad] = useState(false);
  const [isCommentModalVisible, setIsCommentModalVisible] = useState(false);

  useImperativeHandle(parentRef, () => ({
    play,
    unload,
    stop,
  }));

  useEffect(() => {
    return () => {
      console.log("unloading");
      unload();
    };
  }, []);

  const handleLoadStart = () => {
    setIsLoading(true);
  };

  const handleReadyForDisplay = () => {
    setIsLoading(false);
  };

  const play = async () => {
    if (video.current == null) {
      return;
    }

    // if video is already playing return
    const status = await video.current.getStatusAsync();
    if (status?.isPlaying) {
      return;
    }
    try {
      await video.current.playAsync();
    } catch (e) {
      console.log("error", e.message);
    }
  };

  const stop = async () => {
    if (video.current == null) {
      return;
    }

    // if video is already stopped return
    const status = await video.current.getStatusAsync();
    if (!status?.isPlaying) {
      return;
    }
    try {
      await video.current.stopAsync();
    } catch (e) {
      console.log(e);
    }
  };

  const unload = async () => {
    if (video.current == null) {
      return;
    }

    try {
      await video.current.unloadAsync();
    } catch (e) {
      console.log(e);
    }
  };

  const handleOpen = (link) => {
    Linking.openURL(link);
  };

  const handlePressIn = () => {
    const timer = setTimeout(async () => {
      await video.current.pauseAsync();
      setVideoStop(true);
    }, 1000);
    setLongPressTimer(timer);
  };

  const handlePressOut = async () => {
    if (longPressTimer) {
      clearTimeout(longPressTimer);
    }
    await video.current.playAsync();
    setVideoStop(false);
  };

  useEffect(() => {
    (async function () {
      const user = await getUserById(item.creator);
      const like = await checkLike(item.id);
      setUser(user);
      setIsLike(like);
    })();
  }, []);

  useEffect(() => {
    setLikesCount(item.likesCount);
  }, [item]);

  const HandleLike = async (id) => {
    if (likeLoading) {
      return;
    }
    setLikeLoading(true);
    if (isLike) {
      setIsLike(false);
      setLikesCount((prev) => prev - 1);
    } else {
      setIsLike(true);
      setLikesCount((prev) => prev + 1);
    }

    await LikePost(id);
    setLikeLoading(false);
  };

  const OpenComments = () => {
    console.log("opening...");
    // bottomSheetModalRef.current.open();
    setIsCommentModalVisible(!isCommentModalVisible);
  };
  const closeModal = () => {
    setIsCommentModalVisible(!isCommentModalVisible);
  };
  const handleProfile = () => {
    stop();
    navigation.navigate("profile", { uid: user.uid });
  };

  const handleMuteToggle = async () => {
    if (video.current) {
      video.current.setIsMutedAsync(!mute);
    }
    setMute((prev) => !prev);
    setShowPopup(true);
    if (timeoutref.current) {
      clearTimeout(timeoutref);
    }
    timeoutref.current = setTimeout(() => {
      setShowPopup(false);
    }, 500);
    console.log("muting...");
  };

  const handleMuteToggleLong = () => {
    setShowPopup(false);
    video.current.setIsMutedAsync(false);
  };

  const [loading, setLoading] = useState(false);

  const { followedUsers, setFollowedUsers } = props;
  const isFollowing = followedUsers.has(item.creator);

  useEffect(() => {
    (async function () {
      if (user) {
        const ans = await checkFollow(user.uid);
        if (ans) {
          followedUsers.add(user.uid);
        } else {
          followedUsers.delete(user.uid);
        }
        setFollowedUsers(new Set(followedUsers));
      }
    })();
  }, [user]);

  const handleFollow = async () => {
    if (loading) {
      console.log("loading...");
      return;
    }
    setLoading(true);
    const isCurrentlyFollowing = followedUsers.has(user.uid);
    await followUser(user.uid, isCurrentlyFollowing);
    if (isCurrentlyFollowing) {
      followedUsers.delete(user.uid);
    } else {
      followedUsers.add(user.uid);
    }
    setFollowedUsers(new Set(followedUsers));
    setLoading(false);
  };

  function isValidUrl(string) {
    try {
      new URL(string);
      return true;
    } catch (_) {
      return false;
    }
  }

  return (
    <>
      {isLoading && (
        <View
          style={{
            justifyContent: "center",
            alignItems: "center",
            backgroundColor: COLORS.primary,
            zIndex: 10,
            position: "absolute",
            bottom: 0,
            top: 0,
            right: 0,
            left: 0,
          }}
        >
          <ActivityIndicator size={79} color={COLORS.secondary} />
        </View>
      )}

      {showPopup && (
        <TouchableWithoutFeedback onPress={handleMuteToggle}>
          <View
            style={{
              justifyContent: "center",
              alignItems: "center",
              display: "flex",
              justifyContent: "center",
              alignItems: "center",
              zIndex: 10,
              position: "absolute",
              bottom: 0,
              top: 0,
              right: 0,
              left: 0,
            }}
          >
            <View style={tw`p-2 bg-black/50 rounded-full`}>
              <Feather
                name={mute ? "volume-x" : "volume-2"}
                size={30}
                color={"white"}
              />
            </View>
          </View>
        </TouchableWithoutFeedback>
      )}

      <TouchableWithoutFeedback
        onPressIn={handlePressIn}
        onPressOut={handlePressOut}
        onPress={handleMuteToggle}
        onLongPress={handleMuteToggleLong}
      >
        <View
          style={tw`absolute bottom-0 left-0 right-0 top-0 justify-end z-2`}
        >
          {!videoStop && (
            <>
              <View style={tw`py-2 px-4 flex gap-2 items-end`}>
                <View style={tw`flex gap-0 items-center`}>
                  <TouchableOpacity onPress={handleProfile}>
                    {user?.photoURL &&
                    isValidUrl(user?.photoURL) &&
                    !imageFailedToLoad ? (
                      <Image
                        source={{ uri: user?.photoURL }}
                        style={{
                          width: 35,
                          height: 35,
                          resizeMode: "contain",
                          marginBottom: 5,
                          borderRadius: 9999,
                          marginBottom: 8,
                        }}
                        onError={() => setImageFailedToLoad(true)}
                      />
                    ) : (
                      <Avatar.Icon
                        size={36}
                        backgroundColor={COLORS.secondary}
                        icon={"account"}
                      />
                    )}
                  </TouchableOpacity>
                </View>
                <View style={tw`flex gap-0 items-center`}>
                  <TouchableOpacity onPress={() => HandleLike(item.id)}>
                    <Image
                      source={
                        isLike
                          ? require("../../../assets/heartfill.png")
                          : require("../../../assets/heart.png")
                      }
                      style={{
                        width: 31,
                        resizeMode: "contain",
                        marginBottom: 5,
                      }}
                    />
                  </TouchableOpacity>
                  <Text style={tw`text-white text-sm font-montserrat`}>
                    {likesCount}
                  </Text>
                </View>
                <View style={tw`flex gap-0 items-center`}>
                  <TouchableOpacity onPress={OpenComments}>
                    <Image
                      source={require("../../../assets/comment.png")}
                      style={{
                        width: 34,
                        resizeMode: "contain",
                        marginBottom: 5,
                      }}
                    />
                  </TouchableOpacity>
                  <Text style={tw`text-white text-sm font-montserrat`}>
                    {item.commentsCount}
                  </Text>
                </View>
                <View style={tw`flex gap-0 items-center`}>
                  <Image
                    source={require("../../../assets/share.png")}
                    style={{
                      width: 34,
                      resizeMode: "contain",
                      marginBottom: 5,
                    }}
                  />
                  <Text style={tw`text-white text-sm font-montserrat`}>
                    Share
                  </Text>
                </View>
              </View>
              <View style={tw`px-4 flex-row justify-start items-center gap-3 `}>
                <Text style={tw`text-white text-lg font-montserrat`}>
                  @{user?.username}
                </Text>
                <TouchableOpacity
                  style={tw`py-1 px-2 rounded-md border border-white flex flex-row items-center gap-1 ${
                    isFollowing ? `bg-transparent text-white` : ""
                  }`}
                  onPress={handleFollow}
                >
                  <Text style={tw`text-white text-base font-montserrat `}>
                    {isFollowing ? "Unfollow" : "Follow"}
                  </Text>
                </TouchableOpacity>
              </View>
              <View style={tw`py-2 px-4`}>
                <FlatList
                  key={index}
                  data={item.hashtags}
                  renderItem={renderItem}
                  keyExtractor={(item, index) => item.key || String(index)}
                  horizontal={true} // Set horizontal to true
                />
              </View>
              <View
                style={tw`flex flex-row items-center gap-2 border-t border-b border-[${COLORS.secondary}] pl-4`}
              >
                <Text
                  style={tw`text-sm text-[${COLORS.secondary}] font-montserrat`}
                >
                  News
                </Text>
                <Text
                  style={tw`text-[10px] text-white flex-1 font-montserrat`}
                  numberOfLines={3}
                >
                  {item.description}
                </Text>
                <TouchableOpacity onPress={() => handleOpen(item.newslink)}>
                  <LinearGradient
                    colors={["#53C8D8", "#668AF7"]}
                    style={tw`py-2`}
                  >
                    <View style={tw`w-8 flex justify-center items-center`}>
                      <Feather
                        name={"chevron-right"}
                        size={30}
                        color={"white"}
                      />
                    </View>
                  </LinearGradient>
                </TouchableOpacity>
              </View>
            </>
          )}
          {isCommentModalVisible && (
            <CommentModel
              isVisible={isCommentModalVisible}
              onClose={closeModal}
              post={item}
            />
          )}
        </View>
      </TouchableWithoutFeedback>
      <View style={tw`flex-1 flex py-2 bg-black`}>
        <Video
          ref={video}
          style={styles.container}
          source={{
            uri: item.media[0],
          }}
          useNativeControls={false}
          resizeMode={ResizeMode.COVER}
          shouldPlay={false}
          isLooping
          usePoster
          onLoadStart={handleLoadStart}
          posterSource={{ uri: item.media[1] }}
          posterStyle={{ resizeMode: "cover", height: "100%" }}
          onReadyForDisplay={handleReadyForDisplay}
        />
      </View>
    </>
  );
});

export default PostSingle;

我不明白代码中的问题出在哪里。 这可能是由于我使用的绝对样式。 但这是一个非常奇怪的问题。 我已经尝试了边距和填充调整,但没有尝试其他任何调整。
注意:它仅在我滚动时发生,并且随着每次滚动而增加

  1. 我的预期产出
  2. 滚动时我得到的重叠输出
react-native react-redux expo tailwind-css mobile-development
1个回答
0
投票

您应该能够使用scrollToIndex,因为您通过getItemLayout获得了平面列表中每个项目的大小,您可以使用滚动到特定索引 scrollToIndex 语法如下

scrollToIndex: (params: {
  index: number;
  animated?: boolean;
  viewOffset?: number;
  viewPosition?: number;
});

您可以了解更多信息https://reactnative.dev/docs/flatlist#scrolltoindex

© www.soinside.com 2019 - 2024. All rights reserved.