В настоящее время существует известное ограничение Flutter, из-за которого виджеты выходят за пределы Stack
и не получают событий жестов.
Однако есть хак , который, похоже, работает, расширяя класс 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
потом.
Кажется довольно странным, что не существует простого и готового способа сделать это. Но я нашел хороший пакет для пабов, который, похоже, мне подходит: 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);
},
),
);
}
}
Также обратите внимание, что для этого больше не требуется обходной путь StackTappableOutside
.