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