Графика в Compose

Многим приложениям требуется возможность точно контролировать, что именно отображается на экране. Это может быть как простое размещение прямоугольника или круга в нужном месте экрана, так и сложная композиция из графических элементов в разных стилях.

Базовое рисование с модификаторами и DrawScope

Основной способ нарисовать что-то пользовательское в Compose – использовать модификаторы, такие как Modifier.drawWithContent, Modifier.drawBehind и Modifier.drawWithCache.

Например, чтобы нарисовать что-то за composable-функцией, можно использовать модификатор drawBehind, который позволяет выполнять команды рисования.

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

Если вам нужна только composable-функция, которая рисует, вы можете использовать Canvas. Функция Canvas – это удобная оболочка для Modifier.drawBehind. Вы размещаете Canvas в своем макете так же, как и любой другой элемент пользовательского интерфейса Compose. В Canvas можно рисовать элементы, точно контролируя их стиль и расположение.

Все модификаторы рисования предоставляют DrawScope – ограниченную среду рисования, которая поддерживает собственное состояние. Это позволяет задать параметры для группы графических элементов. Объект DrawScope содержит несколько полезных полей, например size – объект Size, определяющий текущие размеры DrawScope.

Чтобы что-то нарисовать, вы можете использовать одну из множества функций рисования в DrawScope. Например, следующий код рисует прямоугольник в левом верхнем углу экрана:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Розовый прямоугольник на белом фоне, занимающий четверть экрана
Рисунок 1. Прямоугольник, нарисованный с помощью Canvas в Compose.

Подробнее о модификаторах графики…

Система координат

Чтобы нарисовать что-то на экране, вам нужно знать смещение (x и y) и размер вашего элемента. Для многих методов рисования в DrawScope положение и размер задаются значениями параметров по умолчанию. Параметры по умолчанию обычно размещают элемент в точке [0, 0] на холсте и предоставляют значение size по умолчанию, которое заполняет всю область рисования, как в примере выше – вы можете видеть, что прямоугольник расположен в левом верхнем углу. Чтобы настроить размер и положение элемента, вам необходимо понять систему координат в Compose.

Начало системы координат ([0,0]) находится в верхнем левом пикселе области рисования. Значение x увеличивается при движении вправо, а y – при движении вниз.

Сетка, показывающая систему координат с верхним левым углом [0, 0] и нижним правым углом [ширина, высота]
Рисунок 2. Система координат и сетка.

Например, если вы хотите нарисовать диагональную линию из правого верхнего угла холста в левый нижний, используйте функцию DrawScope.drawLine() и укажите начальное и конечное смещение с соответствующими позициями x и y:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

Базовые преобразования

DrawScope предлагает преобразования, позволяющие изменить место или способ выполнения команд рисования.

Масштаб

Используйте DrawScope.scale(), чтобы увеличить размер операций рисования на коэффициент. Операции, такие как scale(), применяются ко всем операциям рисования в соответствующей лямбда-функции. Например, следующий код увеличивает scaleX в 10 раз, а scaleY – в 15 раз:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Неравномерно масштабированный круг
Рисунок 3. Применение операции масштабирования к кругу на холсте.

Перевести

Используйте DrawScope.translate(), чтобы перемещать операции рисования вверх, вниз, влево или вправо. Например, следующий код перемещает рисунок на 100 пикселей вправо и на 300 пикселей вверх:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

Круг, смещенный от центра
Рисунок 4. Применение операции перевода к кругу на холсте.

Повернуть

Используйте DrawScope.rotate(), чтобы повернуть рисунок вокруг точки поворота. Например, следующий код поворачивает прямоугольник на 45 градусов:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Телефон с прямоугольником, повернутым на 45 градусов, в центре экрана.
Рисунок 5. Мы используем rotate(), чтобы повернуть текущую область рисования, в результате чего прямоугольник поворачивается на 45 градусов.

Отступ

Используйте DrawScope.inset(), чтобы настроить параметры по умолчанию для текущего объекта DrawScope, изменив границы рисунка и соответствующим образом преобразовав рисунки:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Этот код добавляет поля к командам рисования:

Прямоугольник с полями со всех сторон.
Рисунок 6. Применение вставки к командам рисования.

Несколько преобразований

Чтобы применить к рисункам несколько преобразований, используйте функцию DrawScope.withTransform(), которая создает и применяет одно преобразование, объединяющее все нужные изменения. Использование withTransform() эффективнее, чем вложенные вызовы отдельных преобразований, поскольку все преобразования выполняются вместе за одну операцию, а не Compose, требующему вычисления и сохранения каждого из вложенных преобразований.

Например, следующий код применяет к прямоугольнику и перенос, и поворот:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Телефон с повернутым прямоугольником, сдвинутым к краю экрана.
Рисунок 7. Используйте withTransform, чтобы применить поворот и перенос, повернув прямоугольник и сдвинув его влево.

Стандартные операции рисования

Нарисовать текст

Чтобы нарисовать текст в Compose, обычно можно использовать composable-функцию Text. Однако если вы используете DrawScope или хотите нарисовать текст вручную с настройками, вы можете использовать метод DrawScope.drawText().

Чтобы нарисовать текст, создайте TextMeasurer с помощью rememberTextMeasurer и вызовите drawText с измерителем:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

Надпись Hello, нарисованная в Canvas
Рисунок 8. Рисование текста на холсте.

Измерить текст

Рисование текста немного отличается от других команд рисования. Обычно в команде рисования указывается размер (ширина и высота) фигуры или изображения. Размер текста зависит от нескольких параметров, таких как размер шрифта, шрифт, лигатуры и межбуквенный интервал.

В Compose можно использовать TextMeasurer, чтобы получить доступ к измеренному размеру текста в зависимости от вышеуказанных факторов. Если вы хотите нарисовать фон за текстом, вы можете использовать измеренную информацию, чтобы получить размер области, которую занимает текст:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Этот фрагмент кода создает розовый фон для текста:

Многострочный текст, занимающий ⅔ всей области, с фоновым прямоугольником.
Рисунок 9. Многострочный текст, занимающий ⅔ всей области, с фоновым прямоугольником.

Если вы измените ограничения, размер шрифта или любое другое свойство, влияющее на размер, то будет возвращен новый размер. Вы можете задать фиксированный размер для width и height, и тогда текст будет соответствовать заданному TextOverflow. Например, следующий код отображает текст в ⅓ высоты и ⅓ ширины области composable-функции и устанавливает для TextOverflow значение TextOverflow.Ellipsis:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Теперь текст вписывается в ограничения, а в конце появляется многоточие:

Текст на розовом фоне, обрезанный многоточием.
Рисунок 10. TextOverflow.Ellipsis с фиксированными ограничениями на измерение текста.

Нарисовать изображение

Чтобы нарисовать ImageBitmap с помощью DrawScope, загрузите изображение, используя ImageBitmap.imageResource(), а затем вызовите drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Изображение собаки, нарисованное в Canvas
Рисунок 11. Рисую ImageBitmap в Canvas.

Рисовать основные формы

В DrawScope есть много функций для рисования фигур. Чтобы нарисовать фигуру, используйте одну из стандартных функций рисования, например drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

Результат

drawCircle()

нарисовать круг

drawRect()

Нарисовать прямоугольник

drawRoundedRect()

нарисовать прямоугольник со скругленными углами

drawLine()

нарисовать линию

drawOval()

Нарисовать овал

drawArc()

Нарисовать дугу

drawPoints()

баллы за ничью

Нарисовать путь

Контур – это набор математических инструкций, которые при выполнении создают рисунок. DrawScope может нарисовать путь с помощью метода DrawScope.drawPath().

Например, вы хотите нарисовать треугольник. Вы можете сгенерировать путь с помощью таких функций, как lineTo() и moveTo(), используя размер области рисования. Затем вызовите drawPath() с этим новым путем, чтобы получить треугольник.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Перевернутый фиолетовый треугольник, нарисованный в окне создания письма
Рисунок 12. Создание и отрисовка Path в Compose.

Доступ к объекту Canvas

В DrawScope у вас нет прямого доступа к объекту Canvas. Чтобы получить доступ к объекту Canvas, для которого можно вызывать функции, используйте DrawScope.drawIntoCanvas().

Например, если у вас есть пользовательский объект Drawable, который вы хотите нарисовать на холсте, вы можете получить доступ к холсту и вызвать метод Drawable#draw(), передав ему объект Canvas:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

Овальный черный объект ShapeDrawable, занимающий весь размер
Рисунок 13. Открываю холст, чтобы нарисовать Drawable.

Подробнее…

Дополнительную информацию о рисовании в Compose можно найти в следующих ресурсах: