image.png

在这篇文章中,我们将探讨如何在 React Native 应用程序中使用 FlatList 组件实现下拉刷新和滚动刷新功能。作为额外内容,我们将研究如何通过改变不同的参数,如大小和颜色,来自定义 RefreshControl 组件。

在本文的教程部分,我们将构建一个像这样刷新的React Native应用程序:

项目设置

要跟随本教程,你可以参考这个GitHub仓库;它包含了本文中使用的所有代码。然而,我也建议你按照这个指南生成一个全新的Expo React Native应用程序。

一旦项目生成,我们需要创建一些文件夹。以下是文件夹结构的样子:

// Folder structure

app_name
 ┣ assets
 ┃ ┣ adaptive-icon.png
 ┃ ┣ favicon.png
 ┃ ┣ icon.png
 ┃ ┗ splash.png
 ┣ components
 ┃ ┣ Item.jsx
 ┃ ┗ Loader.jsx
 ┣ hooks
 ┃ ┗ useFetchUser.js
 ┣ screens
 ┃ ┣ BottomLoader.jsx
 ┃ ┣ Combined.jsx
 ┃ ┗ TopLoader.jsx
 ┣ .gitignore
 ┣ App.js
 ┣ app.json
 ┣ babel.config.js
 ┣ package-lock.json
 ┗ package.json

创建自定义钩子

React Hook是一种特殊的函数,它让我们无需编写类或组件就能管理状态。我们将设置一个自定义Hook,用于向服务器发起API调用以获取数据,然后将其存储在状态中,供我们的组件使用。

首先,在终端运行以下命令安装 Axios:

npm i axios

然后打开 hooks/useFetchUser.js 文件,导入必要的依赖项,并声明一些常量:

// JavaScript
// useFetchUser.js

import React, { useState } from 'react';
import axios from 'axios';

export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;

接下来,让我们创建 useFetchUser 钩子并声明一些状态:

// JavaScript
// useFetchUser.js

export const useFetchUser = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [success, setSuccess] = useState(false);

// Code comes below here

// Code comes above here

  return { users, isLoading, success, getUsers, errorMessage };
};

现在,我们将创建一个函数,使我们能够进行API调用并根据响应更新状态。在这个例子中,我们将使用来自 https://randomuser.me/api 的分页API。

// JavaScript
// useFetchUser.js

 const getUsers = async (currentPage, isTop = false) => {
    setIsLoading(true);
    setSuccess(false);
    setErrorMessage('');
    try {
      const { data } = await axios.get(
        `https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
      );
      if (isTop) {
        const newList = [...data?.results, ...users];
        const slicedUsers = [...newList].slice(0, MAX_LENGTH);
        setUsers(slicedUsers);
      }
      if (!isTop) {
        const randomIndex = () => Math.ceil(Math.random() * 10);
        const newList = [...users, ...data?.results];
        const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
        setUsers(slicedUsers);
      }
      setSuccess(true);
    } catch (error) {
      const theError =
        error?.response && error.response?.data?.message
          ? error?.response?.data?.message
          : error?.message;
      setErrorMessage(theError);
    }
    setIsLoading(false);
  };

综合起来,代码应该看起来类似于这样:

// JavaScript
// useFetchUser.js

import React, { useState } from 'react';
import axios from 'axios';
export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;

export const useFetchUser = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [success, setSuccess] = useState(false);

  const getUsers = async (currentPage, isTop = false) => {
    setIsLoading(true);
    setSuccess(false);
    setErrorMessage('');
    try {
      const { data } = await axios.get(
        `https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
      );
      if (isTop) {
        const newList = [...data?.results, ...users];
        const slicedUsers = [...newList].slice(0, MAX_LENGTH);
        setUsers(slicedUsers);
      }
      if (!isTop) {
        const randomIndex = () => Math.ceil(Math.random() * 10);
        const newList = [...users, ...data?.results];
        const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
        setUsers(slicedUsers);
      }
      setSuccess(true);
    } catch (error) {
      const theError =
        error?.response && error.response?.data?.message
          ? error?.response?.data?.message
          : error?.message;
      setErrorMessage(theError);
    }
    setIsLoading(false);
  };

  return { users, isLoading, success, getUsers, errorMessage };
};

以下是一些详细信息,以便更好地理解上述代码中发生的情况。 getUsers 函数是异步的,并接受 currentPageisTop 作为参数。 currentPage 参数按照 https://randomuser.me 的指示传递给 URL,以获取分页项目。 isTop 参数用于确定用户是从顶部刷新内容还是从底部刷新内容。

我们将加载、成功和错误消息的状态分别设置为 truefalse 和一个空字符串。接下来,我们创建一个 try-catch 块。在 try 部分,我们使用 axios 进行 API 调用,并将 FETCH_RESULTS 传递给 URL。然后,我们期望从响应返回的数据长度等于 FETCH_RESULTS 的值。换句话说,如果 FETCH_RESULTS 的值是 10,那么 API 将返回 10 个结果。

在第一个 if 语句中,我们检查 isTop 是否为 true 。在代码块内部,我们创建了一个列表,该列表会在现有用户数组前添加,并创建了第二个列表,该列表会切割我们的 newList ,以确保我们在实际更新用户列表之前处于所需的 MAX_LENGTH 数据范围内。

在第二个 if 语句中,我们创建了一个内联函数来生成一个随机数。接下来,我们将新接收的数据添加到现有的用户列表中,然后切割我们正在创建的列表,以便始终获得相对随机数量的列表结果。这确保用户在从列表底部滚动时不会出现任何闪烁或卡顿。

catch 块中,我们提取任何 error ,然后将其设置为错误消息状态;在 try-catch 块外,我们将加载状态设置为 false 。最后,我们返回 isLoadingsuccessuserserrorMessagegetUsers ,以便它们可以在应用程序的任何地方调用 useFetchUser Hook 时使用。

设置加载器组件

加载器组件将用于指示 API 获取正在进行中。要构建加载器,我们需要导入必要的依赖项,然后传递 isLoadingwithText 参数。最重要的依赖项是 ActivityIndicator ,它处理实际的加载指示器。

这是我们加载器组件的代码:

// JavaScript
// components/Loader.jsx

import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';

export const Loader = ({ isLoading = false, withText = false }) => {
  return isLoading ? (
    <View style={styles.loader}>
      <ActivityIndicator size='large' color='#aaaaaa' />
      {withText ? (
        <Text style={{ color: 'green' }}>Loading users...</Text>
      ) : null}
    </View>
  ) : null;
};
const styles = StyleSheet.create({
  loader: {
    marginVertical: 15,
    alignItems: 'center',
  },
});

为了设计加载器,我们只是简单地添加了垂直边距并将组件居中。

构建项目组件

Item.jsx 组件用于表示我们将构建的 FlatList 组件的渲染项。 Item 函数接受 itemindex 参数,并返回一个带有 Image 和第二个 View 组件的 View 。

第二个 View 组件返回两个 Text 组件,我们用它们来显示来自 item 参数的 nameemail 。 Image 组件也从 item 参数中显示一个大图片。

这是我们的 Item 组件的代码:

// JavaScript
// components/Item.jsx
import { StyleSheet, View, Text, Image } from 'react-native';

export const Item = ({ item, index }) => {
  return (
    <View style={styles.itemWrapper}>
      <Image source={{ uri: item?.picture?.large }} style={styles.itemImage} />
      <View style={styles.itemContentWrapper}>
        <Text
          style={styles.itemName}
        >{`${item?.name?.title} ${item?.name?.first} ${item?.name?.last}`}</Text>
        <Text style={styles.itemEmail}>{`${item?.email}`}</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  itemWrapper: {
    flexDirection: 'row',
    paddingHorizontal: 16,
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderColor: '#dddddd',
  },
  itemImage: {
    width: 51,
    height: 51,
    marginRight: 15,
  },
  itemContentWrapper: {
    justifyContent: 'space-around',
  },
  itemName: {
    fontSize: 17,
  },
  itemEmail: {
    color: '#777777',
  },
});

设置下拉刷新功能

我们将创建一个 TopLoader.jsx 屏幕,向用户演示如何在列表顶部处理下拉刷新。作为构建此屏幕的第一步,我们导入必要的依赖项,包括 FETCH_RESULTSuseFetchUser.jsItem.jsxLoader.jsx 文件:

// JavaScript
// screens/TopLoader.jsx

import { StyleSheet, Text, View, FlatList, RefreshControl } from 'react-native';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';

接下来,我们创建组件并初始化状态:

// JavaScript
// screens/TopLoader.jsx

const TopLoader = () => {
  const flatListRef = useRef(null);
  const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);

  return (
    <View>

    </View>
  );
};
export default memo(TopLoader);

在这里,我们定义了一个 flatListRef ,用于控制自动滚动。我们还调用了 useFetchUser 钩子,并从中解构出以下数据: isLoadingsuccessuserserrorMessagegetUsers

接下来,我们定义状态来处理当前页面,我们将其传递给 getUsers 函数,还有 refreshing 状态,它将控制下拉刷新加载器的可见性。

我们创建一个函数来增加当前页面的状态,另一个函数用来更新 refreshing 的状态:

// JavaScript
// screens/TopLoader.jsx

  const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    loadMoreItem();
  }, []);

在下面的代码中,我们创建了一个 scrollToItem 函数,它作为我们 FlatList 的重要补充。这个函数会在我们的数据发生变化时自动处理滚动:

// JavaScript
// screens/TopLoader.jsx

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

scrollToItem 函数非常重要,因为当在 FlatList 的开始处添加新数据时,React Native 会尝试显示初始的项目集。这种行为可能会让用户感到困惑,因为他们自然会期望从刷新内容的地方继续滚动。

现在,我们只需检查 API 请求是否成功,然后将 refreshing 的状态设置为 true 。我们还调用 scrollToItem 函数,并将 FETCH_RESULTS - 1 作为 index 参数传递:

// JavaScript
// screens/TopLoader.jsx

  useEffect(() => {
    if (success) {
      setRefreshing(false);
      scrollToItem(FETCH_RESULTS - 1);
    }
  }, [success]);

我们也可以添加代码来处理初始页面加载时以及当前页面更改时的 API 调用:

// JavaScript
// screens/TopLoader.jsx

  useEffect(() => {
    getUsers(currentPage, true);
  }, [currentPage]);

在使用 useEffect Hook 时,你可能已经注意到,我们在一个被称为依赖项的数组中添加了额外的值。这些依赖项在 useEffect Hook 的操作中起着至关重要的作用。

如果我们省略依赖数组,我们的代码将在每次重新渲染时执行,如果我们提供一个空的数组作为依赖, useEffect 钩子将只运行一次。然而,当我们在依赖数组中指定特定的值时,钩子将在初始渲染和依赖值改变时触发。

现在是时候添加我们的 jsx 标记了。首先添加以下框架:

// JavaScript
// screens/TopLoader.jsx

    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Pull To Refresh Control
      </Text>
      <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
      />
      {errorMessage ? <Text>{errorMessage}</Text> : null}
    </View>

在这个标记中,我们只有一个 Text 组件,一个 FlatList ,以及第二个 Text 组件,用于渲染可能从 useFetchuser 钩子传递过来的任何错误信息。

我们的 FlatList 组件接受一个我们之前在上面定义的 ref 。它还接受 data ,以 useFetchUser Hook 中的 users 的形式,一个是 renderItem ,这是 Item 组件,然后是 keyExtractor ,这是每个 user 的电子邮件。

为了衡量我们的进步,我们需要在屏幕上显示我们的 TopLoader 。为此,请在 App.js 文件中添加以下代码:

// JavaScript
// App.js

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import TopLoader from './screens/TopLoader';

const App = () => {
  return (
    <View style={styles.container}>
      <TopLoader />
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

现在我们可以打开终端并运行 npm run start 命令来启动服务器,然后分别按 ai 在 Android 或 iOS 上运行应用。

接下来,让我们回到 TopLoader.jsx 组件,并在 FlatList 组件中添加 refreshControl 属性:

// JavaScript
// screens/TopLoader.jsx

      <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
      />

RefreshControl 组件由 React Native 团队提供,用于处理在 FlatListScrollViewListView 顶部的下拉刷新。这个组件接受几个属性,但只有 refreshing 属性是必需的才能使它工作。因此,我们需要将上面定义的 refreshing 状态传递给这个组件。

下一个属性是 onRefresh ,它在刷新开始时被调用。对于这个项目,我们将传递上面创建的 onRefresh 函数,这样当我们刷新时, onRefresh 回调将触发 useEffect 钩子中的 getUsers 函数运行。

// JavaScript
// screens/TopLoader.jsx

 <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />

我们的下拉刷新功能现在应该可以无缝工作了,但我们可以通过添加三个额外的属性: initialScrollIndexmaxToRenderPerBatchListEmptyComponent 来进一步改进它:

 <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />

注意, ListEmptyComponent 用于在列表为空时渲染任何组件;在我们的情况下,我们提供了一个 Loader 组件。

自定义刷新控制

你可以通过提供某些属性来自定义刷新控制器。这些属性允许你修改由组件提供的加载器的背景颜色、文字颜色和其他方面。

你可以在这里找到可以传递给刷新控制组件的属性列表,但需要记住的是,某些属性只在 Android 上有效,而其他一些只在 iOS 上工作。例如, colors 属性接受一个颜色列表,只在 Android 上工作,而 tintColor 属性只在 iOS 上工作。

要为所有使用场景的刷新控制提供自定义颜色,你需要组合使用这两个属性:

// JavaScript
// screens/TopLoader.jsx

 <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            tintColor='red'
            colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
          />

以下展示的是我们应用程序的顶部加载屏幕,具有下拉刷新功能和新的自定义刷新控制:

设置滚动刷新功能

滚动刷新的概念是,当用户到达列表底部并继续向下拖动列表时,它将通过获取下一组结果自动更新。为了实现这个功能,我们在 FlatList 组件中使用了两个特定的属性: onEndReachedonEndReachedThreshold

onEndReached 属性在用户滚动到距离列表末尾一定距离时触发,这个距离由 onEndReachedThreshold 属性定义。这个阈值决定了列表底部边缘距离内容末尾的可见距离有多远。当阈值设置为零时,用户滚动到列表的最底部, onEndReached 回调将立即被激活。

为了演示,让我们打开 components/BottomLoader.jsx 文件并添加以下代码:

// JavaScript
// screens/BottomLoader.jsx

import { FlatList, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';


const BottomLoader = () => {
  const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
  const [currentPage, setCurrentPage] = useState(1);

  const loadMoreItem = (e) => {
    setCurrentPage((prev) => prev + 1);
  };

  useEffect(() => {
    getUsers(currentPage);
  }, [currentPage]);

  return (
    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Infinite Bottom Loader
      </Text>
      <FlatList
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
      />
      {errorMessage ? <Text>{errorMessage}</Text> : null}
    </View>
  );
};
export default BottomLoader;

在上述代码中,我们导入了所需的依赖项并初始化了 useFetchUser Hook 以获取数据。然后我们建立了 current page 状态,并创建了一个名为 loadMoreItem 的函数,该函数增加了 currentPage 状态。

useEffect 钩子观察当前页面状态的变化,并触发 getUsers 回调。我们还包含了与我们为 TopLoader 提供的类似的标记。

要实现滚动刷新功能,我们只需用以下代码更新我们的 FlatList 组件:

// JavaScript
// screens/BottomLoader.jsx

     <FlatList
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        ListFooterComponent={<Loader isLoading={isLoading} />}
        onEndReached={loadMoreItem}
        onEndReachedThreshold={0}
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
      />

注意,在上述代码中, ListFooterComponent 用于表示 API 请求的加载状态,但可以是选择的任何自定义组件。

为了测试我们所构建的内容,更新 App.js 文件,如下所示:

// JavaScript
// App.js

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import BottomLoader from './screens/BottomLoader';

const App = () => {
  return (
    <View style={styles.container}>
      <BottomLoader />
    </View>
  );
};
export default App;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

这是我们的应用程序,展示了滚动刷新功能的实际效果:

结合刷新功能

接下来,我们将把下拉刷新功能和滚动刷新功能整合到一个 FlatList 组件中。

首先,打开 screens/Combined.jsx 文件并导入所需的依赖项和组件:

// JavaScript
// screens/Combined.jsx

import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';

接下来,我们创建我们的组件,并在两个地方调用 useFetchUser Hook,用于顶部和底部的加载器,如下所示:

// JavaScript
// screens/Combined.jsx

const Combined = () => { 
const flatListRef = useRef(null);

  const {
    isLoading: isLoadingTop,
    success: successTop,
    users: usersTop,
    errorMessage: errorMessageTop,
    getUsers: getUsersTop,
  } = useFetchUser();
  const {
    isLoading: isLoadingBottom,
    success: successBottom,
    users: usersBottom,
    errorMessage: errorMessageBottom,
    getUsers: getUsersBottom,
  } = useFetchUser();

return (
  <View>

  </View>
)

export default Combined;

在 JavaScript 中,我们可以通过在值后面加一个冒号来重命名一个解构的值:

const { isLoading } = useFetchUser();

// isLoading can be renamed to isLoadingTop
const { isLoading: isLoadingTop } = useFetchUser();

现在,我们定义 combinedUserscurrentPagerefreshingisTop 状态:

// JavaScript
// screens/Combined.jsx

  const [combinedUsers, setConfirmedUsers] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);
  const [isTop, setIsTop] = useState(false);

然后我们定义 loadMoreItem 函数:

// JavaScript
// screens/Combined.jsx  
const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

接下来,我们定义 onRefresh 回调和 scrollToItem 函数:

// JavaScript
// screens/Combined.jsx  

const onRefresh = useCallback(() => {
    setRefreshing(true);
    setIsTop(true);
    loadMoreItem();
  }, []);

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

然后,我们定义 useEffect 钩子来获取数据:

// JavaScript
// screens/Combined.jsx  

  useEffect(() => {
    if (isTop) {
      getUsersTop(currentPage, isTop);
    } else {
      getUsersBottom(currentPage, isTop);
    }
  }, [currentPage]);

在上述代码中,我们检查 isTop 的值何时发生变化,以确定每次 currentPage 状态改变时调用的函数。

然后,我们为下拉刷新和滚动到底部的 API 请求成功时定义动作:

// JavaScript
// screens/Combined.jsx  

useEffect(() => {
    if (successTop) {
      setRefreshing(false);
      setConfirmedUsers(usersTop);
      if (combinedUsers.length > 0) {
        scrollToItem(FETCH_RESULTS - 1);
      }
    }
  }, [successTop]);

  useEffect(() => {
    if (successBottom) {
      setConfirmedUsers(usersBottom);
    }
  }, [successBottom]);

接下来,我们像这样组合标记:

// JavaScript
// screens/Combined.jsx    

  <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Combined Bidirectional FlatList
      </Text>
      {errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
      <FlatList
        ref={flatListRef}
        data={combinedUsers}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            // tintColor='red'
            // colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
            // tintColor='transparent'
            // colors={['transparent']}
            // style={{ backgroundColor: 'transparent' }}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        // ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
        ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
        onEndReached={() => {
          loadMoreItem();
          setIsTop(false);
        }}
        onEndReachedThreshold={0}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />
      {errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
    </View>

我们的组件应该看起来像这样:

// JavaScript
// screens/Combined.jsx 

import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
const Combined = () => {
  const flatListRef = useRef(null);
  const {
    isLoading: isLoadingTop,
    success: successTop,
    users: usersTop,
    errorMessage: errorMessageTop,
    getUsers: getUsersTop,
  } = useFetchUser();
  const {
    isLoading: isLoadingBottom,
    success: successBottom,
    users: usersBottom,
    errorMessage: errorMessageBottom,
    getUsers: getUsersBottom,
  } = useFetchUser();

  const [combinedUsers, setConfirmedUsers] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);
  const [isTop, setIsTop] = useState(false);

  const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    setIsTop(true);
    loadMoreItem();
  }, []);

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

  useEffect(() => {
    if (isTop) {
      getUsersTop(currentPage, isTop);
    } else {
      getUsersBottom(currentPage, isTop);
    }
  }, [currentPage]);

  useEffect(() => {
    if (successTop) {
      setRefreshing(false);
      setConfirmedUsers(usersTop);
      if (combinedUsers.length > 0) {
        scrollToItem(FETCH_RESULTS - 1);
      }
    }
  }, [successTop]);

  useEffect(() => {
    if (successBottom) {
      setConfirmedUsers(usersBottom);
    }
  }, [successBottom]);

  return (
    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Combined Bidirectional FlatList
      </Text>
      {errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
      <FlatList
        ref={flatListRef}
        data={combinedUsers}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            // tintColor='red'
            // colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
            // tintColor='transparent'
            // colors={['transparent']}
            // style={{ backgroundColor: 'transparent' }}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        // ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
        ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
        onEndReached={() => {
          loadMoreItem();
          setIsTop(false);
        }}
        onEndReachedThreshold={0}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />
      {errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
    </View>
  );
};
export default Combined;
const styles = StyleSheet.create({});

要查看我们构建的内容,请更新 App.js 文件,如下所示:

// JavaScript
// App.js  

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Combined from './screens/Combined';

const App = () => {
  return (
    <View style={styles.container}>
      <Combined />
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

我们的最终应用程序应该像这样运行:

总结

在这篇文章中,我们研究了如何在 React Native 项目中实现下拉刷新和滚动刷新,以及如何将这些功能结合起来。我们还讨论了如何定制刷新控制组件。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝