Начнём вот с чего: не путайте CustomPaint и CustomPainter.

CustomPainter — это абстрактный класс. В его наследнике мы переопределяем методы, с помощью которых можно раскрашивать и создавать элементы пользовательского интерфейса. Он позволяет полностью контролировать внешний вид, начиная от простых форм и линий и заканчивая сложной графикой.

А CustomPaint — виджет, который встраивает наследника CustomPainter в дерево виджетов и предоставляет холст для рисования. О нём мы подробно поговорим ниже, а пока рассмотрим наследника CustomPainter.

class Painter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
  }

  @override
  bool shouldRepaint(covariant Painter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}

Как видите, для рисования нам нужно реализовать два метода: paint и shouldRepaint.

paint

Метод вызывается каждый раз, когда нужно отрисовать CustomPainter, — в теле этого метода будут описываться все операции рисования.

Он принимает два аргумента:

  • объект Canvas (подробнее о нём расскажем далее);
  • объект Size, который предоставляет max constraints, переданные родительским виджетом.

Одна важная особенность CustomPainter заключается в том, что можно нарисовать что-нибудь и за пределами переданных размеров, но нет гарантии, что отрисованный объект не будет обрезан или не перекроет соседние виджеты, поэтому стоит избегать рисования за пределами переданных размеров.

shouldRepaint

Возвращает булево значение, на основе которого принимается решение, надо ли вызывать метод paint для перерисовки или нет. Вызывается каждый раз, когда новый объект CustomPainter устанавливается в RenderObject-дереве объекту RenderCustomPaint. На деле чаще всего это означает изменение параметров, переданных в конструкторе CustomPainter.

Теперь давайте подробнее поговорим CustomPaint и самой важной части этого виджета — Canvas.

CustomPaint

Виджет, через который CustomPainter встраивается в дерево. Создаёт Canvas (холст) для рисования переданного CustomPainter. Основные параметры:

  • painter — объект типа CustomPainter, который рисуется перед переданным виджетом child.
  • foregroundPainter — объект типа CustomPainter, который рисуется после переданного виджета child.
  • child — виджет, который рисуется после painter и до foregroundPainter. Canvas (холст) принимает размеры переданного виджета. Этот параметр опциональный и может быть не заполнен, в таком случае размеры Canvas (холста) берутся из параметра size.
  • size — объект типа Size. Задаёт размеры Canvas (холста) в том случае, если не был передан параметр child. Не обязателен для заполнения в случае передачи child.
  • isComplex — флаг, указывающий на необходимость кэширования CustomPainter. Установка флага может быть уместна, если в методе paint выполняется много операций вычисления, происходит работа со слоями, трансформация холста.
  • willChange — флаг, указывающий что CustomPainter в скором времени будет перерисован и нет необходимости кэшировать его. Установка флага может быть уместна, если CustomPainter находится в процессе анимации и в следующем кадре изменит своё состояние.

Canvas

Canvas (англ. холст) — это фундаментальный инструмент, который лежит в основе механизма отрисовки чего либо на экране. Canvas подобно Декартовой системе координат имеет две оси — X (горизонтальная) и Y (вертикальная). Но есть одно важное отличие: вектор оси Y направлен не вверх, а вниз. Таким образом начало координат находится в левом верхнем углу.

flt

С подготовкой закончили, в следующей главе приступим непосредственно к рисованию на холсте.

Графика

Рисование на Сanvas можно сравнить с рисованием в программе Paint — мы выбираем цвет, выбираем фигуру и просто переносим нашу фигуру на холст.

Класс Canvas содержит большое количество методов для рисования различных геометрических фигур, линий и кривых. Познакомимся с ним на практике и что-нибудь нарисуем. Например, домик.

Для этого создадим наследника класса CustomPainter:

class HousePainter extends CustomPainter {
  const HousePainter();

  @override
  void paint(Canvas canvas, Size size) { }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Пока что это просто белый холст, но с каждым новым изученным методом мы будем дорисовывать нужные элементы, чтобы в конечном итоге получилось изображение дома.

Group

drawPaint

Для начала перекрасим фон в цвет неба. Для этого воспользуемся методом drawPaint, который принимает объект Paint. В Paint задаём цвет, который хотим нанести на холст:

...	
  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
  }

  /// Рисуем небо
  void _drawSky(Canvas canvas) {
    canvas.drawPaint(Paint()..color = Colors.blue.shade300);
  }
...

Group

А что за объект Paint?

Это декларация того, как будет отрисован объект на холсте. Вот его основные параметры:

  • сolor — позволяет задать цвет, которым будет отрисован объект; по умолчанию прозрачный;

  • style — режим отрисовки фигуры;

    • PaintingStyle.stroke - рисовать только контур переданным цветом (сolor) и шириной (strokeWidth).
    • PaintingStyle.fill - рисовать переданным цветом (сolor) тело фигуры и контур шириной (strokeWidth).
  • strokeWidth — ширина линии контура фигуры;

    💡 Хотел бы также акцентировать внимание одном нюансе, который мне кажется не совсем явным. Ширина линии добавляется по половине с каждой стороны от контура по которому проходит линия, стоит это учитывать, особенно если необходимо нарисовать фигуру по размеру холста.

    В примере нарисованы несколько кругов которые отличаются только параметром strokeWidth. Радиус равен половине ширины экрана и хорошо видно что круг не влезает полностью. Об этом необходимо помнить и учитывать strokeWidth/2 в размерах фигур.

    Untitled

  • strokeCap — вид концов линий.

    • StrokeCap.butt - линия заканчивается в координатах которые были указаны в параметрах. Тык.
    • StrokeCap.round- на концы линии добавляется окружность с радиусом в половину ширину контура (strokeWidth/2). Т.е. итоговый размер линии увеличивается на ширину контура (strokeWidth). Тык.
    • StrokeCap.square - на концы линии добавляется прямоугольник с шириной в половину ширину контура (strokeWidth/2). Т.е. итоговая ширина линии увеличивается на ширину контура (strokeWidth). Тык.

💡 Важно: мы перечислили только самые популярные параметра класса Paint, которые нужны в большинстве случаев. Остальные параметры рассмотрим на практике далее по тексту.

drawLine

Теперь нарисуем траву, на которой будет стоять дом. Тут нам поможет метод drawLine — он рисует прямую линию из одной координаты в другую:

...	
  static const _groundHeight = 100.0;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
  }

  /// Рисуем траву
  void _drawGround(Canvas canvas, Size size) {
    final painter = Paint()
      // Цвет травы
      ..color = Colors.green.shade300
      // Ширина линии в пикселях
      ..strokeWidth = _groundHeight;

    // Y-координата линии
    final dY = size.height - painter.strokeWidth / 2;

    final startPoint = Offset(0, dY);
    final endPoint = Offset(size.width, dY);

    canvas.drawLine(
      startPoint,
      endPoint,
      painter,
    );
  }
...

Group

Координата Y рассчитывается так:

...
final dY = size.height - painter.strokeWidth / 2;
...
  • size.height — самая нижняя координата выделенного под холст пространства, в данном случае самый низ экрана до системных кнопок.
  • painter.strokeWidth / 2 — середина линии. Важно всегда учитывать параметр strokeWidth при позиционировании фигур на холсте.

drawRect

Нарисуем наконец стены домика. Наш дом будет прямоугольной формы, и для изображения фигуры воспользуемся методом drawRect.

Принимает параметры:

  • rect — объект Rect, представляющий прямоугольник с координатами;
  • paint — объект Paint для настройки визуализации фигуры.
...
  static const _houseHeight = 275.0;
  static const _houseWidth = 250.0;
  static const _houseOnGroundOffset = 20.0;

	@override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
  }

  /// Рисуем стены
  void _drawWalls(Canvas canvas, Size size) {
    final painter = Paint()
      // Цвет стены
      ..color = Colors.pink.shade300;

    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfHouseWidth = _houseWidth / 2;

    final housePositionOnGround = size.height - _groundHeight + _houseOnGroundOffset;

    // Нижняя левая координата дома
    final leftBottomCornerPoint = Offset(
      horizontalCenter - halfOfHouseWidth,
      housePositionOnGround,
    );

    // Верхняя правая координата дома
    final rightTopCornerPoint = Offset(
      horizontalCenter + halfOfHouseWidth,
      housePositionOnGround - _houseHeight,
    );

    final rect = Rect.fromPoints(
      leftBottomCornerPoint,
      rightTopCornerPoint,
    );

    canvas.drawRect(rect, painter);
  }
...

Group

drawRRect

Пока что это не сильно похоже на домик, предлагаю запилить ему дверь. Для этого воспользуемся методом drawRRect. Метод очень похож на предыдущий, но позволяет также закруглить углы прямоугольника. Принимает параметры:

  • rrect — объект RRect, представляющий прямоугольник с закруглёнными углами с координатами. Для создания объекта можно использовать именованный конструктор RRect.fromRectAndRadius. Он принимает объект Rect, который уже был упомянут ранее в разделе drawRect , и объект Radius, который представляет конфигурацию закругления углов.
  • paint — объект Paint для настройки визуализации фигуры.
...
  static const _doorHeight = _houseHeight * 0.5;
  static const _doorWidth = _houseWidth * 0.3;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
    _drawDoor(canvas, size);
  }

  /// Рисуем дверь
  void _drawDoor(Canvas canvas, Size size) {
    final painter = Paint()
      // Цвет двери
      ..color = Colors.orange.shade700;

    // Радиус закругления углов
    const radius = Radius.circular(16);

    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfHouseWidth = _houseWidth / 2;

    final housePositionOnGround =
        size.height - _groundHeight + _houseOnGroundOffset;

    final doorBottomY = housePositionOnGround - 10;

    final doorLeftX = horizontalCenter - halfOfHouseWidth + 20;

    // Нижняя левая координата двери
    final leftBottomCornerPoint = Offset(
      doorLeftX,
      doorBottomY,
    );

    // Верхняя правая координата двери
    final rightTopCornerPoint = Offset(
      doorLeftX + _doorWidth,
      doorBottomY - _doorHeight,
    );

    final rect = Rect.fromPoints(
      leftBottomCornerPoint,
      rightTopCornerPoint,
    );

    final rrect = RRect.fromRectAndRadius(
      rect,
      radius,
    );

    canvas.drawRRect(rrect, painter);
  }
...

Group

drawOval

У двери не хватает ручки, это нужно исправить. Нарисуем её с помощью метода drawOval.

Метод рисует эллипс, вписанный в переданный прямоугольник. Принимает параметры:

  • rect — объект Rect, представляющий прямоугольник с координатами;
  • paint — объект Paint для настройки визуализации фигуры.
...
  static const _doorHandleHeight = 5.0;
  static const _doorHandleWidth = 15.0;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
    _drawDoor(canvas, size);
    _drawDoorHandle(canvas, size);
  }

  /// Рисуем дверную ручку
  void _drawDoorHandle(Canvas canvas, Size size) {
    final painter = Paint()
      // Цвет ручки
      ..color = Colors.black;
		
    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfHouseWidth = _houseWidth / 2;

    final housePositionOnGround =
        size.height - _groundHeight + _houseOnGroundOffset;

    final doorBottomY = housePositionOnGround - 10;

    final doorLeftX = horizontalCenter - halfOfHouseWidth + 20;

    // Нижняя левая координата двери
    final leftBottomCornerPoint = Offset(
      doorLeftX,
      doorBottomY,
    );

    // Верхняя правая координата двери
    final rightTopCornerPoint = Offset(
      doorLeftX + _doorWidth,
      doorBottomY - _doorHeight,
    );

    final doorRect = Rect.fromPoints(
      leftBottomCornerPoint,
      rightTopCornerPoint,
    );
		
		// Позиция дверной ручки относительно правой стороны двери по центру
    final doorHandleCenter = doorRect.centerRight.translate(-15, 0);
    final doorHandleRect = Rect.fromCenter(
      center: doorHandleCenter,
      width: _doorHandleWidth,
      height: _doorHandleHeight,
    );

    canvas.drawOval(doorHandleRect, painter);
  }
...

Group

drawCircle

Прорубим в домике круглое окно. Для этого используем метод drawCircle. Метод принимает 3 параметра:

  • center — координата центра окружности;
  • radius — радиус окружности;
  • paint — объект Paint для настройки визуализации фигуры.

💡 Метод drawOval также можно использовать для отрисовки окружности/круга, так как окружность — это частный случай эллипса, вписанного в квадрат.

...
  static const _windowRadius = 35.0;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
    _drawDoor(canvas, size);
    _drawDoorHandle(canvas, size);
    _drawWindow(canvas, size);
  }

  /// Рисуем окно
  void _drawWindow(Canvas canvas, Size size) {
    final painter = Paint()..color = Colors.blue.shade600;

    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfHouseWidth = _houseWidth / 2;

    final housePositionOnGround =
        size.height - _groundHeight + _houseOnGroundOffset;

    final windowCenterY = housePositionOnGround - _doorHeight * 0.7;

    final windowCenterX =
        horizontalCenter + halfOfHouseWidth - _windowRadius - 40;

    final windowCenter = Offset(windowCenterX, windowCenterY);

    canvas.drawCircle(
      windowCenter,
      _windowRadius,
      painter,
    );
  }
...

Group

drawArc

Над окошком не хватает навеса. А раз оно у нас круглое, то и навес должен быть в форме дуги. Тут мы воспользуемся методом drawArc. Он принимает параметры:

  • rect — объект Rect, представляющий прямоугольник с координатами.
  • startAngle — угол в радианах с которого начинается дуга. В системе координат холста вектор оси Y направлен вниз, поэтому угол будет внизу окружности.
  • sweepAngle — угол в радианах на который сдвигается. Чтобы было проще понять, можно представить, что образуется дуга от 0 до sweepAngle и сдвигается на startAngle.

flt

  • useCenter — если true, то соединяет дугу с центром окружности, образуя сегмент.
  • paint — объект Paint для настройки визуализации фигуры.
...
  static const _windowRoofThickness = 10.0;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
    _drawDoor(canvas, size);
    _drawDoorHandle(canvas, size);
    _drawWindow(canvas, size);
    _drawWindowRoof(canvas, size);
  }

  /// Рисуем крышу окна
  void _drawWindowRoof(Canvas canvas, Size size) {
    final painter = Paint()
      ..color = Colors.pink.shade400
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = _windowRoofThickness;

    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfHouseWidth = _houseWidth / 2;

    final housePositionOnGround =
        size.height - _groundHeight + _houseOnGroundOffset;

    final windowCenterY = housePositionOnGround - _doorHeight * 0.7;
    final windowCenterX =
        horizontalCenter + halfOfHouseWidth - _windowRadius - 40;

    final windowCenter = Offset(windowCenterX, windowCenterY);

    // Размер окна + половина от ширины крыши
    const rectSize = _windowRadius * 2 + _windowRoofThickness / 2;

    final rect = Rect.fromCenter(
      center: windowCenter,
      width: rectSize,
      height: rectSize,
    );

    canvas.drawArc(
      rect,
      // Угол с которого начинаем дугу
      math.pi * 7 / 6,
      // Продолжительность дуги
      math.pi * 4 / 6,
      false,
      painter,
    );
  }
...

Group

drawPath

Итак, дом практически готов, остался финальный элемент — крыша. Пусть она будет классической треугольной формы.

Вы могли подумать, что по аналогии с предыдущими методами мы воспользуемся методом drawTriangle. Однако такого метода у объекта Canvas нет, и здесь на помощь нам придёт метод drawPath. Он принимает объекты Path и Painter в качестве входных параметров.

Path

Инструмент для отрисовки векторной графики. Позволяет задать любую фигуру набором методов, описывающих путь от начальной точки до конечной через любые промежуточные координаты. Содержит набор добавленных точек и текущую позицию.

Методы

Рассмотрим некоторые методы класс Path для лучшего понимания концепции. Методы класса Path оперируют:

  • абсолютными координатами — принимают координаты [x, y], которые равны координатам на холсте.
  • относительными координатами — принимают [x, y], которые определяют координату как смещение от текущей позиции Path на переданное значение.

Path и параметры Paint

Данные параметры класса Paint используются для настройки соединения сегментов Path.

  • strokeJoin — вид соединения сегментов линии.
    • StrokeJoin.miter — внешние стороны сегментов продлеваются, чтобы образовать острый угол. При определённом пороге настраиваемом с помощью параметра strokeMiterLimit принимает вид StrokeJoin.bevel.
    • StrokeJoin.round — добавляет закругление на месте соединения сегментов.
    • StrokeJoin.bevel — внешние стороны сегментов обрезаются, придавая скошенный вид.
  • strokeMiterLimit — пороговое значение, при котором StrokeJoin.miter заменяется на StrokeJoin.bevel. По умолчанию 4.0.
...
  static const _houseRoofHeight = 75.0;
  static const _houseRoofHorizontalOffset = 25.0;
  static const _houseRoofVerticalOffset = 10.0;

  @override
  void paint(Canvas canvas, Size size) {
    _drawSky(canvas);
    _drawGround(canvas, size);
    _drawWalls(canvas, size);
    _drawDoor(canvas, size);
    _drawDoorHandle(canvas, size);
    _drawWindow(canvas, size);
    _drawWindowRoof(canvas, size);
    _drawHouseRoof(canvas, size);
  }

  /// Рисуем крышу дома
  void _drawHouseRoof(Canvas canvas, Size size) {
    final painter = Paint()..color = Colors.pink.shade400;

    // Координата X середины холста
    final horizontalCenter = size.width / 2;
    // Половина от ширины дома
    const halfOfRoofWidth = _houseWidth / 2 + _houseRoofHorizontalOffset;

    final housePositionOnGround =
        size.height - _groundHeight + _houseOnGroundOffset;
    final roofBottomY =
        housePositionOnGround - _houseHeight + _houseRoofVerticalOffset;

    // Левый угол крыши
    final roofLeftX = horizontalCenter - halfOfRoofWidth;
    // Правый угол крыши
    final roofRightX = horizontalCenter + halfOfRoofWidth;

    final path = Path()
      // Сдвигаем курсор на левый угол крыши
      ..moveTo(roofLeftX, roofBottomY)
      // Рисуем линию с левого угла крыши до центра
      ..lineTo(
        horizontalCenter,
        housePositionOnGround - _houseHeight - _houseRoofHeight,
      )
      // Рисуем линию центра до правого угла крыши
      ..lineTo(roofRightX, roofBottomY)
      // Соединяем начальную позицию с конечной
      ..close();

    canvas.drawPath(path, painter);
  }
...

Group

Вот такой замечательный домик у нас получился — теперь оживим картинку и анимируем смену дня и ночи.

Анимация

Конструктор CustomPainter принимает параметр Listenable repaint.

CustomPainter перерисовывается каждый раз, когда repaint уведомляет о своём изменении. Обратите внимание, что при отрисовке анимации в виджете не вызывается setState и shouldRepaint всегда возвращает falseCustomPainter сам определяет, когда перерисовать холст, прослушивая объект Listenable.

Основной плюс в том, что мы перерисовываем только холст и нет необходимости вызывать setState или использовать AnimationBuilder для обновления UI под новое значение анимации.

Оживляем пример

Для имитации смены дня и ночи будем перекрашивать фон, а также включать свет в окне.

Для начала объявим поле Animation<double> timeOfDayAnimation — его мы передаём в суперконструктор, и по его значению мы будем определять, какое время суток отображать.

...  
	/// Value 0.0 - night
  /// Value 1.0 - day
  final Animation<double> timeOfDayAnimation;

  const HousePainter({
    required this.timeOfDayAnimation,
  }) : super(repaint: timeOfDayAnimation);

Теперь поменяем метод _drawSky: будем менять цвет неба в зависимости от значения timeOfDayAnimation.

...
static final ColorTween _skyTween = ColorTween(
    end: Colors.blue.shade300,
    begin: Colors.grey.shade700,
  );

...

	/// Рисуем небо
  void _drawSky(Canvas canvas) {
    canvas.drawPaint(Paint()
      ..color = _skyTween.evaluate(timeOfDayAnimation) ?? Colors.transparent);
  }
...

Так же сделаем эффект включения света в окне.

...
static final _windowTween = ColorTween(
    end: Colors.blue.shade600,
    begin: Colors.yellow.shade600,
  ).chain(CurveTween(curve: Curves.easeInOutQuint));

...

	/// Рисуем окно
  void _drawWindow(Canvas canvas, Size size) {
    final painter = Paint()
      ..color = _windowTween.evaluate(timeOfDayAnimation) ?? Colors.transparent;
		...
	}
...

Итоговый результат можно посмотреть ниже.

На этом пример с домиком можно считать завершённым. Мы рассмотрели основные методы, с помощью которых можно рисовать различные геометрические фигуры на холсте.

Далее разберёмся как писать на холсте текст.

Текст

Для отрисовки на холсте текста воспользуемся классом TextPainter. Процесс отрисовки можно разбить на три этапа:

  • декларация;
  • расчёт размеров;
  • отрисовка.

Рассмотрим каждый этап подробнее.

Декларация

Есть два параметра, которые обязательно необходимо передать TextPainter, чтобы нарисовать текст, — это text и textDirection:

  • text — принимает наследников InlineSpan, т. е. TextSpan и WidgetSpan;
  • textDirection — принимает enum TextDirection, который определяет направление текста.

Необязательные параметры TextDirection, скорее всего, будут знакомы читателю по виджетам Text и RichText, поэтому подробно рассматривать их смысла нет.

Расчёт размеров

Следующим этапом является вызов метода layout. Принимает два опциональных параметра:

  • minWidth — минимальная ширина, которая выделяется тексту для расположения. По умолчанию 0.0.

💡 Если вы хотите поменять выравнивание для однострочного текста, необходимо передать минимальную ширину больше ширины строки. Например, size.width холста.

  • maxWidth — максимальная ширина которая выделяется тексту для расположения. По умолчанию double.infinity.

💡 Чтобы текст «перетекал» на следующую строку, необходимо ограничить максимальную ширину. Например, передать size.width холста.

С помощью этого метода рассчитывается положение символов с учётом стиля текста, других переданных параметров в размерах, установленных через minWidth и maxWidth. После вызова метода layout можно обратиться к полю size, чтобы получить итоговые размеры переданного текста.

Отрисовка

Последний этап — вызов метода paint . Принимает два параметра:

  • canvas — объект Canvas холст, на котором будет отрисован текст.
  • offset — объект Offset для позиционирования текста на холсте. Задаёт смещение текста по координатам холста относительно верхнего левого угла.
class TextCustomPainter  extends CustomPainter {
  const TextCustomPainter ();

  @override
  void paint(Canvas canvas, Size size) {
    // Координата Y середины холста
    final verticalCenter = size.height / 2;
    // Смещение
    final offset = Offset(0, verticalCenter);

    TextPainter(
      text: const TextSpan(
        text: 'Текст написан с помощью CustomPainter',
        style: TextStyle(color: Colors.black),
      ),
      textDirection: TextDirection.ltr,
      textAlign: TextAlign.center,
    )
      // Расчёт размеров, который занимает текст
      ..layout(minWidth: size.width)
      // Отрисовка текста на холсте
      ..paint(
        canvas,
        offset,
      );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Group

Далее — отрисуем на холсте картинку.

Изображения

На практике изображения с помощью Canvas практически никогда не отрисовываются —это нетривиальная задача, которая требует много императивных операций для получения изображения в нужном формате. Также многие операции (масштабирование или подстраивание изображения под доступные размеры) будут требовать собственной реализации от разработчика.

Для работы с изображениями лучше использовать виджет Image, который предлагает удобный декларативный подход и покрывает большую часть кейсов использования изображения.

Но раз Canvas поддерживает отрисовку изображений — мы не можем о ней не рассказать.

Итак, с помощью Canvas можно отрисовывать в том числе и растровые изображения. Он поддерживает следующие форматы: JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, WBMP.

Работа с изображениями делится на два этапа: загрузку и отрисовку. Расскажем о них подробнее.

Загрузка

Так как загрузка изображения — это асинхронный процесс, получить изображение в методе paint не удастся, и придётся передать его в параметрах конструктора CustomPainter. Это накладывает определённые неудобства и ограничения.

Во-первых, процесс получения изображения требует выполнения ряда низкоуровневых операций:

Future<ui.Image> _loadImage(String imageAssetPath) async {
	// Загружаем из постоянной памяти изображение в виде byte-данных
  final ByteData data = await rootBundle.load(imageAssetPath);
  
	// Преобразуем byte-данные в объект Codec
  final codec = await ui.instantiateImageCodec(
    data.buffer.asUint8List(),
		// Если указать желаемый размер только по одной оси —
		// изображение сохранит оригинальное соотношение сторон
    targetHeight: 300,
  );

	// Извлекаем следующий кадр анимации
  final frame = await codec.getNextFrame();
	// Возвращаем объект Image для этого кадра
  return frame.image;
}

Мы можем указать размеры изображения только заранее (либо не указывать и изображение будет получено в оригинальном размере). Для того чтобы изменить размеры изображения, например, под размер холста, потребуется использовать трансформацию через объект Paint — об этом расскажем далее.

Отрисовка

Для отрисовки изображения на холсте используется метод drawImage. Он принимает параметры:

  • image — объект класса Image из библиотеки dart.ui.
  • offset — объект класса Offset. Смещение левого верхнего угла изображения относительно системы координат холста.
  • paint — объект класса Paint.
class ImagePainter extends CustomPainter {
  final ui.Image image;

  ImagePainter(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawImage(image, const Offset(0, 0), Paint());
  }

  @override
  bool shouldRepaint(ImagePainter oldDelegate) {
    return false;
  }
}

Group

Image и параметры Paint

Данные параметры класса Paint используются для модификации изображения.

  • imageFilter — объект класса ImageFilter. Представляет собой набор операций преобразования растрового изображения.
  • filterQuality — enum FilterQuality. Управляет соотношением производительности и качества, используемым при сэмплинге растровых изображений.
  • invertColors — флаг, инвертирует цвета изображения.

Вот и всё! В этой главе мы научились создавать разнообразные геометрические фигуры и линии на холсте. Кроме того, мы узнали, как добавлять текст и изображения.

В следующем параграфе мы продолжим говорить о CustomPainter — разберём продвинутые аспекты: работу со слоями, использование шейдеров, обрезку и трансформацию холста и многое другое.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф3.5. Продвинутые анимации
Следующий параграф3.7. CustomPainter: продвинутые концепции