Графика в Compose

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

Простой рисунок с модификаторами и DrawScope

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

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

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

Если вам нужен только отрисовываемый компонент, используйте компонент 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. Прямоугольник, нарисованный с помощью Холста в режиме создания письма.

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

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

Чтобы нарисовать что-либо на экране, вам нужно знать смещение (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() эффективнее, чем вложенные вызовы отдельных преобразований, поскольку все преобразования выполняются вместе в рамках одной операции, а не по отдельности, как при вложенных преобразованиях.

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

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, обычно используется компонуемый элемент Text. Однако если вы находитесь в DrawScope или хотите нарисовать текст вручную с настройками, можно использовать метод DrawScope.drawText().

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

val textMeasurer = rememberTextMeasurer()

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

Изображение надписи "Привет", нарисованной в приложении "Холст"
Рисунок 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. Например, следующий код отрисовывает текст, занимающий треть высоты и треть ширины области, и задает для 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 на холсте.

Как рисовать простые фигуры

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

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

API

Результат

drawCircle()

нарисуй круг

drawRect()

draw rect

drawRoundedRect()

draw rounded rect

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. Вы можете использовать DrawScope.drawIntoCanvas(), чтобы получить доступ к самому объекту Canvas, для которого можно вызывать функции.

Например, если вы хотите нарисовать на холсте пользовательский объект 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 можно найти в следующих ресурсах: