首先申明,第一次写掘金技术文档,请各位大佬轻喷......
还请欢迎多多技术交流,B站用户名称 撸码小狂魔 ~ 嘿嘿.
需求来源:
临近过年,无心上班,研究技术,填补空缺.
作为Android原生开发出生,使用了Flutter开发大概有2年,深感有点回不去了.
唔~说正事吧:
平常躺尸时刷App发现某些App的垂直滑动列表中,如果出现视频items,那么屏幕上的首个视频item会自动播放.
一直在想如何使用Flutter撸一个同样效果的封装实现,今天有时间来研究,
大概思路如下:
- 需要一个垂直的ListVuew(有点废话哈)
- 须知道滚动的状态,并且要有各状态的回调,比如:开始滚动 \ 滚动中 \ 滚动结束
- 时刻明白自己滚了多远(这个简单,ScrollController就这一点好处了)
- 最重要的一点:各个列表项中Widget的高度是多少(不知道还计算个毛线)
- 最后......在滚动结束后的回调......要有屏幕显示的列表的....第一项.......索引(Index)值
抱着这些想法,看看我的实现效果Demo
最终效果:
我的Flutter环境:
由于公司项目中使用了闲鱼的 flutter_boost 'v3.0-preview.18',所以一直没有升到最新的2.8的版本,一升就GG
工程结构:
特别简单,就三个文件,最核心的文件已封装在 ListViewVerTopIndexInScreenShowWidget.dart文件中,其它的两个文件只是为了做Demo展示用
这里贴上核心文件ListViewVerTopIndexInScreenShowWidget.dart的代码
import 'package:flutter/material.dart';
/**
* create by 蒲杰
* 994792649@qq.com
* 2022/1/24 下午1:47
* 说明:
**/
// ignore_for_file: file_names
class ListViewVerTopIndexInScreenShowWidget extends StatefulWidget {
// 列表数量
final int itemCount;
// 列表Widget创建回调
final IndexedWidgetBuilder itemBuilder;
// 列表分割Widget创建回调
final IndexedWidgetBuilder separatorBuilder;
// 滑动开始回调
final Function? scrollStartCallBack;
// 滑动中回调
final Function? scrollUpdateCallBack;
// 滑动结束回调
final Function(int topIndexInScreen)? scrollEndCallBack;
const ListViewVerTopIndexInScreenShowWidget({
Key? key,
required this.itemCount,
required this.itemBuilder,
required this.separatorBuilder,
this.scrollStartCallBack,
this.scrollUpdateCallBack,
this.scrollEndCallBack,
}) : super(key: key);
@override
State<ListViewVerTopIndexInScreenShowWidget> createState() =>
_ListViewVerTopIndexInScreenShowWidgetState();
}
class _ListViewVerTopIndexInScreenShowWidgetState
extends State<ListViewVerTopIndexInScreenShowWidget> {
ScrollController controller = ScrollController();
final Map<int, double> _indexChildAndSizeH = <int, double>{};
final Map<int, double> _indexDividerAndSizeH = <int, double>{};
@override
void dispose() {
super.dispose();
controller.dispose();
}
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (Notification notification) {
if (notification is ScrollStartNotification) {
// 滚动开始
if (widget.scrollStartCallBack != null) {
widget.scrollStartCallBack!();
}
}
if (notification is ScrollUpdateNotification) {
// 滚动中
if (widget.scrollUpdateCallBack != null) {
widget.scrollUpdateCallBack!();
}
}
if (notification is ScrollEndNotification) {
// 停止滚动
if (notification.metrics.extentBefore == 0) {
// 滚动到头部
if (widget.scrollEndCallBack != null) {
widget.scrollEndCallBack!(0);
}
return true;
}
// if (notification.metrics.extentAfter == 0) {
// debugPrint("滚动到底部");
// }
double scrollOffset = controller.offset;
// debugPrint("滚动偏移为: $scrollOffset");
int indexKey = 0;
double indexOffsetCount = 0.0;
for (int i in _indexChildAndSizeH.keys.toList()) {
indexKey = i;
indexOffsetCount += _indexChildAndSizeH[i]!.toDouble();
double dividerOffset = 0;
if (indexKey - 1 >= 0) {
dividerOffset = _indexDividerAndSizeH[indexKey - 1]!.toDouble();
}
indexOffsetCount += dividerOffset;
if (indexOffsetCount -
scrollOffset -
_indexChildAndSizeH[i]!.toDouble() -
dividerOffset >
0) {
break;
}
}
if (widget.scrollEndCallBack != null) {
widget.scrollEndCallBack!(indexKey);
}
}
return true;
},
child: ListView.separated(
controller: controller,
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
return _ListItem(
framFirstCallBack: (sizeH) {
_indexChildAndSizeH[index] = sizeH ?? 0;
},
child: widget.itemBuilder(context, index),
);
},
separatorBuilder: (context, index) {
return _ListItem(
framFirstCallBack: (sizeH) {
_indexDividerAndSizeH[index] = sizeH ?? 0;
},
child: widget.separatorBuilder(context, index),
);
},
itemCount: widget.itemCount,
),
);
}
}
class _ListItem extends StatefulWidget {
final Widget child;
final Function(double? size) framFirstCallBack;
const _ListItem({
required this.child,
required this.framFirstCallBack,
Key? key,
}) : super(key: key);
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<_ListItem> with WidgetsBindingObserver {
late BuildContext _context;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance?.addPostFrameCallback((_) {
widget.framFirstCallBack(_context.size?.height);
});
}
@override
Widget build(BuildContext context) {
_context = context;
return widget.child;
}
}
代码解释1,为嘛使用ListView.separated:
因为考虑到可能会做列表项的分割UI操作,所以使用此列表的创建方式,如果您不想使用分割UI,在separatorBuilder的回调返回Widget中返回为SizedBox()即可
代码解释2,如何监听滚动的状态
最开始想在ListView的ScrollController上动文章,想着使用ScrollController的滑动监听+最近滑动时间的判断之类的骚操作,怎么尝试怎么不对.
冷静半小时后,查看ScrollController的源码发现了Notification,突然顿开...可以通过Widget的通知监听来考察滑动的状态,一百度,还真有相关的通知监听.so....完美解决
这里贴上核心代码
NotificationListener(
onNotification: (Notification notification) {
if (notification is ScrollStartNotification) {
// 滚动开始
}
if (notification is ScrollUpdateNotification) {
// 滚动中
}
if (notification is ScrollEndNotification) {
// 停止滚动
if (notification.metrics.extentBefore == 0) {
// 滚动到头部
}
if (notification.metrics.extentAfter == 0) {
// 滚动到底部
}
}
return true;
},
child: ***,
)
代码解释3:_ListItem这个Widget是干什么的
如果你细心一点的话,你会发现这是一个StatefulWidget(有状态)的Widget.
并且在它的State上挂(混入)了一个WidgetsBindingObserver(生命周期监听),用来获取此Widget第一次渲染完毕后能拿到的Widget宽高等信息,并且回传至上级Widget进行管理和使用.
这里贴上核心代码
WidgetsBinding.instance?.addObserver(this);
// 第一次渲染完毕的回调
WidgetsBinding.instance?.addPostFrameCallback((_) {
widget.framFirstCallBack(_context.size?.height);
});
到此,Flutter获取垂直列表在屏幕中显示的第一项索引(Index )值的实现封装完毕,如果您想使用,只需复制核心文件中的代码,即可进行使用.
各位大佬如有其它的实现方式,欢迎交流.