2

I have a particular case of scroll in my code, a certain container handle mouse wheel event in order to increment or decrement a counter. All these boxes are contained in column and in a singleChildScrollView.

The problem is when I use my mouse wheel under this special container, my counter increment but the SingleChildScrollView catch the event too...

Here the problem:

enter image description here

I can fix this using MouseRegion over my SpecialContainer and change the physics to NeverScrollableScrollPhysics in my SingleChildScrollView when I enter the MouseRegion. But it doesn't look very optimized because all children will be rebuild. In my original project, this case would result in unnecessary rebuild.

If you have a idiomatic solution like catch event or something else without rebuilding children, I'll be happy! :)

you can use this code to reproduce my problem:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
 runApp(const MyApp());
}
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
 return MaterialApp(
 title: 'Capture Scroll',
 theme: ThemeData(
 primarySwatch: Colors.indigo,
 ),
 home: const CaptureScrollWidget(title: 'Capture Scroll'),
 );
 }
}
class CaptureScrollWidget extends StatefulWidget {
 const CaptureScrollWidget({Key? key, required this.title}) : super(key: key);
 final String title;
 @override
 State<CaptureScrollWidget> createState() => _CaptureScrollWidgetState();
}
class _CaptureScrollWidgetState extends State<CaptureScrollWidget> {
 int _counter = 0;
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 appBar: AppBar(
 title: Text(widget.title),
 ),
 body: Center(
 child: SizedBox(
 height: 500,
 child: SingleChildScrollView(
 controller: ScrollController(),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: <Widget>[
 box(Colors.green),
 box(Colors.blue),
 specialBox(Colors.purple),
 box(Colors.red),
 box(Colors.yellow),
 ],
 ),
 ),
 ),
 ), // This trailing comma makes auto-formatting nicer for build methods.
 );
 }
 Widget box(Color color) {
 return Container(width: 400, height: 200, color: color.withOpacity(0.4));
 }
 Widget specialBox(Color color) {
 return Listener(
 behavior: HitTestBehavior.opaque,
 onPointerSignal: (PointerSignalEvent event) {
 if (event is PointerScrollEvent) {
 setState(() {
 _counter += event.scrollDelta.dy.sign.round();
 });
 }
 },
 child: Container(
 width: 400,
 height: 200,
 color: color.withOpacity(0.4),
 child: Center(
 child: Text(
 _counter.toString(),
 style: const TextStyle(fontSize: 48),
 )),
 ),
 );
 }
}
Md. Yeasin Sheikh
64.8k7 gold badges49 silver badges85 bronze badges
asked Jul 24, 2022 at 17:50

1 Answer 1

2

You can use a bool on State class and MouseRegion to detect mouse position.

 bool isHovered = false;

Provide scroll physics based on this bool.

child: SingleChildScrollView(
 physics: isHovered ? NeverScrollableScrollPhysics() : null,
MouseRegion(
 onEnter: (v) {
 isHovered = true;
 },
 onExit: (v) {
 isHovered = false;
 },
 child: specialBox(Colors.purple),
),

Separating box class to widget.

class CaptureScrollWidget extends StatefulWidget {
 const CaptureScrollWidget({Key? key, required this.title}) : super(key: key);
 final String title;
 @override
 State<CaptureScrollWidget> createState() => _CaptureScrollWidgetState();
}
class _CaptureScrollWidgetState extends State<CaptureScrollWidget> {
 int _counter = 0;
 bool isHovered = false;
 @override
 Widget build(BuildContext context) {
 debugPrint("rebuild the Scaffold");
 return Scaffold(
 appBar: AppBar(
 title: Text(widget.title),
 ),
 body: Center(
 child: SizedBox(
 height: 500,
 child: SingleChildScrollView(
 physics: isHovered ? NeverScrollableScrollPhysics() : null,
 controller: ScrollController(),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: <Widget>[
 const box(Colors.green),
 const box(Colors.blue),
 MouseRegion(
 onEnter: (v) {
 isHovered = true;
 },
 onExit: (v) {
 isHovered = false;
 },
 child: specialBox(Colors.purple),
 ),
 const box(Colors.red),
 const box(Colors.yellow),
 ],
 ),
 ),
 ),
 ), // This trailing comma makes auto-formatting nicer for build methods.
 );
 }
 Widget specialBox(Color color) {
 debugPrint("build specialBox");
 return Listener(
 behavior: HitTestBehavior.deferToChild,
 onPointerSignal: (PointerSignalEvent event) {
 if (event is PointerScrollEvent) {
 setState(() {
 _counter += event.scrollDelta.dy.sign.round();
 });
 }
 },
 child: Container(
 width: 400,
 height: 200,
 color: color.withOpacity(0.4),
 child: Center(
 child: Text(
 _counter.toString(),
 style: const TextStyle(fontSize: 48),
 )),
 ),
 );
 }
}
class box extends StatelessWidget {
 final Color color;
 const box(this.color, {super.key});
 @override
 Widget build(BuildContext context) {
 debugPrint("build box");
 return Container(width: 400, height: 200, color: color.withOpacity(0.4));
 }
}

If you need more control not to rebuild the parent widget, use ValueNotifier

class CaptureScrollWidget extends StatelessWidget {
 const CaptureScrollWidget({Key? key, required this.title}) : super(key: key);
 final String title;
 @override
 Widget build(BuildContext context) {
 debugPrint("rebuild the Scaffold");
 ValueNotifier<int> _counter = ValueNotifier(0);
 ValueNotifier<bool> isHovered = ValueNotifier(false);
 return Scaffold(
 appBar: AppBar(
 title: Text(title),
 ),
 body: Center(
 child: SizedBox(
 height: 500,
 child: ValueListenableBuilder<bool>(
 valueListenable: isHovered,
 builder: (context, value, child) => SingleChildScrollView(
 physics: value ? const NeverScrollableScrollPhysics() : null,
 controller: ScrollController(),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: <Widget>[
 const box(Colors.green),
 const box(Colors.blue),
 MouseRegion(
 onEnter: (v) {
 isHovered.value = true;
 },
 onExit: (v) {
 isHovered.value = false;
 },
 child: Listener(
 behavior: HitTestBehavior.deferToChild,
 onPointerSignal: (PointerSignalEvent event) {
 if (event is PointerScrollEvent) {
 _counter.value += event.scrollDelta.dy.sign.round();
 }
 },
 child: Container(
 width: 400,
 height: 200,
 color: Colors.purple.withOpacity(0.4),
 child: Center(
 child: ValueListenableBuilder(
 valueListenable: _counter,
 builder: (context, value, child) => Text(
 value.toString(),
 style: const TextStyle(fontSize: 48),
 ),
 )),
 ),
 ),
 ),
 const box(Colors.red),
 const box(Colors.yellow),
 ],
 ),
 ),
 ),
 ),
 ), // This trailing comma makes auto-formatting nicer for build methods.
 );
 }
}
class box extends StatelessWidget {
 final Color color;
 const box(this.color, {super.key});
 @override
 Widget build(BuildContext context) {
 debugPrint("build box");
 return Container(width: 400, height: 200, color: color.withOpacity(0.4));
 }
}
answered Jul 24, 2022 at 18:02
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for your answer :), but I have a question: if the state change, all children will be rebuild. Is there another solution without making all children rebuild?
I think it is better to create separate widget instead of helper method on that case. On performance cases.
I think you will choose last option which is stateless Widget or const constructor.
Yes the last solution seems good! I will try in a moment, thanks :)

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.