Codelab นี้เป็นส่วนหนึ่งของหลักสูตร Android ขั้นสูงใน Kotlin คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้หากทำตาม Codelab ตามลำดับ แต่ไม่จำเป็นต้องทำ Codelab ของหลักสูตรทั้งหมดแสดงอยู่ในหน้า Landing Page ของ Codelab Android ขั้นสูงใน Kotlin
บทนำ
สำหรับโค้ดแล็บนี้ การตัดคือวิธีระบุภูมิภาคของรูปภาพ Canvas หรือบิตแมปที่วาดหรือไม่ได้วาดบนหน้าจออย่างเลือกสรร จุดประสงค์อย่างหนึ่งของการตัดคือการลดการแสดงพิกเซลซ้ำ การวาดทับเกิดขึ้นเมื่อมีการวาดพิกเซลบนหน้าจอมากกว่า 1 ครั้งเพื่อแสดงรูปภาพสุดท้าย เมื่อลดการวาดทับ คุณจะลดจำนวนครั้งที่วาดพิกเซลหรือภูมิภาคของจอแสดงผล เพื่อเพิ่มประสิทธิภาพการวาดให้ได้สูงสุด นอกจากนี้ คุณยังใช้การครอบตัดเพื่อสร้างเอฟเฟกต์ที่น่าสนใจในการออกแบบอินเทอร์เฟซผู้ใช้และภาพเคลื่อนไหวได้ด้วย
ตัวอย่างเช่น เมื่อวาดกองการ์ดที่ซ้อนทับกันดังที่แสดงด้านล่าง แทนที่จะวาดการ์ดแต่ละใบจากล่างขึ้นบนทั้งหมด การวาดเฉพาะส่วนที่มองเห็นได้มักจะมีประสิทธิภาพมากกว่า "โดยปกติ" เนื่องจากโอเปอเรชันการครอบตัดก็มีค่าใช้จ่ายเช่นกัน และโดยรวมแล้ว ระบบ Android จะทำการเพิ่มประสิทธิภาพการวาดภาพเป็นจำนวนมาก

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

โดยทั่วไปแล้ว พื้นที่การตัดจะเป็นสี่เหลี่ยมผืนผ้า แต่ก็อาจเป็นรูปร่างใดก็ได้หรือเป็นการผสมผสานของรูปร่างต่างๆ แม้แต่ข้อความ นอกจากนี้ คุณยังระบุได้ว่าจะรวมหรือยกเว้นภูมิภาคภายในภูมิภาคการครอบตัด เช่น คุณอาจสร้างขอบเขตการครอบตัดแบบวงกลมและแสดงเฉพาะสิ่งที่อยู่นอกวงกลม
ในโค้ดแล็บนี้ คุณจะได้ทดลองใช้การตัดคลิปในรูปแบบต่างๆ
สิ่งที่คุณควรทราบอยู่แล้ว
คุณควรคุ้นเคยกับสิ่งต่อไปนี้
- วิธีสร้างแอปด้วย
Activityและเรียกใช้โดยใช้ Android Studio - วิธีสร้างและวาดบน
Canvas - วิธีสร้าง
Viewที่กำหนดเอง และลบล้างonDraw()และonSizeChanged()
สิ่งที่คุณจะได้เรียนรู้
- วิธีตัดออบเจ็กต์เพื่อวาดบน
Canvas - วิธีบันทึกและกู้คืนสถานะการวาดของ Canvas
- วิธีใช้การเปลี่ยนรูปแบบกับ Canvas และข้อความ
สิ่งที่คุณต้องดำเนินการ
- สร้างแอปที่วาดรูปร่างที่ตัดบนหน้าจอเพื่อแสดงวิธีต่างๆ ในการตัดและผลลัพธ์ที่ได้ต่อระดับการมองเห็นของรูปร่างเหล่านั้น
- นอกจากนี้ คุณยังวาดข้อความที่แปลและเอียงได้ด้วย
แอป ClippingExample แสดงวิธีใช้และรวมรูปร่างเพื่อระบุส่วนของ Canvas ที่แสดงในมุมมอง แอปสุดท้ายจะมีลักษณะคล้ายกับภาพหน้าจอด้านล่าง

คุณจะสร้างแอปนี้ตั้งแต่ต้น ดังนั้นคุณจะต้องตั้งค่าโปรเจ็กต์ กำหนดมิติข้อมูลและสตริง และประกาศตัวแปรบางอย่าง
ขั้นตอนที่ 1: สร้างโปรเจ็กต์ ClippingExample
- สร้างโปรเจ็กต์ Kotlin ชื่อ
ClippingExampleโดยใช้เทมเพลตกิจกรรมเปล่า ใช้com.example.androidเป็นคำนำหน้าชื่อแพ็กเกจ - เปิด
MainActivity.kt - ใน
onCreate()ให้แทนที่มุมมองเนื้อหาเริ่มต้นและตั้งค่ามุมมองเนื้อหาเป็นอินสแตนซ์ใหม่ของClippedViewนี่จะเป็นมุมมองที่กําหนดเองสําหรับตัวอย่างการตัดคลิปที่คุณจะสร้างต่อไป
setContentView(ClippedView(this))- สร้างไฟล์และคลาส Kotlin ใหม่สำหรับมุมมองที่กำหนดเองชื่อ
ClippedViewซึ่งขยายViewในระดับเดียวกับMainActivity.ktตั้งค่าลายเซ็นตามที่แสดงด้านล่าง งานที่เหลือทั้งหมดจะอยู่ในClippedViewนี้ คำอธิบายประกอบ@JvmOverloadsจะสั่งให้คอมไพเลอร์ Kotlin สร้างการโอเวอร์โหลดสำหรับฟังก์ชันนี้ซึ่งจะแทนที่ค่าพารามิเตอร์เริ่มต้น
class ClippedView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}ขั้นตอนที่ 2: เพิ่มมิติข้อมูลและทรัพยากรสตริง
- กําหนดมิติข้อมูลที่จะใช้สําหรับมุมมองที่ตัดในไฟล์ทรัพยากรใหม่ใน
res/values/dimens.xmlมิติข้อมูลเริ่มต้นเหล่านี้ได้รับการฮาร์ดโค้ดและปรับขนาดให้พอดีกับหน้าจอขนาดเล็ก
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="clipRectRight">90dp</dimen>
<dimen name="clipRectBottom">90dp</dimen>
<dimen name="clipRectTop">0dp</dimen>
<dimen name="clipRectLeft">0dp</dimen>
<dimen name="rectInset">8dp</dimen>
<dimen name="smallRectOffset">40dp</dimen>
<dimen name="circleRadius">30dp</dimen>
<dimen name="textOffset">20dp</dimen>
<dimen name="strokeWidth">4dp</dimen>
<dimen name="textSize">18sp</dimen>
</resources>หากต้องการให้แอปดูดีบนหน้าจอที่ใหญ่ขึ้น (และดูรายละเอียดได้ง่ายขึ้น) คุณสามารถสร้างไฟล์ dimens ที่มีค่าที่ใหญ่ขึ้นซึ่งใช้ได้กับหน้าจอที่ใหญ่ขึ้นเท่านั้น
- ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ values แล้วเลือกใหม่ > ไฟล์ทรัพยากรค่า
- ในกล่องโต้ตอบไฟล์ทรัพยากรใหม่ ให้ตั้งชื่อไฟล์เป็น
dimensในตัวระบุที่ใช้ได้ ให้เลือกความกว้างของหน้าจอที่เล็กที่สุด แล้วคลิกปุ่ม >> เพื่อเพิ่มลงในตัวระบุที่เลือก ป้อน 480 ในช่องความกว้างหน้าจอที่เล็กที่สุด แล้วคลิกตกลง

- ไฟล์ควรแสดงในโฟลเดอร์ค่าดังที่แสดงด้านล่าง

- หากไม่เห็นไฟล์ ให้เปลี่ยนไปที่มุมมองไฟล์โปรเจ็กต์ของแอป เส้นทางแบบเต็มของไฟล์ใหม่จะเป็นดังที่แสดงด้านล่าง
ClippingExample/app/src/main/res/values-sw480dp/dimens.xml

- แทนที่เนื้อหาเริ่มต้นของไฟล์
values-sw480dp/dimens.xmlด้วยมิติข้อมูลด้านล่าง
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="clipRectRight">120dp</dimen>
<dimen name="clipRectBottom">120dp</dimen>
<dimen name="rectInset">10dp</dimen>
<dimen name="smallRectOffset">50dp</dimen>
<dimen name="circleRadius">40dp</dimen>
<dimen name="textOffset">25dp</dimen>
<dimen name="strokeWidth">6dp</dimen>
</resources>- ใน
strings.xmlให้เพิ่มสตริงต่อไปนี้ ระบบจะใช้ค่าเหล่านี้เพื่อแสดงข้อความบน Canvas
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>ขั้นตอนที่ 3: สร้างและเริ่มต้นออบเจ็กต์ Paint และ Path
- เปลี่ยนกลับไปเป็นมุมมอง Android ของโปรเจ็กต์
- ใน
ClippedViewให้กำหนดตัวแปรPaintเพื่อใช้วาด เปิดใช้การป้องกันรอยหยัก และใช้ความกว้างของเส้นและขนาดข้อความที่กำหนดไว้ในมิติข้อมูล ดังที่แสดงด้านล่าง
private val paint = Paint().apply {
// Smooth out edges of what is drawn without affecting shape.
isAntiAlias = true
strokeWidth = resources.getDimension(R.dimen.strokeWidth)
textSize = resources.getDimension(R.dimen.textSize)
}- ใน
ClippedViewให้สร้างและเริ่มต้นPathเพื่อจัดเก็บเส้นทางของสิ่งที่วาดไว้ในเครื่อง นำเข้าandroid.graphics.Path
private val path = Path()ขั้นตอนที่ 4: ตั้งค่ารูปร่าง
ในแอปนี้ คุณกำลังแสดงรูปร่างหลายแถวและ 2 คอลัมน์ที่ตัดในรูปแบบต่างๆ
โดยมีลักษณะร่วมดังนี้
- สี่เหลี่ยมผืนผ้า (จัตุรัส) ขนาดใหญ่ที่ทำหน้าที่เป็นคอนเทนเนอร์
- เส้นทแยงมุมข้ามสี่เหลี่ยมผืนผ้าขนาดใหญ่
- วงกลม
- สตริงข้อความสั้นๆ

ในขั้นตอนนี้ คุณจะตั้งค่ามิติข้อมูลสำหรับรูปร่างเหล่านั้นจากทรัพยากร เพื่อให้คุณต้องรับมิติข้อมูลเพียงครั้งเดียวเมื่อใช้ในภายหลัง
- ใน
ClippedViewใต้pathให้เพิ่มตัวแปรสำหรับมิติข้อมูลสำหรับสี่เหลี่ยมผืนผ้าการตัดรอบชุดรูปร่างทั้งหมด
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)- เพิ่มตัวแปรสำหรับการแทรกของสี่เหลี่ยมผืนผ้าและการออฟเซ็ตของสี่เหลี่ยมผืนผ้าขนาดเล็ก
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)- เพิ่มตัวแปรสำหรับรัศมีของวงกลม นี่คือรัศมีของวงกลมที่วาดภายในสี่เหลี่ยมผืนผ้า
private val circleRadius = resources.getDimension(R.dimen.circleRadius)- เพิ่มออฟเซ็ตและขนาดข้อความสำหรับข้อความที่วาดภายในสี่เหลี่ยมผืนผ้า
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)ขั้นตอนที่ 4: ตั้งค่าตำแหน่งแถวและคอลัมน์
รูปร่างของแอปนี้จะแสดงใน 2 คอลัมน์และ 4 แถว โดยพิจารณาจากค่าของมิติข้อมูลที่ตั้งค่าไว้ด้านบน การคำนวณนี้ไม่ได้เป็นส่วนหนึ่งของ Codelab นี้ แต่โปรดดูการคำนวณนี้ขณะคัดลอกโค้ดที่ระบุในขั้นตอนนี้
- ตั้งค่าพิกัดสำหรับ 2 คอลัมน์
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight- เพิ่มพิกัดสำหรับแต่ละแถว รวมถึงแถวสุดท้ายสำหรับข้อความที่แปลงแล้ว
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)- เรียกใช้แอป แอปควรเปิดขึ้นพร้อมกับหน้าจอสีขาวว่างเปล่าใต้ชื่อแอป

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

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

(1) ก่อนอื่น ให้แปล Canvas ไปยังตำแหน่งที่ต้องการวาดสี่เหลี่ยม กล่าวคือ แทนที่จะคำนวณตำแหน่งที่ต้องวาดสี่เหลี่ยมผืนผ้าถัดไปและรูปร่างอื่นๆ ทั้งหมด คุณจะย้ายCanvasจุดเริ่มต้น ซึ่งก็คือระบบพิกัด
(2) จากนั้นวาดสี่เหลี่ยมผืนผ้าที่จุดเริ่มต้นใหม่ของ Canvas กล่าวคือ คุณวาดรูปร่างในตำแหน่งเดียวกันในระบบพิกัดที่แปลแล้ว ซึ่งจะง่ายกว่าและมีประสิทธิภาพมากกว่าเล็กน้อย
(3) สุดท้าย ให้กู้คืน Canvas กลับเป็น Origin เดิม
อัลกอริทึมที่คุณจะใช้มีดังนี้
- ใน
onDraw()ให้เรียกใช้ฟังก์ชันเพื่อเติมสีพื้นหลังสีเทาในCanvasและวาดรูปร่างเดิม - เรียกใช้ฟังก์ชันสำหรับสี่เหลี่ยมผืนผ้าที่ครอบตัดแต่ละรายการและข้อความที่จะวาด
สำหรับสี่เหลี่ยมผืนผ้าหรือข้อความแต่ละรายการ ให้ทำดังนี้
- บันทึกสถานะปัจจุบันของ
Canvasเพื่อให้คุณรีเซ็ตกลับไปเป็นสถานะเริ่มต้นได้ - แปล
Originของ Canvas เป็นตำแหน่งที่ต้องการวาด - ใช้รูปร่างและเส้นทางการตัด
- วาดสี่เหลี่ยมผืนผ้าหรือข้อความ
- กู้คืนสถานะของ
Canvas
ขั้นตอน: ลบล้าง onDraw()
- แทนที่
onDraw()ดังที่แสดงในโค้ดด้านล่าง คุณเรียกฟังก์ชันสำหรับรูปร่างแต่ละรูปที่วาด ซึ่งคุณจะใช้ในภายหลัง
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawBackAndUnclippedRectangle(canvas)
drawDifferenceClippingExample(canvas)
drawCircularClippingExample(canvas)
drawIntersectionClippingExample(canvas)
drawCombinedClippingExample(canvas)
drawRoundedRectangleClippingExample(canvas)
drawOutsideClippingExample(canvas)
drawSkewedTextExample(canvas)
drawTranslatedTextExample(canvas)
// drawQuickRejectExample(canvas)
}- สร้าง Stub สำหรับฟังก์ชันการวาดแต่ละฟังก์ชันเพื่อให้โค้ดคอมไพล์ต่อไปได้ คุณสามารถคัดลอกโค้ดด้านล่าง
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}แอปวาดสี่เหลี่ยมผืนผ้าและรูปร่างเดียวกัน 7 ครั้ง โดยครั้งแรกจะไม่มีการตัด จากนั้นจะมีการตัด 6 ครั้งโดยใช้เส้นทางการตัดต่างๆ เมธอด drawClippedRectangle() จะแยกโค้ดสำหรับการวาดสี่เหลี่ยมผืนผ้า 1 รูป ดังที่แสดงด้านล่าง

ขั้นตอนที่ 1: สร้างเมธอด drawClippedRectangle()
- สร้าง
drawClippedRectangle()เมธอดที่รับอาร์กิวเมนต์canvasประเภทCanvas
private fun drawClippedRectangle(canvas: Canvas) {
}- ภายใน
drawClippedRectangle()method ให้ตั้งค่าขอบเขตของสี่เหลี่ยมผืนผ้าสำหรับการครอบตัดสำหรับรูปร่างทั้งหมด ใช้สี่เหลี่ยมผืนผ้าสำหรับการตัดที่จำกัดให้วาดเฉพาะสี่เหลี่ยมจัตุรัส
canvas.clipRect(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom
)เมธอด Canvas.clipRect(...) จะลดพื้นที่ของหน้าจอที่การดำเนินการวาดในอนาคตสามารถเขียนได้ โดยจะตั้งค่าขอบเขตการครอบตัดให้เป็นจุดตัดเชิงพื้นที่ของสี่เหลี่ยมผืนผ้าการครอบตัดปัจจุบันและสี่เหลี่ยมผืนผ้าที่ส่งผ่านไปยัง clipRect() clipRect() มีหลายรูปแบบที่ยอมรับรูปแบบต่างๆ สำหรับภูมิภาคและอนุญาตให้ดำเนินการต่างๆ บนสี่เหลี่ยมผืนผ้าสำหรับการตัด
- เติมสีขาวใน
canvasได้ ทั้ง Canvas เนื่องจากคุณไม่ได้วาดสี่เหลี่ยมผืนผ้า แต่กำลังตัด เนื่องจากมีสี่เหลี่ยมผืนผ้าสำหรับการตัด จึงมีการเติมเฉพาะพื้นที่ที่กำหนดโดยสี่เหลี่ยมผืนผ้าสำหรับการตัด ทำให้เกิดสี่เหลี่ยมผืนผ้าสีขาว ส่วนที่เหลือของพื้นผิวจะยังคงเป็นสีเทา
canvas.drawColor(Color.WHITE)- เปลี่ยนสีเป็นสีแดงและวาดเส้นทแยงมุมภายในสี่เหลี่ยมผืนผ้าสำหรับการมาสก์
paint.color = Color.RED
canvas.drawLine(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom,paint
)- ตั้งค่าสีเป็นสีเขียวและวาดวงกลมภายในสี่เหลี่ยมผืนผ้าสำหรับการมาสก์
paint.color = Color.GREEN
canvas.drawCircle(
circleRadius,clipRectBottom - circleRadius,
circleRadius,paint
)- ตั้งค่าสีเป็นสีน้ำเงินและวาดข้อความที่จัดแนวกับขอบด้านขวาของสี่เหลี่ยมผืนผ้าที่ครอบตัด ใช้
canvas.drawText()เพื่อวาดข้อความ
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
context.getString(R.string.clipping),
clipRectRight,textOffset,paint
)ขั้นตอนที่ 2: ใช้เมธอด drawBackAndUnclippedRectangle()
- หากต้องการดูวิธีการทำงานของ
drawClippedRectangle()ให้วาดสี่เหลี่ยมผืนผ้าแรกที่ไม่ได้ตัดโดยใช้เมธอดdrawBackAndUnclippedRectangle()ดังที่แสดงด้านล่าง บันทึกcanvasแปลเป็นตำแหน่งแถวและคอลัมน์แรก วาดโดยเรียกใช้drawClippedRectangle()แล้วกู้คืนcanvasเป็นสถานะก่อนหน้า
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
canvas.drawColor(Color.GRAY)
canvas.save()
canvas.translate(columnOne,rowOne)
drawClippedRectangle(canvas)
canvas.restore()
}- เรียกใช้แอป คุณควรเห็นสี่เหลี่ยมผืนผ้าสีขาวแรกที่มีวงกลม เส้นสีแดง และข้อความบนพื้นหลังสีเทา

ในตัวอย่างการตัดต่อไปนี้ คุณจะใช้วิธีการต่างๆ ในการรวมภูมิภาคการตัดเข้าด้วยกันเพื่อสร้างเอฟเฟกต์กราฟิก และเรียนรู้วิธีรวมภูมิภาคการตัดเพื่อสร้างรูปร่างที่ต้องการ
แต่ละวิธีมีรูปแบบเดียวกัน
- บันทึกสถานะปัจจุบันของ Canvas:
canvas.save()
บริบทกิจกรรมจะรักษาสแต็กของสถานะการวาด สถานะการวาดประกอบด้วยเมทริกซ์การแปลงปัจจุบันและภูมิภาคการตัดปัจจุบัน คุณสามารถบันทึกสถานะปัจจุบัน ดำเนินการที่เปลี่ยนสถานะของภาพวาด (เช่น การแปลหรือหมุน Canvas) แล้วกู้คืนสถานะของภาพวาดที่บันทึกไว้ได้ (หมายเหตุ: คำสั่งนี้คล้ายกับคำสั่ง "stash" ใน Git)
เมื่อภาพวาดมีการเปลี่ยนรูปแบบ การเชื่อมโยงและการเลิกทำการเปลี่ยนรูปแบบโดยการย้อนกลับอาจทำให้เกิดข้อผิดพลาดได้ เช่น หากคุณแปล ยืด แล้วหมุน การดำเนินการจะซับซ้อนขึ้นอย่างรวดเร็ว แต่ให้บันทึกสถานะของ Canvas ใช้การเปลี่ยนรูป วาด แล้วกู้คืนสถานะก่อนหน้าแทน
เช่น คุณอาจกำหนดภูมิภาคการครอบตัดและบันทึกสถานะนั้น จากนั้นแปลผืนผ้าใบ เพิ่มภูมิภาคการครอบตัด และหมุน หลังจากวาดภาพแล้ว คุณสามารถคืนค่าสถานะการครอบตัดเดิม และดำเนินการแปลและเปลี่ยนรูปภาพให้เอียงแบบอื่นได้ ดังที่แสดงในแผนภาพ

- แปลงต้นทางของ Canvas เป็นพิกัดแถว/คอลัมน์:
canvas.translate()
การย้ายต้นทางของ Canvas และวาดสิ่งเดียวกันในระบบพิกัดใหม่นั้นง่ายกว่าการย้ายองค์ประกอบทั้งหมดเพื่อวาดมาก (เคล็ดลับ: คุณใช้วิธีเดียวกันนี้ในการหมุนองค์ประกอบได้)
- ใช้การเปลี่ยนรูปแบบกับ
pathหากมี - ใช้การครอบตัด:
canvas.clipPath(path) - วาดรูปร่าง
drawClippedRectangle() or drawText() - กู้คืนสถานะ Canvas ก่อนหน้า:
canvas.restore()
ขั้นตอนที่ 1: ใช้ drawDifferenceClippingExample(canvas)
เพิ่มโค้ดเพื่อวาดสี่เหลี่ยมผืนผ้าที่ 2 ซึ่งใช้ความแตกต่างระหว่างสี่เหลี่ยมผืนผ้า 2 รูปที่ใช้การตัด เพื่อสร้างเอฟเฟกต์กรอบรูป

ใช้โค้ดด้านล่างซึ่งจะทำสิ่งต่อไปนี้
- บันทึก Canvas
- แปลต้นทางของ Canvas เป็นพื้นที่เปิดไปยังแถวแรก คอลัมน์ที่สอง ทางด้านขวาของสี่เหลี่ยมผืนผ้าแรก
- ใช้สี่เหลี่ยมผืนผ้าสำหรับการตัด 2 รูป โอเปอเรเตอร์
DIFFERENCEจะลบสี่เหลี่ยมผืนผ้าที่ 2 ออกจากสี่เหลี่ยมผืนผ้าที่ 1
- เรียกใช้เมธอด
drawClippedRectangle()เพื่อวาด Canvas ที่แก้ไขแล้ว - กู้คืนสถานะ Canvas
private fun drawDifferenceClippingExample(canvas: Canvas) {
canvas.save()
// Move the origin to the right for the next rectangle.
canvas.translate(columnTwo,rowOne)
// Use the subtraction of two clipping rectangles to create a frame.
canvas.clipRect(
2 * rectInset,2 * rectInset,
clipRectRight - 2 * rectInset,
clipRectBottom - 2 * rectInset
)
// The method clipRect(float, float, float, float, Region.Op
// .DIFFERENCE) was deprecated in API level 26. The recommended
// alternative method is clipOutRect(float, float, float, float),
// which is currently available in API level 26 and higher.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
canvas.clipRect(
4 * rectInset,4 * rectInset,
clipRectRight - 4 * rectInset,
clipRectBottom - 4 * rectInset,
Region.Op.DIFFERENCE
)
} else {
canvas.clipOutRect(
4 * rectInset,4 * rectInset,
clipRectRight - 4 * rectInset,
clipRectBottom - 4 * rectInset
)
}
drawClippedRectangle(canvas)
canvas.restore()
}- เรียกใช้แอปของคุณ ซึ่งควรมีลักษณะดังนี้

ขั้นตอนที่ 2: ใช้ drawCircularClippingExample(canvas)
จากนั้นเพิ่มโค้ดเพื่อวาดสี่เหลี่ยมผืนผ้าที่ใช้พื้นที่การตัดแบบวงกลมซึ่งสร้างจากเส้นทางวงกลม ซึ่งโดยพื้นฐานแล้วคือการนำวงกลมออก (ไม่วาด) และแสดงพื้นหลังสีเทาแทน

private fun drawCircularClippingExample(canvas: Canvas) {
canvas.save()
canvas.translate(columnOne, rowTwo)
// Clears any lines and curves from the path but unlike reset(),
// keeps the internal data structure for faster reuse.
path.rewind()
path.addCircle(
circleRadius,clipRectBottom - circleRadius,
circleRadius,Path.Direction.CCW
)
// The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
// API level 26. The recommended alternative method is
// clipOutPath(Path), which is currently available in
// API level 26 and higher.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipPath(path, Region.Op.DIFFERENCE)
} else {
canvas.clipOutPath(path)
}
drawClippedRectangle(canvas)
canvas.restore()
}ขั้นตอนที่ 3: ใช้ drawIntersectionClippingExample(canvas)
จากนั้นเพิ่มโค้ดเพื่อวาดจุดตัดของสี่เหลี่ยมผืนผ้า 2 รูปในแถวและคอลัมน์ที่ 2


โปรดทราบว่าลักษณะของภูมิภาคนี้จะแตกต่างกันไปตามความละเอียดของหน้าจอ ทดลองใช้smallRectOffsetมิติข้อมูลเพื่อเปลี่ยนขนาดของภูมิภาคที่มองเห็นได้ smallRectOffset ที่เล็กลงจะทำให้ภูมิภาคบนหน้าจอใหญ่ขึ้น
private fun drawIntersectionClippingExample(canvas: Canvas) {
canvas.save()
canvas.translate(columnTwo,rowTwo)
canvas.clipRect(
clipRectLeft,clipRectTop,
clipRectRight - smallRectOffset,
clipRectBottom - smallRectOffset
)
// The method clipRect(float, float, float, float, Region.Op
// .INTERSECT) was deprecated in API level 26. The recommended
// alternative method is clipRect(float, float, float, float), which
// is currently available in API level 26 and higher.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipRect(
clipRectLeft + smallRectOffset,
clipRectTop + smallRectOffset,
clipRectRight,clipRectBottom,
Region.Op.INTERSECT
)
} else {
canvas.clipRect(
clipRectLeft + smallRectOffset,
clipRectTop + smallRectOffset,
clipRectRight,clipRectBottom
)
}
drawClippedRectangle(canvas)
canvas.restore()
}
ขั้นตอนที่ 4: ใช้ drawCombinedClippingExample(canvas)
จากนั้นรวมรูปร่าง วงกลม และสี่เหลี่ยมผืนผ้า แล้ววาดเส้นทางเพื่อกำหนดขอบเขตการครอบตัด

private fun drawCombinedClippingExample(canvas: Canvas) {
canvas.save()
canvas.translate(columnOne, rowThree)
path.rewind()
path.addCircle(
clipRectLeft + rectInset + circleRadius,
clipRectTop + circleRadius + rectInset,
circleRadius,Path.Direction.CCW
)
path.addRect(
clipRectRight / 2 - circleRadius,
clipRectTop + circleRadius + rectInset,
clipRectRight / 2 + circleRadius,
clipRectBottom - rectInset,Path.Direction.CCW
)
canvas.clipPath(path)
drawClippedRectangle(canvas)
canvas.restore()
}
ขั้นตอนที่ 5: ใช้ drawRoundedRectangleClippingExample(canvas)
จากนั้นเพิ่มสี่เหลี่ยมผืนผ้าโค้งมนซึ่งเป็นรูปร่างการตัดที่ใช้กันโดยทั่วไป

- สร้างและเริ่มต้นตัวแปรสี่เหลี่ยมผืนผ้าที่ระดับบนสุด
RectFคือคลาสที่เก็บพิกัดสี่เหลี่ยมในรูปแบบทศนิยม
private var rectF = RectF(
rectInset,
rectInset,
clipRectRight - rectInset,
clipRectBottom - rectInset
)- ใช้ฟังก์ชัน
drawRoundedRectangleClippingExample()ฟังก์ชันaddRoundRect()จะใช้สี่เหลี่ยมผืนผ้า ค่าสำหรับค่า x และ y ของรัศมีมุม และทิศทางในการพันขอบสี่เหลี่ยมผืนผ้าโค้งมนPath.Directionระบุวิธีวางแนวรูปร่างปิด (เช่น สี่เหลี่ยมผืนผ้า วงรี) เมื่อเพิ่มลงในเส้นทางCCWย่อมาจากทวนเข็มนาฬิกา
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
canvas.save()
canvas.translate(columnTwo,rowThree)
path.rewind()
path.addRoundRect(
rectF,clipRectRight / 4,
clipRectRight / 4, Path.Direction.CCW
)
canvas.clipPath(path)
drawClippedRectangle(canvas)
canvas.restore()
}
ขั้นตอนที่ 6: ใช้ drawOutsideClippingExample(canvas)
ตัดขอบด้านนอกรอบสี่เหลี่ยมผืนผ้าโดยเพิ่มระยะขอบของสี่เหลี่ยมผืนผ้าสำหรับการตัดขอบเป็น 2 เท่า

private fun drawOutsideClippingExample(canvas: Canvas) {
canvas.save()
canvas.translate(columnOne,rowFour)
canvas.clipRect(2 * rectInset,2 * rectInset,
clipRectRight - 2 * rectInset,
clipRectBottom - 2 * rectInset)
drawClippedRectangle(canvas)
canvas.restore()
}
ขั้นตอนที่ 7: ใช้ drawTranslatedTextExample(canvas)
การวาดข้อความไม่ได้แตกต่างจากรูปร่างอื่นๆ และคุณสามารถใช้การเปลี่ยนรูปแบบกับข้อความได้ เช่น คุณสามารถแปลข้อความได้โดยการแปล Canvas และวาดข้อความ

- ใช้ฟังก์ชันด้านล่าง
private fun drawTranslatedTextExample(canvas: Canvas) {
canvas.save()
paint.color = Color.GREEN
// Align the RIGHT side of the text with the origin.
paint.textAlign = Paint.Align.LEFT
// Apply transformation to canvas.
canvas.translate(columnTwo,textRow)
// Draw text.
canvas.drawText(context.getString(R.string.translated),
clipRectLeft,clipRectTop,paint)
canvas.restore()
}- เรียกใช้แอปเพื่อดูข้อความที่แปล

ขั้นตอนที่ 8: ใช้ drawSkewedTextExample(canvas)
นอกจากนี้ คุณยังเอียงข้อความได้ด้วย นั่นคือการบิดเบือนในรูปแบบต่างๆ

- สร้างฟังก์ชันด้านล่างใน
ClippedView
private fun drawSkewedTextExample(canvas: Canvas) {
canvas.save()
paint.color = Color.YELLOW
paint.textAlign = Paint.Align.RIGHT
// Position text.
canvas.translate(columnTwo, textRow)
// Apply skew transformation.
canvas.skew(0.2f, 0.3f)
canvas.drawText(context.getString(R.string.skewed),
clipRectLeft, clipRectTop, paint)
canvas.restore()
}- เรียกใช้แอปเพื่อดูข้อความที่เอียงซึ่งวาดก่อนข้อความที่แปล

เมธอด quickReject() Canvas ช่วยให้คุณตรวจสอบได้ว่าสี่เหลี่ยมผืนผ้าหรือเส้นทางที่ระบุจะอยู่นอกภูมิภาคที่มองเห็นในปัจจุบันโดยสมบูรณ์หรือไม่ หลังจากใช้การเปลี่ยนรูปแบบทั้งหมดแล้ว
quickReject() วิธีนี้มีประโยชน์อย่างยิ่งเมื่อคุณสร้างภาพวาดที่ซับซ้อนมากขึ้นและต้องการทำอย่างรวดเร็วที่สุด ด้วย quickReject() คุณจะตัดสินใจได้อย่างมีประสิทธิภาพว่าไม่จำเป็นต้องวาดออบเจ็กต์ใดเลย และไม่จำเป็นต้องเขียนตรรกะการตัดกันของคุณเอง
- เมธอด
quickReject()จะแสดงผลtrueหากสี่เหลี่ยมผืนผ้าหรือเส้นทางไม่ปรากฏบนหน้าจอเลย สำหรับส่วนที่ทับซ้อนกันบางส่วน คุณยังคงต้องตรวจสอบด้วยตนเอง EdgeTypeเป็นAA(Antialiased: จัดการขอบโดยการปัดเศษ เนื่องจากอาจมีการปรับให้เรียบ) หรือBW(Black-White: จัดการขอบโดยการปัดเศษเป็นขอบเขตพิกเซลที่ใกล้ที่สุด) สำหรับการปัดเศษเป็นพิกเซลที่ใกล้ที่สุด
quickReject() มีหลายเวอร์ชัน และคุณยังดูได้ในเอกสารประกอบด้วย
| quickReject |
| quickReject |
| quickReject |
ในแบบฝึกหัดนี้ คุณจะวาดในแถวใหม่ใต้ข้อความและภายใน clipRect เหมือนเดิม
- คุณโทรหา
quickReject()ก่อนโดยใช้สี่เหลี่ยมผืนผ้าinClipRectangleซึ่งซ้อนทับกับclipRectดังนั้นquickReject()จะแสดงผลเป็นเท็จclipRectจะเต็มไปด้วยBLACKและจะวาดสี่เหลี่ยมผืนผ้าinClipRectangle

- จากนั้นเปลี่ยนโค้ดและเรียกใช้
quickReject()โดยมีnotInClipRectanglequickReject()จะแสดงค่าเป็นจริงclipRectจะมีค่าเป็นWHITEและจะไม่วาดnotInClipRectangle

เมื่อมีภาพวาดที่ซับซ้อน คุณจะทราบได้อย่างรวดเร็วว่ารูปร่างใดอยู่นอกภูมิภาคการตัดอย่างสมบูรณ์ และรูปร่างใดที่คุณอาจต้องทำการคำนวณและวาดเพิ่มเติม เนื่องจากรูปร่างนั้นอยู่ภายในภูมิภาคการตัดบางส่วนหรือทั้งหมด
ขั้นตอน: ทดลองใช้ quickReject()
- ที่ระดับบนสุด ให้สร้างตัวแปรสำหรับพิกัด y ของแถวเพิ่มเติม
private val rejectRow = rowFour + rectInset + 2*clipRectBottom- เพิ่มฟังก์ชัน
drawQuickRejectExample()ต่อไปนี้ลงในClippedViewอ่านโค้ดเนื่องจากมีทุกสิ่งที่คุณจำเป็นต้องทราบเพื่อใช้quickReject()
private fun drawQuickRejectExample(canvas: Canvas) {
val inClipRectangle = RectF(clipRectRight / 2,
clipRectBottom / 2,
clipRectRight * 2,
clipRectBottom * 2)
val notInClipRectangle = RectF(RectF(clipRectRight+1,
clipRectBottom+1,
clipRectRight * 2,
clipRectBottom * 2))
canvas.save()
canvas.translate(columnOne, rejectRow)
canvas.clipRect(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom
)
if (canvas.quickReject(
inClipRectangle, Canvas.EdgeType.AA)) {
canvas.drawColor(Color.WHITE)
}
else {
canvas.drawColor(Color.BLACK)
canvas.drawRect(inClipRectangle, paint
)
}
canvas.restore()
}- ใน
onDraw()ให้ยกเลิกการแสดงความคิดเห็นของการเรียกใช้drawQuickRejectExample() - เรียกใช้แอป แล้วคุณจะเห็นสี่เหลี่ยมผืนผ้าสีดำ ซึ่งเป็นภูมิภาคการคลิปที่เติมสี และส่วนต่างๆ ของ
inClipRectangleเนื่องจากสี่เหลี่ยมผืนผ้า 2 รูปซ้อนทับกันquickReject()จึงแสดงfalseและวาดinClipRectangle

- ใน
drawQuickRejectExample()ให้เปลี่ยนโค้ดเพื่อเรียกใช้quickReject()กับnotInClipRectangle.ตอนนี้quickReject()จะแสดงtrueและภูมิภาคการครอบตัดจะเปลี่ยนเป็นสีขาว

ดาวน์โหลดโค้ดสำหรับ Codelab ที่เสร็จสมบูรณ์
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping
หรือคุณจะดาวน์โหลดที่เก็บเป็นไฟล์ Zip, แตกไฟล์ และเปิดใน Android Studio ก็ได้
Contextของกิจกรรมจะรักษาสถานะที่คงการเปลี่ยนรูปแบบและภูมิภาคการครอบตัดสำหรับCanvas- ใช้
canvas.save()และcanvas.restore()เพื่อวาดและกลับสู่สถานะเดิมของ Canvas - หากต้องการวาดรูปร่างหลายรายการบน Canvas คุณจะคำนวณตำแหน่งของรูปร่างเหล่านั้นหรือย้าย (แปล) ต้นทางของพื้นผิวการวาดก็ได้ ซึ่งจะช่วยให้สร้างเมธอดอรรถประโยชน์สำหรับลำดับการวาดซ้ำได้ง่ายขึ้น
- โดยสามารถกำหนดรูปร่างของพื้นที่การตัดได้ตามต้องการ ไม่ว่าจะเป็นรูปร่างเดียวหรือหลายรูปร่างรวมกัน หรือจะเป็นเส้นทางก็ได้
- คุณสามารถเพิ่ม ลบ และตัดส่วนภูมิภาคที่ตัดออกเพื่อรับภูมิภาคที่ต้องการได้
- คุณใช้การเปลี่ยนรูปแบบกับข้อความได้โดยการเปลี่ยนรูปแบบ Canvas
- เมธอด
quickReject()Canvasช่วยให้คุณตรวจสอบได้ว่าสี่เหลี่ยมผืนผ้าหรือเส้นทางที่ระบุจะอยู่นอกภูมิภาคที่มองเห็นในปัจจุบันโดยสมบูรณ์หรือไม่
หลักสูตร Udacity:
เอกสารประกอบสำหรับนักพัฒนาแอป Android
Canvasชั้นเรียนBitmapชั้นเรียนViewชั้นเรียนPaintชั้นเรียน- การกำหนดค่า
Bitmap.config Region.Opโอเปอเรเตอร์Pathชั้นเรียนCanvasชั้นเรียนBitmapชั้นเรียนViewชั้นเรียนPaintชั้นเรียน- การกำหนดค่า
Bitmap.config Region.Opโอเปอเรเตอร์Pathชั้นเรียนandroid.graphicsเครื่องมือกราฟิก- การกำหนดค่า
Bitmap.ConfigCanvas - Canvas และ Drawables
- canvas.translate() ทำอะไร
- ทำความเข้าใจ save() และ restore() สำหรับบริบท Canvas
- การตัด
- การเบิกเกินบัญชี
@JvmOverloads
นอกจากนี้ โปรดดูชุดบทความสถาปัตยกรรมกราฟิกเพื่อดูคำอธิบายเชิงลึกเกี่ยวกับวิธีที่เฟรมเวิร์ก Android วาดภาพบนหน้าจอ
ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้
- มอบหมายการบ้านหากจำเป็น
- สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
- ให้คะแนนงานการบ้าน
ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม
หากคุณกำลังทำ Codelab นี้ด้วยตนเอง โปรดใช้แบบฝึกหัดเหล่านี้เพื่อทดสอบความรู้ของคุณ
ตอบคำถามต่อไปนี้
คำถามที่ 1
คุณเรียกใช้วิธีใดเพื่อยกเว้นรูปร่างจากการวาดอย่างมีประสิทธิภาพ
▢ excludeFromDrawing()
▢ quickReject()
▢ onDraw()
▢ clipRect()
คำถามที่ 2
Canvas.save() และ Canvas.restore() จะบันทึกและกู้คืนข้อมูลใด
▢ สี ความกว้างของเส้น ฯลฯ
▢ การเปลี่ยนรูปแบบปัจจุบันเท่านั้น
▢ การเปลี่ยนรูปแบบและภูมิภาคการตัดปัจจุบัน
▢ เฉพาะภูมิภาคการครอบตัดปัจจุบัน
คำถามที่ 3
Paint.Align ระบุ
▢ วิธีจัดแนวรูปร่างการวาดต่อไปนี้
▢ ด้านใดของต้นทางที่ดึงข้อความมา
▢ ตำแหน่งในภูมิภาคการครอบตัดที่จัดแนว
▢ ด้านใดของข้อความที่จะจัดแนวกับต้นทาง
ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab Android ขั้นสูงใน Kotlin