hit 0.1.0
hit: ^0.1.0 copied to clipboard
Decouple Flutter layout size from hit-test size, and deliver taps outside parent bounds.
hit #
Separate paint/layout size from hit size in Flutter, and deliver taps that fall outside a widget’s layout box.
Use it when a control must stay visually small (icon, grip, 1px edge, chip ×) but still meet a comfortable / WCAG touch target without pushing neighbors.
Deferred out-of-bounds hit testing is inspired by defer_pointer (gskinnerTeam/flutter-defer-pointer): a handler higher in the tree (HitScope) receives hits for targets that opt out of local hit-testing (Hit.defer / overflowing HitLayer).
Why #
Flutter layout and hit-testing share the same box. Growing padding to enlarge a tap target also grows layout. Overflowing a child past its parent usually stops receiving hits.
hit splits those concerns:
| Piece | Role |
|---|---|
HitLayer |
Layout follows paintChild; hitChild can be larger and overflow |
HitScope |
Delivers overflow / out-of-bounds hits to registered targets |
Hit.defer / Hit.before |
Explicit deferred hit (and optional paint) outside parent bounds |
Install #
dependencies:
hit: ^0.1.0
import 'package:hit/hit.dart';
Quick start #
Minimum icon with a 48×48 hit target — layout stays 24×24:
HitScope(
child: HitLayer(
alignment: Alignment.center,
behavior: HitTestBehavior.deferToChild,
hitChild: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onPressed,
child: const SizedBox(width: 48, height: 48),
),
paintChild: const IgnorePointer(
child: Icon(Icons.add, size: 24),
),
),
)
Wrap a tight ancestor in HitScope whenever hitChild overflows paintChild. Without it, overflow corners never receive events.
API #
HitLayer #
Two-child render object:
paintChild— visual layer; defines layout sizehitChild— gesture / hover layer; may be largeralignment— where paint sits inside the hit box (Alignment.centerby default)behavior— how paint and hit interact (HitTestBehavior)link— optionalHitLink; defaults to the nearestHitScope
When hitChild overflows layout, the layer registers on the link and local hitTest returns false so Flutter does not clip overflow away. Hits are delivered through HitScope.
Non-overflowing layers stay on the normal local hit path and do not register.
HitScope #
Ancestor that hit-tests (and optionally paints) deferred targets.
HitScope(
// link: myLink, // optional shared HitLink
child: /* … */,
)
- Nesting is supported; nearest scope wins (
HitScope.maybeOf/of). - Prefer many small scopes over one app-wide scope.
- Pass an explicit
linkto register with an outer scope instead of the nearest one.
RenderHitScope does not clip deferred hits to its own size, but intermediate parents (ClipRect, tight boxes that reject outside hits) above the scope can still block the walk.
Hit.defer / Hit.before #
For widgets that hang outside a parent without using HitLayer:
HitScope(
child: SizedBox(
width: 100,
height: 100,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
bottom: -20,
child: Hit.defer(
child: GestureDetector(
onTap: onBadgeTap,
child: const Badge(),
),
),
),
// …
],
),
),
)
Hit.defer— deferred hit; optionalpaintOnTop: trueto paint after the scoped subtreeHit.before— deferred hit and paint under the scoped subtree
Local hitTest is always false; delivery is only via the scope.
HitTestBehavior #
| Value | Meaning |
|---|---|
translucent |
Test paint and hit when both overlap |
deferToChild |
Prefer paint; hit only if paint missed |
opaque |
Same as defer for the layer; a deferred opaque hit stops scanning further deferred targets in the scope |
HitLink #
Registry of deferred targets for a scope. Usually owned by HitScope; pass explicitly to share or target a non-nearest scope.
Performance notes #
Deferred hit-testing is O(n) over registered targets on that scope (AABB cached per hit-test pass; transforms are recomputed so scroll stays correct). To keep it fast:
- Keep
HitScopetight around overflow regions - Prefer non-overflowing
HitLayerwhen the hit fits in paint size (no registration) - Prefer
deferToChild/opaqueovertranslucentwhen you do not need dual hits - Keep
hitChildshallow (GestureDetector+SizedBox) - Avoid nesting deferred targets under one huge root scope
- Use
Hit.deferonly when you need out-of-bounds delivery
A handful of min-target / edge / handle layers is cheap. Hundreds of deferred targets under one scope is not.
Example #
cd example
flutter run
Demos include icon buttons, resize handles, window edges, chip/tab dismiss, list/sliver row actions, dense toolbars, slider thumbs, and Hit.defer overflow.
License #
MIT — see LICENSE.