前言
日常开发中,经常会碰到需要监听ListView滚动到什么位置,或者屏幕上显示了哪几个Widget的情况,而 ScrollController 只能监听滚动的距离,而项目中ListView的Widget的高度又不固定,上网搜搜,发现了 ScrollablePositionedList 可以实现,但是在 ScrollablePositionedList 外部嵌套 SmartRefresher 时会有无法下拉刷新的Bug,两大神级插件无法共用的即视感,没办法,只能自己动手了,摸鱼时间-1 -1 -1.
思路
既然我们可以监听ListView滚动的位置,那么只需要知道ListView中每个Item在ListView中的位置就可以了,而获取所在位置使用GlobalKey就可以拿到,但是众所周知,GlobalKey拿到的位置或者宽高都得先在屏幕上渲染之后才存在真实的值,那我们先写个方法,让widget渲染一帧之后回调给我们
import 'package:flutter/material.dart';
///渲染完第一帧之后回调
class AfterRenderingFirstFrame extends StatefulWidget {
const AfterRenderingFirstFrame({
Key? key,
required this.child,
required this.callback
}) : super(key: key);
final Widget child;
final VoidCallback callback;
@override
State<AfterRenderingFirstFrame> createState() => _AfterRenderingFirstFrameState();
}
class _AfterRenderingFirstFrameState extends State<AfterRenderingFirstFrame> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.endOfFrame.then((_) {
widget.callback();
});
}
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void didUpdateWidget(AfterRenderingFirstFrame oldWidget) {
super.didUpdateWidget(oldWidget);
if(oldWidget.child != widget.child) {
WidgetsBinding.instance.endOfFrame.then((_) {
widget.callback();
});
}
}
}
didUpdateWidget也会回调是因为正常情况下数据是异步拿到的,或者后更新ListView的情况,只要child发生改变就回调一次,用于更新该Item在ListView的位置
具体实现
先定义可能需要的变量
final _listKey = GlobalKey(); //<--可以知道listview在屏幕上的位置
final listViewController = ScrollController(); //<--用于监听listview滚动的距离
final List<Item> dataList = []; //数据列表
double currentPosition = 0; //记录当前滚动的位置,可以用于对比下次滚动是上滚还是下滚
ValueNotifier<int> currentIndex = ValueNotifier(0); //游标,记录当前滚动到哪个Item上
Item类是实际业务中的数据,但是需要在item中加个变量,用于记录当前Widget在listview的位置
class Item {
String? value;
double? position; //增加一个字段用于记录当前Item在ListView中的位置
Item({this.value, this.position});
}
接下来我们需要获取Widget在listview的位置渲染之后并赋值回去即可,使用
RenderBox renderBox = GlobalKey().currentContext!.findRenderObject() as RenderBox;
renderBox.localToGlobal(Offset(x, y)).dy;
localToGlobal可以获得以某个坐标为基础,在此坐标上加上自己在屏幕的位置,如果坐标为Offset.zero的话就是自己在屏幕上的位置
但是ListView获得在屏幕上的位置是不够的,如图
ListView是可以滚动的,如果获取其中Item在屏幕中的位置则是不准确的,而需要获取Widget在Listview位置则可以获取
Item在屏幕的位置 + ListView滚动的距离 - ListView所在位置
所得结果就是item在listview上的位置,对了,这里所有的位置和距离都是相对Y轴的
不多说了,上代码
先用extension封装一下GlobalKey
extension GlobalKeyExtension on GlobalKey {
///y轴基于某个点位置
double localToDy(double localDy) {
RenderBox renderBox = currentContext!.findRenderObject() as RenderBox;
return renderBox.localToGlobal(Offset(0, localDy)).dy;
}
///y轴相对屏幕顶部位置
double get dy {
RenderBox renderBox = currentContext!.findRenderObject() as RenderBox;
return renderBox.localToGlobal(Offset.zero).dy;
}
///获取widget的高度
double get height {
RenderBox renderBox = currentContext!.findRenderObject() as RenderBox;
return renderBox.size.height;
}
}
监听ListView滚动,使用listview滚动控制器即可,之前定义了currentPosition变量用来和当前listview滚动作比较,可以知道是上滑还是下滑列表
currentIndex是ValueNotifier类型,用于局部刷新,当然不需要则换成int类型也一样
//监听滚动的ListView显示的第一个的item
listViewController.addListener(() {
if(listViewController.offset < currentPosition) {
//当前滚动位置小于上次记录的位置
//说明listview在往上滚动
//按道理来说 [position] 不会为空,但是宁杀错勿错过(没自信用!)
if((dataList[currentIndex.value].position??0) > listViewController.offset) {
if(currentIndex.value > 0) {
currentIndex.value = currentIndex.value - 1;
}
}
}else {
//正常来说这个是一定成立的,除非一个屏幕只显示了一个item
if(dataList.length > currentIndex.value + 1) {
if(listViewController.offset > (dataList[currentIndex.value + 1].position??0)) {
currentIndex.value = currentIndex.value + 1;
}
}
}
currentPosition = listViewController.offset;
});
而界面就随便写写了,主要实现功能即可
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ValueListenableBuilder(
valueListenable: currentIndex,
builder: (_, value, child) {
return Text(
"当前滚动到第$value个Item"
);
},
),
centerTitle: true,
),
backgroundColor: Colors.white,
body: SizedBox(
key: _listKey,
child: ListView.builder(
controller: listViewController,
itemCount: dataList.length,
itemBuilder: (_, index) {
final _key = GlobalKey();
return AfterRenderingFirstFrame(
child: Container(
key: _key,
height: 200,
color: getRandomColor,
alignment: Alignment.center,
child: Text(
dataList[index].value??""
),
),
callback: () {
double listY = _listKey.dy; //获取listview在屏幕中的位置
double listItemY = _key.localToDy(listViewController.offset); //获取item在屏幕位置加listview滚动距离
dataList[index].position = listItemY - listY; //相减即可得到item在list所在位置
},
);
},
),
),
);
}
//获取随机颜色
Color get getRandomColor {
Random random = Random();
int r = random.nextInt(256);
int g = random.nextInt(256);
int b = random.nextInt(256);
return Color.fromARGB(255, r, g, b);
}
最终效果
总结
第一次发文章,写得乱七八糟的,不知道jym能不能看懂,之前写的笔记都是给自己看的,最近鱼被摸秃了想着发篇文章打发一下时间,也顺便记录一下。