Home > Blockchain >  Flutter Button is not getting hit event in overlay
Flutter Button is not getting hit event in overlay

Time:01-20

I want to build a context menu for each entry in a list. I try this with an overlay, but now most of the Buttons inside of the Overlay don't work anymore. The overlay is a stack with the options and a close button in the second Layer of the Stack (I kind of need it this way for the layout). I tried to remove the stack, but that didn't help.

The close button works fine, but the menu-options can't be hit. The hit-event passes though them and would hit anything blow it.

Edit:

What I have is this: enter image description here

The green box with the buttons "User", "Add" and "close" open after a click on a Button "options". The Button "close" is clickable and closes the overly, just as wanted. The other two buttons ("User" and "Add") and not clickable and not even response to the mouse hover.

I need these two buttons to be usable when visible. For now they only print something into the console but this will change later to call a new Page

Can you tell me, what I am missing or what is wrong with my code?

This is a simplified Version of the code with the same Problem. The class MyDelegate extends SingleChildLayoutDelegate is for positioning the overlay

import 'package:flutter/material.dart';

class OverlayMenu extends StatefulWidget {
  const OverlayMenu({Key? key}) : super(key: key);

  @override
  State<OverlayMenu> createState() => _OverlayMenuState();
}

class _OverlayMenuState extends State<OverlayMenu> {

  OverlayEntry? entry;

  @override
  void initState() {
    super.initState();
  }

  @override
  dispose() {
    _hideOverLay();
    super.dispose();
  }

  _hideOverLay() {
    entry?.remove();
    entry = null;
  }

  _showContextMenu(BuildContext context, LayerLink layerLink) {
    _hideOverLay();
    final overlay = Overlay.of(context)!;
    RenderBox renderBox = context.findRenderObject() as RenderBox;
    var anchorSize = renderBox.size;
    entry = OverlayEntry(
      builder: (context) => CompositedTransformFollower(
        showWhenUnlinked: false,
        link: layerLink,
        child: CustomSingleChildLayout(
          // # Added Line 1 #
          delegate: MyDelegate(anchorSize), // # Added Line 2 #
          child: ContextMenu(
            onClose: () => _hideOverLay(),
          ),
        ),
      ),
    );
    overlay.insert(entry!);
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: List.generate(10, (index) => ListEntry(
            onPressed: (context, link) => _showContextMenu(context, link),
        )),
      ),
    );
  }
}

class ListEntry extends StatelessWidget {
  final Function(BuildContext, LayerLink) onPressed;
  ListEntry({Key? key, required this.onPressed}) : super(key: key);
  final layerLink = LayerLink();


  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 5),
      child: Container(
        color: Colors.grey,
        child: Row(
          children: [
            Expanded(child: Text("Text")),
            Builder(
              builder: (context) {
                return CompositedTransformTarget(
                  link: layerLink,
                  child: ElevatedButton(
                    onPressed: () => onPressed.call(context, layerLink),
                    child: SizedBox(width: 50, child: Text("options")),
                  ),
                );
              }
            )
          ],
        ),
      )
    );
  }
}


class MyDelegate extends SingleChildLayoutDelegate {
  final Size anchorSize;

  MyDelegate(this.anchorSize);

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    // allow the child to be smaller than parent's constraint
    return constraints.loosen();
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(anchorSize.width - childSize.width,
        anchorSize.height - childSize.height);
  }

  @override
  bool shouldRelayout(_) => true;
}

class ContextMenu extends StatelessWidget {
  final Function() onClose;
  const ContextMenu({Key? key, required this.onClose})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 240,
      height: 150,
      child: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.only(right: 18, bottom: 10),
            child: Container(
              color: Colors.greenAccent,
              padding: const EdgeInsets.fromLTRB(10, 10, 10, 30),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _buildButton(
                    onPressed: () => print("User"),
                    icon: Icon(Icons.account_circle),
                    child: "User",
                  ),
                  SizedBox(height: 5,),
                  _buildButton(
                    onPressed: () => print("Add"),
                    icon: Icon(Icons.add),
                    child: "Add",
                  ),
                ],
              ),
            ),
          ),
          Align(
            alignment: Alignment.bottomRight,
            child: ElevatedButton(
              onPressed: onClose,
              child: SizedBox(width: 50, child: Text("close")),
            ),
          )
        ],
      ),
    );
  }

  _buildButton({
    required Widget icon,
    required String child,
    required Function() onPressed,
  }) {
    return ElevatedButton(
      onPressed: onClose,
      child: Row(
        children: [
          icon,
          Text(child),
        ],
      ),
    );
  }
}

CodePudding user response:

I think that your Offset of MyDelegate is creating problems, you can see it if you set the Offset to zero (MyDelegate code):

@override
Offset getPositionForChild(Size size, Size childSize) {
  return Offset.zero;
}

Now your buttons inside overlay should work, but your overlay is not positioned correctly. To fix that we will define overlay width and height, and then set the Offset to the CompositedTransformFollower like this:

_showContextMenu(BuildContext context, LayerLink layerLink) {
  _hideOverLay();
  final overlay = Overlay.of(context)!;
  RenderBox renderBox = context.findRenderObject() as RenderBox;
  var anchorSize = renderBox.size;
  // assign overlay height and width here
  double overlayHeight = 160; // was previously on SizedBox height of your ContextMenu Widget
  double overlayWidth = 240; // was previously assigned on SizedBox width of your ContextMenu Widget
  entry = OverlayEntry(
    builder: (context) => Positioned(
      width: overlayWidth,
      height: overlayHeight,
      child: CompositedTransformFollower(
        showWhenUnlinked: false,
        offset: Offset(
          anchorSize.width - overlayWidth,
          (anchorSize.height - overlayHeight),
        ),
        link: layerLink,
        child: CustomSingleChildLayout(
          // # Added Line 1 #
          delegate: MyDelegate(anchorSize), // # Added Line 2 #
          child: ContextMenu(
            onClose: () => _hideOverLay(),
          ),
        ),
      ),
    ),
  );
  overlay.insert(entry!);
}

Since we are already using the Positioned widget, we can get rid of the CompositedTransformFollower and its offset, as well as the CustomSingleChildLayout and MyDelegate, so the simplest solution would be:

_showContextMenu(BuildContext context, LayerLink layerLink) {
  _hideOverLay();
  final overlay = Overlay.of(context)!;
  // assign overlay height and width here
  double overlayHeight = 160; // was previously on SizedBox height of your ContextMenu Widget
  double overlayWidth = 240;  // was previously assigned on SizedBox width of your ContextMenu Widget
  RenderBox renderBox = context.findRenderObject() as RenderBox;
  Offset offset = renderBox.localToGlobal(Offset.zero);
  final xPosition = offset.dx;
  final yPosition = offset.dy;
  entry = OverlayEntry(
    builder: (context) {
      return Positioned(
        height: overlayHeight,
        width: overlayWidth,
        left: xPosition / 2,
        // show it on top of the clicked item
        top: yPosition - overlayHeight,
        // you don't need CustomSingleChildLayout with MyDelegate anymore
        child: ContextMenu(
          onClose: () => _hideOverLay(),
        ),
      );
    },
  );
  overlay.insert(entry!);
}

Now you can remove the SizedBox height and width in your ContextMenu widget since we are assigning it to the overlay now.

  • Related