การทําลายวัตถุของผืนผ้าใบ

Codelab นี้เป็นส่วนหนึ่งของหลักสูตร Android ขั้นสูงใน Kotlin คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้ หากเรียนผ่าน Codelab ตามลําดับ แต่ไม่บังคับ Codelab ของหลักสูตรทั้งหมดจะแสดงอยู่ในหน้า Landing Page ขั้นสูงของ Android ใน Kotlin Codelab

บทนำ

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

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

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

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

ใน Codelab นี้ คุณจะได้ทดลองตัดคลิปด้วยวิธีที่หลากหลาย

สิ่งที่ควรทราบอยู่แล้ว

คุณควรทําความคุ้นเคยกับสิ่งต่อไปนี้

  • วิธีสร้างแอปที่มี Activity และเรียกใช้ด้วย Android Studio
  • วิธีสร้างและวาดรูปใน Canvas
  • วิธีสร้าง View แบบกําหนดเอง รวมถึงลบล้าง onDraw() และ onSizeChanged()

สิ่งที่คุณจะได้เรียนรู้

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

สิ่งที่คุณจะทํา

  • สร้างแอปที่วาดรูปร่างเป็นคลิปบนหน้าจอเพื่อแสดงวิธีตัดคลิปแบบต่างๆ และผลลัพธ์ที่ได้เกี่ยวกับการเปิดเผยรูปร่างเหล่านั้น
  • คุณสามารถวาดข้อความที่แปลแล้วและเอียงได้

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

คุณกําลังจะสร้างแอปนี้ตั้งแต่ต้น ดังนั้นคุณจะต้องสร้างโปรเจ็กต์ กําหนดมิติข้อมูลและสตริง และประกาศตัวแปรบางอย่าง

ขั้นตอนที่ 1: สร้างโปรเจ็กต์ ClippingExample

  1. สร้างโปรเจ็กต์ Kotlin ชื่อ ClippingExample ด้วยเทมเพลตกิจกรรมเปล่า ใช้ com.example.android สําหรับคํานําหน้าชื่อแพ็กเกจ
  2. เปิด MainActivity.kt
  3. ในเมธอด onCreate() ให้แทนที่มุมมองเนื้อหาเริ่มต้นและตั้งค่ามุมมองเนื้อหาเป็นอินสแตนซ์ใหม่ของ ClippedView ซึ่งจะเป็นมุมมองที่กําหนดเองสําหรับตัวอย่างคลิปที่คุณจะสร้างถัดไป
setContentView(ClippedView(this))
  1. ในระดับเดียวกับ MainActivity.kt ให้สร้างไฟล์และคลาส Kotlin ใหม่สําหรับมุมมองที่กําหนดเองชื่อ ClippedView ที่ขยาย View แสดงลายเซ็นด้านล่าง ผลงานที่เหลือของคุณจะอยู่ใน 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 ให้คลิกขวาที่โฟลเดอร์ค่าแล้วเลือก New > Value file file
  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 ให้เพิ่มสตริงต่อไปนี้ ซึ่งจะใช้แสดงข้อความบนผืนผ้าใบ
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

ขั้นตอนที่ 3: สร้างและเริ่มต้นสีและวัตถุเส้นทาง

  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 ของผืนผ้าใบไปยังตําแหน่งที่คุณต้องการวาด
  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. สร้างต้นขั้วสําหรับแต่ละฟังก์ชันของภาพวาดเพื่อให้โค้ดจะรวบรวมต่อไป คัดลอกรหัสด้านล่างได้
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() ให้กําหนดขอบเขตของรูปสี่เหลี่ยมผืนผ้าสําหรับตัดทั้งรูปร่าง ใช้รูปสี่เหลี่ยมผืนผ้าที่ย่อส่วนเพื่อวาดเฉพาะสี่เหลี่ยมจัตุรัส
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

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

  1. เติม 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.save()

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

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

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

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

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

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

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

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

ใช้โค้ดด้านล่างเพื่อดําเนินการต่อไปนี้

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

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

boolean

การตั้งค่าด่วนจะปฏิเสธ(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

การตั้งค่าด่วนจะปฏิเสธ(RectF rect, Canvas.EdgeType type)

boolean

การตั้งค่าด่วนจะปฏิเสธ(เส้นทาง path, Canvas.EdgeType type)

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

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

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

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

ขั้นตอนที่: ทดสอบด้วยตัดสินใจปฏิเสธ (())

  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
  • เมธอด quickReject() Canvas ช่วยให้คุณตรวจสอบได้ว่าสี่เหลี่ยมผืนผ้าหรือเส้นทางที่ระบุจะอยู่นอกพื้นที่ที่มองเห็นได้ในปัจจุบันทั้งหมดหรือไม่

หลักสูตร Udacity:

เอกสารประกอบสําหรับนักพัฒนาซอฟต์แวร์ Android

หรืออ่านชุดบทความกราฟิกกราฟิกเพื่อดูคําอธิบายโดยละเอียดว่า Android เฟรมเวิร์กแสดงบนหน้าจออย่างไร

ส่วนนี้จะอธิบายการบ้านและรายงานสําหรับนักเรียนที่ทํางานผ่าน Codelab นี้ซึ่งเป็นส่วนหนึ่งของหลักสูตรที่นําโดยผู้สอน สิ่งที่ผู้สอนต้องทํามีดังนี้

  • มอบหมายการบ้านหากจําเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานทําการบ้าน
  • ตัดเกรดการบ้าน

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

หากคุณใช้ Codelab ด้วยตัวเอง ก็ให้ใช้การบ้านเพื่อทดสอบความรู้ของคุณได้

ตอบคําถามเหล่านี้

คำถามที่ 1

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

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

คำถามที่ 2

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

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

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

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

▢ ภูมิภาคการตัดคลิปปัจจุบันเท่านั้น

คำถามที่ 3

Paint.Align ระบุดังนี้

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

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

▢ บริเวณที่คลิปถูกตัดนั้นอยู่ในแนวเดียวกัน

▢ ด้านใดของข้อความเพื่อให้สอดคล้องกันกับต้นทาง

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