目前存在一个已知的 Flutter 限制,它会使小部件溢出
Stack
并且无法接收手势事件。
但是,有一个 hack 似乎可以通过扩展
Stack
类来工作。
这是我更新的黑客实现。
class StackTappableOutside extends Stack {
const StackTappableOutside({
super.key,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
RenderStack createRenderObject(BuildContext context) {
return RenderStackTappableOutside(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
clipBehavior: clipBehavior,
);
}
}
class RenderStackTappableOutside extends RenderStack {
RenderStackTappableOutside({
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}
我测试了它,它在
Column
和 Row
中运行良好
(可复制,只需复制和粘贴)
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class StackTappableOutside extends Stack {
const StackTappableOutside({
super.key,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
RenderStack createRenderObject(BuildContext context) {
return RenderStackTappableOutside(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
clipBehavior: clipBehavior,
);
}
}
class RenderStackTappableOutside extends RenderStack {
RenderStackTappableOutside({
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}
class TapOutsideStackDemo extends StatefulWidget {
const TapOutsideStackDemo({super.key});
@override
State<TapOutsideStackDemo> createState() => _TapOutsideStackDemoState();
}
class _TapOutsideStackDemoState extends State<TapOutsideStackDemo> {
List<int> items = [0, 1, 2, 3, 4];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tap Outside Stack Demo'),
),
body: _body(),
);
}
Widget _itemBuilder(int index, int element) {
final showAddButton = index > 0;
return StackTappableOutside(
clipBehavior: Clip.none,
children: [
Container(
child: ListTile(
title: Text('Todo List Item $element'),
subtitle: Text('Add a new item after'),
),
decoration: BoxDecoration(
color: Colors.deepOrange,
border: Border.all(color: Colors.yellow),
),
),
if (showAddButton)
Positioned(
top: -24,
right: 8,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green,
),
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
print('add after');
},
),
),
),
],
);
}
Widget _body() {
return Column(
children: items.mapIndexed(_itemBuilder).toList(),
);
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final element = items[index];
return _itemBuilder(index, element);
},
);
}
}
void main() {
runApp(MaterialApp(
home: TapOutsideStackDemo(),
));
}
使用上面的代码,当您从任何地方点击绿色按钮时,它会打印:
add after
add after
add after
但是,我有一个要求将其设为
ListView
而不是Column
。所以我改变了
Widget _body() {
return Column(
children: items.mapIndexed(_itemBuilder).toList(),
);
}
至:
Widget _body() {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final element = items[index];
return _itemBuilder(index, element);
},
);
}
突然,绿色按钮只有一半起作用了。只有底部有效。
我还注意到,如果我们用
GestureDetector
包装列表项,即使使用之前有效的简单 Column
,黑客也会停止工作。
有人可以解释为什么它在 ListView 上不起作用吗?
有什么方法可以强制它在
ListView
和 GestureDetector
上工作吗?
我需要强制使用 ListView
,因为我需要随后实现 ReorderableListView.builder
。
似乎很奇怪,似乎没有简单的开箱即用的方法来做到这一点。但我发现了一个不错的 pub 包,它似乎适合我:https://pub.dev/packages/defer_pointer
对于您的示例,它的工作原理是将整个
ListView.builder
包裹在 DeferredPointerHandler
内,然后将 IconButton
包裹在 DeferPointer
内。
这是您修改后的代码示例:
class _TapOutsideStackDemoState extends State<TapOutsideStackDemo> {
List<int> items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tap Outside Stack Demo'),
),
body: _body(),
);
}
Widget _itemBuilder(int index, int element) {
final showAddButton = index > 0;
return Stack(
clipBehavior: Clip.none,
children: [
Container(
child: ListTile(
title: Text('Todo List Item $element'),
subtitle: Text('Add a new item after'),
),
decoration: BoxDecoration(
color: Colors.deepOrange,
border: Border.all(color: Colors.yellow),
),
),
if (showAddButton)
Positioned(
top: -24,
right: 8,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.green,
),
child: DeferPointer(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
print('add before $element');
},
),
),
),
),
],
);
}
Widget _body() {
// return Column(
// children: items.mapIndexed(_itemBuilder).toList(),
// );
return DeferredPointerHandler(
child: ListView.builder(
itemCount: items.length,
hitTestBehavior: HitTestBehavior.translucent,
itemBuilder: (context, index) {
final element = items[index];
return _itemBuilder(index, element);
},
),
);
}
}