Skip to content

样式与文案 ​

Classic Header ​

tsx
<SmartRefreshLayout
  headerStyle="classic"
  primaryColor="#0f766e"
  indicatorColor="#ffffff"
  titleColor="#ffffff"
  classicSpinnerStyle="fixed-behind"
  classicEnableLastTime
  onRefresh={reload}
  onLoadMore={loadNextPage}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

classicSpinnerStyle 支持 scale、translate 与 fixed-behind。Android 与 iOS 都支持这些 核心样式。Android 上运行时切换 Spinner 会重建 Header;若请求正在进行,会等待空闲后应用。

Material Header ​

tsx
<SmartRefreshLayout
  headerStyle="material"
  primaryColor="#e8573d"
  indicatorColor="#ffffff"
  materialShowBezierWave
  materialEnableHeaderTranslationContent={false}
  materialProgressBackgroundColor="#e8573d"
  onRefresh={reload}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

materialShowBezierWave 与 materialEnableHeaderTranslationContent 是 Android 官方 Header 的 细节开关,iOS 会安全忽略。两端的 Material Header 都支持 indicatorColor 与 materialProgressBackgroundColor;iOS 的 primaryColor 仅用于 Classic Header/Footer。

自定义 Lottie Header ​

refreshHeader 会将内容挂载进原生 Header 槽位,并覆盖 Classic/Material Header。默认高度为 80 逻辑 像素,可用 refreshHeaderHeight 调整。下例让 Lottie 在 pulling 和 ready 阶段由 progress 跟随下拉 距离;不要用 isDragging 过滤回调,否则松手取消时的回弹不会把动画带回 0。进入 refreshing 后移除 受控 progress,改用 play() 和 loop;完成后在 idle 阶段 pause()、reset()。

tsx
import { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, Text, View } from 'react-native';
import LottieView from 'lottie-react-native';
import {
  SmartRefreshLayout,
  type HeaderMovingEvent,
} from 'expo-smartrefreshlayout';

const rows = ['Inbox', 'Mentions', 'Saved'];
const wait = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

export function LottieRefreshList() {
  const lottieRef = useRef<LottieView>(null);
  const refreshingRef = useRef(false);
  const [refreshing, setRefreshing] = useState(false);
  const [headerProgress, setHeaderProgress] = useState(0);
  const [headerOffset, setHeaderOffset] = useState(0);

  refreshingRef.current = refreshing;

  useEffect(() => {
    const animation = lottieRef.current;
    if (refreshing) {
      animation?.play();
      return;
    }

    animation?.pause();
    animation?.reset();
  }, [refreshing]);

  const handleHeaderMoving = useCallback(({ percent, offset }: HeaderMovingEvent) => {
    // 保留回弹事件,让取消下拉时动画回到首帧。
    if (refreshingRef.current) {
      return;
    }

    setHeaderProgress(Math.min(Math.max(percent, 0), 1));
    setHeaderOffset(Math.max(0, Math.round(offset)));
  }, []);

  const refresh = useCallback(async () => {
    refreshingRef.current = true;
    setRefreshing(true);

    try {
      await wait(900);
    } finally {
      refreshingRef.current = false;
      setRefreshing(false);
      setHeaderProgress(0);
      setHeaderOffset(0);
    }
  }, []);

  const handleAnimationLoaded = useCallback(() => {
    if (refreshingRef.current) {
      lottieRef.current?.play();
    }
  }, []);

  return (
    <SmartRefreshLayout
      style={{ flex: 1 }}
      loadMoreEnabled={false}
      refreshHeaderHeight={80}
      refreshHeaderSpinnerStyle="translate"
      refreshHeaderTriggerRate={1}
      refreshHeaderMaxDragRate={2}
      refreshHeaderFinishDuration={250}
      refreshHeader={
        <View
          pointerEvents="none"
          style={{ alignItems: 'center', height: 80, justifyContent: 'center' }}
        >
          <LottieView
            ref={lottieRef}
            source={require('../assets/load.json')}
            progress={refreshing ? undefined : headerProgress}
            loop
            style={{ height: 48, width: 48 }}
            onAnimationLoaded={handleAnimationLoaded}
          />
          <Text>
            {refreshing
              ? '正在刷新...'
              : headerProgress >= 1
                ? '松开刷新'
                : `下拉 ${headerOffset} px`}
          </Text>
        </View>
      }
      onHeaderMoving={handleHeaderMoving}
      onRefresh={refresh}
    >
      <FlatList
        data={rows}
        keyExtractor={(item) => item}
        renderItem={({ item }) => <Text style={{ padding: 24 }}>{item}</Text>}
      />
    </SmartRefreshLayout>
  );
}

onHeaderMoving 的 offset、height、maxDragHeight 都是逻辑像素(Android dp / iOS pt), percent >= 1 表示达到刷新阈值。refreshHeaderSpinnerStyle 支持 scale、translate、 fixed-behind;refreshHeaderTriggerRate 有效范围为 (0, 1],refreshHeaderMaxDragRate 有效范围为 [1, 9]。9 是跨端上限,避免 Android 内核将 >= 10 解释为物理像素高度。完成态需要停留或播放 原生完成动画时,使用 refreshHeaderFinishDuration(0..60000 毫秒);它不同于 finishRefresh({ delay }),后者只延迟发起完成命令。

仅自定义 Header 会提供生命周期事件:onHeaderInitialized 在原生尺寸初始化后触发, onHeaderReleased 在用户释放并开始回弹/释放动画时触发,onHeaderStart 在真正进入刷新动画时触发, 三者的 payload 都是 { height, maxDragHeight }。onHeaderFinish 在原生进入完成态时触发,payload 是 { success };其结果来自原生完成回调,不是从 onRefresh Promise 推断。

自定义文案 ​

tsx
<SmartRefreshLayout
  messages={{
    pullDown: '下拉刷新',
    releaseToRefresh: '松开刷新',
    refreshing: '正在刷新...',
    refreshComplete: '刷新完成',
    pullUp: '上拉加载更多',
    releaseToLoadMore: '松开加载',
    loadingMore: '正在加载...',
    noMoreData: '没有更多数据',
  }}
  onRefresh={reload}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

messages 接受部分字段,未传项继续使用内置英文默认值。建议产品在应用层集中提供本地化文案。

触觉反馈 ​

hapticsEnabled 默认开启,在拖拽达到释放阈值时触发一次反馈。若应用已经在相同手势节点提供反馈, 可传 false 避免重复。

Released under the MIT License.