1

I'd like to display a tutorial overlay on a screen in my Flutter app. I even found a good candidate which almost does what I'd want: that's the https://pub.dev/packages/overlay_tutorial/ plugin. The user would press a help button on the screen, the overlay would pop up with the cut outs. I don't want anything to receive clicks though (https://github.com/TabooSun/overlay_tutorial/issues/26), however the overlay should disappear if the user clicks anywhere on the screen.

I tried to use AbsorbPointer and it successfully intercepts the clicks and there's no more click through and things happening bellow the overlay. See my fork: https://github.com/CsabaConsulting/overlay_tutorial/commit/9d809d51bcf55b9c8044d07d151c696bdb55abe6 However now there's no way to click away the overlay either.

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AbsorbPointer(
          absorbing: widget.enabled && widget.absorbPointer,
          ignoringSemantics: true,
          child: _OverlayTutorialBackbone(
            overlayColor: widget.overlayColor,
            enabled: widget.enabled,
            overlayTutorialHoles: _overlayTutorialHoles,
            onEntryRectCalculated: () {
              _updateChildren();
            },
            child: widget.child,
          ),
        ),
        if (widget.enabled) ...[
          ..._overlayTutorialHoles.entries
              .map((entry) {

What I'd need is an onTap or an onClick handler for the AbsorbPointer. I could have wrap stuff into a GestureDetector, but that would intercept clicks and gestures all the time. What's a good solution here?

Csaba Toth
  • 10,021
  • 5
  • 75
  • 121
  • Can't you just wrap `widget.child` in the `AbsorbPointer`, instead of the `_OverlayTutorialBackbone`?. Like `_OverlayTutorialBackbone` -> `AbsorbPointer` -> `widget.child` – PatrickMahomes Jul 30 '21 at 06:22
  • That's definitely a direction towards some solution, however it doesn't change anything alone. I'll perform this order swap, then I'll have to figure out how to capture the click at the level of `_OverlayTutorialBackbone`. The `_OverlayTutorialBackbone` is a `SingleChildRenderObjectWidget`. – Csaba Toth Jul 30 '21 at 18:05
  • Is that a problem? By moving `AbsorbPointer` down, you can now wrap `_OverlayTutorialBackbone` in a `GestureDetector`. `AbsorbPointer` only prevents taps from reaching widgets below it in the tree, so it should work fine. – PatrickMahomes Jul 31 '21 at 02:10
  • @PatrickMahomes The problem is that if I wrap the shebang into a Gesture detector then I can shoo away the overlay, but the GestureDetector still stays. The overlay is also always there, it's just intelligent and doesn't paint itself when it's off. The other thing is that I wrap the _OverlayTutorialBackbone then I don't even need the AbsorbPointer (at least in terms of taps) because the Gesture detector swallows the tap anyway. Maybe I should look up how to optionally pass through gesture thorough the GestureDetector? – Csaba Toth Jul 31 '21 at 07:33
  • I don't think you need to worry about that behavior. It seems like Flutter only uses the lowest available `GestureDetector` in the hierarchy. I'll make an answer so you can see. – PatrickMahomes Jul 31 '21 at 07:58

1 Answers1

2

*this answer follows from my comment

If you paste this code into DartPad, you can see that when one GestureDetector is above another in the widget tree, only the onTap of the lower one gets executed. But, if the lower GestureDetector is wrapped in AbsorbPointer with absorbing: true only then does the parent GestureDetector get called. Thus, you should be able to get away with following what I suggested in the comments. Even if this weren't true, you could still just conditionally set the onTap of your parent GestureDetector to null.

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool absorbing = false;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        GestureDetector(
          onTap: () {
            print('GestureDetector 1');
          },
          child: AbsorbPointer(
            absorbing: absorbing,
            child: GestureDetector(
              onTap: () {
                print('GestureDetector 2');
              },
              child: Container(
                height: 200.0,
                width: 200.0,
                color: Colors.red,
                alignment: Alignment.center,
                child: Text(
                  'Press me and check console',
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
              ),
            ),
          ),
        ),
        GestureDetector(
          onTap: () {
            setState(() {
              absorbing = !absorbing;
            });
          },
          child: Container(
            height: 200.0,
            width: 200.0,
            color: Colors.blue,
            alignment: Alignment.center,
            child: Text(
              'Change absorb\n(absorbing: $absorbing)',
              textAlign: TextAlign.center,
              style: TextStyle(
                color: Colors.white,
              ),
            ),
          ),
        ),
      ],
    );
  }
}
PatrickMahomes
  • 844
  • 1
  • 6
  • 8