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

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

การวาดภาพพื้นฐานด้วยตัวแก้ไขและ 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 ข้อความหลายบรรทัดซึ่งใช้พื้นที่ 2 ใน 3 ของพื้นที่ทั้งหมด โดยมีพื้นหลังเป็นสี่เหลี่ยมผืนผ้า

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

การเข้าถึงออบเจ็กต์ 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 ได้ที่แหล่งข้อมูลต่อไปนี้