กราฟิกในการเขียน

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

การวาดภาพพื้นฐานด้วยตัวแก้ไขและ DrawScope

วิธีหลักในการวาดอะไรบางอย่างที่กำหนดเองใน Compose คือการใช้ตัวแก้ไข เช่น Modifier.drawWithContent Modifier.drawBehind และ Modifier.drawWithCache

เช่น หากต้องการวาดอะไรบางอย่างไว้ด้านหลัง Composable คุณสามารถใช้ตัวแก้ไข drawBehind เพื่อเริ่มเรียกใช้คำสั่งการวาดได้

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

หากต้องการเพียงแค่ Composable ที่วาด คุณสามารถใช้ Composable Canvas ได้ องค์ประกอบ Canvas เป็น Wrapper ที่สะดวกสำหรับ Modifier.drawBehind คุณวาง Canvas ในเลย์เอาต์ได้เช่นเดียวกับที่คุณทำกับองค์ประกอบ UI อื่นๆ ของ Compose ภายใน Canvas คุณสามารถวาดองค์ประกอบพร้อมควบคุมสไตล์และ ตำแหน่งได้อย่างแม่นยำ

ตัวแก้ไขการวาดทั้งหมดจะแสดง DrawScope ซึ่งเป็นสภาพแวดล้อมการวาดที่กำหนดขอบเขต ซึ่งรักษาสถานะของตัวเอง ซึ่งช่วยให้คุณตั้งค่าพารามิเตอร์สำหรับกลุ่ม องค์ประกอบกราฟิกได้ DrawScope มีฟิลด์ที่มีประโยชน์หลายอย่าง เช่น size ออบเจ็กต์ Size ที่ระบุขนาดปัจจุบันของ DrawScope

หากต้องการวาดอะไรบางอย่าง คุณสามารถใช้ฟังก์ชันการวาดมากมายบน DrawScope ตัวอย่างเช่น โค้ดต่อไปนี้จะวาดสี่เหลี่ยมผืนผ้าที่มุมซ้ายบนของหน้าจอ

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

สี่เหลี่ยมผืนผ้าสีชมพูวาดบนพื้นหลังสีขาวซึ่งกินพื้นที่ 1 ใน 4 ของหน้าจอ
รูปที่ 1 สี่เหลี่ยมผืนผ้าที่วาดโดยใช้ Canvas ใน Compose

ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวแก้ไขการวาดภาพต่างๆ ได้ในเอกสารประกอบตัวแก้ไขกราฟิก

ระบบพิกัด

หากต้องการวาดสิ่งใดสิ่งหนึ่งบนหน้าจอ คุณต้องทราบออฟเซ็ต (x และ y) รวมถึงขนาดของ รายการ เมื่อใช้เมธอดการวาดหลายๆ เมธอดใน DrawScope ระบบจะระบุตําแหน่งและขนาด โดยใช้ค่าพารามิเตอร์เริ่มต้น โดยทั่วไป พารามิเตอร์เริ่มต้นจะ วางตำแหน่งรายการที่จุด [0, 0] บน Canvas และระบุค่าเริ่มต้น size ที่เติมพื้นที่วาดภาพทั้งหมด ดังตัวอย่างด้านบน คุณจะเห็นว่า สี่เหลี่ยมผืนผ้าจะวางอยู่ที่ด้านซ้ายบน หากต้องการปรับขนาดและตำแหน่งของ ไอเทม คุณต้องทำความเข้าใจระบบพิกัดใน Compose

จุดเริ่มต้นของระบบพิกัด ([0,0]) อยู่ที่พิกเซลซ้ายสุดบนสุดใน พื้นที่วาด x จะเพิ่มขึ้นเมื่อเลื่อนไปทางขวา และ y จะเพิ่มขึ้นเมื่อเลื่อนลง

ตารางกริดที่แสดงระบบพิกัดซึ่งแสดงมุมซ้ายบน [0, 0] และมุมขวาล่าง [ความกว้าง, ความสูง]
รูปที่ 2 ระบบพิกัดการวาด / กริดการวาด

เช่น หากต้องการวาดเส้นทแยงมุมจากมุมขวาบนของ พื้นที่ Canvas ไปยังมุมซ้ายล่าง คุณสามารถใช้ฟังก์ชัน DrawScope.drawLine() และระบุออฟเซ็ตเริ่มต้นและสิ้นสุดด้วย ตำแหน่ง x และ y ที่สอดคล้องกันได้ดังนี้

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

การเปลี่ยนรูปแบบพื้นฐาน

DrawScope มีการแปลงเพื่อเปลี่ยนตำแหน่งหรือวิธีเรียกใช้คำสั่งวาด

สเกล

ใช้ DrawScope.scale() เพื่อเพิ่มขนาดของการดำเนินการวาดภาพตามปัจจัย การดำเนินการ เช่น scale() มีผลกับการดำเนินการวาดทั้งหมดภายใน Lambda ที่เกี่ยวข้อง ตัวอย่างเช่น โค้ดต่อไปนี้จะเพิ่ม scaleX 10 เท่าและ scaleY 15 เท่า

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

วงกลมที่ปรับขนาดไม่สม่ำเสมอ
รูปที่ 3 การใช้การดำเนินการปรับขนาดกับวงกลมใน Canvas

แปลภาษา

ใช้ DrawScope.translate() เพื่อย้ายการวาดขึ้น ลง ซ้าย หรือขวา ตัวอย่างเช่น โค้ดต่อไปนี้จะเลื่อนภาพวาดไปทางขวา 100 พิกเซลและขึ้นไป 300 พิกเซล

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

วงกลมที่เลื่อนออกจากจุดศูนย์กลาง
รูปที่ 4 การใช้การดำเนินการแปลกับวงกลมบน Canvas

หมุน

ใช้ DrawScope.rotate() เพื่อหมุนการวาดรอบจุดหมุน ตัวอย่างเช่น โค้ดต่อไปนี้จะหมุนสี่เหลี่ยมผืนผ้า 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
        )
    }
}

โทรศัพท์ที่มีสี่เหลี่ยมผืนผ้าหมุน 45 องศาอยู่ตรงกลางหน้าจอ
รูปที่ 5 เราใช้ rotate() เพื่อใช้การหมุนกับขอบเขตการวาดปัจจุบัน ซึ่งจะหมุนสี่เหลี่ยมผืนผ้า 45 องศา

ส่วนที่เว้นไว้

ใช้ DrawScope.inset() เพื่อปรับพารามิเตอร์เริ่มต้นของDrawScopeปัจจุบัน โดยเปลี่ยนขอบเขตการวาดและแปลภาพวาดตามนั้น

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

โค้ดนี้จะเพิ่มระยะขอบให้กับคำสั่งการวาดได้อย่างมีประสิทธิภาพ

สี่เหลี่ยมผืนผ้าที่มีการเว้นที่ว่างรอบด้าน
รูปที่ 6 ใช้ส่วนที่เว้นไว้กับคำสั่งวาด

การเปลี่ยนรูปแบบหลายรายการ

หากต้องการใช้การเปลี่ยนรูปแบบหลายรายการกับภาพวาด ให้ใช้ฟังก์ชัน DrawScope.withTransform() ซึ่งจะสร้างและ ใช้การเปลี่ยนรูปแบบเดียวที่รวมการเปลี่ยนแปลงที่ต้องการทั้งหมด การใช้ withTransform() มีประสิทธิภาพมากกว่าการเรียกการแปลงที่ซ้อนกันแต่ละรายการ เนื่องจากระบบจะทำการแปลงทั้งหมดพร้อมกันใน การดำเนินการเดียว แทนที่ Compose จะต้องคำนวณและบันทึกการแปลงที่ซ้อนกันแต่ละรายการ

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

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

โทรศัพท์ที่มีสี่เหลี่ยมผืนผ้าหมุนแล้วเลื่อนไปด้านข้างของหน้าจอ
รูปที่ 7 ใช้ withTransform เพื่อใช้ทั้งการหมุนและการเลื่อน โดยหมุนสี่เหลี่ยมผืนผ้าและเลื่อนไปทางซ้าย

การดำเนินการวาดภาพทั่วไป

วาดข้อความ

โดยปกติแล้ว หากต้องการวาดข้อความใน Compose คุณจะใช้ Composable Text ได้ อย่างไรก็ตาม หากคุณอยู่ในDrawScopeหรือต้องการวาดข้อความด้วยตนเองพร้อมการปรับแต่ง คุณสามารถใช้วิธีDrawScope.drawText() ได้

หากต้องการวาดข้อความ ให้สร้าง TextMeasurer โดยใช้ rememberTextMeasurer และเรียกใช้ drawText ด้วยเครื่องมือวัด

val textMeasurer = rememberTextMeasurer()

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

แสดงคำว่า "Hello" ที่วาดใน Canvas
รูปที่ 8 การวาดข้อความบน Canvas

วัดข้อความ

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

Compose ช่วยให้คุณใช้ TextMeasurer เพื่อเข้าถึงขนาดข้อความที่วัดได้โดยขึ้นอยู่กับปัจจัยข้างต้น หากต้องการวาดพื้นหลัง ด้านหลังข้อความ คุณสามารถใช้ข้อมูลที่วัดได้เพื่อดูขนาดของ พื้นที่ที่ข้อความใช้

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

ข้อมูลโค้ดนี้จะสร้างพื้นหลังสีชมพูในข้อความ

ข้อความหลายบรรทัดซึ่งใช้พื้นที่ ⅔ ของพื้นที่ทั้งหมด โดยมีสี่เหลี่ยมผืนผ้าเป็นพื้นหลัง
รูปที่ 9 ข้อความหลายบรรทัดซึ่งมีขนาด ⅔ ของพื้นที่ทั้งหมด โดยมีสี่เหลี่ยมผืนผ้าเป็นพื้นหลัง

การปรับข้อจำกัด ขนาดแบบอักษร หรือพร็อพเพอร์ตี้ใดๆ ที่ส่งผลต่อขนาดที่วัดได้ จะทำให้มีการรายงานขนาดใหม่ คุณตั้งค่าขนาดคงที่สำหรับทั้ง width และ height ได้ จากนั้นข้อความจะใช้TextOverflow ที่ตั้งค่าไว้ ตัวอย่างเช่น โค้ดต่อไปนี้จะแสดงข้อความใน ⅓ ของความสูงและ ⅓ ของความกว้าง ของพื้นที่ที่ประกอบได้ และตั้งค่า TextOverflow เป็น 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()
)

ตอนนี้ข้อความจะแสดงในข้อจำกัดโดยมีจุดไข่ปลาที่ท้ายข้อความ

ข้อความที่วาดบนพื้นหลังสีชมพู โดยมีเครื่องหมายจุดไข่ปลาตัดข้อความ
รูปที่ 10 TextOverflow.Ellipsis โดยมีข้อจำกัดคงที่ในการวัดข้อความ

วาดรูปภาพ

หากต้องการวาด ImageBitmap ด้วย DrawScope ให้โหลดรูปภาพโดยใช้ ImageBitmap.imageResource() แล้วเรียกใช้ drawImage ดังนี้

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

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

รูปภาพสุนัขที่วาดบน Canvas
รูปที่ 11 วาดImageBitmapใน Canvas

วาดรูปร่างพื้นฐาน

DrawScope มีฟังก์ชันวาดรูปร่างมากมาย หากต้องการวาดรูปร่าง ให้ใช้ฟังก์ชันวาดที่กำหนดไว้ล่วงหน้า เช่น drawCircle

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

API

เอาต์พุต

drawCircle()

วาดวงกลม

drawRect()

วาดสี่เหลี่ยม

drawRoundedRect()

วาดสี่เหลี่ยมผืนผ้ามุมมน

drawLine()

ขีดเส้น

drawOval()

วาดวงรี

drawArc()

วาดส่วนโค้ง

drawPoints()

วาดจุด

วาดเส้นทาง

เส้นทางคือชุดคำสั่งทางคณิตศาสตร์ที่ทำให้เกิดภาพวาดเมื่อ ดำเนินการ DrawScope สามารถวาดเส้นทางโดยใช้วิธี DrawScope.drawPath()

เช่น สมมติว่าคุณต้องการวาดสามเหลี่ยม คุณสร้างเส้นทางด้วยฟังก์ชันต่างๆ เช่น lineTo() และ moveTo() โดยใช้ขนาดของพื้นที่วาดได้ จากนั้นเรียกใช้ drawPath() ด้วยเส้นทางที่สร้างขึ้นใหม่นี้เพื่อรับสามเหลี่ยม

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

สามเหลี่ยมเส้นทางสีม่วงกลับหัวที่วาดบน Compose
รูปที่ 12 การสร้างและวาดPathใน Compose

การเข้าถึงออบเจ็กต์ Canvas

DrawScope จะทำให้คุณไม่มีสิทธิ์เข้าถึงออบเจ็กต์ Canvas โดยตรง คุณสามารถใช้ DrawScope.drawIntoCanvas() เพื่อรับ สิทธิ์เข้าถึงออบเจ็กต์ Canvas เองซึ่งคุณสามารถเรียกใช้ฟังก์ชันได้

เช่น หากคุณมี Drawable ที่กำหนดเองซึ่งต้องการวาดลงใน Canvas คุณสามารถเข้าถึง Canvas และเรียกใช้ Drawable#draw() โดยส่งออบเจ็กต์ 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()
)

ShapeDrawable สีดำรูปวงรีที่ใช้ขนาดเต็ม
รูปที่ 13 การเข้าถึง Canvas เพื่อวาด Drawable

ดูข้อมูลเพิ่มเติม

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