ห่างหายกันไปนาน วันนี้ก็ได้กลับมาเขียนบทความต่อเสียที โดยขอหยิบเรื่องการบันทึกภาพหน้าจอหรือที่เรียกกันว่า Screen Capture มาเล่าสู่กันฟังครับ
Capture!!
โดยปกติแล้วเวลาผู้ใช้จะ Screen Capture ก็จะกดปุ่ม Power + Volume Down แล้วแต่ว่าจะเป็นยี่ห้อไหน เวอร์ชันอะไร เพราะแต่ละรุ่นจะมีวิธีกดแตกต่างกันเล็กน้อย
แต่ว่า Screen Capture แบบนั้นจะทำในระดับ System ดังนั้นผู้ใช้ก็จะได้ภาพหน้าจอทั้งหมดเลย แต่ทว่าในบางแอปก็อยากจะใส่ฟีเจอร์ Screen Capture เพิ่มเข้าไปด้วย เพื่อให้ผู้ใช้สามารถกดบันทึกได้ทันทีโดยไม่ต้องมานั่งกด Screen Capture เอง ยกตัวอย่างเช่น ผู้ที่หลงเข้ามาอ่านพัฒนาแอปอย่าง Online Shopping ขึ้นมา เวลาลูกค้ากดซื้อของก็จะออกใบเสร็จให้ดูจากในแอปได้เลย แต่ถ้าจะให้ดีกว่านั้น ผู้ใช้ก็น่าจะกดบันทึกภาพใบเสร็จได้เนอะ? และนั่นก็คือผู้ที่หลงเข้ามาอ่านจะต้องเพิ่มคำสั่ง Screen Capture เข้าไปในโค้ดนั่นเอง
มันคือการทำ Screen Capture แบบ Programmatically นั่นเอง
ข้อจำกัดของการทำ Screen Capture แบบ Programmatically
การสั่ง Screen Capture ด้วยโค้ดนั่นจะต่างจากการที่กด Screen Capture แบบ System ตรงที่จะได้แค่ภาพหน้าจอภายในแอปของตัวเองเท่านั้น พื้นที่อื่นๆนอกจากนั้นจะไม่สามารถบันทึกได้
และเบื้องหลังวิธีนี้คือการดึงภาพจาก View มาบันทึก ไม่ได้ไปเรียก Screen Capture จาก System แต่อย่างใด ดังนั้นอย่าหวังว่าจะมีเอฟเฟคดังแช๊ะๆ ถ้าอยากได้ก็ต้องไปทำเองนะ
สมมติว่าอยากจะ Capture เฉพาะ View ที่ชื่อ @+id/layoutLocationInfoContainer
เวลาสั่ง Capture ผ่านโค้ดก็จะสั่งที่ View ตัวนั้นเลย และก็จะได้ผลลัพธ์ออกมาแบบนี้
สิ่งที่ควรรู้ไว้อีกอย่างหนึ่งก็คือภาพที่ Capture จะมีขนาดตามหน้าจอจริงๆ ถ้าหน้าจอความละเอียดสูงก็จะได้ภาพที่มีขนาดใหญ่ตามไปด้วย ดังนั้นจึงไม่เหมาะกับการ Capture อะไรบางอย่างที่จำเป็นต้องกำหนดขนาดตายตัว
ลองดูกันเลยดีกว่า
เพราะเป็นการ Screen Capture โดยใช้วิธีดึงภาพจาก View มาบันทึก ดังนั้นก็อย่าลืมกำหนด ID ให้กับ View ด้วยล่ะ จะได้เรียกผ่าน Programmatically ได้
และเจ้าของบล็อกมักจะพบผู้ที่หลงเข้ามาอ่านหลายๆคนใช้คำสั่งแบบนี้ในการ Capture เนื่องจากหาเจอได้ทันทีใน StackOverflow
private fun capture(view: View): Bitmap {
view.isDrawingCacheEnabled = true
val bitmap = Bitmap.createBitmap(view.drawingCache)
view.isDrawingCacheEnabled = false
return bitmap
}
เป็นการดึงภาพจาก Drawing Cache มาแปะลงบน Bitmap ที่เตรียมไว้นั่นเอง
วิธีนี้อาจจะดูเหมือนใช้งานได้ปกติ แต่เมื่อใดก็ตามที่ภาพจาก Drawing Cache มีขนาดใหญ่ในระดับหนึ่ง Drawing Cache จะมีค่าเป็น null
ทันทีโดยไม่บอกกล่าวอะไร นั่นหมายความว่า NullPointerException มีโอกาสเกิดขึ้นตอนที่ใช้คำสั่ง Bitmap.createBitmap(...)
นั่นเอง
ดังนั้นถ้าผู้ที่หลงเข้ามาอ่านใช้วิธีนี้ก็อย่าลืมเช็คดูว่าภาพที่จะ Capture มีขนาดใหญ่เกินไปมั้ย โดยลองทดสอบบนหน้าจอที่มีความละเอียดสูงดู ถ้าไม่เจอปัญหาอะไรก็สามารถใช้งานได้อยู่นะ แต่ถ้าเป็นไปได้ก็แนะนำให้เปลี่ยนเป็นวิธีอื่นดีกว่า
ถ้าไปอ่านใน Official Documentation ของคลาส View ก็จะพบว่าคำสั่งนี้เลิกใช้งานไปนานแล้ว และเพิ่งจะถูกประกาศ Deprecated ไปใน Android 9.0 Pie (API Level 28)
เพื่อเลี่ยงปัญหา NullPointerException เพราะภาพมีขนาดใหญ่เกิน ขอแนะนำให้ใช้วิธีแบบนี้แทน
private fun capture(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(
view.measuredWidth,
view.measuredHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
วิธีนี้จะเป็นการสั่งให้ View ทำการ Draw ลงบน Canvas แทน ซึ่งคำสั่ง layout()
และ draw()
เป็นคำสั่งในการของ View ที่แสดงภาพบนหน้าจอให้ผู้ใช้เห็นอยู่แล้ว (เพราะเบื้องหลังของ View ก็คือ Canvas นั่นแหละ) เพียงแค่ว่าเอามาใช้กับ Canvas ที่เตรียมไว้เพื่อทำภาพให้กลายเป็น Bitmap
Android Jetpack เตรียมคำสั่งไว้ให้แล้วนะ ไม่ต้องเขียนเองอีกต่อไปแล้ว!
ทีมแอนดรอยด์ได้เพิ่มคำสั่งไว้ให้ในรูปแบบ Extension Function สำหรับคลาส View เรียบร้อยแล้วใน androidx.core:core-ktx
จึงทำให้นักพัฒนาสามารถใช้คำสั่งเพื่อดึงภาพจาก View ออกมาเป็น Bitmap ด้วยคำสั่ง drawToBitmap()
ได้เลย
val view: View = /* ... */
val bitmap = view.drawToBitmap()
โค้ดกระชับ เรียกใช้งานง่ายแบบนี้ ไม่เปลี่ยนมาใช้ก็บ้าแล้ววววว
ของแถม
ถ้าอยากจะบันทึกหน้าจอทั้งหมด ไม่จำเป็นต้องกำหนด ID ให้กับ Layout ตัวนอกสุดก็ได้นะ ให้ดึง Root View ด้วยคำสั่งนี้แทน
val rootView: View = findViewById(android.R.id.content).rootView
และถ้าจะเอา Bitmap ไปบันทึกลง External Storage ของเครื่องก็ใช้คำสั่งแบบนี้
private fun saveBitmapToExternalStorage(bitmap: Bitmap, destination: File) {
val out = FileOutputStream(destination)
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos)
out.write(bos.toByteArray())
out.close()
}
แต่อย่าลืมประกาศขอ Permission สำหรับเขียนข้อมูลลง External Storage ก่อนล่ะ