Home > Software engineering >  Flutter Desktop: Disable scrolling when hover a Container
Flutter Desktop: Disable scrolling when hover a Container

Time:07-25

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),
        )),
      ),
    );
  }
}

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));
  }
}
  • Related