Pengubah grafis

Selain composable Canvas, Compose memiliki beberapa Modifiers grafik berguna yang membantu menggambar konten kustom. Pengubah ini berguna karena dapat diterapkan ke composable mana pun.

Pengubah gambar

Semua perintah gambar dilakukan dengan pengubah gambar di Compose. Ada tiga pengubah gambar utama di Compose:

Pengubah dasar untuk gambar adalah drawWithContent, tempat Anda dapat memutuskan urutan gambar Composable dan perintah gambar yang dikeluarkan di dalam pengubah. drawBehind adalah wrapper praktis di sekitar drawWithContent yang memiliki urutan gambar yang ditetapkan di belakang konten composable. drawWithCache memanggil onDrawBehind atau onDrawWithContent di dalamnya - dan menyediakan mekanisme untuk menyimpan cache objek yang dibuat di dalamnya.

Modifier.drawWithContent: Memilih urutan gambar

Modifier.drawWithContent memungkinkan Anda menjalankan operasi DrawScope sebelum atau setelah konten composable. Pastikan untuk memanggil drawContent, lalu merender konten sebenarnya dari composable. Dengan pengubah ini, Anda dapat menentukan urutan operasi, jika ingin konten Anda digambar sebelum atau setelah operasi gambar kustom.

Misalnya, jika ingin merender gradien radial di atas konten untuk membuat efek lubang kunci senter pada UI, Anda dapat melakukan hal berikut:

var pointerOffset by remember {
    mutableStateOf(Offset(0f, 0f))
}
Column(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput("dragging") {
            detectDragGestures { change, dragAmount ->
                pointerOffset += dragAmount
            }
        }
        .onSizeChanged {
            pointerOffset = Offset(it.width / 2f, it.height / 2f)
        }
        .drawWithContent {
            drawContent()
            // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
            drawRect(
                Brush.radialGradient(
                    listOf(Color.Transparent, Color.Black),
                    center = pointerOffset,
                    radius = 100.dp.toPx(),
                )
            )
        }
) {
    // Your composables here
}

Gambar 1: Modifier.drawWithContent digunakan di atas Composable untuk membuat pengalaman UI jenis senter.

Modifier.drawBehind: Menggambar di belakang composable

Modifier.drawBehind memungkinkan Anda melakukan operasi DrawScope di belakang konten composable yang digambar di layar. Jika melihat implementasi Canvas, Anda mungkin menyadari bahwa implementasi tersebut hanyalah wrapper praktis di sekitar Modifier.drawBehind.

Untuk menggambar persegi panjang membulat di belakang Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

Yang memberikan hasil berikut:

Teks dan latar belakang digambar menggunakan Modifier.drawBehind
Gambar 2: Teks dan latar belakang digambar menggunakan Modifier.drawBehind

Modifier.drawWithCache: Menggambar dan menyimpan cache objek gambar

Modifier.drawWithCache memastikan objek yang dibuat di dalamnya untuk disimpan dalam cache. Objek tersebut di-cache selama ukuran area gambar sama, atau objek status apa pun yang dibaca tidak berubah. Pengubah ini berguna untuk meningkatkan performa panggilan gambar karena menghindari kebutuhan untuk mengalokasikan ulang objek (seperti: Brush, Shader, Path, dll.) yang dibuat pada gambar.

Atau, Anda juga dapat menyimpan cache objek menggunakan remember, di luar pengubah. Namun, hal ini tidak terus-menerus bisa dilakukan karena Anda tidak selalu memiliki akses ke komposisi. Penggunaan drawWithCache mungkin akan lebih efektif jika objek hanya digunakan untuk menggambar.

Misalnya, jika Anda membuat Brush untuk menggambar gradien di belakang Text, penggunaan drawWithCache akan menyimpan cache objek Brush hingga ukuran area gambar berubah:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

Menyimpan cache objek Brush dengan drawWithCache
Gambar 3: Menyimpan cache objek Brush dengan drawWithCache

Pengubah grafis

Modifier.graphicsLayer: Menerapkan transformasi ke composable

Modifier.graphicsLayer adalah pengubah yang membuat konten composable menggambar ke dalam lapisan gambar. Lapisan menyediakan beberapa fungsi yang berbeda, seperti:

  • Isolasi untuk petunjuk menggambarnya (mirip dengan RenderNode). Petunjuk menggambar yang diambil sebagai bagian dari lapisan dapat diterbitkan ulang secara efisien oleh pipeline rendering tanpa mengeksekusi ulang kode aplikasi.
  • Transformasi yang berlaku untuk semua petunjuk menggambar yang dimuat dalam lapisan.
  • Rasterisasi untuk kemampuan komposisi. Jika lapisan dirasterisasi, petunjuk menggambar akan dijalankan dan output akan direkam menjadi buffering offscreen. Menggabungkan buffering seperti itu untuk frame berikutnya lebih cepat daripada mengeksekusi petunjuk individual, tetapi penggabungan akan berperilaku sebagai bitmap saat transformasi seperti penskalaan atau rotasi diterapkan.

Transformasi

Modifier.graphicsLayer menyediakan isolasi untuk petunjuk menggambarnya; misalnya, berbagai transformasi dapat diterapkan menggunakan Modifier.graphicsLayer. Ini dapat dianimasikan atau diubah tanpa perlu menjalankan ulang lambda gambar.

Modifier.graphicsLayer tidak mengubah ukuran atau penempatan yang dapat diukur dari composable Anda, karena hanya memengaruhi fase menggambar. Ini berarti composable Anda mungkin tumpang-tindih dengan composable lainnya jika akhirnya menggambar di luar batas tata letaknya.

Transformasi berikut dapat diterapkan dengan pengubah ini:

Skala - meningkatkan ukuran

scaleX dan scaleY akan memperbesar atau memperkecil konten dalam arah horizontal atau vertikal. Nilai 1.0f menunjukkan tidak ada perubahan skala, nilai 0.5f berarti setengah dari dimensi.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

Gambar 4: scaleX dan scaleY diterapkan ke composable Gambar
Terjemahan

translationX dan translationY dapat diubah dengan graphicsLayer, dan translationX akan memindahkan composable ke kiri atau kanan. Sementara translationY akan memindahkan composable ke atas atau ke bawah.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

Gambar 5: translationX dan translationY diterapkan ke Gambar dengan Modifier.graphicsLayer
Rotasi

Setel rotationX untuk memutar secara horizontal, rotationY untuk memutar secara vertikal, dan rotationZ untuk memutar pada sumbu Z (rotasi standar). Nilai ini ditentukan dalam derajat (0-360).

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Gambar 6: rotationX, rotationY, dan rotationZ disetel pada Gambar dengan Modifier.graphicsLayer
Origin

transformOrigin dapat ditentukan. Kemudian, nilai ini digunakan sebagai titik tempat transformasi terjadi. Semua contoh sejauh ini telah menggunakan TransformOrigin.Center, yang berada di (0.5f, 0.5f). Jika Anda menentukan originnya pada (0f, 0f), transformasi akan dimulai dari sudut kiri atas composable.

Jika mengubah origin dengan transformasi rotationZ, Anda dapat melihat bahwa item diputar di kiri atas composable:

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.transformOrigin = TransformOrigin(0f, 0f)
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Gambar 7: Rotasi diterapkan dengan TransformOrigin disetel ke 0f, 0f

Klip dan Bentuk

Bentuk menentukan garis batas tempat konten dipotong saat clip = true. Dalam contoh ini, kami menetapkan dua kotak untuk memiliki dua klip yang berbeda - satu menggunakan variabel klip graphicsLayer, dan satunya lagi menggunakan wrapper Modifier.clip yang praktis.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .graphicsLayer {
                clip = true
                shape = CircleShape
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(CircleShape)
            .background(Color(0xFF4DB6AC))
    )
}

Konten kotak pertama (teks yang bertuliskan "Hello Compose") dipotong menjadi bentuk lingkaran:

Klip diterapkan ke composable Box
Gambar 8: Klip diterapkan ke composable Box

Jika kemudian Anda menerapkan translationY ke bagian atas lingkaran merah muda, Anda akan melihat bahwa batas Composable masih sama, tetapi lingkaran digambar di balik lingkaran yang ada di bawahnya (dan di luar batasnya).

Klip diterapkan dengan translationY, dan batas merah untuk garis batas
Gambar 9: Klip diterapkan dengan translationY, dan batas merah untuk garis batas

Untuk menyesuaikan composable ke area yang digambar, Anda dapat menambahkan Modifier.clip(RectangleShape) lain di awal rantai pengubah. Konten tersebut tetap berada di dalam batas asli.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .size(200.dp)
            .border(2.dp, Color.Black)
            .graphicsLayer {
                clip = true
                shape = CircleShape
                translationY = 50.dp.toPx()
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }

    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(500.dp))
            .background(Color(0xFF4DB6AC))
    )
}

Klip diterapkan di atas transformasi graphicsLayer
Gambar 10: Klip diterapkan di atas transformasi graphicsLayer

Alfa

Modifier.graphicsLayer dapat digunakan untuk menyetel alpha (opasitas) untuk seluruh lapisan. 1.0f sepenuhnya buram dan 0.0f tidak terlihat.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Gambar yang menerapkan alfa
Gambar 11: Gambar yang menerapkan alfa

Strategi komposisi

Penggunaan alfa dan transparansi mungkin tidak semudah mengubah satu nilai alfa. Selain mengubah alfa, ada juga opsi untuk menyetel CompositingStrategy di graphicsLayer. CompositingStrategy menentukan cara konten composable disusun (disatukan) dengan konten lain yang sudah digambar di layar.

Strategi yang berbeda adalah:

Auto (default)

Strategi komposisi ditentukan oleh parameter graphicsLayer lainnya. Compose merender lapisan ke buffering offscreen jika alfa kurang dari 1,0f atau RenderEffect disetel. Setiap kali alfa kurang dari 1f, lapisan komposisi akan otomatis dibuat untuk merender konten, lalu menggambar buffering offscreen ini ke tujuan dengan alfa yang sesuai. Menyetel RenderEffect atau overscroll akan selalu merender konten ke buffering offscreen, apa pun setelan CompositingStrategy.

Offscreen

Konten composable selalu dirasterisasi ke tekstur atau bitmap offscreen sebelum dirender ke tujuan. Hal ini berguna untuk menerapkan operasi BlendMode untuk menyamarkan konten, dan untuk performa saat merender kumpulan petunjuk gambar yang kompleks.

Contoh penggunaan CompositingStrategy.Offscreen adalah dengan BlendModes. Perhatikan contoh di bawah, misalnya Anda ingin menghapus bagian dari composable Image dengan mengeluarkan perintah gambar yang menggunakan BlendMode.Clear. Jika compositingStrategy tidak disetel ke CompositingStrategy.Offscreen, BlendMode akan berinteraksi dengan semua konten di bawahnya.

Image(
    painter = painterResource(id = R.drawable.dog),
    contentDescription = "Dog",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .size(120.dp)
        .aspectRatio(1f)
        .background(
            Brush.linearGradient(
                listOf(
                    Color(0xFFC5E1A5),
                    Color(0xFF80DEEA)
                )
            )
        )
        .padding(8.dp)
        .graphicsLayer {
            compositingStrategy = CompositingStrategy.Offscreen
        }
        .drawWithCache {
            val path = Path()
            path.addOval(
                Rect(
                    topLeft = Offset.Zero,
                    bottomRight = Offset(size.width, size.height)
                )
            )
            onDrawWithContent {
                clipPath(path) {
                    // this draws the actual image - if you don't call drawContent, it wont
                    // render anything
                    this@onDrawWithContent.drawContent()
                }
                val dotSize = size.width / 8f
                // Clip a white border for the content
                drawCircle(
                    Color.Black,
                    radius = dotSize,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    ),
                    blendMode = BlendMode.Clear
                )
                // draw the red circle indication
                drawCircle(
                    Color(0xFFEF5350), radius = dotSize * 0.8f,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    )
                )
            }
        }
)

Jika CompositingStrategy disetel ke Offscreen, tekstur offscreen akan dibuat untuk menjalankan perintah (hanya menerapkan BlendMode ke konten composable ini). Kemudian, akan merendernya di atas elemen yang sudah dirender di layar, sehingga tidak memengaruhi konten yang sudah digambar.

Modifier.drawWithContent pada Gambar yang menunjukkan indikasi lingkaran, dengan BlendMode.Clear di dalam aplikasi
Gambar 12: Modifier.drawWithContent pada Gambar yang menunjukkan indikasi lingkaran, dengan BlendMode.Clear dan CompositingStrategy.Offscreen di dalam aplikasi

Jika Anda tidak menggunakan CompositingStrategy.Offscreen, hasil penerapan BlendMode.Clear akan menghapus semua piksel dalam tujuan, terlepas dari apa yang telah disetel– membuat buffering rendering jendela (hitam) terlihat. Banyak BlendModes yang melibatkan alfa tidak akan berfungsi seperti yang diharapkan tanpa buffering offscreen. Perhatikan lingkaran hitam di sekitar indikator lingkaran merah:

Modifier.drawWithContent pada Gambar yang menunjukkan indikasi lingkaran, dengan BlendMode.Clear dan tidak ada CompositingStrategy yang disetel
Gambar 13: Modifier.drawWithContent pada Gambar yang menunjukkan indikasi lingkaran, dengan BlendMode.Clear dan tidak ada CompositingStrategy yang disetel

Untuk memahaminya sedikit lebih lanjut: jika aplikasi memiliki latar belakang jendela yang transparan, dan Anda tidak menggunakan CompositingStrategy.Offscreen, BlendMode akan berinteraksi dengan seluruh aplikasi. Tindakan ini akan menghapus semua piksel untuk menampilkan aplikasi atau wallpaper di bawahnya, seperti dalam contoh berikut:

Tidak ada CompositingStrategy yang disetel dan menggunakan BlendMode.Clear dengan aplikasi yang memiliki latar belakang jendela yang transparan. Wallpaper merah muda ditampilkan melalui area di sekitar lingkaran status merah.
Gambar 14: Tidak ada CompositingStrategy yang disetel dan menggunakan BlendMode.Clear dengan aplikasi yang memiliki latar belakang jendela yang transparan. Perhatikan bagaimana wallpaper merah muda ditampilkan di area di sekitar lingkaran status merah.

Perlu diperhatikan bahwa saat menggunakan CompositingStrategy.Offscreen, tekstur offscreen yang merupakan ukuran area gambar akan dibuat dan dirender kembali di layar. Setiap perintah gambar yang dilakukan dengan strategi ini akan dipotong secara default ke area ini. Cuplikan kode di bawah ini mengilustrasikan perbedaan saat beralih untuk menggunakan tekstur offscreen:

@Composable
fun CompositingStrategyExamples() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize(Alignment.Center)
    ) {
        // Does not clip content even with a graphics layer usage here. By default, graphicsLayer
        // does not allocate + rasterize content into a separate layer but instead is used
        // for isolation. That is draw invalidations made outside of this graphicsLayer will not
        // re-record the drawing instructions in this composable as they have not changed
        Canvas(
            modifier = Modifier
                .graphicsLayer()
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            // ... and drawing a size of 200 dp here outside the bounds
            drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }

        Spacer(modifier = Modifier.size(300.dp))

        /* Clips content as alpha usage here creates an offscreen buffer to rasterize content
        into first then draws to the original destination */
        Canvas(
            modifier = Modifier
                // force to an offscreen buffer
                .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the
            content gets clipped */
            drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }
    }
}

CompositingStrategy.Auto vs CompositingStrategy.Offscreen - offscreen memotong ke area, sedangkan auto tidak melakukannya
Gambar 15: CompositingStrategy.Auto vs CompositingStrategy.Offscreen - offscreen memotong ke area, sedangkan auto tidak melakukannya
ModulateAlpha

Strategi komposisi ini memodulasi alfa untuk setiap petunjuk menggambar yang direkam dalam graphicsLayer. Tindakan ini tidak akan membuat buffering offscreen untuk alfa di bawah 1,0f kecuali jika RenderEffect disetel, sehingga dapat lebih efisien untuk rendering alfa. Namun, hal ini dapat memberikan hasil yang berbeda untuk konten yang tumpang-tindih. Untuk kasus penggunaan jika sebelumnya diketahui bahwa konten tidak tumpang-tindih, hal ini dapat memberikan performa yang lebih baik daripada CompositingStrategy.Auto dengan nilai alfa kurang dari 1.

Contoh lain dari strategi komposisi yang berbeda adalah di bawah - menerapkan alfa yang berbeda untuk berbagai bagian composable, dan menerapkan strategi Modulate:

@Preview
@Composable
fun CompositingStrategy_ModulateAlpha() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp)
    ) {
        // Base drawing, no alpha applied
        Canvas(
            modifier = Modifier.size(200.dp)
        ) {
            drawSquares()
        }

        Spacer(modifier = Modifier.size(36.dp))

        // Alpha 0.5f applied to whole composable
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    alpha = 0.5f
                }
        ) {
            drawSquares()
        }
        Spacer(modifier = Modifier.size(36.dp))

        // 0.75f alpha applied to each draw call when using ModulateAlpha
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    compositingStrategy = CompositingStrategy.ModulateAlpha
                    alpha = 0.75f
                }
        ) {
            drawSquares()
        }
    }
}

private fun DrawScope.drawSquares() {

    val size = Size(100.dp.toPx(), 100.dp.toPx())
    drawRect(color = Red, size = size)
    drawRect(
        color = Purple, size = size,
        topLeft = Offset(size.width / 4f, size.height / 4f)
    )
    drawRect(
        color = Yellow, size = size,
        topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f)
    )
}

val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

ModulateAlpha menerapkan kumpulan alfa ke setiap perintah gambar
Gambar 16: ModulateAlpha menerapkan kumpulan alfa ke setiap perintah gambar

Menulis konten composable ke bitmap

Kasus penggunaan umum adalah membuat Bitmap dari composable. Untuk menyalin konten composable ke Bitmap, buat GraphicsLayer menggunakan rememberGraphicsLayer().

Alihkan perintah gambar ke lapisan baru menggunakan drawWithContent() dan graphicsLayer.record{}. Kemudian, gambar lapisan di kanvas yang terlihat menggunakan drawLayer:

val coroutineScope = rememberCoroutineScope()
val graphicsLayer = rememberGraphicsLayer()
Box(
    modifier = Modifier
        .drawWithContent {
            // call record to capture the content in the graphics layer
            graphicsLayer.record {
                // draw the contents of the composable into the graphics layer
                this@drawWithContent.drawContent()
            }
            // draw the graphics layer on the visible canvas
            drawLayer(graphicsLayer)
        }
        .clickable {
            coroutineScope.launch {
                val bitmap = graphicsLayer.toImageBitmap()
                // do something with the newly acquired bitmap
            }
        }
        .background(Color.White)
) {
    Text("Hello Android", fontSize = 26.sp)
}

Anda dapat menyimpan bitmap ke disk dan membagikannya. Untuk mengetahui detail selengkapnya, lihat cuplikan contoh lengkap. Pastikan untuk memeriksa izin di perangkat sebelum mencoba menyimpan ke disk.

Pengubah gambar kustom

Untuk membuat pengubah kustom Anda sendiri, implementasikan antarmuka DrawModifier. Hal ini memberi Anda akses ke ContentDrawScope, yang sama dengan yang ditampilkan saat menggunakan Modifier.drawWithContent(). Kemudian, Anda dapat mengekstrak operasi gambar umum ke pengubah gambar kustom untuk membersihkan kode dan menyediakan wrapper yang praktis; misalnya, Modifier.background() adalah DrawModifier yang praktis.

Misalnya, jika ingin mengimplementasikan Modifier yang membalik konten secara vertikal, Anda dapat membuatnya sebagai berikut:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

Kemudian, gunakan pengubah terbalik ini yang diterapkan di Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

Pengubah Balik Kustom pada Teks
Gambar 17: Pengubah Balik Kustom pada Teks

Referensi lainnya

Untuk contoh lainnya penggunaan graphicsLayer dan gambar kustom, lihat referensi berikut: