Leveraging Flutter’s Rect for Precise Hit‑Testing in Custom Widgets
Use Flutter’s Rect class to build precise, high‑performance hit‑testing logic in custom widgets. Learn how to override RenderBox.hitTest, cache Rects, and validate the approach with a draggable square example.
10 Aug 2026, 06:36 UTC

Why Rect Matters for Hit‑Testing
When you build a custom widget that needs to respond to taps, the default GestureDetector works fine for most cases. But if your widget has a non‑rectangular shape, a custom hit area, or needs to enforce boundaries during drag operations, you’ll hit the limits of a simple detector. Rect – a lightweight, immutable rectangle representation in dart:ui – lets you define hit areas precisely and perform hit‑testing with Rect.contains. This approach is faster than repeatedly querying the widget tree and gives you full control over the coordinate space.
Understanding the Rect API
Rect.fromLTWH(left, top, width, height)– the most common constructor.- Properties:
left,top,right,bottom,width,height,center. - Methods:
contains(Offset point),overlaps(Rect other),inflate(double delta),deflate(double delta). - Immutability: every method returns a new
Rect; you can’t modify aRectin place.
Implementing Custom Hit‑Testing with a RenderBox
Below is a minimal RenderBox that uses a Rect to define its hit area and to constrain a draggable child. The key is overriding hitTest and delegating to Rect.contains.
class _DraggableSquareRenderBox extends RenderBox {
Size _size = const Size.square(100);
Offset _dragOffset = Offset.zero;
bool _isDragging = false;
@override
void performLayout() {
size = constraints.constrain(_size);
}
// The hit area is the same as the box’s size.
Rect get _hitRect => Rect.fromLTWH(0, 0, size.width, size.height);
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (_hitRect.contains(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
@override
void paint(PaintingContext context, Offset offset) {
final paint = Paint()..color = _isDragging ? Colors.blue : Colors.orange;
context.canvas.drawRect(offset << _hitRect, paint);
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent) {
_isDragging = true;
_dragOffset = event.localPosition;
} else if (event is PointerMoveEvent) {
final delta = event.localPosition - _dragOffset;
_dragOffset = event.localPosition;
// Constrain movement to the parent’s bounds.
final parentRect = Rect.fromLTWH(0, 0, constraints.maxWidth, constraints.maxHeight);
final newOffset = (offset + delta).clamp(parentRect.topLeft, parentRect.bottomRight - size);
markNeedsLayout();
// Update the box’s position by translating the offset.
// In a real implementation, you would store the offset in a stateful widget.
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
_isDragging = false;
}
}
}
class DraggableSquare extends LeafRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) => _DraggableSquareRenderBox();
}
Key points in the example:
hitTestusesRect.containsto decide if a pointer event belongs to this box.- The hit area can be any shape by replacing
_hitRectwith a customRect(e.g., inflated to create a margin). - Because
Rectis immutable, we avoid mutating state during hit‑testing; we simply create a newRectwhen needed.
Performance Considerations
Using Rect for hit‑testing is generally cheaper than the default GestureDetector because:
- It skips the widget tree traversal – the
RenderBoxreceives the event directly. - Hit‑testing is a single
containscall, which is O(1).
However, Rect is immutable. If you recompute a new Rect on every frame (e.g., inside hitTest when the size changes), you can incur allocation overhead. The trade‑off is simple: cache the Rect when the size is stable, and only rebuild it when performLayout runs.
Validating the Approach
- Run the minimal app with the
DraggableSquarewidget inside aContainerthat haswidth: 300andheight: 300. - Enable
debugPrintRepaintRainbowEnabled = trueto visualize repaint boundaries. - Open
Debug Consoleand search forhitTestCount; you should see fewer hit‑test calls than with aGestureDetectorwrapping the same square. - Profile the app with Flutter DevTools
Performancetab and confirm that thehitTestmethod is called only when a pointer lands inside the rectangle.
When to Use Rect Over GestureDetector
- Custom shapes or non‑rectangular hit areas.
- High‑frequency hit‑testing, such as in games or drag‑drop interfaces.
- When you need to enforce movement constraints during a drag.
- When you want to reduce widget overhead in a tight performance loop.
Actionable Steps to Integrate Rect
- Create a custom
RenderBoxor extend an existing one. - Define a
Rectthat represents the desired hit area. - Override
hitTestand callRect.containswith the pointer’s local position. - Cache the
RectinperformLayoutif the size doesn’t change per frame. - Test with a minimal app, check hit‑test counts, and profile with DevTools.
- Iterate on the hit area if you need a margin or a custom shape.
By following this pattern you’ll get a lightweight, precise hit‑testing mechanism that scales well with complex custom widgets.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.