Flutter: Hit-test outside parent bounds with `DeferPointer`

One thing that has always felt a little limiting in Flutter for us has been its inability to perform hit-testing for a button or gesture detector that is outside the parents bounds. This has been a popular issue in the Flutter bug-base over the years, getting something around 150+ upvotes if you add up all incarnations of the issue.

The concept of “negative margins” is common in most rich interfaces stacks, like html, unity, flash etc, and is useful for a wide variety of simple techniques and tricks. Some of the more common use cases are drop-down menus, sliding panels, “hanging” ui elements or just general layout tweaks. For years these techniques have been unusable in Flutter because anything you put in negative margins, will no longer accept gestures.

Notice how neither of these buttons can be interacted with, this is very sad :'(

To be fair, Flutter does provide the Overlay widget, to ostensibly handle these use cases. The problem is that it has an API surface that is a bit awkward to use and gets quite complicated when it comes to CompositedTransformFollower and CompositedTransformTarget classes. It’s also fairly inflexible as contents of Overlay always render on top of the entire page route, which is not always desired.

From our experience Overlay is well suited to full-screen overlays that exist in a fixed position, but not really ideal for these inline popups that need to be attached or aligned to some widget within the tree. We find these use cases are generally far simpler to implement using simple offsets.

Finally with the `defer_pointer package we can make it happen:
https://pub.dev/packages/defer_pointer

What is it?

(almost entirely) Based on an example posted by github user @shrouxm, defer_pointer provides an alternative to Overlay which allows you to easily render and hit test a widget outside its parent bounds.

Using DeferPointer hit tests work perfectly

The core idea is pretty simple: If we can’t do the hit test ourselves, then just punt it to another widget further down the tree!

Usage

  1. Wrap a DeferredPointerHandler somewhere above the buttons that you wish to hit-test.
  2. Wrap a DeferPointer widget around the buttons themselves.
Widget build(BuildContext context) {
    return DeferredPointerHandler(
       child: SizedBox(
           width: 100,
           height: 100,
           child: Stack(clipBehavior: Clip.none, children: [
             // Hang button off the bottom of the content
             Positioned(
               bottom: -30,
               child: DeferPointer(child: _SomeBtn(false)),
             ),
             // Content
             Positioned.fill(child: SomeContent()),
           ]))));
  }

In the above example the DeferredPointerHandler will handle all hit testing for any DeferPointer.child widgets, enabling them to escape their parents bounds.

Success Kid - Wikipedia
VICTORY!

You can allow the child to be painted normally, or use the paintOnTop parameter to move the paint up the tree into the DeferredPointerHandler:

return DeferPointer(
    paintOnTop: true,
    child: TextButton(...));

This is useful for dropdowns or panels where you always want the content to render above everything around it, while still retaining the flexibility to render in place in cases where the design calls for it.

Examples

The package provides 4 usage examples:

1. An auto-complete search field:

2. A simple example of offsetting 2 buttons outside their stack: 

3. A classic desktop/web style dropdown menu

4. A animated menu based on the Flow widget: 

For more details check out the full README if you find any issues, please let us know here.

Hopefully this can free up your layouts and bit and help you, literally, break outside that box 😀

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

2 Comments

  1. d unfortunately all the answers directed me to use the overlay!

    I had started using OverflowBox in one of my projects and then I was surprised that the card doesn’t respond to the click!

    After a lot of searching, I had to give up OverflowBox!

    Many thanks for this plugin ❤

  2. For a long time, I have been trying to find a solution to this problem, and unfortunately all the answers directed me to use the overlay!

    I had started using OverflowBox in one of my projects and then I was surprised that the card doesn’t respond to the click!

    After a lot of searching, I had to give up OverflowBox!

    Many thanks for this plugin ❤

Leave a Reply

Your email address will not be published. Required fields are marked *