// MIT License // // Copyright (c) 2024 Simon Lightfoot // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MaterialApp( debugShowCheckedModeBanner: false, home: Home(), )); } class Home extends StatefulWidget { const Home({super.key}); @override State createState() => _HomeState(); } class _HomeState extends State { @override Widget build(BuildContext context) { return Material( child: ListView.builder( itemCount: 20, itemBuilder: (BuildContext context, int index) { return ScrollParentOnOverflow( child: AspectRatio( aspectRatio: 1.9, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 32.0, vertical: 12.0, ), child: ColoredBox( color: Colors.grey.shade200, child: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: Column( children: [ for (int i = 0; i < 10; i++) // ElevatedButton( onPressed: () { print('pressed button $index/$i'); }, child: Text('Button $index/$i'), ), ], ), ), ), ), ), ); }, ), ); // return Material( // child: CustomScrollView( // slivers: [ // for (int index = 0; index < 20; index++) // // SliverList( // delegate: SliverChildBuilderDelegate( // childCount: 10, // (BuildContext context, int i) { // return Center( // child: ElevatedButton( // onPressed: () { // print('pressed button $index/$i'); // }, // child: Text('Button $index/$i'), // ), // ); // }, // ), // ), // ], // ), // ); } } class ScrollParentOnOverflow extends StatefulWidget { const ScrollParentOnOverflow({ super.key, this.overscroll = false, required this.child, }); final bool overscroll; final Widget child; @override State createState() => _ScrollParentOnOverflowState(); } class _ScrollParentOnOverflowState extends State { DragStartDetails? _startDetails; Drag? _drag; void dragCancelCallback() { _startDetails = null; _drag = null; } bool _onScrollNotification(ScrollNotification notification) { switch (notification) { case ScrollStartNotification(:final DragStartDetails dragDetails): _startDetails = dragDetails; return false; case OverscrollNotification(:final DragUpdateDetails dragDetails): if (_drag case Drag drag) { drag.update(dragDetails); return true; } if (_startDetails != null) { final pos = Scrollable.of(context).position; final drag = pos.drag(_startDetails!, dragCancelCallback); drag.update(dragDetails); _drag = drag; return true; } case ScrollEndNotification(:final dragDetails): if (_drag != null) { if (dragDetails != null) { _drag!.end(dragDetails); } else { _drag!.cancel(); } _drag = null; _startDetails = null; return true; } } return false; } @override Widget build(BuildContext context) { return NotificationListener( onNotification: _onScrollNotification, child: ScrollConfiguration( behavior: ScrollConfiguration.of(context) // .copyWith(overscroll: widget.overscroll), child: widget.child, ), ); } }