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

Brush ใน Compose จะอธิบายวิธีวาดสิ่งต่างๆ บนหน้าจอ โดยจะ กำหนดสีที่จะวาดในพื้นที่วาด (เช่น วงกลม สี่เหลี่ยมจัตุรัส เส้นทาง) มีแปรงในตัวบางอย่างที่มีประโยชน์สำหรับการวาด เช่น 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 pair โดยมีสีเหลืองน้อยกว่าสีแดงและสีน้ำเงิน

แปรงที่กำหนดค่าด้วยจุดหยุดสีต่างๆ
รูปที่ 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: ขอบจะยึดกับสีสุดท้าย จากนั้นจะระบายสีที่ใกล้เคียงที่สุดสำหรับส่วนที่เหลือของภูมิภาค แคลมป์โหมดไทล์
TileMode.Decal: แสดงผลได้สูงสุดเท่าขนาดขอบเขตเท่านั้น TileMode.Decal ใช้สีดำโปร่งใสเพื่อสุ่มตัวอย่างเนื้อหานอกขอบเขตเดิม ในขณะที่ TileMode.Clamp จะสุ่มตัวอย่างสีขอบ สติกเกอร์โหมดไทล์

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

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

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

หากไม่ทราบขนาดพื้นที่วาด (เช่น หากกำหนด 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 ครั้งล่าสุด หรือหากออบเจ็กต์สถานะที่ใช้ในการสร้างเชเดอร์มีการ เปลี่ยนแปลง

โค้ดต่อไปนี้จะสร้าง Shader 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))

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

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

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

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

แปรง AGSL RuntimeShader

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

หากต้องการสร้างแปรง Shader ให้กำหนด Shader เป็นสตริง Shader ของ 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 สี ตามระยะทาง ซึ่งจะสร้างเอฟเฟกต์การไล่ระดับสี

จากนั้นสร้าง Shader Brush และตั้งค่า Uniform สำหรับ 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)
    )
}

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

Custom AGSL Shader ที่ทำงานใน Compose
รูปที่ 7: ตัวปรับแสงเงา AGSL ที่กำหนดเองซึ่งทำงานใน Compose

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

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

ดูตัวอย่างเพิ่มเติมเกี่ยวกับการใช้ Brush ใน Compose ได้จากแหล่งข้อมูลต่อไปนี้