跪拜 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.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗