Đồ hoạ trong Compose

Nhiều ứng dụng cần có khả năng điều khiển chính xác nội dung được vẽ trên màn hình. Việc này có thể đơn giản như đặt một hộp hoặc một vòng tròn trên màn hình ở đúng vị trí, hoặc có thể là sự sắp xếp chi tiết các thành phần đồ hoạ theo nhiều kiểu.

Thao tác vẽ cơ bản bằng đối tượng sửa đổi và DrawScope

Cách chính để vẽ nội dung tuỳ chỉnh nào đó trong Compose là sử dụng đối tượng sửa đổi, chẳng hạn như Modifier.drawWithContent, Modifier.drawBehindModifier.drawWithCache.

Ví dụ: để vẽ một nội dung nào đó phía sau thành phần kết hợp, bạn có thể sử dụng đối tượng sửa đổi drawBehind để bắt đầu thực thi các lệnh vẽ:

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

Nếu tất cả những gì bạn cần là một thành phần kết hợp để vẽ, thì bạn có thể sử dụng thành phần kết hợp Canvas. Thành phần kết hợp Canvas là một trình bao bọc tiện lợi xung quanh Modifier.drawBehind. Bạn đặt Canvas trong bố cục theo cách tương tự như cách bạn thực hiện cho bất kỳ thành phần trên giao diện người dùng nào khác trong Compose. Trong Canvas, bạn có thể vẽ các thành phần có quyền điều khiển chính xác về kiểu và vị trí của các thành phần đó.

Tất cả các đối tượng sửa đổi của thao tác vẽ đều hiển thị DrawScope, một môi trường vẽ theo phạm vi duy trì trạng thái riêng. Điều này cho phép bạn đặt các tham số cho một nhóm gồm các thành phần đồ hoạ. DrawScope cung cấp một số trường hữu ích, như size, đối tượng Size giúp xác định kích thước hiện tại của DrawScope.

Để vẽ một nội dung nào đó, bạn có thể sử dụng một trong nhiều hàm vẽ trên DrawScope. Ví dụ: mã sau đây sẽ vẽ một hình chữ nhật ở góc trên cùng bên trái màn hình:

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

Hình chữ nhật màu hồng được vẽ trên nền trắng chiếm 1/4 màn hình
Hình 1. Hình chữ nhật được vẽ bằng Canvas trong Compose.

Để tìm hiểu thêm về nhiều đối tượng sửa đổi của thao tác vẽ, vui lòng xem tài liệu về Đối tượng sửa đổi đồ hoạ.

Hệ toạ độ

Để vẽ một nội dung nào đó trên màn hình, bạn cần biết độ lệch (xy) và kích thước của mục. Với nhiều phương thức vẽ trên DrawScope, vị trí và kích thước sẽ do các giá trị tham số mặc định cung cấp. Các tham số mặc định thường đặt mục ở [0, 0] trỏ vào canvas và cung cấp size mặc định lấp đầy toàn bộ vùng vẽ như trong ví dụ trên – bạn có thể thấy hình chữ nhật được đặt ở trên cùng bên trái. Để điều chỉnh kích thước và vị trí của mục, bạn cần biết về hệ toạ độ trong Compose.

Điểm gốc của hệ toạ độ ([0,0]) là điểm ảnh ở ngoài cùng bên trái trong vùng vẽ. x tăng khi di chuyển sang phải và y tăng khi di chuyển xuống dưới.

Lưới minh hoạ hệ toạ độ hiển thị toạ độ trên cùng bên trái [0, 0] và toạ độ dưới cùng bên phải [chiều rộng, chiều cao]
Hình 2. Hệ toạ độ vẽ/lưới vẽ.

Ví dụ: nếu muốn vẽ một đường chéo từ góc trên cùng bên phải của vùng canvas đến góc dưới cùng bên trái, bạn có thể sử dụng hàm DrawScope.drawLine(), đồng thời chỉ định độ lệch bắt đầu và kết thúc bằng vị trí x và y tương ứng:

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

Các phép biến đổi cơ bản

DrawScope cung cấp các phép biến đổi để thay đổi vị trí hoặc cách thực thi các lệnh vẽ.

Điều chỉnh theo tỷ lệ

Sử dụng DrawScope.scale() để tăng kích thước của các thao tác vẽ theo hệ số. Các thao tác như scale() áp dụng cho mọi thao tác vẽ trong hàm lambda tương ứng. Ví dụ: mã sau đây tăng scaleX thêm 10 lần và scaleY thêm 15 lần:

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

Hình tròn được điều chỉnh theo tỷ lệ không đồng nhất
Hình 3. Áp dụng toán tử điều chỉnh theo tỷ lệ cho hình tròn trên Canvas.

Dịch chuyển

Sử dụng DrawScope.translate() để di chuyển các thao tác vẽ lên trên, xuống dưới, sang trái hoặc sang phải. Ví dụ: mã sau đây di chuyển bản vẽ 100 px sang phải và 300 px lên trên:

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

Một hình tròn đã lệch ra khỏi tâm
Hình 4. Áp dụng thao tác dịch chuyển cho một hình tròn trên Canvas.

Xoay

Sử dụng DrawScope.rotate() để xoay các thao tác vẽ quanh điểm chuyển đổi. Ví dụ: mã sau đây xoay hình chữ nhật một góc 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
        )
    }
}

Một chiếc điện thoại có hình chữ nhật xoay 45 độ ở giữa màn hình
Hình 5. Chúng tôi sử dụng rotate() để áp dụng chế độ xoay cho phạm vi vẽ hiện tại. Chế độ này xoay hình chữ nhật một góc 45 độ.

Phần lồng ghép

Sử dụng DrawScope.inset() để điều chỉnh các tham số mặc định của DrawScope hiện tại, thay đổi ranh giới vẽ và theo đó dịch chuyển bản vẽ:

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

Mã này sẽ thêm khoảng đệm vào các lệnh vẽ một cách hiệu quả:

Một hình chữ nhật được thêm vào khoảng đệm ở xung quanh
Hình 6. Áp dụng phần lồng ghép cho các lệnh vẽ.

Nhiều phép biến đổi

Để áp dụng nhiều phép biến đổi cho các bản vẽ, hãy sử dụng hàm DrawScope.withTransform(). Hàm này sẽ tạo và áp dụng một phép biến đổi giúp kết hợp mọi thay đổi mà bạn muốn. Việc sử dụng withTransform() hiệu quả hơn việc tạo các lệnh gọi lồng nhau cho các phép biến đổi riêng lẻ, vì bạn có thể thực hiện mọi phép biến đổi trong một thao tác duy nhất. Khi đó, Compose sẽ không cần tính toán và lưu lại từng phép biến đổi lồng nhau nữa.

Ví dụ: Mã sau đây áp dụng cho cả quá trình dịch chuyển và xoay đối với hình chữ nhật:

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

Chiếc điện thoại có một hình chữ nhật xoay được dịch chuyển sang một bên của màn hình
Hình 7. Sử dụng withTransform để áp dụng cả chế độ xoay và chế độ dịch chuyển, xoay hình chữ nhật và dịch chuyển hình chữ nhật đó sang trái.

Thao tác vẽ phổ biến

Vẽ văn bản

Để vẽ văn bản trong Compose, thông thường, bạn có thể dùng thành phần kết hợp Text. Tuy nhiên, nếu đang ở trong DrawScope hoặc nếu muốn vẽ văn bản theo cách thủ công bằng chế độ tuỳ chỉnh, thì bạn có thể sử dụng phương thức DrawScope.drawText().

Để vẽ văn bản, hãy tạo một TextMeasurer bằng rememberTextMeasurer rồi gọi drawText bằng trình đo lường:

val textMeasurer = rememberTextMeasurer()

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

Hiển thị chữ Hello (Xin chào) được vẽ trên Canvas
Hình 8. Vẽ văn bản trên Canvas.

Đo lường văn bản

Thao tác vẽ văn bản hoạt động hơi khác so với các lệnh vẽ khác. Thông thường, bạn cung cấp kích thước (chiều rộng và chiều cao) cho lệnh vẽ để vẽ hình dạng/hình ảnh. Đối với văn bản, có một vài tham số giúp kiểm soát kích thước của văn bản được hiển thị, chẳng hạn như kích thước phông chữ, phông chữ, dấu gạch nối và khoảng cách chữ cái.

Với Compose, bạn có thể dùng TextMeasurer để truy cập vào kích thước văn bản được đo lường, tuỳ thuộc vào các yếu tố ở trên. Nếu muốn vẽ nền phía sau văn bản, bạn có thể sử dụng thông tin đã đo lường để biết kích thước của vùng văn bản:

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

Đoạn mã này tạo ra nền màu hồng trên văn bản:

Văn bản nhiều dòng chiếm 2⁄3 kích thước của toàn bộ vùng, với nền là một hình chữ nhật
Hình 9. Văn bản nhiều dòng chiếm 2⁄3 kích thước của toàn bộ vùng, với nền là một hình chữ nhật.

Điều chỉnh các quy tắc ràng buộc, kích thước phông chữ hoặc bất kỳ thuộc tính nào ảnh hưởng đến kết quả kích thước đo lường trong một kích thước mới được báo cáo. Bạn có thể đặt kích thước cố định cho cả widthheight, sau đó văn bản sẽ tuân theo tập hợp TextOverflow. Ví dụ: mã sau đây hiển thị văn bản bằng 1⁄3 chiều cao và 1⁄3 chiều rộng của vùng thành phần kết hợp và đặt TextOverflow thành 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()
)

Giờ đây, văn bản được vẽ trong các điều kiện ràng buộc có dấu ba chấm ở cuối:

Văn bản được vẽ trên nền màu hồng, với dấu ba chấm cắt qua văn bản.
Hình 10. TextOverflow.Ellipsis có các điều kiện ràng buộc cố định đối với việc đo lường văn bản.

Vẽ hình ảnh

Để vẽ ImageBitmap bằng DrawScope, hãy tải hình ảnh lên bằng ImageBitmap.imageResource() rồi gọi drawImage:

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

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

Hình ảnh một chú chó được vẽ trên Canvas
Hình 11. Vẽ ImageBitmap trên Canvas.

Vẽ các hình dạng cơ bản

Có nhiều hàm vẽ hình dạng trên DrawScope. Để vẽ một hình dạng, hãy dùng một trong các hàm vẽ định sẵn, chẳng hạn như drawCircle:

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

API

Đầu ra

drawCircle()

vẽ hình tròn

drawRect()

vẽ hình chữ nhật

drawRoundedRect()

vẽ hình chữ nhật góc tròn

drawLine()

vẽ đường kẻ

drawOval()

vẽ hình bầu dục

drawArc()

vẽ hình cung

drawPoints()

vẽ điểm

Vẽ đường dẫn

Đường dẫn là một loạt các lệnh toán học dẫn đến một thao tác vẽ sau khi được thực thi. DrawScope có thể vẽ một đường dẫn bằng phương thức DrawScope.drawPath().

Ví dụ: giả sử bạn muốn vẽ một hình tam giác. Bạn có thể tạo đường dẫn bằng các hàm như lineTo()moveTo() sử dụng kích thước của vùng vẽ. Sau đó, hãy gọi drawPath() bằng đường dẫn mới tạo này để có hình tam giác.

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

Một tam giác đường dẫn màu tím lộn ngược được vẽ trên Compose
Hình 12. Tạo và vẽ một Path trong Compose.

Truy cập vào đối tượng Canvas

Với DrawScope, bạn không có quyền truy cập trực tiếp vào đối tượng Canvas. Bạn có thể sử dụng DrawScope.drawIntoCanvas() để truy cập ngay vào đối tượng Canvas mà bạn có thể gọi hàm trên đối tượng đó.

Ví dụ: nếu có Drawable tuỳ chỉnh mà bạn muốn vẽ vào canvas, bạn có thể truy cập vào canvas và gọi Drawable#draw(), truyền vào đối tượng 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()
)

Một hình dạng có thể vẽ (ShapeDrawable) màu đen hình bầu dục có kích thước tối đa
Hình 13. Truy cập vào canvas để vẽ Drawable.

Tìm hiểu thêm

Để biết thêm thông tin về tính năng Vẽ trong Compose, hãy xem các tài nguyên sau: