20 July 2013

Screen Capture หรือ Capture ภาพบน Layout ผ่านโค้ด

Updated on


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

        แต่คำสั่งดังกล่าวจะต้องขอ Permission สำหรับเขียนข้อมูลลงใน External Storage ด้วยนะ ไปเพิ่ม Permission ใน Android Manifest ด้วยล่ะ

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

        และก็ต้องเขียนคำสั่งสำหรับ Runtime Permission ด้วยนะ เพื่อให้รองรับกับแอนดรอยด์เวอร์ชันใหม่ๆ ลองไปอ่านเพิ่มเติมได้ที่ สิ่งที่นักพัฒนาต้องรู้เกี่ยวกับระบบ Runtime Permission ใหม่ของแอนดรอยด์ รีบปรับโค้ดก่อนจะสายเกินไป