iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0
Mobile Development

Flutter - 複製貼上到開發套件之旅系列 第 26

【第二六天 - 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),
      ),
    );
  }
}

上一篇
【第二五天 - Flutter 知名外送平台畫面練習(上)】
下一篇
【第二七天 - Flutter 知名外送平台畫面練習(下)】
系列文
Flutter - 複製貼上到開發套件之旅30

尚未有邦友留言

立即登入留言