flutter下拉刷新上拉加载的简单实现方式三

时间:2024-11-16 07:58:02

        使用 `CustomScrollView` 结合 `SliverList` 实现了一个支持下拉刷新和上拉加载更多功能的滚动列表,对下面代码进行解析学习。

import 'dart:math';

import 'package:flutter/material.dart';

import 'custom_pull/gsy_refresh_sliver.dart';
import 'package:flutter/cupertino.dart' as IOS;

class NewMyRefreshDemoPage extends StatefulWidget {
  const NewMyRefreshDemoPage({super.key});

  @override
  State<StatefulWidget> createState() => _NewMyRefreshDemoState();
}

class _NewMyRefreshDemoState extends State<NewMyRefreshDemoPage> {
  final GlobalKey<CupertinoSliverRefreshControlState> sliverRefreshKey =
      GlobalKey<CupertinoSliverRefreshControlState>();

  final int pageSize = 30;

  bool disposed = false;

  List<String> dataList = [];

  final ScrollController _scrollController = ScrollController();

  Future<void> onRefresh() async {
    await Future.delayed(const Duration(seconds: 2));
    dataList.clear();
    for (int i = 0; i < pageSize; i++) {
      dataList.add("refresh");
    }
    if (disposed) {
      return;
    }
    setState(() {});
  }

  Future<void> loadMore() async {
    await Future.delayed(const Duration(seconds: 2));
    for (int i = 0; i < pageSize; i++) {
      dataList.add("loadmore");
    }
    if (disposed) {
      return;
    }
    setState(() {});
  }

  @override
  void didChangeDependencies() {
    Future.delayed(const Duration(milliseconds: 500), () {
      _scrollController.animateTo(-141,
          duration: const Duration(milliseconds: 600), curve: Curves.linear);
      return true;
    });
  }

  @override
  void dispose() {
    disposed = true;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("NewMyRefreshDemoPage"),
      ),
      body: NotificationListener(
        onNotification: (ScrollNotification notification) {
          //通知 CupertinoSliverRefreshControl 当前的拖拽状态
          sliverRefreshKey.currentState?.notifyScrollNotification(notification);
          if (notification is ScrollEndNotification) {
            if (_scrollController.position.pixels > 0 &&
                _scrollController.position.pixels ==
                    _scrollController.position.maxScrollExtent) {
              loadMore();
            }
          }
          return false;
        },
        child: CustomScrollView(
          controller: _scrollController,
          physics: const BouncingScrollPhysics(
              parent: AlwaysScrollableScrollPhysics()),
          slivers: <Widget>[
            CupertinoSliverRefreshControl(
              key: sliverRefreshKey,
              refreshIndicatorExtent: 100,
              refreshTriggerPullDistance: 140,
              onRefresh: onRefresh,
              builder: buildSimpleRefreshIndicator,
            ),
            //列表
            SliverSafeArea(
                sliver: SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  if (index == dataList.length) {
                    return Container(
                      margin: const EdgeInsets.all(10),
                      child: const Align(
                        child: CircularProgressIndicator(),
                      ),
                    );
                  }
                  return Card(
                    child: Container(
                      height: 60,
                      alignment: Alignment.centerLeft,
                      child: Text("Item ${dataList[index]} $index"),
                    ),
                  );
                },
                childCount: (dataList.length >= pageSize)
                    ? dataList.length + 1
                    : dataList.length,
              ),
            ))
          ],
        ),
      ),
    );
  }
}

Widget buildSimpleRefreshIndicator(
    BuildContext context,
    RefreshIndicatorMode? refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent) {
  const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut);
  return Align(
    alignment: Alignment.bottomCenter,
    child: Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: refreshState != RefreshIndicatorMode.refresh
          ? Opacity(
              opacity: opacityCurve.transform(
                  min(pulledExtent / refreshTriggerPullDistance, 1.0)),
              child: const Icon(
                IOS.CupertinoIcons.down_arrow,
                color: IOS.CupertinoColors.inactiveGray,
                size: 36.0,
              ),
            )
          : Opacity(
              opacity: opacityCurve
                  .transform(min(pulledExtent / refreshIndicatorExtent, 1.0)),
              child: const IOS.CupertinoActivityIndicator(radius: 14.0),
            ),
    ),
  );
}

相关代码解析

NewMyRefreshDemoPage

这是一个 `StatefulWidget`,表示可以维护状态的组件。核心功能是提供一个支持刷新和加载更多的列表页面。

_NewMyRefreshDemoState

`State` 类的子类,负责管理 `NewMyRefreshDemoPage` 的状态。包含了 `dataList` 用于存储列表数据,以及控制滚动和刷新状态的逻辑。

相关状态管理

  • `sliverRefreshKey`:一个 `GlobalKey`,用于访问 `CupertinoSliverRefreshControl` 的状态。
  • `pageSize`:每次刷新或加载的项目数。
  • `disposed`:用于标识状态是否已被销毁,避免在组件卸载后进行状态更新。
  • `dataList`:存储列表项的数据。
  • `_scrollController`:用于控制滚动行为的 `ScrollController`。

相关方法

  • `onRefresh`:模拟网络请求,清空并重新填充列表数据。
  • `loadMore`:模拟网络请求,向列表追加更多数据。
  • `didChangeDependencies`:在组件依赖变化时调用,初始化时会在短暂延时后滚动到一个指定位置以触发下拉刷新。
  • `dispose`:在组件卸载时调用,标记状态为已销毁。

UI 构建相关

`build` 方法

Scaffold:页面的基础结构,包含一个 `AppBar` 和 `body`。

NotificationListener:监听滚动事件,用于通知刷新控件当前的拖拽状态,以及检测是否到达滚动底部以加载更多。

CustomScrollView:允许在同一滚动视图中使用多个 sliver 组件。

  • CupertinoSliverRefreshControl:提供下拉刷新功能。
  • SliverSafeArea & SliverList:用于在安全区域内展示列表项。

`buildSimpleRefreshIndicator` 方法

  • `refreshState`:当前刷新状态。
  • `pulledExtent`:拖动距离。
  • `refreshTriggerPullDistance`:触发刷新的拖动距离。
  • `refreshIndicatorExtent`:指示器的最大显示高度。

GlobalKey

final GlobalKey<CupertinoSliverRefreshControlState> sliverRefreshKey =
      GlobalKey<CupertinoSliverRefreshControlState>();

用于标识 `CupertinoSliverRefreshControl` 的全局键,允许在其他地方访问其状态。在 Flutter 中,`GlobalKey` 是一个强大的工具,用于唯一地标识和访问某个特定的组件实例。通过使用 `GlobalKey`,你可以在树的其他地方访问这个组件的状态或执行某些操作。

GlobalKey 是一个特殊的键,可以在 widget 树中唯一标识一个 widget。它允许访问与之关联的 widget 状态(`State` 对象),这在需要跨越 widget 树访问状态时非常有用。

CupertinoSliverRefreshControlState

这是 `CupertinoSliverRefreshControl` 的状态类。使用 `GlobalKey` 可以直接访问这个状态,进行一些状态更新或调用状态中的方法。

使用场景:

  • 访问刷新控件的状态:通过 `sliverRefreshKey.currentState`,你可以访问 `CupertinoSliverRefreshControl` 的状态对象。
  • 通知滚动状态:在 `NotificationListener` 中,使用 `sliverRefreshKey.currentState?.notifyScrollNotification(notification)` 来通知刷新控件当前的滚动状态。

CupertinoSliverRefreshControl

`CupertinoSliverRefreshControl` 是 Flutter 中一个用于实现 iOS 风格下拉刷新效果的组件。它通常用于 `CustomScrollView` 中,以提供流畅的下拉刷新体验,类似于 iOS 应用的原生行为。

主要特性

  • iOS 风格:以 iOS 的外观和行为为设计基础,提供原生般的用户体验。
  • 与 Sliver 兼容:设计用于与 `CustomScrollView` 和 sliver 系统一起使用,适合实现复杂的滚动布局。
  • 自动处理刷新逻辑:通过提供 `onRefresh` 回调,轻松实现数据刷新逻辑。

主要属性

refreshTriggerPullDistance

定义触发刷新操作所需的下拉距离(默认值为 100.0)。用户必须拉动至少此距离才能触发刷新。

refreshIndicatorExtent

刷新指示器的最大高度(默认值为 60.0)。指示器在用户继续下拉时可以扩展到的最大高度。

builder

自定义刷新指示器的外观。默认情况下,使用内置的 iOS 风格指示器,你可以提供一个自定义构建函数来改变其外观。

onRefresh

异步回调函数,当用户触发刷新操作时调用。通常用于执行异步操作(如网络请求)来更新数据。

注意

  • `CustomScrollView`:用于包裹 `CupertinoSliverRefreshControl` 和 `SliverList`,使得刷新控件可以与列表一起滚动。
  • `CupertinoSliverRefreshControl`:提供下拉刷新功能,`onRefresh` 回调用于定义刷新时的逻辑(如更新数据)。
  • 刷新逻辑:在 `_handleRefresh` 中模拟了一个网络请求,通过 `Future.delayed` 来延迟刷新操作。

buildSimpleRefreshIndicator

`buildSimpleRefreshIndicator` 是一个用于自定义刷新指示器外观的函数。它通常在实现下拉刷新功能时与 `CupertinoSliverRefreshControl` 一起使用。这个函数根据用户下拉的距离和当前的刷新状态来动态调整指示器的外观。

BuildContext context

Flutter 的上下文对象,用于获取主题、媒体查询等信息。在这个函数中没有被直接使用。

RefreshIndicatorMode? refreshState

刷新指示器的当前状态。可能的值包括:

  • `inactive`:未触发刷新。
  • `drag`:用户正在拖动,但未达到刷新阈值。
  • `armed`:用户拖动超过阈值,松手后将触发刷新。
  • `refresh`:正在刷新中。
  • `done`:刷新完成。

该函数使用此状态来决定显示箭头图标还是加载动画。

double pulledExtent

用户下拉的距离(以像素为单位)。

double refreshTriggerPullDistance

触发刷新的下拉距离阈值。

double refreshIndicatorExtent

刷新指示器的最大显示高度。

Curve

const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut)

使用 `Interval` 定义一个曲线,用于控制不透明度变化的动画效果。`Interval(0.4, 0.8, curve: Curves.easeInOut)` 表示在 40% 到 80% 的时间内应用 `easeInOut` 曲线。

刷新指示器的作用

使用条件表达式根据 `refreshState` 的值显示不同的组件:

  • 如果状态不是 `RefreshIndicatorMode.refresh`,则显示一个箭头图标,且图标的透明度根据 `pulledExtent` 与 `refreshTriggerPullDistance` 的比例动态调整。
  • 如果状态是 `RefreshIndicatorMode.refresh`,则显示一个 `CupertinoActivityIndicator` 加载动画,且透明度根据 `pulledExtent` 与 `refreshIndicatorExtent` 的比例调整。

如何工作

  • 当用户开始下拉时,指示器的箭头图标会根据下拉距离逐渐显现。
  • 当用户的下拉距离超过 `refreshTriggerPullDistance` 并松手后,指示器状态变为 `refresh`,显示加载动画。
  • 加载动画的透明度也会随着用户下拉距离的变化而变化,提供更好的视觉反馈。