Графика в Compose

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

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

Основной способ отрисовки пользовательского объекта в Compose — использование модификаторов, таких как Modifier.drawWithContent , Modifier.drawBehind и Modifier.drawWithCache .

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

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

Если вам нужен только элемент, который рисует, вы можете использовать элемент Canvas . Canvas — это удобная обертка над Modifier.drawBehind . Вы размещаете Canvas в своем макете так же, как и любой другой элемент Compose UI. Внутри 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 обычно можно использовать компонент 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)
})

Рисунок собаки, выполненный на холсте.
Рисунок 11. Рисование ImageBitmap на холсте.

Нарисуйте основные геометрические фигуры.

В 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()
)

Перевернутый фиолетовый треугольник, нарисованный в программе Compose.
Рисунок 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 .

Узнать больше

Для получения более подробной информации о программе Drawing in Compose, ознакомьтесь со следующими ресурсами:

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}