【第二六天 - Flutter 知名外送平台画面练习(中)】

前言

接续上一篇 【第二五天 - Flutter 知名外送平台画面练习(上)】~~。

今日的程序码 => GITHUB

我们建立好 FappBar 後。再 HomePage 来使用它。这里将会介绍用到的套件。和整个 TabBarController 和 ScrollController 的互动方式

设定 Yaml 档案

  scroll_to_index: ^2.0.0
  rect_getter: ^1.0.0

套件一(scroll_to_index)

更多资讯请参考 => 官方文件

  1. 宣告 AutoScrollController
AutoScrollController scrollController = AutoScrollController();
  1. AutoScrollTag 使用
AutoScrollTag(
  key: ValueKey(index),
  controller: controller,
  index: index,
  child: child
)
  1. 需要 Scroll 到指定的 index
controller.scrollToIndex(index, preferPosition: AutoScrollPosition.begin)

套件二(scroll_to_index)

  1. 初始化 globalKey
  2. 使用 RectGetter 来观测 child
  3. 使用 Rect rect = RectGetter.getRectFromKey(globalKey); 来取得 rect资料

参考来自 官方文件

// Import package
import 'package:rect_getter/rect_getter.dart';

// Instantiate it

var globalKey = RectGetter.createGlobalKey();
var rectGetter = new RectGetter(
    key: globalKey,
    child: _child,
);

or

var rectGetter = new RectGetter.defaultKey(
    child: _child,
);


// and add it to your layout .

// then you can get rect by

Rect rect = rectGetter.getRect();

or

Rect rect = RectGetter.getRectFromKey(globalKey);

HomePage 的使用

大致讲一下逻辑,和思路。

  1. 初始化 wholePageRectGetter,用来关注整个画面的大小
  2. 使用 NotificationListener 来管理 CustomScrollViewTabBar 的互动,换句话说,就是当点击、听直、滑动等...,一系列的 ScrollNotification 事件触发时,我就要去计算我的画面,然後坐我想要做的事情
  3. 建立 SliverScrollView 也就是包含 AppBar(SliverAppBar)、Body(SliverList)
  4. 建立 FAppBar,可以参考前一篇 【第二五天 - Flutter 知名外送平台画面练习(上)】
  5. 建立 animateAndScrollTo 的 function。
  6. 建立 onCollapsed,来让 AppBar 操作。
  7. 建立 SliverList,并且在 SliverChildListDelegate,里面使用 List.generate,并在 List.generate 里面回传 item 样式。
  8. 设定 item 样式,并为每一个 item 初始化 RectGetter,并使用 AutoScrollTag 来达到 scroll_to_index
  9. 处理 NotificationListeneronScrollNotification
  10. 取得萤幕可看到的 item index 有哪些,因此建立一个 getVisibleItemsIndex
  11. 修改 animateAndScrollTo 的 function。
class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>
    with SingleTickerProviderStateMixin {
  /// 使否展开
  bool isCollapsed = false;
  late AutoScrollController scrollController;
  late TabController tabController;

  /// 展开高度
  final double expandedHeight = 500.0;

  /// 页面资料
  final PageData data = ExampleData.data;

  /// 折叠高度
  final double collapsedHeight = kToolbarHeight;

  /// Instantiate RectGetter
  final wholePage = RectGetter.createGlobalKey();
  Map<int, dynamic> itemKeys = {};

  /// prevent animate when press on tab bar
  /// 避免当我们点击 tab bar 时,动画还在动,还在计算。
  bool pauseRectGetterIndex = false;

  @override
  void initState() {
    /// tabController 出使话
    tabController = TabController(length: data.categories.length, vsync: this);
    scrollController = AutoScrollController();
    super.initState();
  }

  @override
  void dispose() {
    scrollController.dispose();
    tabController.dispose();
    super.dispose();
  }

  /// 取得萤幕可看到的 index 有哪些
  List<int> getVisibleItemsIndex() {
    // get ListView Rect
    Rect? rect = RectGetter.getRectFromKey(wholePage);
    List<int> items = [];
    if (rect == null) return items;
    itemKeys.forEach((index, key) {
      Rect? itemRect = RectGetter.getRectFromKey(key);
      if (itemRect == null) return;
      // y 轴座越大,代表越下面
      // 如果 item 上方的座标 比 listView 的下方的座标 的位置的大 代表不在画面中。
      // bottom meaning => The offset of the bottom edge of this widget from the y axis.
      // top meaning => The offset of the top edge of this widget from the y axis.
      if (itemRect.top > rect.bottom) return;
      // 如果 item 下方的座标 比 listView 的上方的座标 的位置的小 代表不在画面中。
      if (itemRect.bottom < rect.top) return;
      items.add(index);
    });

    return items;
  }

  /// 用来传递给 appBar 的 function
  void onCollapsed(bool value) {
    if (this.isCollapsed == value) return;
    setState(() => this.isCollapsed = value);
  }

  /// true表示消费掉当前通知不再向上一级NotificationListener传递通知,false则会再向上一级NotificationListener传递通知;
  bool onScrollNotification(ScrollNotification notification) {
    // 不想让上一层知道,无需做动作。
    if (pauseRectGetterIndex) return true;
    // 取得标签的长度
    int lastTabIndex = tabController.length - 1;
    // 取得现在画面上可以看得到的 Items Index
    List<int> visibleItems = getVisibleItemsIndex();

    bool reachLastTabIndex = visibleItems.isNotEmpty &&
        visibleItems.length <= 2 &&
        visibleItems.last == lastTabIndex;
    // 如果到达最後一个 index 就跳转到最後一个 index
    if (reachLastTabIndex) {
      tabController.animateTo(lastTabIndex);
    } else {
      // 取得画面中的 item 的中间值。例:2,3,4 中间的就是 3
      // 求一个数字列表的乘积
      int sumIndex = visibleItems.reduce((value, element) => value + element);
      // 5 ~/ 2 = 2  => Result is an int 取整数
      int middleIndex = sumIndex ~/ visibleItems.length;
      if (tabController.index != middleIndex)
        tabController.animateTo(middleIndex);
    }
    return false;
  }

  /// TabBar 的动画。
  void animateAndScrollTo(int index) {
    pauseRectGetterIndex = true;
    tabController.animateTo(index);
    // Scroll 到 index 并使用 begin 的模式,结束後,把 pauseRectGetterIndex 设为 false 暂停执行 ScrollNotification
    scrollController
        .scrollToIndex(index, preferPosition: AutoScrollPosition.begin)
        .then((value) => pauseRectGetterIndex = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true, //是否延伸body至顶部。
      backgroundColor: scheme.background,
      body: RectGetter(
        key: wholePage,

        /// NotificationListener 是一个由下往上传递通知,true 阻止通知、false 传递通知,确保指监听滚动的通知
        /// ScrollNotification => https://www.jianshu.com/p/d80545454944
        child: NotificationListener<ScrollNotification>(
          child: buildSliverScrollView(),
          onNotification: onScrollNotification,
        ),
      ),
    );
  }

  /// CustomScrollView + SliverList + SliverAppBar
  Widget buildSliverScrollView() {
    return CustomScrollView(
      controller: scrollController,
      slivers: [
        buildAppBar(),
        buildBody(),
      ],
    );
  }

  /// AppBar
  SliverAppBar buildAppBar() {
    return FAppBar(
      data: data,
      context: context,
      expandedHeight: expandedHeight,
      // 期许展开的高度
      collapsedHeight: collapsedHeight,
      // 折叠高度
      isCollapsed: isCollapsed,
      onCollapsed: onCollapsed,
      tabController: tabController,
      onTap: (index) => animateAndScrollTo(index),
    );
  }

  /// Body
  SliverList buildBody() {
    return SliverList(
      delegate: SliverChildListDelegate(List.generate(
        data.categories.length,
        (index) {
          return buildCategoryItem(index);
        },
      )),
    );
  }

  /// ListItem
  Widget buildCategoryItem(int index) {
    // 建立 itemKeys 的 Key
    itemKeys[index] = RectGetter.createGlobalKey();
    Category category = data.categories[index];
    return RectGetter(
      // 传GlobalKey,之後可以 RectGetter.getRectFromKey(key) 的方式获得 Rect
      key: itemKeys[index],
      child: AutoScrollTag(
        key: ValueKey(index),
        index: index,
        controller: scrollController,
        child: CategorySection(category: category),
      ),
    );
  }
}

<<:  Day 26:53. Maximum Subarray (2)

>>:  IT铁人DAY 25-Iterator 迭代器模式

Day04 UIKit 03 - SceneDelegate

SceneDelegate 从 iOS 13 开始,SceneDelegate 承担了 AppDel...

JS 08 - 静态方法

大家好! 我们进入今天的主题吧! 物件方法 如果要推入项目至阵列,我们会使用原型方法。 但是,为什麽...

Day_28 Ad blocking

相信有许多人很讨厌网页广告,会在网页上安装广告拦截器如AdBlock、AdGuard等。但有很多装置...

Day 22 贝式分类器 Bayesian Classifier

介绍: 贝式分类器(Bayesian Classifier)是一种基於机率模型的机器学习模型。它有很...

CMoney菁英软件工程师战斗营_Week 8

来到CMoney近两个月 不到两星期就要发表我们游戏专题了 或许会有人不知道做游戏专题有什麽好处 比...