Графика в 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 . Применение операции масштабирования к кругу на Canvas.

Переводить

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

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

Круг, сместившийся от центра
Рисунок 4 . Применение операции перевода к кругу на Canvas.

Поворот

Используйте 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 посетите следующие ресурсы:

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