一、背景
有一个需求,是仿企业照微信的多选(效果大家自己去看)。我想到了两种方案:
- 是遍历所有的item,通过他们的key去获取到对应的位置,跟悬浮控件的位置做一个对比,如果悬浮控件在item范围内就去匹配成功,然后把它下面的所以的item勾选上。
- 我们直接吧悬浮控件放到第一个可见item 上,然后去获取当前可见的item位置即可。
我打算采用第二种方式,因为第一种方式所有的item都需要加key,然后匹配需要做遍历,这样数据量大,性能就差。
二、listview可见位置
思路:我们直接通过listview.builder是没办法自定义SliverChildBuilderDelegate,我们可以通过listview.custom来自定义SliverChildBuilderDelegate,通过自定义我们可以重写didFinishLayout方法,拿到里面缓存的第一个item和最后一个item。可见item的跟缓存item是差5个的,可以间接算出来,后面发现其实不太行,上下滑动之后会显示之前滑动时候的可见位置。正解是:这个里面还有个estimateMaxScrollOffset方法,正常来说通过它可以获取到可见的第一个和最后一个item位置。但是我一开始使用这个方法,不会被回调,后面不知道修改了什么,就会回调,然后这个位置是准确的。
看下listview.builder的源码
ListView.builder({
Key? key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false,
EdgeInsetsGeometry? padding,
this.itemExtent,
this.prototypeItem,
required IndexedWidgetBuilder itemBuilder,
int? itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double? cacheExtent,
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
}) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount!),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both.',
),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
);
我们可以看到childrenDelegate是直接定义好了的。
在看看listview.custom 的源码
const ListView.custom({
Key? key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false,
EdgeInsetsGeometry? padding,
this.itemExtent,
this.prototypeItem,
required this.childrenDelegate,
double? cacheExtent,
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
}) : assert(childrenDelegate != null),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
),
super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
);
childrenDelegate这个是一个必传参数。
三、完整代码如下:
class MyChildrenDelegate extends SliverChildBuilderDelegate {
MyChildrenDelegate(
Widget Function(BuildContext, int) builder, {
int? childCount,
bool addAutomaticKeepAlive = false,
bool addRepaintBoundaries = true,
}) : super(builder,
childCount: childCount,
addAutomaticKeepAlives: addAutomaticKeepAlive,
addRepaintBoundaries: addRepaintBoundaries);
// Return a Widget for the given Exception
Widget _createErrorWidget(dynamic exception, StackTrace stackTrace) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stackTrace,
library: 'widgets library',
context: ErrorDescription('building'),
);
FlutterError.reportError(details);
return ErrorWidget.builder(details);
}
@override
Widget? build(BuildContext context, int index) {
assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount!))
return null;
Widget? child;
try {
child = builder(context, index);
} catch (exception, stackTrace) {
child = _createErrorWidget(exception, stackTrace);
}
if (child == null) {
return null;
}
final Key? key = child.key != null ? _SaltedValueKey(child.key!) : null;
if (addRepaintBoundaries)
child = RepaintBoundary(child: child);
if (addSemanticIndexes) {
final int? semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return KeyedSubtree(key: key, child: child);
}
@override
int? get estimatedChildCount => childCount;
@override
bool shouldRebuild(covariant SliverChildBuilderDelegate oldDelegate) => true;
@override
void didFinishLayout(int firstIndex, int lastIndex) {
debugPrint( " didFinishLayout firstIndex $firstIndex lastIndex $lastIndex");
super.didFinishLayout(firstIndex, lastIndex);
}
///监听 在可见的列表中 显示的第一个位置和最后一个位置
@override
double? estimateMaxScrollOffset(int firstIndex, int lastIndex, double leadingScrollOffset, double trailingScrollOffset) {
print('firstIndex sss : $firstIndex, lastIndex ssss : $lastIndex, leadingScrollOffset ssss : $leadingScrollOffset,'
'trailingScrollOffset ssss : $trailingScrollOffset ' );
return super.estimateMaxScrollOffset(firstIndex, lastIndex, leadingScrollOffset, trailingScrollOffset);
}
}
class _SaltedValueKey extends ValueKey {
const _SaltedValueKey(Key key): assert(key != null), super(key);
}