Bút vẽ: hiệu ứng chuyển màu và chương trình đổ bóng

Brush trong Compose mô tả cách vẽ một thứ gì đó trên màn hình: yếu tố này xác định (các) màu được vẽ trong khu vực vẽ (tức là một hình tròn, hình vuông, đường dẫn). Có một số Bút vẽ tích hợp hữu ích cho việc vẽ, chẳng hạn như LinearGradient, RadialGradient hoặc một bút vẽ SolidColor thuần tuý.

Bạn có thể sử dụng bút vẽ với các hàm gọi vẽ Modifier.background(), TextStyle hoặc DrawScope để áp dụng kiểu vẽ cho nội dung đang vẽ.

Ví dụ: bạn có thể áp dụng bút vẽ chuyển màu (gradient) theo chiều ngang để vẽ một hình tròn trong DrawScope:

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Hình tròn được vẽ bằng bút vẽ Chuyển màu theo chiều ngang
Hình 1: Hình tròn được vẽ bằng bút vẽ Chuyển màu theo chiều ngang

Bút vẽ chuyển màu

Bạn có thể sử dụng nhiều bút vẽ chuyển màu tích hợp sẵn để đạt được các hiệu ứng chuyển màu khác nhau. Những bút vẽ này cho phép bạn chỉ định danh sách màu sắc mà bạn muốn tạo hiệu ứng chuyển màu.

Danh sách các bút vẽ chuyển màu có sẵn và kết quả tương ứng:

Loại bút vẽ chuyển màu Kết quả
Brush.horizontalGradient(colorList) Chuyển màu theo chiều ngang
Brush.linearGradient(colorList) Chuyển màu tuyến tính
Brush.verticalGradient(colorList) Chuyển màu theo chiều dọc
Brush.sweepGradient(colorList)
Lưu ý: Để chuyển đổi mượt mà giữa các màu, hãy đặt màu cuối thành màu bắt đầu.
Chuyển màu quét
Brush.radialGradient(colorList) Chuyển màu xuyên tâm

Thay đổi cách phân bổ màu bằng colorStops

Để tuỳ chỉnh cách màu xuất hiện trong hiệu ứng chuyển màu, bạn có thể điều chỉnh giá trị colorStops cho từng màu. colorStops phải được chỉ định dưới dạng phân số, nằm trong khoảng từ 0 đến 1. Các giá trị lớn hơn 1 sẽ dẫn đến việc các màu đó không hiển thị trong hiệu ứng chuyển màu.

Bạn có thể định cấu hình điểm dừng màu (color stop) để có số lượng khác nhau, chẳng hạn như ít hơn hoặc nhiều hơn một màu:

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(Brush.horizontalGradient(colorStops = colorStops))
)

Các màu được phân tán ở độ lệch đã cho như được xác định trong cặp colorStop, ít màu vàng hơn so với màu đỏ và màu xanh dương.

Bút vẽ được định cấu hình với các điểm dừng màu khác nhau
Hình 2: Bút vẽ được định cấu hình với các điểm dừng màu khác nhau

Lặp lại mẫu bằng TileMode

Mỗi bút vẽ chuyển màu có thể tuỳ ý đặt một TileMode trên đó. Bạn có thể không nhận thấy TileMode nếu chưa đặt điểm bắt đầu và kết thúc cho hiệu ứng chuyển màu vì theo mặc định, màu này sẽ lấp đầy toàn bộ khu vực. TileMode sẽ chỉ xếp kề hiệu ứng chuyển màu nếu kích thước của vùng đó lớn hơn kích thước Bút vẽ.

Mã sau sẽ lặp lại mẫu chuyển màu 4 lần, vì endX được đặt thành 50.dp và kích thước được đặt thành 200.dp:

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val tileSize = with(LocalDensity.current) {
    50.dp.toPx()
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(
            Brush.horizontalGradient(
                listColors,
                endX = tileSize,
                tileMode = TileMode.Repeated
            )
        )
)

Bảng sau đây trình bày chi tiết hoạt động của các Chế độ xếp kề (Tile Mode) cho ví dụ HorizontalGradient ở trên:

TileMode Đầu ra
TileMode.Repeated: Viền được lặp lại từ màu cuối cùng đến màu đầu tiên. TileMode Repeated (Chế độ xếp kề kiểu Lặp lại)
TileMode.Mirror: Viền được phản chiếu từ màu cuối cùng đến màu đầu tiên. TileMode Mirror (Chế độ xếp kề kiểu Phản chiếu)
TileMode.Clamp: Viền được gắn với màu cuối cùng. Sau đó, công cụ này sẽ vẽ màu gần nhất cho phần khu vực còn lại. Tile Mode Clamp (Chế độ xếp kề kiểu Kẹp)
TileMode.Decal: Chỉ hiển thị tối đa bằng kích thước của ranh giới. TileMode.Decal tận dụng màu đen trong suốt để lấy mẫu nội dung bên ngoài ranh giới ban đầu, trong khi TileMode.Clamp lấy mẫu màu viền. Đề can chế độ xếp kề

TileMode hoạt động theo cách tương tự như các chế độ chuyển màu theo hướng, chỉ khác là hướng sẽ lặp lại.

Thay đổi kích thước bút vẽ

Nếu biết kích thước của vùng sẽ được vẽ, bạn có thể đặt ô endX như chúng ta thấy trong phần TileMode. Nếu đang ở DrawScope, bạn có thể sử dụng thuộc tính size để lấy kích thước của vùng đó.

Nếu không biết kích thước của vùng được vẽ (ví dụ: nếu Brush được chỉ định cho Văn bản), bạn có thể mở rộng Shader và sử dụng kích thước của vùng được vẽ trong hàm createShader.

Trong ví dụ này, hãy chia kích thước cho 4 để lặp lại mẫu 4 lần:

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
    object : ShaderBrush() {
        override fun createShader(size: Size): Shader {
            return LinearGradientShader(
                colors = listColors,
                from = Offset.Zero,
                to = Offset(size.width / 4f, 0f),
                tileMode = TileMode.Mirror
            )
        }
    }
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(customBrush)
)

Kích thước chương trình đổ bóng chia cho 4
Hình 3: Kích thước chương trình đổ bóng chia cho 4

Bạn cũng có thể thay đổi kích thước bút vẽ của mọi chế độ chuyển màu khác, chẳng hạn như chuyển màu dạng hình tròn. Nếu bạn không chỉ định kích thước và tâm, thì hiệu ứng chuyển màu sẽ chiếm toàn bộ ranh giới của DrawScope và tâm của hiệu ứng chuyển màu dạng hình tròn mặc định là tâm của ranh giới DrawScope. Điều này dẫn đến tâm của hiệu ứng chuyển màu dạng hình tròn xuất hiện dưới dạng tâm của kích thước nhỏ hơn (chiều rộng hoặc chiều cao):

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            Brush.radialGradient(
                listOf(Color(0xFF2be4dc), Color(0xFF243484))
            )
        )
)

Đặt chế độ chuyển màu dạng hình tròn mà không thay đổi kích thước
Hình 4: Đặt chế độ Chuyển màu dạng hình tròn mà không thay đổi kích thước

Khi thay đổi chế độ chuyển màu dạng hình tròn để đặt kích thước bán kính thành kích thước tối đa, bạn có thể thấy chế độ này sẽ tạo ra hiệu ứng chuyển màu dạng hình tròn tốt hơn:

val largeRadialGradient = object : ShaderBrush() {
    override fun createShader(size: Size): Shader {
        val biggerDimension = maxOf(size.height, size.width)
        return RadialGradientShader(
            colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)),
            center = size.center,
            radius = biggerDimension / 2f,
            colorStops = listOf(0f, 0.95f)
        )
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(largeRadialGradient)
)

Bán kính lớn hơn trong chế độ chuyển màu dạng hình tròn dựa trên kích thước của khu vực
Hình 5: Bán kính lớn hơn trong chế độ chuyển màu dạng hình tròn dựa trên kích thước của khu vực

Lưu ý rằng kích thước thực tế được chuyển vào khi tạo chương trình đổ bóng được xác định từ vị trí gọi. Theo mặc định, Brush sẽ phân bổ lại Shader trong nội bộ nếu kích thước khác với lần tạo Brush gần đây nhất hoặc nếu một đối tượng trạng thái được dùng khi tạo chương trình đổ bóng đã thay đổi.

Mã sau đây tạo chương trình đổ bóng 3 lần với các kích thước khác nhau, khi kích thước của vùng được vẽ thay đổi:

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
val brush = Brush.horizontalGradient(colorStops = colorStops)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .drawBehind {
            drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area
            inset(10f) {
      /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the
       inset scope reduces the drawing  area by 10 pixels on the left, top, right,
      bottom sides */
                drawRect(brush = brush)
                inset(5f) {
        /* will allocate a shader to occupy the 170 x 170 dp drawing area as the
         inset scope reduces the  drawing area by 5 pixels on the left, top,
         right, bottom sides */
                    drawRect(brush = brush)
                }
            }
        }
)

Dùng hình ảnh làm bút vẽ

Để sử dụng ImageBitmap làm Brush, hãy tải hình ảnh lên dưới dạng ImageBitmap và tạo một bút vẽ ImageShader:

val imageBrush =
    ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

// Use ImageShader Brush with background
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(imageBrush)
)

// Use ImageShader Brush with TextStyle
Text(
    text = "Hello Android!",
    style = TextStyle(
        brush = imageBrush,
        fontWeight = FontWeight.ExtraBold,
        fontSize = 36.sp
    )
)

// Use ImageShader Brush with DrawScope#drawCircle()
Canvas(onDraw = {
    drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))

Bút vẽ được áp dụng cho một số loại bản vẽ: nền, Văn bản và Canvas. Kết quả như sau:

Đã sử dụng Bút vẽ ImageShader theo nhiều cách
Hình 6: Sử dụng Bút vẽ ImageShader để vẽ nền, vẽ Văn bản và vẽ Hình tròn

Lưu ý rằng giờ đây, văn bản cũng được hiển thị bằng cách dùng ImageBitmap để vẽ các điểm ảnh cho văn bản.

Ví dụ nâng cao: Bút vẽ tuỳ chỉnh

Bút vẽ RuntimeShader AGSL

AGSL cung cấp một tập con chức năng của chương trình đổ bóng GLSL. Bạn có thể viết chương trình đổ bóng bằng AGSL và sử dụng cùng với Bút vẽ trong Compose.

Để tạo Bút vẽ chương trình đổ bóng, trước tiên, hãy xác định Chương trình đổ bóng là chuỗi chương trình đổ bóng AGSL:

@Language("AGSL")
val CUSTOM_SHADER = """
    uniform float2 resolution;
    layout(color) uniform half4 color;
    layout(color) uniform half4 color2;

    half4 main(in float2 fragCoord) {
        float2 uv = fragCoord/resolution.xy;

        float mixValue = distance(uv, vec2(0, 1));
        return mix(color, color2, mixValue);
    }
""".trimIndent()

Chương trình đổ bóng ở trên lấy 2 màu đầu vào, tính khoảng cách từ dưới cùng bên trái (vec2(0, 1)) của vùng được vẽ và thực hiện mix giữa 2 màu dựa trên khoảng cách. Thao tác này sẽ tạo ra hiệu ứng chuyển màu.

Sau đó, hãy tạo Bút vẽ chương trình đổ bóng và đặt kiểu đồng nhất cho resolution – kích thước của vùng vẽ và color cũng như color2 mà bạn muốn dùng làm màu đầu vào cho hiệu ứng chuyển màu tuỳ chỉnh của mình:

val Coral = Color(0xFFF3A397)
val LightYellow = Color(0xFFF8EE94)

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Preview
fun ShaderBrushExample() {
    Box(
        modifier = Modifier
            .drawWithCache {
                val shader = RuntimeShader(CUSTOM_SHADER)
                val shaderBrush = ShaderBrush(shader)
                shader.setFloatUniform("resolution", size.width, size.height)
                onDrawBehind {
                    shader.setColorUniform(
                        "color",
                        android.graphics.Color.valueOf(
                            LightYellow.red, LightYellow.green,
                            LightYellow
                                .blue,
                            LightYellow.alpha
                        )
                    )
                    shader.setColorUniform(
                        "color2",
                        android.graphics.Color.valueOf(
                            Coral.red,
                            Coral.green,
                            Coral.blue,
                            Coral.alpha
                        )
                    )
                    drawRect(shaderBrush)
                }
            }
            .fillMaxWidth()
            .height(200.dp)
    )
}

Khi chạy phương thức này, bạn có thể thấy các thông tin sau đây hiển thị trên màn hình:

Chương trình đổ bóng AGSL tuỳ chỉnh chạy trong Compose
Hình 7: Chương trình đổ bóng AGSL tuỳ chỉnh chạy trong Compose

Lưu ý rằng bạn có thể làm được nhiều việc hơn với chương trình đổ bóng thay vì chỉ sử dụng tính năng chuyển màu vì đó là mọi phép tính dựa trên toán học. Để biết thêm thông tin về AGSL, vui lòng xem tài liệu về AGSL.

Tài nguyên khác

Để biết thêm ví dụ về cách sử dụng Bút vẽ trong Compose, hãy xem các tài nguyên sau: