Флаттер. Вложенная прокрутка внутри PageView
У меня есть простые страницы с PageView
виджет, а внутри есть ListView
. И где прокруткаPageView
не будет работать. Причина проста. Поскольку событие указателя потребляется вложенным дочерним элементом.
@override
Widget build(BuildContext context) {
setupController();
return PageView(
controller: controllerPage,
scrollDirection: Axis.vertical,
children: <Widget>[
ListView.builder(
controller: controller,
padding: EdgeInsets.all(AppDimens.bounds),
itemCount: 15,
itemBuilder: (context, index){
return Container(
height: 100,
color: index %2 == 0
? Colors.amber : Colors.blueAccent,
);
},
),
Container(color: Colors.green),
Container(color: Colors.blue),
],
);
}
Мой вопрос: есть ли разумный способ заставить все работать вместе? Вы можете увидеть вертикальную ось дляPageView
, но точно такая же проблема возникнет при использовании горизонтальной оси PageView
и горизонтальный ListView
.
Что я пробовал до сих пор? У меня есть обходной путь. Даже не сложно, просто кажется, что это не так хорошо и неуклюже. ИспользуяAbsorbPointer
и настраиваемые контроллеры для прокрутки.
final controller = ScrollController();
final controllerPage = PageController(keepPage: true);
bool hasNestedScroll = true;
void setupController() {
controller.addListener(() {
if (controller.offset + 5 > controller.position.maxScrollExtent &&
!controller.position.outOfRange) {
/// Swap to Inactive, if it was not
if (hasNestedScroll) {
setState(() {
hasNestedScroll = false;
});
}
} else {
/// Swap to Active, if it was not
if (!hasNestedScroll) {
setState(() {
hasNestedScroll = true;
});
}
}
});
controllerPage.addListener(() {
if (controllerPage.page == 0) {
setState(() {
hasNestedScroll = true;
});
}
});
}
@override
Widget build(BuildContext context) {
setupController();
return PageView(
controller: controllerPage,
scrollDirection: Axis.vertical,
children: <Widget>[
AbsorbPointer(
absorbing: !hasNestedScroll,
child: ListView.builder(
controller: controller,
padding: EdgeInsets.all(AppDimens.bounds),
itemCount: 15,
itemBuilder: (context, index){
return Container(
height: 100,
color: index %2 == 0
? Colors.amber : Colors.blueAccent,
);
},
),
),
Container(color: Colors.green),
Container(color: Colors.blue),
],
);
}
2 ответа
Недавно я застрял в подобном сценарии, и после некоторых исследований и чтения о том, как жесты работают под капотом во флаттере, я нашел достаточно хорошее рабочее решение. Поэтому я использовал RawGestureDetector, который дает вам низкоуровневую обработку жестов виджетами, и отключил прокрутку для PageView и ListView, установив ихphysics
к NeverScrollableScrollPhysics
. Это означало прокрутку этих виджетов с помощью соответствующих контроллеров -PageViewController
а также ScrollController
. Теперь в соответствии с положением и состоянием ListView выбирается активный контроллер из двух и перетаскивается с помощью контроллера. Основной класс будет выглядеть примерно так.
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(appBar: AppBar(), body: NestedContainerWidget()));
}
}
class NestedContainerWidget extends StatefulWidget {
@override
_NestedContainerWidgetState createState() => _NestedContainerWidgetState();
}
class _NestedContainerWidgetState extends State<NestedContainerWidget> {
PageController _pageController;
ScrollController _listScrollController;
ScrollController _activeScrollController;
Drag _drag;
@override
void initState() {
super.initState();
_pageController = PageController();
_listScrollController = ScrollController();
}
@override
void dispose() {
_pageController.dispose();
_listScrollController.dispose();
super.dispose();
}
void _handleDragStart(DragStartDetails details) {
if (_listScrollController.hasClients &&
_listScrollController.position.context.storageContext != null) {
final RenderBox renderBox =
_listScrollController.position.context.storageContext.findRenderObject();
if (renderBox.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition)) {
_activeScrollController = _listScrollController;
_drag = _activeScrollController.position.drag(details, _disposeDrag);
return;
}
}
_activeScrollController = _pageController;
_drag = _pageController.position.drag(details, _disposeDrag);
}
void _handleDragUpdate(DragUpdateDetails details) {
if (_activeScrollController == _listScrollController &&
details.primaryDelta < 0 &&
_activeScrollController.position.pixels ==
_activeScrollController.position.maxScrollExtent) {
_activeScrollController = _pageController;
_drag?.cancel();
_drag = _pageController.position.drag(
DragStartDetails(
globalPosition: details.globalPosition, localPosition: details.localPosition),
_disposeDrag);
}
_drag?.update(details);
}
void _handleDragEnd(DragEndDetails details) {
_drag?.end(details);
}
void _handleDragCancel() {
_drag?.cancel();
}
void _disposeDrag() {
_drag = null;
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
physics: const NeverScrollableScrollPhysics(),
children: [
Center(child: Text('Page 1')),
ListView(
controller: _listScrollController,
physics: const NeverScrollableScrollPhysics(),
children: List.generate(
20,
(int index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
);
}
}
Посмотрите на логику в _handleDragStart
а также _handleDragUpdate
который определяет контроллер, который следует прокручивать в соответствии с состоянием ListView
виджет и его состояние прокрутки.
Я написал подробную статью по этой проблеме, вы можете проверить ее здесь - https://medium.com/@Mak95/nested-scrolling-listview-inside-pageview-in-flutter-a57b7a6241b1
Решение может быть улучшено, поэтому комментарии будут приветствоваться.
Я использовал этот ответ , чтобы помочь мне решить ту же проблему.
в основном, вы слушаете прокручиваемый дочерний элемент для
OverscrollNotification
с использованием
NotificationListener
и прокрутите родителя с помощью его контроллера.
код:
final pageCntrler = PageController();
_scrollDown() async {
await pageCntrler.nextPage(
duration: scrollDuration,
curve: scrollCurve,
);
}
_scrollUp() async {
await pageCntrler.previousPage(
duration: scrollDuration,
curve: scrollCurve,
);
}
@override
Widget build(BuildContext context) {
return PageView(
controller: pageCntrler,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
children: [
NotificationListener(
onNotification: (notification) {
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
_scrollDown();
} else {
_scrollUp();
}
}
},
child: SingleChildScrollView(),
)
],
);
}