ห่างหายกันไปนาน วันนี้ก็ได้กลับมาเขียนบทความต่อเสียที โดยขอหยิบเรื่องการบันทึกภาพหน้าจอหรือที่เรียกกันว่า 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 ก่อนล่ะ