Начнём вот с чего: не путайте 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 направлен не вверх, а вниз. Таким образом начало координат находится в левом верхнем углу.
С подготовкой закончили, в следующей главе приступим непосредственно к рисованию на холсте.
Графика
Рисование на Сanvas
можно сравнить с рисованием в программе Paint — мы выбираем цвет, выбираем фигуру и просто переносим нашу фигуру на холст.
Класс Canvas
содержит большое количество методов для рисования различных геометрических фигур, линий и кривых. Познакомимся с ним на практике и что-нибудь нарисуем. Например, домик.
Для этого создадим наследника класса CustomPainter
:
class HousePainter extends CustomPainter {
const HousePainter();
@override
void paint(Canvas canvas, Size size) { }
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Пока что это просто белый холст, но с каждым новым изученным методом мы будем дорисовывать нужные элементы, чтобы в конечном итоге получилось изображение дома.
drawPaint
Для начала перекрасим фон в цвет неба. Для этого воспользуемся методом drawPaint
, который принимает объект Paint
. В Paint
задаём цвет, который хотим нанести на холст:
...
@override
void paint(Canvas canvas, Size size) {
_drawSky(canvas);
}
/// Рисуем небо
void _drawSky(Canvas canvas) {
canvas.drawPaint(Paint()..color = Colors.blue.shade300);
}
...
А что за объект Paint
?
Это декларация того, как будет отрисован объект на холсте. Вот его основные параметры:
-
сolor
— позволяет задать цвет, которым будет отрисован объект; по умолчанию прозрачный; -
style
— режим отрисовки фигуры;PaintingStyle.stroke
- рисовать только контур переданным цветом (сolor
) и шириной (strokeWidth
).PaintingStyle.fill
- рисовать переданным цветом (сolor
) тело фигуры и контур шириной (strokeWidth
).
-
strokeWidth
— ширина линии контура фигуры;💡 Хотел бы также акцентировать внимание одном нюансе, который мне кажется не совсем явным. Ширина линии добавляется по половине с каждой стороны от контура по которому проходит линия, стоит это учитывать, особенно если необходимо нарисовать фигуру по размеру холста. В примере нарисованы несколько кругов которые отличаются только параметром
strokeWidth
. Радиус равен половине ширины экрана и хорошо видно что круг не влезает полностью. Об этом необходимо помнить и учитыватьstrokeWidth/2
в размерах фигур. -
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,
);
}
...
Координата 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);
}
...
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);
}
...
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);
}
...
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,
);
}
...
drawArc
Над окошком не хватает навеса. А раз оно у нас круглое, то и навес должен быть в форме дуги. Тут мы воспользуемся методом drawArc
. Он принимает параметры:
rect
— объектRect
, представляющий прямоугольник с координатами.startAngle
— угол в радианах с которого начинается дуга. В системе координат холста вектор оси Y направлен вниз, поэтому угол будет внизу окружности.sweepAngle
— угол в радианах на который сдвигается. Чтобы было проще понять, можно представить, что образуется дуга от 0 доsweepAngle
и сдвигается наstartAngle
.
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,
);
}
...
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);
}
...
Вот такой замечательный домик у нас получился — теперь оживим картинку и анимируем смену дня и ночи.
Анимация
Конструктор CustomPainter
принимает параметр Listenable repaint
.
CustomPainter
перерисовывается каждый раз, когда repaint
уведомляет о своём изменении. Обратите внимание, что при отрисовке анимации в виджете не вызывается setState
и shouldRepaint
всегда возвращает false
— CustomPainter
сам определяет, когда перерисовать холст, прослушивая объект 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
— принимает enumTextDirection
, который определяет направление текста.
Необязательные параметры 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;
}
Далее — отрисуем на холсте картинку.
Изображения
На практике изображения с помощью 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;
}
}
Image и параметры Paint
Данные параметры класса Paint
используются для модификации изображения.
imageFilter
— объект классаImageFilter
. Представляет собой набор операций преобразования растрового изображения.filterQuality
— enumFilterQuality
. Управляет соотношением производительности и качества, используемым при сэмплинге растровых изображений.invertColors
— флаг, инвертирует цвета изображения.
Вот и всё! В этой главе мы научились создавать разнообразные геометрические фигуры и линии на холсте. Кроме того, мы узнали, как добавлять текст и изображения.
В следующем параграфе мы продолжим говорить о CustomPainter
— разберём продвинутые аспекты: работу со слоями, использование шейдеров, обрезку и трансформацию холста и многое другое.