Проверка попаданий и оптимизация перерисовки
Реализуйте собственную проверку попаданий и логику shouldRepaint, чтобы рисование оставалось производительным.
«Проверка попаданий и оптимизация перерисовки» — бесплатный урок Flutter Mobile Development на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Flutter Mobile Development, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Flutter Mobile Development содержит 4 уроков всего.
Части этого урока еще не переведены и отображаются на английском.
Why Hit Testing and Repaint Matter
When you draw with CustomPainter you take full control of the canvas. But two costs are easy to overlook: hit testing (deciding which pixels respond to taps) and repainting (re-running your paint code every frame).
- A naive painter that always returns
truefromshouldRepaintrepaints on every rebuild, burning GPU and battery. - By default a custom-painted region is transparent to pointer events, so taps fall through to widgets behind it.
This lesson shows how to make custom drawings both interactive and efficient.
Anatomy of a CustomPainter
A CustomPainter exposes two key overrides: paint for drawing and shouldRepaint for deciding when to re-run paint. There is also an optional hitTest for pointer logic.
paint(Canvas canvas, Size size)— your drawing commands.shouldRepaint(covariant CustomPainter old)— returntrueonly when visual inputs changed.hitTest(Offset position)— returntrue/false/nullto control pointer routing.
class CirclePainter extends CustomPainter {
final Color color;
final double radius;
CirclePainter({required this.color, required this.radius});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
canvas.drawCircle(size.center(Offset.zero), radius, paint);
}
@override
bool shouldRepaint(covariant CirclePainter old) =>
old.color != color || old.radius != radius;
}shouldRepaint: The Core Optimization
shouldRepaint is called when a new painter instance is supplied to CustomPaint. If it returns false, Flutter skips the paint phase and reuses the cached layer.
- Compare only the fields that affect pixels.
- Returning
trueunconditionally defeats the optimization. - Returning
falsewhen something did change causes stale, frozen visuals.
Compare values, not identity — two painter instances with equal fields should yield false.
@override
bool shouldRepaint(covariant CirclePainter old) {
// Repaint ONLY when a visual input differs.
return old.color != color || old.radius != radius;
}
// Anti-pattern: always repaints, no matter what.
// bool shouldRepaint(old) => true;Repaint Boundaries Isolate Work
Even a correct shouldRepaint can't help if your painter shares a layer with a frequently-rebuilding subtree. CustomPaint automatically wraps itself in a RepaintBoundary when given a child, but for a standalone painter you often add one explicitly.
- A
RepaintBoundarygives the painted region its own compositing layer. - Repaints inside the boundary don't force ancestors or siblings to repaint.
- Use the Flutter DevTools "Highlight Repaints" overlay to see which layers flash.
RepaintBoundary(
child: CustomPaint(
size: const Size(200, 200),
painter: CirclePainter(color: Colors.blue, radius: 60),
),
)Default Hit Testing Behavior
By itself a CustomPaint does not capture pointer events on the painted shapes — its hitTest defaults to returning null, meaning "defer to default box behavior." To make drawings tappable you wrap them in a gesture detector and/or override hitTest.
- Return
true: this position is a hit; consume the event. - Return
false: not a hit; let it pass to widgets behind. - Return
null: use the default (the whole bounding box is a hit if opaque).
Implementing Precise hitTest
Override hitTest to restrict taps to the actual drawn shape rather than the rectangular bounds. For a circle, test the distance from the center against the radius.
This lets the corners of the bounding box pass touches through to whatever is behind, which feels natural for non-rectangular art.
class CirclePainter extends CustomPainter {
final Color color;
final double radius;
final Offset center;
CirclePainter({required this.color, required this.radius, required this.center});
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(center, radius, Paint()..color = color);
}
@override
bool hitTest(Offset position) {
// True only inside the circle, not the whole box.
return (position - center).distance <= radius;
}
@override
bool shouldRepaint(covariant CirclePainter old) =>
old.color != color || old.radius != radius || old.center != center;
}Modeling Distance Without Flutter
The math behind circular hit testing is just the Euclidean distance. Here is a pure-Dart model you can run on any judge to verify the logic before wiring it into hitTest.
A point is inside the circle when its distance to the center is less than or equal to the radius.
import 'dart:math';
bool insideCircle(double px, double py, double cx, double cy, double r) {
final dx = px - cx;
final dy = py - cy;
return sqrt(dx * dx + dy * dy) <= r;
}
void main() {
const cx = 100.0, cy = 100.0, r = 60.0;
print(insideCircle(120, 110, cx, cy, r)); // true (inside)
print(insideCircle(10, 10, cx, cy, r)); // false (corner)
print(insideCircle(160, 100, cx, cy, r)); // true (on edge)
}Hit Testing with Path.contains
For arbitrary shapes, build a Path in paint and reuse it in hitTest via Path.contains. Storing the path as a field avoids rebuilding it twice per frame.
path.contains(position)returnstruewhen the point lies inside the filled region.- Cache the path; rebuild it only when geometry inputs change.
class StarPainter extends CustomPainter {
final Path _path;
final Color color;
StarPainter({required this.color, required Size size})
: _path = _buildStar(size);
static Path _buildStar(Size size) {
final p = Path();
p.moveTo(size.width / 2, 0);
p.lineTo(size.width, size.height);
p.lineTo(0, size.height);
p.close();
return p;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(_path, Paint()..color = color);
}
@override
bool hitTest(Offset position) => _path.contains(position);
@override
bool shouldRepaint(covariant StarPainter old) => old.color != color;
}Repainting Only on Animation Ticks
For animated painters, pass the driving Listenable (such as an AnimationController) to CustomPainter's repaint super-parameter. The painter then repaints when the animation ticks — without rebuilding the whole widget tree.
super(repaint: animation)wires the painter to listen directly.- This skips
setStateand avoids rebuilding siblings every frame.
class PulsePainter extends CustomPainter {
final Animation<double> animation;
PulsePainter(this.animation) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final r = 20 + animation.value * 40;
canvas.drawCircle(
size.center(Offset.zero), r, Paint()..color = Colors.teal);
}
// No need to compare values: the repaint Listenable drives it.
@override
bool shouldRepaint(covariant PulsePainter old) => false;
}Wiring Gestures to a Painter
Hit testing on the painter decides whether the painted region is opaque to pointers, but you still attach a GestureDetector to react. Combine them: precise hitTest plus a detector gives shape-accurate taps.
- The detector's
onTapDowngives a localOffset. - You can re-run the same geometry check to identify which sub-shape was hit.
GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTapUp: (details) {
final local = details.localPosition;
if ((local - center).distance <= radius) {
debugPrint('Circle tapped at $local');
}
},
child: CustomPaint(
size: const Size(200, 200),
painter: CirclePainter(
color: Colors.blue, radius: 60, center: const Offset(100, 100)),
),
)A Repaint Optimization Checklist
Bring it together with a practical checklist for performant custom painting:
- Compare values in
shouldRepaint— never blanket-returntrue. - Use
repaint:for animations instead ofsetState. - Wrap in
RepaintBoundaryto isolate the painted layer. - Cache
PathandPaintobjects; don't allocate per frame. - Override
hitTestso non-rectangular art passes through unused space. - Profile with DevTools repaint highlighting and the timeline.
Quick Check: shouldRepaint
Consider what your shouldRepaint implementation should return to keep painting performant.
Recap
You learned how to keep custom-painted Flutter UIs both interactive and fast:
- shouldRepaint should compare only visual inputs by value, returning
truejust when they change. - hitTest controls pointer routing: precise shape tests let non-rectangular art pass unused taps through.
- Path.contains and distance math give shape-accurate hit detection; cache the path.
- RepaintBoundary isolates the painted layer so repaints don't cascade.
- repaint: wires animations directly to the painter, avoiding full rebuilds.
Together these techniques eliminate wasted frames and make your canvas drawings feel native and responsive.
Часто задаваемые вопросы
Урок «Проверка попаданий и оптимизация перерисовки» бесплатный?
Да — полный текст урока «Проверка попаданий и оптимизация перерисовки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Flutter Mobile Development, подпишись на CoddyKit PRO. Курс Flutter Mobile Development содержит 4 уроков всего.
Чему я научусь в уроке «Проверка попаданий и оптимизация перерисовки»?
Реализуйте собственную проверку попаданий и логику shouldRepaint, чтобы рисование оставалось производительным. Ты практикуешь Flutter Mobile Development с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Flutter Mobile Development?
Предыдущий опыт не требуется. Flutter Mobile Development на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Проверка попаданий и оптимизация перерисовки»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Flutter Mobile Development?
Да. Каждый урок Flutter Mobile Development включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- API Canvas: контуры, кисти и шейдеры
- Создание интерактивного пользовательского виджета диаграммы
- Обрезка, режимы наложения и композиция слоёв
- Проверка попаданий и оптимизация перерисовки