当我在做某某知名平台的画面时,我发现其实健在都没有这个效果的 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 见罗!!
<<: JavaScript学习日记 : Day30 - JavaScript动画
7.3 eventual consistency 还没写QQ 这章会来聊聊 consistency...
DDU-DU DDU-DU~ 十几亿的观看次数...跟四十多亿次的某只MV还有一段距离XD 预防针:...
这个得上一篇:https://ithelp.ithome.com.tw/articles/10258...
Stock market is full of uncertainly factor , and ...
本篇延续手刻tableRWD应用,将范例配合选取器改为响应式隐藏栏位并能展开 上几篇介绍table...