ห่างหายกันไปนาน วันนี้ก็ได้กลับมาเขียนบทความต่อเสียที โดยขอหยิบเรื่องการบันทึกภาพหน้าจอหรือที่เรียกกันว่า Screen Capture มาเล่าสู่กันฟังครับ

Capture!!


โดยปกติแล้วเวลาผู้ใช้จะ Screen Capture ก็จะกดปุ่ม Power + Volume Down หรือ Power + Home แล้วแต่ว่าจะเป็นยี่ห้อไหน เวอร์ชันอะไร เพราะแต่ละรุ่นจะมีวิธีกดแตกต่างกันเล็กน้อย (แต่ส่วนใหญ่จะกลายเป็น 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 ตรงที่จะได้แค่ภาพหน้าจอภายในแอปฯของตัวเองเท่านั้น พื้นที่อื่นๆนอกจากนั้นจะไม่สามารถบันทึกได้

และเบื้องหลังวิธีนี้คือการดึงภาพจาก Layout/View มาบันทึก ไม่ได้ไปเรียก Screen Capture จาก System แต่อย่างใด ดังนั้นอย่าหวังว่าจะมีเอฟเฟคดังแช๊ะๆ ถ้าอยากได้ก็ต้องไปทำเองนะ

สมมติว่าเจ้าของบล็อกอยากจะ Capture เฉพาะ Layout ที่ชื่อ @+id/expandingscrollview_container

เวลาสั่ง Capture ผ่านโค้ดก็จะสั่งที่ Layout ตัวนั้นเลย และก็จะได้ผลลัพธ์ออกมาแบบนี้

สิ่งที่ควรรู้ไว้อีกอย่างหนึ่งก็คือภาพที่ Capture จะมีขนาดตามหน้าจอจริงๆ ถ้าหน้าจอความละเอียดสูงก็จะได้ภาพที่มีขนาดใหญ่ตามไปด้วย ดังนั้นจึงไม่เหมาะกับการ Capture อะไรบางอย่างที่จำเป็นต้องกำหนดขนาดตายตัว

ลองดูกันเลยดีกว่า


เพราะเป็นการ Screen Capture โดยใช้วิธีดึงภาพจาก Layout/View มาบันทึก ดังนั้นก็อย่าลืมกำหนด ID ให้กับ Layout/View ด้วยล่ะ จะได้เรียกจากใน Java ได้

เจ้าของบล็อกมักจะพบผู้ที่หลงเข้ามาอ่านหลายๆคนใช้คำสั่งแบบนี้ในการ Capture เนื่องจากหาเจอได้ทันทีใน StackOverflow

val layoutContent : LinearLayout ... private fun capture() { layoutContent.isDrawingCacheEnabled = true val bitmap = Bitmap.createBitmap(layoutContent.drawingCache) layoutContent.isDrawingCacheEnabled = false // Do something with bitmap instance }


เป็นการดึงภาพจาก Drawing Cache มาแปะลงบน Bitmap ที่เตรียมไว้นั่นเอง

วิธีนี้อาจจะดูเหมือนใช้งานได้ปกติ แต่เมื่อใดก็ตามที่ภาพจาก Drawing Cache มีขนาดใหญ่ในระดับหนึ่ง Drawing Cache จะมีค่าเป็น Null ทันทีโดยไม่บอกกล่าวอะไร นั่นหมายความว่า NullPointerException มีโอกาสเกิดขึ้นตอนที่ใช้คำสั่ง createBitmap(…) นั่นเอง

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

ถ้าไปอ่านใน Official Documentation ของคลาส View ก็จะพบว่าคำสั่งนี้เลิกใช้งานไปนานแล้ว และเพิ่งจะถูกประกาศ Deprecated ไปใน Android P

เพื่อเลี่ยงปัญหา NullPointerException เพราะภาพมีขนาดใหญ่เกิน ขอแนะนำให้ใช้วิธีแบบนี้แทน

val layoutContent : LinearLayout ... private fun capture() { val bitmap = Bitmap.createBitmap(layoutContent.measuredWidth, layoutContent.measuredHeight, Bitmap.Config.ARGB_8888)) val canvas = Canvas(bitmap) layoutContent.layout(0, 0, layoutContent.measuredWidth, layoutContent.measureHeight) layoutContent.draw(canvas) // Do something with bitmap instance }


วิธีนี้จะเป็นการสั่งให้ Layout/View ทำการ Draw ลงบน Canvas แทน ซึ่งคำสั่ง layout() และ draw() เป็นคำสั่งในการของ Layout/View ที่แสดงภาพบนหน้าจอให้ผู้ใช้เห็นอยู่แล้ว (เพราะเบื้องหลังของ Layout/View ก็คือ Canvas นั่นแหละ) เพียงแค่ว่าเอามาใช้กับ Canvas ที่เตรียมไว้เพื่อทำภาพให้กลายเป็น Bitmap

ของแถม


ถ้าอยากจะบันทึกหน้าจอทั้งหมด ไม่จำเป็นต้องกำหนด ID ให้กับ Layout ตัวนอกสุดก็ได้นะ ให้ดึง Root View ด้วยคำสั่งนี้แทน

val rootView = findViewById<View>(android.R.id.content).rootView


และถ้าจะเอา Bitmap ไปบันทึกลง External Storage ของเครื่องก็ใช้คำสั่งแบบนี้

private fun saveBitmapToExternalStorage(bitmap: Bitmap, directory: String, fileName: String) { val file = File(Environment.getExternalStorageDirectory(), "$directory/$fileName.jpg") val out = FileOutputStream(file) val bos = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos) out.write(bos.toByteArray()) out.close() }