Grafik dalam Compose

Banyak aplikasi harus dapat mengontrol dengan akurat apa yang digambar di layar. Hal ini mungkin semudah menempatkan kotak atau lingkaran di layar di tempat yang tepat, atau mungkin pengaturan elemen grafik yang rumit dalam banyak gaya yang berbeda.

Gambar dasar dengan pengubah dan DrawScope

Cara inti untuk menggambar sesuatu yang khusus di Compose adalah dengan pengubah, seperti Modifier.drawWithContent, Modifier.drawBehind, dan Modifier.drawWithCache.

Misalnya, untuk menggambar sesuatu di belakang composable, Anda dapat menggunakan pengubah drawBehind untuk mulai menjalankan perintah gambar:

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

Jika hanya memerlukan composable yang menggambar, Anda dapat menggunakan composable Canvas. Composable Canvas adalah wrapper yang praktis di sekitar Modifier.drawBehind. Anda menempatkan Canvas di tata letak dengan cara yang sama seperti elemen UI Compose lainnya. Dalam Canvas, Anda dapat menggambar elemen dengan kontrol gaya dan lokasinya yang akurat.

Semua pengubah gambar mengekspos DrawScope, cakupan lingkungan gambar yang mempertahankan statusnya sendiri. Ini memungkinkan Anda menyetel parameter untuk sekelompok elemen grafik. DrawScope menyediakan beberapa kolom yang berguna, seperti size, objek Size yang menentukan dimensi DrawScope saat ini.

Untuk menggambar sesuatu, Anda dapat menggunakan salah satu dari berbagai fungsi gambar di DrawScope. Misalnya, kode berikut menggambar persegi panjang di pojok kiri atas layar:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Persegi panjang merah muda digambar dengan latar belakang putih yang menempati seperempat layar
Gambar 1. Persegi panjang digambar menggunakan Canvas di Compose.

Untuk mempelajari lebih lanjut berbagai pengubah gambar, lihat dokumentasi Pengubah Grafik.

Sistem koordinat

Untuk menggambar sesuatu di layar, Anda perlu mengetahui offset (x dan y) dan ukuran item Anda. Dengan banyak metode gambar di DrawScope, posisi dan ukuran diberikan oleh parameter value default. Parameter default biasanya memosisikan item di titik [0, 0] pada kanvas dan menyediakan size default yang mengisi seluruh area gambar, seperti dalam contoh di atas - Anda dapat melihat kotak persegi panjang berada di posisi kiri atas. Untuk menyesuaikan ukuran dan posisi item, Anda perlu memahami sistem koordinat di Compose.

Origin sistem koordinat ([0,0]) berada di piksel paling kiri atas di area gambar. x meningkat saat bergerak ke kanan dan y meningkat saat bergerak ke bawah.

Petak yang menunjukkan sistem koordinat yang menampilkan bagian kiri atas [0, 0] dan bagian kanan bawah [lebar, tinggi]
Gambar 2. Sistem koordinat gambar/petak gambar.

Misalnya, jika Anda ingin menggambar garis diagonal dari pojok kanan atas area kanvas ke pojok kiri bawah, Anda dapat menggunakan fungsi DrawScope.drawLine() serta dapat menentukan offset awal dan akhir dengan posisi x dan y yang sesuai:

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
    )
}

Transformasi dasar

DrawScope menawarkan transformasi untuk mengubah lokasi atau cara perintah gambar dieksekusi.

Skala

Gunakan DrawScope.scale() untuk meningkatkan ukuran operasi gambar berdasarkan suatu faktor. Operasi seperti scale() berlaku untuk semua operasi gambar dalam lambda yang sesuai. Misalnya, kode berikut meningkatkan scaleX 10 kali dan scaleY 15 kali:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Lingkaran yang diskalakan secara tidak seragam
Gambar 3. Menerapkan operasi skala ke lingkaran di Canvas.

Terjemahan

Gunakan DrawScope.translate() untuk memindahkan operasi gambar ke atas, bawah, kiri, atau kanan. Misalnya, kode berikut memindahkan gambar 100 px ke kanan dan 300 px ke atas:

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

Lingkaran yang telah dipindahkan dari tengah
Gambar 4. Menerapkan operasi terjemahan ke lingkaran di Canvas.

Rotasi

Gunakan DrawScope.rotate() untuk memutar operasi gambar mengitari titik pivot. Misalnya, kode berikut memutar persegi panjang 45 derajat:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Ponsel dengan persegi panjang yang diputar 45 derajat di tengah layar
Gambar 5. Kami menggunakan rotate() untuk menerapkan rotasi ke cakupan gambar saat ini, yang memutar persegi panjang sebesar 45 derajat.

Inset

Gunakan DrawScope.inset() untuk menyesuaikan parameter default DrawScope saat ini, yang mengubah batas gambar dan menerjemahkan gambar sesuai parameter tersebut:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Kode ini menambahkan padding secara efektif ke perintah gambar:

Persegi panjang yang telah diberi padding
Gambar 6. Menerapkan inset ke perintah gambar.

Beberapa transformasi

Untuk menerapkan beberapa transformasi ke gambar, gunakan fungsi DrawScope.withTransform(), yang membuat dan menerapkan satu transformasi yang menggabungkan semua perubahan yang diinginkan. Menggunakan withTransform() lebih efisien daripada membuat panggilan bertingkat ke setiap transformasi, karena semua transformasi dijalankan bersamaan dalam satu operasi, bukan Compose yang perlu menghitung dan menyimpan setiap transformasi bertingkat.

Misalnya, kode berikut menerapkan terjemahan dan rotasi ke persegi panjang:

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
        )
    }
}

Ponsel dengan kotak yang diputar bergeser ke sisi layar
Gambar 7. Gunakan withTransform untuk menerapkan rotasi dan terjemahan, memutar persegi panjang, dan menggesernya ke kiri.

Operasi gambar umum

Menggambar teks

Untuk menggambar teks di Compose, Anda biasanya dapat menggunakan composable Text. Namun, jika Anda berada di DrawScope atau ingin menggambar teks secara manual dengan penyesuaian, Anda dapat menggunakan metode DrawScope.drawText().

Untuk menggambar teks, buat TextMeasurer menggunakan rememberTextMeasurer dan panggil drawText dengan pengukur:

val textMeasurer = rememberTextMeasurer()

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

Menampilkan Halo yang digambar di Canvas
Gambar 8. Menggambar teks di Canvas.

Mengukur teks

Cara kerja teks gambar sedikit berbeda dengan perintah gambar lainnya. Biasanya, Anda memberikan perintah gambar ukuran (lebar dan tinggi) untuk menggambar bentuk/gambar. Dengan teks, ada beberapa parameter yang mengontrol ukuran teks yang dirender, seperti ukuran font, font, ligatur, dan spasi huruf.

Dengan Compose, Anda dapat menggunakan TextMeasurer untuk mendapatkan akses ke ukuran teks yang diukur, bergantung pada faktor di atas. Jika ingin menggambar latar belakang di belakang teks, Anda dapat menggunakan informasi terukur untuk mendapatkan ukuran area yang digunakan teks:

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

Cuplikan kode ini menghasilkan latar belakang merah muda pada teks:

Teks multibaris yang menempati ⅔ dari seluruh area, dengan latar belakang persegi panjang
Gambar 9. Teks multibaris yang menempati ⅔ dari seluruh area, dengan latar belakang persegi panjang.

Menyesuaikan batasan, ukuran font, atau properti apa pun yang memengaruhi ukuran terukur akan menghasilkan ukuran baru yang dilaporkan. Anda dapat menyetel ukuran tetap untuk width dan height, lalu teks akan mengikuti kumpulan TextOverflow. Misalnya, kode berikut merender teks dalam ⅓ tinggi dan ⅓ dari lebar area composable, dan menyetel TextOverflow ke 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()
)

Teks sekarang digambar dalam batasan dengan elipsis di bagian akhir:

Teks yang digambar dengan latar belakang merah muda, dengan elipsis yang memotong teks.
Gambar 10. TextOverflow.Ellipsis dengan batasan tetap untuk mengukur teks.

Membuat gambar

Untuk menggambar ImageBitmap dengan DrawScope, muat gambar menggunakan ImageBitmap.imageResource(), lalu panggil drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Gambar seekor anjing yang digambar di Canvas
Gambar 11. Menggambar ImageBitmap di Canvas.

Menggambar bentuk dasar

Ada banyak fungsi menggambar bentuk di DrawScope. Untuk menggambar bentuk, gunakan salah satu dari fungsi gambar yang telah ditetapkan, seperti drawCircle:

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

API

Output

drawCircle()

gambar lingkaran

drawRect()

gambar persegi panjang

drawRoundedRect()

gambar persegi panjang sudut bulat

drawLine()

gambar garis

drawOval()

gambar oval

drawArc()

gambar busur

drawPoints()

gambar titik-titik

Menggambar path

Path adalah serangkaian petunjuk matematika yang menghasilkan gambar setelah dieksekusi. DrawScope dapat menggambar path menggunakan metode DrawScope.drawPath().

Misalnya, Anda ingin menggambar segitiga. Anda dapat membuat path dengan fungsi seperti lineTo() dan moveTo() menggunakan ukuran area gambar. Kemudian, panggil drawPath() dengan jalur yang baru dibuat ini untuk membuat bentuk segitiga.

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

Segitiga path ungu terbalik yang digambar di Compose
Gambar 12. Membuat dan menggambar Path di Compose.

Mengakses objek Canvas

Dengan DrawScope, Anda tidak memiliki akses langsung ke objek Canvas. Anda dapat menggunakan DrawScope.drawIntoCanvas() untuk mendapatkan akses ke objek Canvas yang dapat Anda gunakan untuk memanggil fungsi.

Misalnya, jika Anda memiliki Drawable kustom yang ingin digambar ke kanvas, Anda dapat mengakses kanvas dan memanggil Drawable#draw(), dengan meneruskan objek 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 hitam oval dengan ukuran penuh
Gambar 13. Mengakses kanvas untuk menggambar Drawable.

Mempelajari lebih lanjut

Untuk informasi selengkapnya tentang Menggambar di Compose, lihat referensi berikut: