แปรง: การไล่ระดับสีและตัวปรับแสงเงา

Brush ในองค์ประกอบจะอธิบายวิธีวาดสิ่งต่างๆ บนหน้าจอ โดยจะเป็นตัวกำหนดสีที่วาดในพื้นที่วาด (เช่น วงกลม สี่เหลี่ยมจัตุรัส เส้นทาง) แปรงในตัวที่มีประโยชน์สำหรับการวาดมีอยู่ 2-3 แบบ เช่น LinearGradient, RadialGradient หรือแปรงSolidColorธรรมดา

คุณสามารถใช้แปรงกับคอลวาด Modifier.background(), TextStyle หรือ DrawScope เพื่อใช้รูปแบบการวาดกับเนื้อหาที่กำลังวาด

เช่น คุณสามารถใช้แปรงไล่ระดับสีแนวนอนเพื่อวาดวงกลมในDrawScope

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)

วงกลมที่วาดด้วยไล่ระดับสีแนวนอน
รูปที่ 1: วงกลมที่วาดด้วยการไล่ระดับสีแนวนอน

แปรงไล่ระดับสี

มีแปรงไล่สีในตัวมากมายที่สามารถใช้เพื่อสร้างเอฟเฟกต์ไล่สีแบบต่างๆ แปรงเหล่านี้ช่วยให้คุณระบุรายการสีที่ต้องการสร้างการไล่ระดับได้

รายการแปรงไล่ระดับสีที่ใช้ได้และเอาต์พุตที่เกี่ยวข้อง

ประเภทแปรงไล่ระดับสี เอาต์พุต
Brush.horizontalGradient(colorList) การไล่ระดับสีแนวนอน
Brush.linearGradient(colorList) การไล่ระดับสีแบบเส้นตรง
Brush.verticalGradient(colorList) ไล่เฉดสีแนวตั้ง
Brush.sweepGradient(colorList)
หมายเหตุ: หากต้องการให้สีเปลี่ยนอย่างราบรื่น ให้ตั้งค่าสีสุดท้ายเป็นสีเริ่มต้น
การไล่ระดับสีแบบกวาด
Brush.radialGradient(colorList) การไล่ระดับสีแบบรัศมี

เปลี่ยนการกระจายสีด้วย colorStops

หากต้องการปรับแต่งลักษณะที่สีปรากฏในไล่ระดับสี ให้ปรับค่า colorStops ของแต่ละสี colorStops ควรระบุเป็นเศษส่วนระหว่าง 0 ถึง 1 ค่าที่มากกว่า 1 จะทำให้สีเหล่านั้นไม่แสดงผลเป็นส่วนหนึ่งของไล่ระดับ

คุณสามารถกําหนดค่าจุดหยุดสีให้มีจํานวนต่างกัน เช่น สีใดสีหนึ่งมีจำนวนน้อยหรือมาก

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

สีจะกระจายอยู่ที่ออฟเซตที่ระบุตามที่ระบุไว้ในcolorStop คู่ โดยสีเหลืองจะน้อยกว่าสีแดงและน้ำเงิน

แปรงที่กำหนดค่าให้มีจุดสิ้นสุดสีต่างกัน
รูปที่ 2: แปรงที่กำหนดค่าให้มีจุดสิ้นสุดสีต่างกัน

วางรูปแบบซ้ำด้วย TileMode

แปรงไล่สีแต่ละรายการจะมีตัวเลือกในการตั้งค่า TileMode คุณอาจไม่สังเกตเห็น TileMode หากไม่ได้ตั้งค่าจุดเริ่มต้นและจุดสิ้นสุดของไล่ระดับสี เนื่องจากค่าเริ่มต้นจะเป็นแบบเต็มพื้นที่ TileMode จะวางรูปแบบไล่ระดับสีเป็นตารางก็ต่อเมื่อพื้นที่มีขนาดใหญ่กว่าขนาดของแปรง

รหัสต่อไปนี้จะทำซ้ำรูปแบบไล่ระดับสี 4 ครั้ง เนื่องจากมีการตั้งค่า endX เป็น 50.dp และตั้งค่าขนาดเป็น 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
            )
        )
)

ตารางต่อไปนี้แสดงรายละเอียดว่าโหมดการ์ดแต่ละโหมดทําอะไรกับตัวอย่างHorizontalGradientด้านบน

TileMode เอาต์พุต
TileMode.Repeated: ขอบจะซ้ำกันจากสีสุดท้ายไปจนถึงสีแรก TileMode Repeated
TileMode.Mirror: ขอบจะสะท้อนจากสีสุดท้ายไปยังสีแรก TileMode Mirror
TileMode.Clamp: ขอบถูกจำกัดให้เป็นสีสุดท้าย จากนั้นจะทาสีส่วนที่เหลือของภูมิภาคด้วยสีที่ใกล้เคียงที่สุด Clamp โหมดการต่อไทล์
TileMode.Decal: แสดงผลไม่เกินขนาดของขอบเขต TileMode.Decal ใช้ประโยชน์จากสีดําโปร่งใสเพื่อสุ่มตัวอย่างเนื้อหาที่อยู่นอกขอบเขตเดิม ส่วน TileMode.Clamp จะสุ่มตัวอย่างสีขอบ ป้ายโหมดกระเบื้อง

TileMode ทํางานในลักษณะที่คล้ายกันกับไล่ระดับสีตามทิศทางอื่นๆ ความแตกต่างคือทิศทางที่เกิดซ้ำ

เปลี่ยนขนาดแปรง

หากทราบขนาดของพื้นที่ที่จะวาดด้วยแปรง คุณสามารถตั้งค่าไทล์ endX ตามที่ได้เห็นในส่วน TileMode ด้านบน หากคุณอยู่ใน DrawScope คุณสามารถใช้พร็อพเพอร์ตี้ size ของ DrawScope เพื่อดูขนาดของพื้นที่

หากไม่ทราบขนาดของพื้นที่วาด (เช่น หากกำหนด Brush เป็นข้อความ) คุณสามารถขยาย Shader และใช้ขนาดของพื้นที่วาดในฟังก์ชัน createShader ได้

ในตัวอย่างนี้ ให้หารขนาดด้วย 4 เพื่อแสดงรูปแบบซ้ำ 4 ครั้ง

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

ขนาด Shader หารด้วย 4
รูปที่ 3: ขนาด Shader หารด้วย 4

นอกจากนี้ คุณยังเปลี่ยนขนาดแปรงของการไล่ระดับสีอื่นๆ เช่น การไล่ระดับสีแบบรัศมี ได้ด้วย หากไม่ได้ระบุขนาดและจุดกึ่งกลาง การไล่ระดับสีจะกินพื้นที่เต็มขอบเขตของ DrawScope และจุดกึ่งกลางของการไล่ระดับสีแบบรัศมีจะเป็นจุดกึ่งกลางของขอบเขต DrawScope โดยค่าเริ่มต้น ซึ่งส่งผลให้ศูนย์กลางของการไล่ระดับสีแบบรัศมีปรากฏเป็นศูนย์กลางของมิติข้อมูลขนาดเล็ก (ความกว้างหรือความสูง)

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

ชุดการไล่ระดับสีแบบรัศมีที่ไม่มีการเปลี่ยนแปลงขนาด
รูปที่ 4: การตั้งค่าการไล่ระดับสีแบบวงกลมโดยไม่มีการเปลี่ยนแปลงขนาด

เมื่อเปลี่ยนการไล่ระดับสีแบบรัศมีเพื่อกำหนดขนาดรัศมีเป็นมิติข้อมูลสูงสุด คุณจะเห็นผลลัพธ์การไล่ระดับสีแบบรัศมีที่ดีขึ้น

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

รัศมีที่ใหญ่ขึ้นในไล่ระดับสีแบบรัศมี โดยอิงตามขนาดของพื้นที่
รูปที่ 5: รัศมีที่ใหญ่ขึ้นในไล่ระดับสีแบบวงกลม โดยอิงตามขนาดของพื้นที่

โปรดทราบว่าขนาดจริงที่ส่งไปยังการสร้างโปรแกรมเปลี่ยนสีจะกำหนดจากตำแหน่งที่เรียกใช้ โดยค่าเริ่มต้น Brush จะจัดสรร Shader ใหม่ภายในหากขนาดแตกต่างจากการสร้าง Brush ครั้งล่าสุด หรือหากออบเจ็กต์สถานะที่ใช้ในการสร้างโปรแกรมเปลี่ยนสีมีการเปลี่ยนแปลง

โค้ดต่อไปนี้จะสร้างโปรแกรมเปลี่ยนสี 3 ครั้งในขนาดที่แตกต่างกัน เนื่องจากขนาดของพื้นที่วาดภาพมีการเปลี่ยนแปลง

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

ใช้รูปภาพเป็นแปรง

หากต้องการใช้ ImageBitmap เป็น Brush ให้โหลดรูปภาพเป็น ImageBitmap แล้วสร้างแปรง 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))

แปรงใช้กับภาพวาดหลายประเภท ได้แก่ พื้นหลัง ข้อความ และผืนผ้าใบ ซึ่งจะแสดงผลลัพธ์ต่อไปนี้

การใช้ ImageShader Brush ในลักษณะต่างๆ
รูปที่ 6: การใช้แปรง ImageShader เพื่อวาดพื้นหลัง วาดข้อความ และวาดวงกลม

โปรดสังเกตว่าตอนนี้ข้อความยังได้รับการแสดงผลโดยใช้ ImageBitmap เพื่อวาดพิกเซลของข้อความด้วย

ตัวอย่างขั้นสูง: แปรงที่กำหนดเอง

แปรง AGSL RuntimeShader

AGSL มีความสามารถของ Shader GLSL บางส่วน คุณเขียน Shader เป็น AGSL และใช้กับแปรงใน Compose ได้

หากต้องการสร้างแปรงชิดเดอร์ ให้กำหนดชิดเดอร์เป็นสตริงชิดเดอร์ 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()

Shader ด้านบนใช้สีอินพุต 2 สี คำนวณระยะทางจากด้านซ้ายล่าง (vec2(0, 1)) ของพื้นที่วาด และทำการ mix ระหว่าง 2 สีโดยอิงตามระยะทาง ซึ่งจะทำให้เกิดเอฟเฟกต์การไล่ระดับสี

จากนั้นสร้างแปรงแรเงา และตั้งค่าแบบคงที่สำหรับ resolution ซึ่งเป็นขนาดของพื้นที่วาด และ color และ color2 ที่ต้องการใช้เป็นอินพุตสำหรับไล่ระดับสีที่กำหนดเอง

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

เมื่อเรียกใช้ คุณจะเห็นว่าระบบแสดงผลต่อไปนี้บนหน้าจอ

Shader AGSL ที่กําหนดเองซึ่งทํางานใน Compose
รูปที่ 7: Shader AGSL ที่กําหนดเองซึ่งทํางานในคอมโพซ

โปรดทราบว่าคุณใช้เฉดสีทำอะไรได้มากกว่าแค่การทำเกล็ดสี เนื่องจากเป็นการคำนวณทั้งหมดที่อิงตามคณิตศาสตร์ ดูข้อมูลเพิ่มเติมเกี่ยวกับ AGSL ได้ที่เอกสารประกอบ AGSL

แหล่งข้อมูลเพิ่มเติม

ดูตัวอย่างเพิ่มเติมในการใช้แปรงในเครื่องมือเขียนได้ที่แหล่งข้อมูลต่อไปนี้