當我在做某某知名平台的畫面時,我發現其實健在都沒有這個效果的 plugin,可以快速達到想要做的事情。
先來宣傳一下我做的套件 => vertical_scrollable_tabview 版本 0.0.2
程式碼 => GITHUB
之後這個版本可能會再去修改,讓整個使用上面的體驗更好,可以比較好控制一些效果。
也非常的歡迎發 PR 給我呦~~
其實就是大概差不多的東西,多了一個叫做 example
的 flutter application。
目標:
./example/lib
寫好一個 sample code,給使用這個套件的人看要怎麼使用這個套件。./lib
開始寫套件。這個範例也是基於前幾天的 【第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)),
);
}
}
可以看到我在 TabBar
的 onTap
裡面使用了 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 見囉!!