【第二八天 - Flutter 开发套件之旅(上)】

前言

当我在做某某知名平台的画面时,我发现其实健在都没有这个效果的 plugin,可以快速达到想要做的事情。
先来宣传一下我做的套件 => vertical_scrollable_tabview 版本 0.0.2

程序码 => GITHUB

之後这个版本可能会再去修改,让整个使用上面的体验更好,可以比较好控制一些效果。
也非常的欢迎发 PR 给我呦~~

开心的专案

https://ithelp.ithome.com.tw/upload/images/20210915/20134548h8FSZq7VdF.png

讲讲 plugin 和 Application 的不同

其实就是大概差不多的东西,多了一个叫做 example的 flutter application。
目标:

  • ./example/lib 写好一个 sample code,给使用这个套件的人看要怎麽使用这个套件。
  • ./lib 开始写套件。

Example

这个范例也是基於前几天的 【第26、26、27天 - Flutter 知名外送平台画面练习】 的文章,做简化。
不使用 SliverAppBar 了,而是使用普通的 appBar

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Vertical Scrollable TabView Demo',
        theme: ThemeData(
          primarySwatch: Colors.purple,
        ),
        home: MyHomePage(title: 'Vertical Scrollable TabView Plugin'));
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {

  final List<Category> data = ExampleData.data;
  // TabController More Information => https://api.flutter.dev/flutter/material/TabController-class.html
  late TabController tabController;

  @override
  void initState() {
    tabController = TabController(length: data.length, vsync: this);
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text(widget.title),
        bottom: TabBar(
          isScrollable: true,
          controller: tabController,
          indicatorPadding: EdgeInsets.symmetric(horizontal: 16.0),
          indicatorColor: Colors.cyan,
          labelColor: Colors.cyan,
          unselectedLabelColor: Colors.white,
          indicatorWeight: 3.0,
          tabs: data.map((e) {
            return Tab(text: e.title);
          }).toList(),
          onTap: (index) {
            VerticalScrollableTabBarStatus.setIndex(index);
          },
        ),
      ),
      body: VerticalScrollableTabView(
          tabController: tabController,
          listItemData: data,
          verticalScrollPosition: VerticalScrollPosition.middle,
          eachItemChild: (object, index) =>
              CategorySection(category: object as Category)),
    );
  }
}

套件如何使用

可以看到我在 TabBaronTap 里面使用了 VerticalScrollableTabBarStatus.setIndex(index); 这个 method,这里是被要求一定要放的,没放的话会出现画面上的 bug。
可以看到 套件的程序码范例

// Required it
TabBar(
    onTap: (index) {
        VerticalScrollableTabBarStatus.setIndex(index); <- Required 
    },
)

再来是使用 VerticalScrollableTabView 这个元件。里面的 listItemData 是一个 List<dynamic>
的型态, eachItemChild 则是每个 item 的样式。并且会有一个 object 回传,可以把 object 指定为 listItemData 里面的 dynamic 的物件。

verticalScrollPosition: VerticalScrollPosition.begin,则是动画的位置。和 scroll_to_index,里面的 AutoScrollPosition效果一样。

VerticalScrollableTabView(
    tabController: tabController,                             <- Required TabBarController
    listItemData: data,                                       <- Required List<dynamic>
    eachItemChild: (object,index){
        return CategorySection(category: object as Category); <- Object and index
    },
    verticalScrollPosition: VerticalScrollPosition.begin,
),

套件

可以看到这边 VerticalScrollableTabBarStatus 里面我放了 static,这样子的写法不好,非常不好,但是因为我想不出来要怎麽写才会好。

我有想过使用 inheritedwidget 来写,可是我发现这样子的话连 TabBar 都要自己造一个,我觉得这样虽然把 static 拿掉了,可是这样子反而让使用上程序码更多...。

欢迎发 PR 给我:)

我自己认为套件的目的就是要让程序码变少,就可以达到想要多样性的效果。

/// Detect TabBar Status, isOnTap = is to check TabBar is on Tap or not, isOnTapIndex = is on Tap Index
/// 增厕 TabBar 的状态,isOnTap 是用来判断是否是被点击的状态,isOnTapIndex 是用来储存 TapBar 的 Index 的。
class VerticalScrollableTabBarStatus {
  static bool isOnTap = false;
  static int isOnTapIndex = 0;

  static void setIndex(int index) {
    VerticalScrollableTabBarStatus.isOnTap = true;
    VerticalScrollableTabBarStatus.isOnTapIndex = index;
  }
}

/// VerticalScrollPosition = is ann Animation style from scroll_to_index plugin's preferPosition,
/// It's show the item position in listView.builder
/// 用来设定动画状态的(参考 scroll_to_index 的 preferPosition 属性)
enum VerticalScrollPosition { begin, middle, end }

class VerticalScrollableTabView extends StatefulWidget {
  /// TabBar Controller to let widget listening TabBar changed
  /// TabBar Controller 用来让 widget 监听 TabBar 的 index 是否有更动
  final TabController _tabController;

  /// Required a List<dynamic> Type,you can put your data that you wanna put in item
  /// 要求 List<dynamic> 的结构,List 里面可以放自己建立的 Object
  final List<dynamic> _listItemData;

  /// A callback that return an Object inside _listItemData and the index of ListView.Builder
  /// A callback 用来回传一个 _listItemData 里面的 Object 型态和 ListView.Builder 的 index
  final Widget Function(dynamic aaa, int index) _eachItemChild;

  /// VerticalScrollPosition = is ann Animation style from scroll_to_index,
  /// It's show the item position in listView.builder
  final VerticalScrollPosition _verticalScrollPosition;

  const VerticalScrollableTabView(
      {required TabController tabController,
      required List<dynamic> listItemData,
      required Widget Function(dynamic aaa, int index) eachItemChild,
      VerticalScrollPosition verticalScrollPosition =
          VerticalScrollPosition.begin})
      : _tabController = tabController,
        _listItemData = listItemData,
        _eachItemChild = eachItemChild,
        _verticalScrollPosition = verticalScrollPosition;

  @override
  _VerticalScrollableTabViewState createState() =>
      _VerticalScrollableTabViewState();
}

class _VerticalScrollableTabViewState extends State<VerticalScrollableTabView>
    with SingleTickerProviderStateMixin {
  /// Instantiate scroll_to_index (套件提供的方法)
  late AutoScrollController scrollController;

  /// When the animation is started, need to pause onScrollNotification to calculate Rect
  /// 动画的时候暂停去运算 Rect
  bool pauseRectGetterIndex = false;

  /// Instantiate RectGetter(套件提供的方法)
  final listViewKey = RectGetter.createGlobalKey();

  /// To save the item's Rect
  /// 用来储存 items 的 Rect 的 Map
  Map<int, dynamic> itemsKeys = {};

  @override
  void initState() {
    widget._tabController.addListener(() {
      // will call two times, because 底层呼叫 2 次 notifyListeners()
      // https://stackoverflow.com/questions/60252355/tabcontroller-listener-called-multiple-times-how-does-indexischanging-work
      if (VerticalScrollableTabBarStatus.isOnTap) {
        animateAndScrollTo(VerticalScrollableTabBarStatus.isOnTapIndex);
        VerticalScrollableTabBarStatus.isOnTap = false;
      }
    });
    scrollController = AutoScrollController();
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return RectGetter(
      key: listViewKey,
      // NotificationListener 是一个由下往上传递通知,true 阻止通知、false 传递通知,确保指监听滚动的通知
      // ScrollNotification => https://www.jianshu.com/p/d80545454944
      child: NotificationListener<ScrollNotification>(
        child: buildScrollView(),
        onNotification: onScrollNotification,
      ),
    );
  }

  Widget buildScrollView() {
    return ListView.builder(
      controller: scrollController,
      itemCount: widget._listItemData.length,
      itemBuilder: (BuildContext context, int index) {
        /// Initial Key of itemKeys
        /// 初始化 itemKeys 的 key
        itemsKeys[index] = RectGetter.createGlobalKey();
        return buildItem(index);
      },
    );
  }

  Widget buildItem(int index) {
    dynamic category = widget._listItemData[index];
    return RectGetter(
      /// when announce GlobalKey,we can use RectGetter.getRectFromKey(key) to get Rect
      /// 宣告 GlobalKey,之後可以 RectGetter.getRectFromKey(key) 的方式获得 Rect
      key: itemsKeys[index],
      child: AutoScrollTag(
        key: ValueKey(index),
        index: index,
        controller: scrollController,
        child: widget._eachItemChild(category, index),
      ),
    );
  }
  /// Animation Function for tabBarListener
  /// This need to put inside TabBar onTap, but in this case we put inside tabBarListener
  void animateAndScrollTo(int index) async {
    // Scroll 到 index 并使用 begin 的模式,结束後,把 pauseRectGetterIndex 设为 false 暂停执行 ScrollNotification
    pauseRectGetterIndex = true;
    widget._tabController.animateTo(index);
    switch (widget._verticalScrollPosition) {
      case VerticalScrollPosition.begin:
        scrollController
            .scrollToIndex(index, preferPosition: AutoScrollPosition.begin)
            .then((value) => pauseRectGetterIndex = false);
        break;
      case VerticalScrollPosition.middle:
        scrollController
            .scrollToIndex(index, preferPosition: AutoScrollPosition.middle)
            .then((value) => pauseRectGetterIndex = false);
        break;
      case VerticalScrollPosition.end:
        scrollController
            .scrollToIndex(index, preferPosition: AutoScrollPosition.end)
            .then((value) => pauseRectGetterIndex = false);
        break;
    }
  }

  /// onScrollNotification of NotificationListener
  /// true表示消费掉当前通知不再向上一级NotificationListener传递通知,false则会再向上一级NotificationListener传递通知;
  bool onScrollNotification(ScrollNotification notification) {
    if (pauseRectGetterIndex) return true;
    /// get tabBar index
    /// 取得 tabBar 的长度
    int lastTabIndex = widget._tabController.length - 1;

    List<int> visibleItems = getVisibleItemsIndex();
    /// define what is reachLastTabIndex
    bool reachLastTabIndex = visibleItems.isNotEmpty &&
        visibleItems.length <= 2 &&
        visibleItems.last == lastTabIndex;
    /// if reachLastTabIndex, then scroll to last index
    /// 如果到达最後一个 index 就跳转到最後一个 index
    if (reachLastTabIndex) {
      widget._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 (widget._tabController.index != middleIndex)
        widget._tabController.animateTo(middleIndex);
    }
    return false;
  }
  /// getVisibleItemsIndex on Screen
  /// 取得现在画面上可以看得到的 Items Index
  List<int> getVisibleItemsIndex() {
    // get ListView Rect
    Rect? rect = RectGetter.getRectFromKey(listViewKey);
    List<int> items = [];
    if (rect == null) return items;
    itemsKeys.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;
  }
}

套件就先讲到这边,明天谈谈上架要注意的事情,还有如何上架。
那我们铁人赛 Day29 见罗!!


<<:  JavaScript学习日记 : Day30 - JavaScript动画

>>:  Day 30 後记-TUTK跟一些P2P介绍

【Day 9】Replica 之间的一致性

7.3 eventual consistency 还没写QQ 这章会来聊聊 consistency...

Ruby基本介绍(八)Rspec幼稚园等级实作

DDU-DU DDU-DU~ 十几亿的观看次数...跟四十多亿次的某只MV还有一段距离XD 预防针:...

第38天~

这个得上一篇:https://ithelp.ithome.com.tw/articles/10258...

第14车厢-点开看更多?tableRWD应用篇

本篇延续手刻tableRWD应用,将范例配合选取器改为响应式隐藏栏位并能展开 上几篇介绍table...