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

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

val textMeasurer = rememberTextMeasurer()

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

Показ 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 %}