跪拜 Guibai
← All articles
Frontend · Android · Flutter

A Flutter Package Decouples Paint from Hit-Testing So Click Zones Can Overflow Layout Bounds

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Flutter's tight coupling of layout and hit-testing forces trade-offs between compact UIs and usable touch targets. This package removes that constraint, making it practical to build draggable handles, slider thumbs, and badge overlays that remain tappable even when they visually overflow their containers.

Summary

Flutter binds layout size and hit-testing together by default, so enlarging a tap target with padding also shifts surrounding widgets, and overflow regions are not clickable. The `hit` package decouples painting from hit detection through `HitLayer`, `HitLink`, and `HitScope` components. A `HitLayer` accepts two children — one for visual content and one for the hit area — and reports only the visual child's size to the parent while allowing the hit child to be larger and offset.

When a hit area overflows its layout bounds, it registers with a `HitScope` ancestor via a `HitLink` registry. The scope scans registered targets on pointer events, transforms coordinates through the render tree, and dispatches hits in newest-first order. Variants like `Hit.defer` handle widgets placed entirely outside a parent's clip, and `Hit.before` draws behind the subtree for edge shadows or background decorations.

Scrolling lists pose a challenge because an out-of-bounds target drawn through an outer scope can desync during scroll. The package solves this with Flutter compositing layers: a `LeaderLayer` at the original position and a `FollowerLayer` in the scope, letting the GPU track transforms without per-frame repaint calculations.

Takeaways
`HitLayer` accepts separate `hitChild` and `paintChild` widgets; the parent layout uses only the paint child's size while the hit child can be larger and offset via alignment.
Overflowing hit areas register with an ancestor `HitScope` through a `HitLink` registry instead of following Flutter's default hit path.
`HitScope` scans registered targets newest-first, transforms coordinates through the render tree, and dispatches hits to the correct local coordinate space.
`Hit.defer` is for widgets already placed outside a parent's clip boundary, while `Hit.before` draws behind the subtree for edge decorations that still receive events.
Scrolling list support uses `LeaderLayer` and `FollowerLayer` compositing so the GPU tracks position changes without per-frame scope repaints.
The package handles `Transform.translate`, nested scopes, `ClipRect` blocking, and opaque target subtree skipping, confirmed by test coverage.
Conclusions

The compositing-layer approach for scrollable lists is the most technically sophisticated part of the package and addresses a real failure mode that simpler hit-area hacks ignore.

By only registering overflow targets in the deferred scan list rather than all widgets, the design avoids the performance cost of a global hit-test override.

The newest-first registration order deliberately mirrors visual stacking, which is a thoughtful detail that prevents counterintuitive tap behavior on overlapping overflow targets.

Concepts & terms
HitLayer
A Flutter widget that holds two children — a paintChild for visual content and layout sizing, and a hitChild for the clickable area — allowing the hit target to be larger than the visual widget without affecting parent layout.
HitScope
An ancestor widget that maintains a registry of overflow hit targets and scans them on pointer events, performing coordinate transformation to correctly dispatch taps to widgets that have overflowed their parent bounds.
LeaderLayer / FollowerLayer
Flutter compositing layers used to synchronize the position of an out-of-bounds widget inside a scrolling list with its drawn representation in an outer scope, letting the GPU handle position tracking instead of requiring per-frame repaints.
From the discussion

The conversation quickly veers from the hit-testing package to Flutter's upcoming native interop, with one thread clarifying that direct Dart-to-native calls will replace Platform Channels. Only two comments engage with the package itself: one points to a prior pub.dev solution, and another compares the technique to a DeferPointer-plus-Stack pattern, noting the new package swaps Stack for a custom RenderObject.

Flutter's direct native interop, expected in August, will eliminate the need for Platform Channels by allowing native interface calls from Dart.
A similar hit-test area package already exists on pub.dev.
The package's approach resembles wrapping widgets with DeferPointer inside a Stack, but replaces the Stack with a custom RenderObject.
Featured comments
茶树米

I used to wrap my own solution with DeferPointer, but the downside was heavy use of Stack. It feels like this package's approach is similar to DeferPointer, just replacing Stack with its own RenderObject.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗