การตัดออบเจ็กต์ Canvas

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

  1. สร้างโปรเจ็กต์ Kotlin ชื่อ ClippingExample โดยใช้เทมเพลตกิจกรรมเปล่า ใช้ com.example.android เป็นคำนำหน้าชื่อแพ็กเกจ
  2. เปิด MainActivity.kt
  3. ในonCreate() ให้แทนที่มุมมองเนื้อหาเริ่มต้นและตั้งค่ามุมมองเนื้อหาเป็นอินสแตนซ์ใหม่ของ ClippedView นี่จะเป็นมุมมองที่กําหนดเองสําหรับตัวอย่างการตัดคลิปที่คุณจะสร้างต่อไป
setContentView(ClippedView(this))
  1. สร้างไฟล์และคลาส 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: เพิ่มมิติข้อมูลและทรัพยากรสตริง

  1. กําหนดมิติข้อมูลที่จะใช้สําหรับมุมมองที่ตัดในไฟล์ทรัพยากรใหม่ใน 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 ที่มีค่าที่ใหญ่ขึ้นซึ่งใช้ได้กับหน้าจอที่ใหญ่ขึ้นเท่านั้น

  1. ใน Android Studio ให้คลิกขวาที่โฟลเดอร์ values แล้วเลือกใหม่ > ไฟล์ทรัพยากรค่า
  2. ในกล่องโต้ตอบไฟล์ทรัพยากรใหม่ ให้ตั้งชื่อไฟล์เป็น dimens ในตัวระบุที่ใช้ได้ ให้เลือกความกว้างของหน้าจอที่เล็กที่สุด แล้วคลิกปุ่ม >> เพื่อเพิ่มลงในตัวระบุที่เลือก ป้อน 480 ในช่องความกว้างหน้าจอที่เล็กที่สุด แล้วคลิกตกลง

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

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

  1. แทนที่เนื้อหาเริ่มต้นของไฟล์ 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>
  1. ใน strings.xml ให้เพิ่มสตริงต่อไปนี้ ระบบจะใช้ค่าเหล่านี้เพื่อแสดงข้อความบน Canvas
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

ขั้นตอนที่ 3: สร้างและเริ่มต้นออบเจ็กต์ Paint และ Path

  1. เปลี่ยนกลับไปเป็นมุมมอง Android ของโปรเจ็กต์
  2. ใน 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)
}
  1. ใน ClippedView ให้สร้างและเริ่มต้น Path เพื่อจัดเก็บเส้นทางของสิ่งที่วาดไว้ในเครื่อง นำเข้า android.graphics.Path
private val path = Path()

ขั้นตอนที่ 4: ตั้งค่ารูปร่าง

ในแอปนี้ คุณกำลังแสดงรูปร่างหลายแถวและ 2 คอลัมน์ที่ตัดในรูปแบบต่างๆ

โดยมีลักษณะร่วมดังนี้

  • สี่เหลี่ยมผืนผ้า (จัตุรัส) ขนาดใหญ่ที่ทำหน้าที่เป็นคอนเทนเนอร์
  • เส้นทแยงมุมข้ามสี่เหลี่ยมผืนผ้าขนาดใหญ่
  • วงกลม
  • สตริงข้อความสั้นๆ

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

  1. ใน 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)
  1. เพิ่มตัวแปรสำหรับการแทรกของสี่เหลี่ยมผืนผ้าและการออฟเซ็ตของสี่เหลี่ยมผืนผ้าขนาดเล็ก
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. เพิ่มตัวแปรสำหรับรัศมีของวงกลม นี่คือรัศมีของวงกลมที่วาดภายในสี่เหลี่ยมผืนผ้า
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. เพิ่มออฟเซ็ตและขนาดข้อความสำหรับข้อความที่วาดภายในสี่เหลี่ยมผืนผ้า
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

ขั้นตอนที่ 4: ตั้งค่าตำแหน่งแถวและคอลัมน์

รูปร่างของแอปนี้จะแสดงใน 2 คอลัมน์และ 4 แถว โดยพิจารณาจากค่าของมิติข้อมูลที่ตั้งค่าไว้ด้านบน การคำนวณนี้ไม่ได้เป็นส่วนหนึ่งของ Codelab นี้ แต่โปรดดูการคำนวณนี้ขณะคัดลอกโค้ดที่ระบุในขั้นตอนนี้

  1. ตั้งค่าพิกัดสำหรับ 2 คอลัมน์
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. เพิ่มพิกัดสำหรับแต่ละแถว รวมถึงแถวสุดท้ายสำหรับข้อความที่แปลงแล้ว
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)
  1. เรียกใช้แอป แอปควรเปิดขึ้นพร้อมกับหน้าจอสีขาวว่างเปล่าใต้ชื่อแอป

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

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

(1) ก่อนอื่น ให้แปล Canvas ไปยังตำแหน่งที่ต้องการวาดสี่เหลี่ยม กล่าวคือ แทนที่จะคำนวณตำแหน่งที่ต้องวาดสี่เหลี่ยมผืนผ้าถัดไปและรูปร่างอื่นๆ ทั้งหมด คุณจะย้ายCanvasจุดเริ่มต้น ซึ่งก็คือระบบพิกัด

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

(3) สุดท้าย ให้กู้คืน Canvas กลับเป็น Origin เดิม

อัลกอริทึมที่คุณจะใช้มีดังนี้

  1. ใน onDraw() ให้เรียกใช้ฟังก์ชันเพื่อเติมสีพื้นหลังสีเทาใน Canvas และวาดรูปร่างเดิม
  2. เรียกใช้ฟังก์ชันสำหรับสี่เหลี่ยมผืนผ้าที่ครอบตัดแต่ละรายการและข้อความที่จะวาด

สำหรับสี่เหลี่ยมผืนผ้าหรือข้อความแต่ละรายการ ให้ทำดังนี้

  1. บันทึกสถานะปัจจุบันของ Canvas เพื่อให้คุณรีเซ็ตกลับไปเป็นสถานะเริ่มต้นได้
  2. แปลOriginของ Canvas เป็นตำแหน่งที่ต้องการวาด
  3. ใช้รูปร่างและเส้นทางการตัด
  4. วาดสี่เหลี่ยมผืนผ้าหรือข้อความ
  5. กู้คืนสถานะของ Canvas

ขั้นตอน: ลบล้าง onDraw()

  1. แทนที่ 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)
    }
  1. สร้าง 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()

  1. สร้างdrawClippedRectangle()เมธอดที่รับอาร์กิวเมนต์ canvas ประเภท Canvas
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. ภายในdrawClippedRectangle() method ให้ตั้งค่าขอบเขตของสี่เหลี่ยมผืนผ้าสำหรับการครอบตัดสำหรับรูปร่างทั้งหมด ใช้สี่เหลี่ยมผืนผ้าสำหรับการตัดที่จำกัดให้วาดเฉพาะสี่เหลี่ยมจัตุรัส
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

เมธอด Canvas.clipRect(...) จะลดพื้นที่ของหน้าจอที่การดำเนินการวาดในอนาคตสามารถเขียนได้ โดยจะตั้งค่าขอบเขตการครอบตัดให้เป็นจุดตัดเชิงพื้นที่ของสี่เหลี่ยมผืนผ้าการครอบตัดปัจจุบันและสี่เหลี่ยมผืนผ้าที่ส่งผ่านไปยัง clipRect() clipRect() มีหลายรูปแบบที่ยอมรับรูปแบบต่างๆ สำหรับภูมิภาคและอนุญาตให้ดำเนินการต่างๆ บนสี่เหลี่ยมผืนผ้าสำหรับการตัด

  1. เติมสีขาวใน canvas ได้ ทั้ง Canvas เนื่องจากคุณไม่ได้วาดสี่เหลี่ยมผืนผ้า แต่กำลังตัด เนื่องจากมีสี่เหลี่ยมผืนผ้าสำหรับการตัด จึงมีการเติมเฉพาะพื้นที่ที่กำหนดโดยสี่เหลี่ยมผืนผ้าสำหรับการตัด ทำให้เกิดสี่เหลี่ยมผืนผ้าสีขาว ส่วนที่เหลือของพื้นผิวจะยังคงเป็นสีเทา
canvas.drawColor(Color.WHITE)
  1. เปลี่ยนสีเป็นสีแดงและวาดเส้นทแยงมุมภายในสี่เหลี่ยมผืนผ้าสำหรับการมาสก์
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. ตั้งค่าสีเป็นสีเขียวและวาดวงกลมภายในสี่เหลี่ยมผืนผ้าสำหรับการมาสก์
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. ตั้งค่าสีเป็นสีน้ำเงินและวาดข้อความที่จัดแนวกับขอบด้านขวาของสี่เหลี่ยมผืนผ้าที่ครอบตัด ใช้ 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()

  1. หากต้องการดูวิธีการทำงานของdrawClippedRectangle() ให้วาดสี่เหลี่ยมผืนผ้าแรกที่ไม่ได้ตัดโดยใช้เมธอด drawBackAndUnclippedRectangle() ดังที่แสดงด้านล่าง บันทึก canvas แปลเป็นตำแหน่งแถวและคอลัมน์แรก วาดโดยเรียกใช้ drawClippedRectangle() แล้วกู้คืน canvas เป็นสถานะก่อนหน้า
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. เรียกใช้แอป คุณควรเห็นสี่เหลี่ยมผืนผ้าสีขาวแรกที่มีวงกลม เส้นสีแดง และข้อความบนพื้นหลังสีเทา

ในตัวอย่างการตัดต่อไปนี้ คุณจะใช้วิธีการต่างๆ ในการรวมภูมิภาคการตัดเข้าด้วยกันเพื่อสร้างเอฟเฟกต์กราฟิก และเรียนรู้วิธีรวมภูมิภาคการตัดเพื่อสร้างรูปร่างที่ต้องการ

แต่ละวิธีมีรูปแบบเดียวกัน

  1. บันทึกสถานะปัจจุบันของ Canvas: canvas.save()

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

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

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

  1. แปลงต้นทางของ Canvas เป็นพิกัดแถว/คอลัมน์: canvas.translate()

การย้ายต้นทางของ Canvas และวาดสิ่งเดียวกันในระบบพิกัดใหม่นั้นง่ายกว่าการย้ายองค์ประกอบทั้งหมดเพื่อวาดมาก (เคล็ดลับ: คุณใช้วิธีเดียวกันนี้ในการหมุนองค์ประกอบได้)

  1. ใช้การเปลี่ยนรูปแบบกับ path หากมี
  2. ใช้การครอบตัด: canvas.clipPath(path)
  3. วาดรูปร่าง drawClippedRectangle() or drawText()
  4. กู้คืนสถานะ Canvas ก่อนหน้า: canvas.restore()

ขั้นตอนที่ 1: ใช้ drawDifferenceClippingExample(canvas)

เพิ่มโค้ดเพื่อวาดสี่เหลี่ยมผืนผ้าที่ 2 ซึ่งใช้ความแตกต่างระหว่างสี่เหลี่ยมผืนผ้า 2 รูปที่ใช้การตัด เพื่อสร้างเอฟเฟกต์กรอบรูป

ใช้โค้ดด้านล่างซึ่งจะทำสิ่งต่อไปนี้

  1. บันทึก Canvas
  2. แปลต้นทางของ Canvas เป็นพื้นที่เปิดไปยังแถวแรก คอลัมน์ที่สอง ทางด้านขวาของสี่เหลี่ยมผืนผ้าแรก
  3. ใช้สี่เหลี่ยมผืนผ้าสำหรับการตัด 2 รูป โอเปอเรเตอร์ DIFFERENCE จะลบสี่เหลี่ยมผืนผ้าที่ 2 ออกจากสี่เหลี่ยมผืนผ้าที่ 1
  1. เรียกใช้เมธอด drawClippedRectangle() เพื่อวาด Canvas ที่แก้ไขแล้ว
  2. กู้คืนสถานะ 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()
}
  1. เรียกใช้แอปของคุณ ซึ่งควรมีลักษณะดังนี้

ขั้นตอนที่ 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)

จากนั้นเพิ่มสี่เหลี่ยมผืนผ้าโค้งมนซึ่งเป็นรูปร่างการตัดที่ใช้กันโดยทั่วไป

  1. สร้างและเริ่มต้นตัวแปรสี่เหลี่ยมผืนผ้าที่ระดับบนสุด RectF คือคลาสที่เก็บพิกัดสี่เหลี่ยมในรูปแบบทศนิยม
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. ใช้ฟังก์ชัน 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 และวาดข้อความ

  1. ใช้ฟังก์ชันด้านล่าง
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()
}
  1. เรียกใช้แอปเพื่อดูข้อความที่แปล

ขั้นตอนที่ 8: ใช้ drawSkewedTextExample(canvas)

นอกจากนี้ คุณยังเอียงข้อความได้ด้วย นั่นคือการบิดเบือนในรูปแบบต่างๆ

  1. สร้างฟังก์ชันด้านล่างใน 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()
}
  1. เรียกใช้แอปเพื่อดูข้อความที่เอียงซึ่งวาดก่อนข้อความที่แปล

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

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

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

quickReject() มีหลายเวอร์ชัน และคุณยังดูได้ในเอกสารประกอบด้วย

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

ในแบบฝึกหัดนี้ คุณจะวาดในแถวใหม่ใต้ข้อความและภายใน clipRect เหมือนเดิม

  • คุณโทรหา quickReject() ก่อนโดยใช้สี่เหลี่ยมผืนผ้า inClipRectangle ซึ่งซ้อนทับกับ clipRect ดังนั้น quickReject() จะแสดงผลเป็นเท็จ clipRect จะเต็มไปด้วย BLACK และจะวาดสี่เหลี่ยมผืนผ้า inClipRectangle

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

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

ขั้นตอน: ทดลองใช้ quickReject()

  1. ที่ระดับบนสุด ให้สร้างตัวแปรสำหรับพิกัด y ของแถวเพิ่มเติม
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. เพิ่มฟังก์ชัน 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()
}
  1. ใน onDraw() ให้ยกเลิกการแสดงความคิดเห็นของการเรียกใช้ drawQuickRejectExample()
  2. เรียกใช้แอป แล้วคุณจะเห็นสี่เหลี่ยมผืนผ้าสีดำ ซึ่งเป็นภูมิภาคการคลิปที่เติมสี และส่วนต่างๆ ของ inClipRectangle เนื่องจากสี่เหลี่ยมผืนผ้า 2 รูปซ้อนทับกัน quickReject() จึงแสดง false และวาด inClipRectangle

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

ดาวน์โหลดโค้ดสำหรับ Codelab ที่เสร็จสมบูรณ์

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


หรือคุณจะดาวน์โหลดที่เก็บเป็นไฟล์ Zip, แตกไฟล์ และเปิดใน Android Studio ก็ได้

ดาวน์โหลด Zip

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

หลักสูตร Udacity:

เอกสารประกอบสำหรับนักพัฒนาแอป Android

นอกจากนี้ โปรดดูชุดบทความสถาปัตยกรรมกราฟิกเพื่อดูคำอธิบายเชิงลึกเกี่ยวกับวิธีที่เฟรมเวิร์ก Android วาดภาพบนหน้าจอ

ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้

  • มอบหมายการบ้านหากจำเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
  • ให้คะแนนงานการบ้าน

ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม

หากคุณกำลังทำ Codelab นี้ด้วยตนเอง โปรดใช้แบบฝึกหัดเหล่านี้เพื่อทดสอบความรู้ของคุณ

ตอบคำถามต่อไปนี้

คำถามที่ 1

คุณเรียกใช้วิธีใดเพื่อยกเว้นรูปร่างจากการวาดอย่างมีประสิทธิภาพ

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

คำถามที่ 2

Canvas.save() และ Canvas.restore() จะบันทึกและกู้คืนข้อมูลใด

▢ สี ความกว้างของเส้น ฯลฯ

▢ การเปลี่ยนรูปแบบปัจจุบันเท่านั้น

▢ การเปลี่ยนรูปแบบและภูมิภาคการตัดปัจจุบัน

▢ เฉพาะภูมิภาคการครอบตัดปัจจุบัน

คำถามที่ 3

Paint.Align ระบุ

▢ วิธีจัดแนวรูปร่างการวาดต่อไปนี้

▢ ด้านใดของต้นทางที่ดึงข้อความมา

▢ ตำแหน่งในภูมิภาคการครอบตัดที่จัดแนว

▢ ด้านใดของข้อความที่จะจัดแนวกับต้นทาง

ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab Android ขั้นสูงใน Kotlin