Flutter扩展滚动功能插件flutter_extended_scroll的使用

Flutter扩展滚动功能插件flutter_extended_scroll的使用

flutter_extended_scroll package for flutter

make method ensureVisible support offsetTop

Getting Started

pubspec.yaml 文件中添加以下依赖:

dependencies:
  flutter_extended_scroll: ^3.2.2

Example

详细的例子可以在示例代码中查看。

在你的 Dart 文件中导入库:

import 'package:flutter_extended_scroll/flutter_extended_scroll.dart';

示例代码

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

  static const String name = "/detailPage";

  @override
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  late final ExtendedScrollController _scrollController;
  late final RefreshController _refreshController;

  bool isTabClicked = false;

  // 商品、评论、详情、同店好货锚点key
  final cardKeys = <GlobalKey>[
    GlobalKey(debugLabel: 'detail_card_0'),
    GlobalKey(debugLabel: 'detail_card_1'),
    GlobalKey(debugLabel: 'detail_card_2'),
    GlobalKey(debugLabel: 'detail_card_3')
  ];

  @override
  void initState() {
    _scrollController = ExtendedScrollController();
    _refreshController = RefreshController();
    super.initState();
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _refreshController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StoreBuilder<AppState>(
      onInit: (store) async {
        await store.dispatch(InitPageAction());
      },
      onDispose: (store) {
        store.dispatch(ChangeTopTabIndexAction(0));
      },
      builder: (context, store) {
        bool isLoading = store.state.detailPageState.isLoading;

        List<Widget> stackWidgets = [];
        if (isLoading) {
          stackWidgets.add(loadingWidget(context));
        } else {
          stackWidgets.add(NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification notification) {
              double distance = notification.metrics.pixels;
              store.dispatch(ChangePageScrollYAction(distance));

              if (isTabClicked) return false;
              int newIndex = findFirstVisibleItemIndex(cardKeys, context);
              store.dispatch(ChangeTopTabIndexAction(newIndex));
              return false;
            },
            child: Container(
              child: SmartRefresher(
                controller: _refreshController,
                enablePullUp: true,
                enablePullDown: false,
                onLoading: () async {
                  store.dispatch(LoadMoreAction(
                    store.state.detailPageState.pageNum + 1,
                    () => loadMoreSuccess(_refreshController),
                    () => loadMoreFail(_refreshController),
                  ));
                },
                child: ExtendedCustomScrollView(
                  controller: _scrollController,
                  slivers: [
                    goodsInfo(context, cardKeys[0]),
                    appraiseInfo(context, cardKeys[1]),
                    detailCard(context, cardKeys[2]),
                    storeGoodsHeader(context, cardKeys[3]),
                    storeGoods(context)
                  ],
                ),
              ),
            ),
          ));
        }

        stackWidgets.add(
          Positioned(
            top: 0,
            left: 0,
            child: tabHeader(
              context,
              onChange: (index) {
                isTabClicked = true;
                store.dispatch(ChangeTopTabIndexAction(index));
                scroll2PositionByTabIndex(index);
              },
            ),
          ),
        );

        return AnnotatedRegion<SystemUiOverlayStyle>(
          value: SystemUiOverlayStyle.dark,
          child: Column(
            children: [
              Expanded(
                flex: 1,
                child: Scaffold(
                  body: Stack(
                    children: stackWidgets,
                  ),
                  floatingActionButton: BackToTop(_scrollController),
                ),
              ),
              fixedBottom(context)
            ],
          ),
        );
      },
    );
  }

  void scroll2PositionByTabIndex(int index) {
    RenderSliverToBoxAdapter? keyRenderObject = cardKeys[index].currentContext?.findAncestorRenderObjectOfType<RenderSliverToBoxAdapter>();
    if (keyRenderObject != null) {
      _scrollController.position
          .ensureVisible(keyRenderObject, offsetTop: 42 + getStatusHeight(context), duration: const Duration(milliseconds: 300), curve: Curves.linear)
          .then((value) => isTabClicked = false);
    }
  }

  int findFirstVisibleItemIndex(List<GlobalKey<State<StatefulWidget>>> cardKeys, BuildContext context) {
    int i = 0;
    for (; i < cardKeys.length; i++) {
      RenderSliverToBoxAdapter? keyRenderObject = cardKeys[i].currentContext?.findAncestorRenderObjectOfType<RenderSliverToBoxAdapter>();
      if (keyRenderObject != null) {

        final dy = (keyRenderObject.parentData as SliverPhysicalParentData).paintOffset.dy;
        if (dy > 42 + getStatusHeight(context)) {
          break;
        }
      }
    }
    final newIndex = i == 0 ? 0 : i - 1;
    return newIndex;
  }
}

示例代码解释

初始化和释放资源

initState 方法中初始化 _scrollController_refreshController。在 dispose 方法中释放这些控制器。

@override
void initState() {
  _scrollController = ExtendedScrollController();
  _refreshController = RefreshController();
  super.initState();
}

@override
void dispose() {
  _scrollController.dispose();
  _refreshController.dispose();
  super.dispose();
}

构建UI

构建一个包含 ExtendedCustomScrollViewStack 布局,并添加了一些锚点键来跟踪滚动位置。

@override
Widget build(BuildContext context) {
  return StoreBuilder<AppState>(
    onInit: (store) async {
      await store.dispatch(InitPageAction());
    },
    onDispose: (store) {
      store.dispatch(ChangeTopTabIndexAction(0));
    },
    builder: (context, store) {
      bool isLoading = store.state.detailPageState.isLoading;

      List<Widget> stackWidgets = [];
      if (isLoading) {
        stackWidgets.add(loadingWidget(context));
      } else {
        stackWidgets.add(NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification) {
            double distance = notification.metrics.pixels;
            store.dispatch(ChangePageScrollYAction(distance));

            if (isTabClicked) return false;
            int newIndex = findFirstVisibleItemIndex(cardKeys, context);
            store.dispatch(ChangeTopTabIndexAction(newIndex));
            return false;
          },
          child: Container(
            child: SmartRefresher(
              controller: _refreshController,
              enablePullUp: true,
              enablePullDown: false,
              onLoading: () async {
                store.dispatch(LoadMoreAction(
                  store.state.detailPageState.pageNum + 1,
                  () => loadMoreSuccess(_refreshController),
                  () => loadMoreFail(_refreshController),
                ));
              },
              child: ExtendedCustomScrollView(
                controller: _scrollController,
                slivers: [
                  goodsInfo(context, cardKeys[0]),
                  appraiseInfo(context, cardKeys[1]),
                  detailCard(context, cardKeys[2]),
                  storeGoodsHeader(context, cardKeys[3]),
                  storeGoods(context)
                ],
              ),
            ),
          ),
        ));
      }

      stackWidgets.add(
        Positioned(
          top: 0,
          left: 0,
          child: tabHeader(
            context,
            onChange: (index) {
              isTabClicked = true;
              store.dispatch(ChangeTopTabIndexAction(index));
              scroll2PositionByTabIndex(index);
            },
          ),
        ),
      );

      return AnnotatedRegion<SystemUiOverlayStyle>(
        value: SystemUiOverlayStyle.dark,
        child: Column(
          children: [
            Expanded(
              flex: 1,
              child: Scaffold(
                body: Stack(
                  children: stackWidgets,
                ),
                floatingActionButton: BackToTop(_scrollController),
              ),
            ),
            fixedBottom(context)
          ],
        ),
      );
    },
  );
}

滚动到指定位置

scroll2PositionByTabIndex 方法用于根据给定的索引滚动到指定位置。

void scroll2PositionByTabIndex(int index) {
  RenderSliverToBoxAdapter? keyRenderObject = cardKeys[index].currentContext?.findAncestorRenderObjectOfType<RenderSliverToBoxAdapter>();
  if (keyRenderObject != null) {
    _scrollController.position
        .ensureVisible(keyRenderObject, offsetTop: 42 + getStatusHeight(context), duration: const Duration(milliseconds: 300), curve: Curves.linear)
        .then((value) => isTabClicked = false);
  }
}

查找第一个可见项的索引

findFirstVisibleItemIndex 方法用于查找第一个可见项的索引。

int findFirstVisibleItemIndex(List<GlobalKey<State<StatefulWidget>>> cardKeys, BuildContext context) {
  int i = 0;
  for (; i < cardKeys.length; i++) {
    RenderSliverToBoxAdapter? keyRenderObject = cardKeys[i].currentContext?.findAncestorRenderObjectOfType<RenderSliverToBoxAdapter>();
    if (keyRenderObject != null) {

      final dy = (keyRenderObject.parentData as SliverPhysicalParentData).paintOffset.dy;
      if (dy > 42 + getStatusHeight(context)) {
        break;
      }
    }
  }
  final newIndex = i == 0 ? 0 : i - 1;
  return newIndex;
}

更多关于Flutter扩展滚动功能插件flutter_extended_scroll的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter扩展滚动功能插件flutter_extended_scroll的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,flutter_extended_scroll 是一个用于扩展滚动功能的强大插件。它提供了一些额外的功能,比如滚动监听、自动滚动到指定位置等。以下是一个简单的示例,展示如何使用 flutter_extended_scroll 来实现一个带有滚动监听和自动滚动的页面。

首先,确保你已经在 pubspec.yaml 文件中添加了 flutter_extended_scroll 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_extended_scroll: ^x.y.z  # 请替换为最新版本号

然后,运行 flutter pub get 来安装依赖。

接下来,是示例代码:

import 'package:flutter/material.dart';
import 'package:flutter_extended_scroll/flutter_extended_scroll.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ScrollExample(),
    );
  }
}

class ScrollExample extends StatefulWidget {
  @override
  _ScrollExampleState createState() => _ScrollExampleState();
}

class _ScrollExampleState extends State<ScrollExample> with SingleTickerProviderStateMixin {
  late ScrollController _scrollController;
  late AnimationController _animationController;
  late Animation<Offset> _animation;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<Offset>(begin: Offset.zero, end: Offset.byPixels(0, 500)).animate(_animationController);

    _scrollController.addListener(() {
      // 监听滚动事件
      print('Current scroll position: ${_scrollController.position.pixels}');
    });

    // 自动滚动到指定位置
    Future.delayed(Duration(seconds: 1), () {
      _scrollController.animateTo(
        500.0, // 目标位置
        duration: Duration(seconds: 2),
        curve: Curves.easeInOut,
      );
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Extended Scroll Example'),
      ),
      body: ExtendedScrollView(
        controller: _scrollController,
        child: ListView.builder(
          itemCount: 100,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text('Item $index'),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 触发动画滚动
          _animationController.reset();
          _animationController.forward();
          _scrollController.animateTo(
            _animation.value.dy.toDouble(), // 使用动画值作为滚动目标
            duration: _animationController.duration,
            curve: Curves.easeInOut,
          );
        },
        tooltip: 'Animate Scroll',
        child: Icon(Icons.arrow_downward),
      ),
    );
  }
}

在这个示例中,我们做了以下事情:

  1. 添加依赖:在 pubspec.yaml 中添加 flutter_extended_scroll 依赖。
  2. 初始化控制器:在 initState 中初始化 ScrollControllerAnimationController
  3. 滚动监听:通过 _scrollController.addListener 监听滚动事件,并打印当前滚动位置。
  4. 自动滚动:使用 Future.delayed 在1秒后自动滚动到指定位置(500像素)。
  5. 动画滚动:通过点击浮动按钮触发动画滚动,将列表滚动到动画计算出的位置。

注意,ExtendedScrollViewflutter_extended_scroll 插件提供的一个包装组件,它允许你使用 ScrollController 进行更高级的滚动控制。如果插件提供了更多功能,你可以查阅其官方文档以获取更多信息和示例。

回到顶部