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:
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),
)),
),
);
}
}
CodePudding user response:
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));
}
}