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
- สร้างโปรเจ็กต์ Kotlin ชื่อ
ClippingExample
ด้วยเทมเพลตกิจกรรมเปล่า ใช้com.example.android
สําหรับคํานําหน้าชื่อแพ็กเกจ - เปิด
MainActivity.kt
- ในเมธอด
onCreate()
ให้แทนที่มุมมองเนื้อหาเริ่มต้นและตั้งค่ามุมมองเนื้อหาเป็นอินสแตนซ์ใหม่ของClippedView
ซึ่งจะเป็นมุมมองที่กําหนดเองสําหรับตัวอย่างคลิปที่คุณจะสร้างถัดไป
setContentView(ClippedView(this))
- ในระดับเดียวกับ
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: เพิ่มมิติข้อมูลและแหล่งข้อมูลสตริง
- กําหนดขนาดที่คุณจะใช้สําหรับข้อมูลพร็อพเพอร์ตี้ที่ตัดมาในไฟล์ทรัพยากรใหม่ใน
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 ให้คลิกขวาที่โฟลเดอร์ค่าแล้วเลือก New > Value file file
- ในกล่องโต้ตอบไฟล์ทรัพยากรใหม่ ให้เรียกใช้ไฟล์
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
ให้เพิ่มสตริงต่อไปนี้ ซึ่งจะใช้แสดงข้อความบนผืนผ้าใบ
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>
ขั้นตอนที่ 3: สร้างและเริ่มต้นสีและวัตถุเส้นทาง
- เปลี่ยนกลับไปใช้มุมมอง 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
ขั้นตอน: ลบล้าง 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)
}
- สร้างต้นขั้วสําหรับแต่ละฟังก์ชันของภาพวาดเพื่อให้โค้ดจะรวบรวมต่อไป คัดลอกรหัสด้านล่างได้
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()
ให้กําหนดขอบเขตของรูปสี่เหลี่ยมผืนผ้าสําหรับตัดทั้งรูปร่าง ใช้รูปสี่เหลี่ยมผืนผ้าที่ย่อส่วนเพื่อวาดเฉพาะสี่เหลี่ยมจัตุรัส
canvas.clipRect(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom
)
เมธอด Canvas.clipRect(...)
จะลดพื้นที่ของหน้าจอที่จะใช้เขียนภาพวาดในอนาคต โดยจะตั้งค่าขอบเขตคลิปเป็นจุดตัดกันของรูปสี่เหลี่ยมผืนผ้ารูปสี่เหลี่ยมปัจจุบันและสี่เหลี่ยมผืนผ้าส่งเข้าไปใน clipRect()
มีเมธอด clipRect()
รูปแบบต่างๆ มากมายที่ยอมรับรูปแบบที่แตกต่างกันสําหรับภูมิภาค และอนุญาตให้ใช้การทํางานที่แตกต่างกันในรูปสี่เหลี่ยมสําหรับคลิป
- เติม
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.
save(
)
บริบทของกิจกรรมจะเก็บรักษาสถานะการวาดซ้อนไว้ สถานะการวาดจะประกอบไปด้วยเมทริกซ์การเปลี่ยนรูปแบบปัจจุบันและภูมิภาคที่ตัดในปัจจุบัน คุณบันทึกสถานะปัจจุบัน ดําเนินการที่เปลี่ยนสถานะการวาด (เช่น การแปลหรือหมุนผืนผ้าใบ) แล้วคืนค่าสถานะภาพวาดที่บันทึกไว้ได้ (หมายเหตุ: นี่คือคําสั่ง "stash" ใน git!)
เมื่อภาพวาดมีการเปลี่ยนรูปแบบ ห่วงโซ่และเลิกทําการเปลี่ยนรูปแบบโดยย้อนกลับ ตัวอย่างเช่น หากคุณแปล ขยาย แล้วหมุน เนื้อหาก็จะซับซ้อนอย่างรวดเร็ว ให้บันทึกสถานะของผืนผ้าใบ ใช้การเปลี่ยนรูปแบบ วาด แล้วกู้คืนสถานะก่อนหน้าแทน
เช่น คุณอาจกําหนดภูมิภาคที่ตัดแล้วบันทึกสถานะนั้น จากนั้นแปลภาพพิมพ์แคนวาส เพิ่มภูมิภาคที่ตัดออก และหมุน หลังจากวาดรูปบางส่วนแล้ว คุณสามารถคืนค่าสถานะคลิปต้นฉบับได้ แล้วจึงทําการเปลี่ยนรูปแบบและบิดเบือนรูปแบบอีกรูปแบบได้ดังที่แสดงในแผนภาพ
- แปลต้นทางของผืนผ้าใบเป็นพิกัดแถว/คอลัมน์:
canvas.
translate
()
การย้ายต้นทางของ Canvas จะวาดและวาดสิ่งเดียวกันในระบบพิกัดใหม่ได้ง่ายกว่าการย้ายองค์ประกอบทั้งหมดเพื่อวาด (เคล็ดลับ: คุณสามารถใช้เทคนิคเดียวกันสําหรับการหมุนเวียนองค์ประกอบ)
- ใช้การเปลี่ยนรูปแบบกับ
path
ถ้ามี - ใช้คลิป:
canvas.clipPath(path)
- วาดรูปร่าง:
drawClippedRectangle() or drawText()
- คืนค่าสถานะภาพพิมพ์แคนวาสก่อนหน้า:
canvas.restore()
ขั้นตอนที่ 1: ใช้DrawDifferenceClippingExample(canvas)
เพิ่มโค้ดเพื่อวาดสี่เหลี่ยมผืนผ้าที่ 2 ซึ่งใช้ความแตกต่างระหว่างสี่เหลี่ยมผืนผ้ารูปคลิป 2 รูปเพื่อสร้างเอฟเฟกต์กรอบรูป
ใช้โค้ดด้านล่างเพื่อดําเนินการต่อไปนี้
- บันทึกภาพพิมพ์แคนวาส
- แปลงต้นทางของผืนผ้าใบเป็นพื้นที่เปิดให้เป็นแถวแรก คอลัมน์ที่ 2 ทางด้านขวาของรูปสี่เหลี่ยมผืนผ้าแรก
- ใช้รูปสี่เหลี่ยมผืนผ้า 2 รูป โอเปอเรเตอร์
DIFFERENCE
จะลบสี่เหลี่ยมผืนผ้าที่ 2 ออกจากรายการแรก
- เรียกเมธอด
drawClippedRectangle()
เพื่อวาด 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: ใช้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)
ถัดไป ให้เพิ่มสี่เหลี่ยมผืนผ้ามุมมนซึ่งเป็นรูปร่างของคลิปที่นิยมใช้กันทั่วไป
- ที่ด้านบนสุด แล้วสร้างตัวแปรตัวแปรเริ่มต้น
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: นําDraw TranslatedTextExample(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
(การต่อต้านด้วยชื่อแทน: พิจารณาขอบด้วยการปัดเศษออก เนื่องจากอาจมีการป้องกันภาพแทนด้วยชื่อแทน) หรือBW
(สีดํา-ขาว: รักษาขอบขอบมนด้วยการปัดไปยังขอบเขตพิกเซลที่ใกล้ที่สุด) เพื่อปัดเศษให้เป็นพิกเซลที่ใกล้ที่สุด
quickReject()
มีอยู่หลายเวอร์ชันและคุณสามารถดูได้จากเอกสารประกอบ
| การตั้งค่าด่วนจะปฏิเสธ |
| การตั้งค่าด่วนจะปฏิเสธ |
| การตั้งค่าด่วนจะปฏิเสธ |
ในแบบฝึกหัดนี้ คุณจะวาดแถวใหม่ใต้ข้อความ และภายใน clipRect
ได้เหมือนเดิม
- คุณเรียกใช้
quickReject()
ด้วยสี่เหลี่ยมผืนผ้าinClipRectangle
ซึ่งซ้อนทับกับclipRect
ก่อน ดังนั้นquickReject()
จะแสดงผลค่า "เท็จ"clipRect
จึงเติมBLACK
และวาดรูปสี่เหลี่ยมผืนผ้าinClipRectangle
- จากนั้นเปลี่ยนโค้ดและโทรหา
quickReject()
ด้วยnotInClipRectangle
ตอนนี้quickReject()
จะแสดงผลเป็น "จริง" และมีการเติมclipRect
ด้วยWHITE
และไม่มีการวาดnotInClipRectangle
เมื่อมีภาพวาดที่ซับซ้อน ภาพนี้จะบอกได้อย่างรวดเร็วว่ารูปร่างใดอยู่นอกพื้นที่คลิปมากที่สุด และคุณอาจต้องคํานวณเพิ่มเติมและวาดรูป เนื่องจากรูปร่างเหล่านั้นบางส่วนหรือทั้งหมดในส่วนคลิปนั้น
ขั้นตอนที่: ทดสอบด้วยตัดสินใจปฏิเสธ (())
- ที่ระดับบนสุด ให้สร้างตัวแปรสําหรับพิกัด 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
- เมธอด
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.Config
Canvas
- ผืนผ้าใบและภาพวาด
- Canvas.translate() ทําอะไรได้บ้าง
- ทําความเข้าใจ Save() และคืนค่า() สําหรับบริบทของ Canvas
- การตัดคลิป
- แสดงทับ
@JvmOverloads
หรืออ่านชุดบทความกราฟิกกราฟิกเพื่อดูคําอธิบายโดยละเอียดว่า Android เฟรมเวิร์กแสดงบนหน้าจออย่างไร
ส่วนนี้จะอธิบายการบ้านและรายงานสําหรับนักเรียนที่ทํางานผ่าน Codelab นี้ซึ่งเป็นส่วนหนึ่งของหลักสูตรที่นําโดยผู้สอน สิ่งที่ผู้สอนต้องทํามีดังนี้
- มอบหมายการบ้านหากจําเป็น
- สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานทําการบ้าน
- ตัดเกรดการบ้าน
ผู้สอนจะใช้คําแนะนําเหล่านี้เท่าใดก็ได้หรือตามที่ต้องการก็ได้ และสามารถกําหนดให้การบ้านอื่นๆ ที่ตนคิดว่าเหมาะสมได้
หากคุณใช้ Codelab ด้วยตัวเอง ก็ให้ใช้การบ้านเพื่อทดสอบความรู้ของคุณได้
ตอบคําถามเหล่านี้
คำถามที่ 1
วิธีใดที่มีประสิทธิภาพในการยกเว้นรูปร่างจากการวาด
▢ excludeFromDrawing()
▢ quickReject()
▢ onDraw()
▢ clipRect()
คำถามที่ 2
Canvas.save()
และ Canvas.restore()
จะบันทึกและคืนค่าข้อมูลใด
▢ สี ความกว้างของเส้น ฯลฯ
▢ การเปลี่ยนแปลงปัจจุบันเท่านั้น
▢ การเปลี่ยนรูปแบบในปัจจุบันและภูมิภาคที่ตัดออก
▢ ภูมิภาคการตัดคลิปปัจจุบันเท่านั้น
คำถามที่ 3
Paint.Align
ระบุดังนี้
▢ วิธีจัดแนวรูปร่างภาพวาดต่อไปนี้
▢ ข้อความที่ต้นทาง
▢ บริเวณที่คลิปถูกตัดนั้นอยู่ในแนวเดียวกัน
▢ ด้านใดของข้อความเพื่อให้สอดคล้องกันกับต้นทาง
สําหรับลิงก์ไปยังหน้า Codelab อื่นๆ ในหลักสูตรนี้ โปรดดูหน้า Landing Page ขั้นสูงสําหรับ Android ใน Kotlin