Графика в Compose

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

Базовый рисунок с модификаторами и DrawScope

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

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

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

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

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}