01 April 2020

CameraX ตอนที่ 2 - ใช้งาน CameraX แบบง่ายๆด้วย CameraView

Updated on

หลังจากที่ได้รู้เรื่องราวของ CameraX กันแล้ว มาดูกันว่าการใช้งาน CameraX ด้วยวิธีที่ง่ายที่สุดนั้น จะง่ายซักแค่ไหนกันเชียว

CameraView สำหรับการใช้งานกล้องแบบพื้นฐาน

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

นั่นคือที่มาของ CameraView ที่อยู่ใน CameraX เพื่อทำให้นักพัฒนาสามารถใช้งานกล้องแบบง่ายๆ โดยที่ไม่ต้องเขียนโค้ดให้เยอะ

การใช้งานกล้องแบบไหนถึงจะเหมาะกับ CameraView บ้าง?

เพื่อให้ผู้ที่หลงเข้ามาอ่านสามารถรู้ได้ว่าควรจะใช้ CameraView หรือควรเขียนขึ้นมาเอง จึงรวมความสามารถที่มีใน CameraView เพื่อช่วยในการตัดสินใจได้

• แสดงภาพจาก View Finder ของกล้อง
• เลือกใช้งานได้ทั้งกล้องหน้าและกล้องหลัง
• มีปุ่มกดเพื่อถ่ายภาพ
• มีปุ่มบันเพื่อทึกวีดีโอ
• สามารถซูมได้
• เปิด/ปิดการใช้แฟลชกล้อง
• ไม่จำเป็นต้องสนใจ Multi Camera

ถ้าความสามารถเหล่านี้ของ CameraView สามารถตอบโจทย์ความต้องการของคุณได้ล่ะก็ ไม่ต้องเขียนเองหรอก ใช้ CameraView ได้เลย

และที่เจ้าของบล็อกชอบที่สุดก็คือ CameraView รองรับการแตะปรับโฟกัสกล้องตามตำแหน่งที่แตะในภาพจาก View Finder (Tap To Focus) ให้เลยด้วย ไม่ต้องเขียนเองให้เสียเวลา

Dependency ที่จำเป็นสำหรับการใช้งาน CameraView

การใช้ CameraView จะเพิ่ม Dependency ของ CameraX ทั้งหมด 2 ตัวด้วยกัน

implementation "androidx.camera:camera-view:<latest_version>"
implementation "androidx.camera:camera-camera2:<latest_version>"

โดยข้างในจะมี Dependency ของ CameraX Core อยู่แล้ว จึงไม่จำเป็นต้องเพิ่มเข้าไปเอง

และเหตุผลที่ต้องใส่ Dependency ของ Camera2 เพิ่มเข้าไปด้วย ก็เพราะว่าเดิมที CameraX นั้นต้องมีการ Initialize ก่อนจะเริ่มใช้งาน ซึ่งใน Camera2 ได้เตรียมคำสั่งไว้ให้แล้ว และเรียกคำสั่งนั้นผ่าน Content Provider ให้โดยอัตโนมัติ ช่วยให้นักพัฒนาไม่ต้องเพิ่มคำสั่งเอง

Rumtime Permission สำหรับ CameraX

นักพัฒนาจะต้องจัดการกับ Runtime Permission เอง เพราะว่า CameraX ไม่ได้จัดการให้ ดังนั้นก่อนที่จะเรียกใช้คำสั่งของ CameraX จะต้องขอ Permission ให้เรียบร้อยก่อน

การถ่ายภาพนิ่ง - ต้องขอ Camera Permission
การถ่ายวีดีโอ - ต้องขอ Camera Permission และ Audio Record Permission
การบันทึกไฟล์ภาพหรือวีดีโอลงเครื่อง - ต้องขอ Storage Permission ที่จำเป็น แต่ถ้าบันทึกลงใน App-specific Directory ก็ไม่จำเป็นต้องขอ Permission ใดๆ

เมื่อเตรียมคำสั่งสำหรับ Permission เสร็จเรียบร้อยแล้ว ก็เริ่ม Implement คำสั่งของ CameraView ได้เลย

คำสั่งสำหรับ Layout XML

เนื่องจาก CameraView เป็นแค่ View ตัวหนึ่ง ดังนั้นนักพัฒนาจึงสามารถใส่ไปลงใน Layout XML ที่ต้องการได้เลย

<androidx.camera.view.CameraView
    ...
    app:captureMode="image"
    app:flash="auto"
    app:lensFacing="back"
    app:scaleType="centerCrop"
    app:pinchToZoomEnabled="false" />

โดย CameraView จะมี Custom Attribute ของตัวเอง เพื่อให้นักพัฒนากำหนดการทำงานของกล้องได้ทั้งหมด 5 Attribute ด้วยกัน ดังนี้

Capture Mode

กำหนดโหมดของกล้องว่าจะใช้ถ่ายภาพหรือวีดีโอ กำหนดได้ 3 แบบด้วยกัน

Image - ถ่ายภาพนิ่ง
Video - ถ่ายภาพเคลื่อนไหว
Mixed - ถ่ายทั้งภาพเคลื่อนไหวและภาพนิ่ง

ถ้าไม่ได้กำหนดค่าใดๆ จะใช้ค่า Default เป็น Image

Flash

กำหนดว่าจะให้เปิดแฟลชกล้องหรือไม่ โดยกำหนดได้ 3 แบบด้วยกัน

On - เปิดแฟลชกล้องตอนถ่ายภาพ
Off - ปิดแฟลชกล้องตอนถ่ายภาพ
Auto - เปิดหรือปิดแฟลชกล้องให้โดยอัตโนมัติ โดยขึ้นอยู่กับแสงตอนถ่ายภาพ

ถ้าไม่ได้กำหนดค่าใดๆ จะใช้ค่า Default เป็น Off

Lens Facing

กำหนดว่าจะใช้กล้องด้านหน้าหรือด้านหลัง

Front - ใช้กล้องหน้า
Back - ใช้กล้องหลัง

ถ้าไม่ได้กำหนดค่าใดๆ จะใช้ค่า Default เป็น Back

Scale Type

กำหนดรูปแบบในการแสดงภาพจาก View Finder ถ้าขนาดอัตราส่วนของขนาดภาพจาก View Finder ไม่เท่ากับขนาดของ CameraView

Center Crop - ปรับภาพจาก View Finder ให้เต็มพื้นที่ของ View โดยส่วนที่เกินออกมาจากขนาดของ View ก็จะถูก Crop ออกไป
Center Inside - ปรับภาพจาก View Finder ให้อยู่ในพื้นที่ของ View ทั้งหมด โดยไม่มีส่วนไหนของภาพถูก Crop ออก

Pinch To Zoom

เปิด/ปิดการใช้ 2 นิ้วเพื่อปรับระยะซูม​ของกล้อง

True - เปิดใช้งาน
False - ปิดใช้งาน

คำสั่งสำหรับตั้งค่าการทำงานของกล้องผ่านโค้ด

โดยจะมีคำสั่งสำหรับตั้งค่าการทำงานของกล้องใน CameraView จะมีทั้งหมดดังนี้

// Capture Mode
val captureMode: CameraView.CaptureMode = cameraView.captureMode
cameraView.captureMode = CameraView.CaptureMode.VIDEO

// Lens Facing
val cameraLensFacing: Int = cameraView.cameraLensFacing
val hasBackLensFacing: Boolean = cameraView.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)
cameraView.cameraLensFacing = CameraSelector.LENS_FACING_BACK

// Flash
val flashMode: Int = cameraView.flash
cameraView.flash = ImageCapture.FLASH_MODE_OFF

// Torch Mode
val isTorchOn: Boolean = cameraView.isTorchOn
cameraView.enableTorch(true)

// Scale Type
val scaleType: CameraView.ScaleType = cameraView.scaleType
cameraView.scaleType = CameraView.ScaleType.CENTER_CROP

// Pinch To Zoom
val isPinchToZoomEnabled: Boolean = cameraView.isPinchToZoomEnabled
cameraView.isPinchToZoomEnabled = false

// Zoom
val isZoomSupported: Boolean = cameraView.isZoomSupported
val maxZoom: Float = cameraView.maxZoomRatio
val minZoom: Float = cameraView.minZoomRatio
val zoomRatio: Float = cameraView.zoomRatio
cameraView.zoomRatio = (maxZoom - minZoom) / 2f

สำหรับค่าบางอย่างได้อธิบายไปแล้วใน Layout XML ดังนั้นขอเล่าแค่อันที่เพิ่มเข้ามาแล้วกันนะ

Torch Mode

กำหนดว่าจะให้เปิดแฟลชกล้องตลอดเวลาที่ใช้งานกล้องหรือไม่ เพราะโดยปกติแล้วแฟลชกล้องจะทำงานเฉพาะตอนที่กดถ่ายภาพเท่านั้น

True - เปิด Torch Mode
False - ปิดTorch Mode

ถ้าไม่ได้กำหนดค่าใดๆไว้ จะใช้ค่า Default เป็น False

Zoom

กำหนดระยะในการซูมของกล้อง โดยระยะการซูมของกล้องแต่ละเครื่องจะไม่เหมือนกัน ดังนั้นต้องเช็คค่า Minimum และ Maximum ของกล้องทุกครั้ง เพื่อนำไปคำนวณในตอนที่ต้องการกำหนดระยะซูม

และที่สำคัญไม่มี Event Listener สำหรับการเปลี่ยนแปลงระยะซูมภาพ (Zoom Ratio Changed) ดังนั้นถ้าเปิดใช้งาน Pinch To Zoom จะไม่มีทางรู้ได้เลยว่าผู้ใช้ปรับระยะซูมเมื่อไร

ถ้าไม่ได้กำหนดค่าใดๆไว้ จะใช้เป็นระยะซูมที่เป็น Default ของกล้อง

สั่งให้กล้องเริ่มทำงาน

เมื่อกำหนดค่าต่างๆให้กับกล้องเสร็จเรียบร้อยแล้วจะต้องใช้คำสั่ง bindToLifecycle(...) เพื่อให้กล้องเริ่มทำงานแบบนี้

val lifecycleOwner : LifecycleOwner = ...
cameraView.bindToLifecycle(lifecycleOwner)

จะเห็นว่าคำสั่งดังกล่าวมีการกำหนด LifecycleOwner ด้วย เพราะว่า CameraView จะทำงานให้เหมาะสมกับ Lifecycle ของ Activity/Fragment ใน ณ ตอนนั้นนั่นเอง

ซึ่ง LifecycleOwner เป็นสิ่งที่มีอยู่แล้วใน Activity/Fragment ที่สร้างมาจาก AndroidX จึงสามารถใช้คำสั่งแบบนี้ได้

class AwesomeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        cameraView.bindToLifecycle(this)
    }
    ...
}

เพียงเท่านี้ภาพจาก View Finder ของกล้องก็จะแสดงให้เห็นบน CameraView แล้ว

สลับกล้องหน้าและกล้องหลังได้ด้วยนะ!

โดยปกติแล้ว เวลาต้องการสลับไปมาระหว่างกล้องหน้ากับกล้องหลัง มักจะต้องเขียนคำสั่งเอง แต่สำหรับ CameraView นั้น ได้เตรียมคำสั่งไว้ให้แล้ว ใช้งานง่ายมาก

cameraView.toggleCamera()

และถ้าอยากรู้ว่ากำลังใช้งานกล้องตัวไหนอยู่ก็เช็คจาก Lens Facing ได้เลย

การถ่ายภาพ (Image Capture)

การสั่งให้กล้องทำการถ่ายภาพจะต้องกำหนด Capture Mode เป็น Image หรือ Mixed เท่านั้น และจะมีคำสั่งให้เลือก 2 แบบด้วยกัน

takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback)
takePicture(file: File, executor: Executor, callback: ImageCapture.OnImageSavedCallback)

โดยแต่ละคำสั่งจะมีจุดประสงค์ที่แตกต่างกัน

ถ่ายภาพแล้วบันทึกไฟล์ลงในเครื่องให้ทันที

สำหรับการทำงานที่ไม่ต้องการทำอะไรมากนัก แค่อยากให้กดถ่ายภาพแล้วบันทึกเป็นไฟล์ลงในเครื่องทันที สามารถใช้คำสั่งแบบนี้ได้เลย

val file: File =
val executor: Executor =...
cameraView.takePicture(file, executor, object : ImageCapture.OnImageSavedCallback {
    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
        // Success
    }

    override fun onError(exception: ImageCaptureException) {
        // Error
    }
})

สามารถกำหนด Path ที่ต้องการให้บันทึกภาพเป็นคลาส File ได้เลย โดยคำสั่งนี้จะส่ง Callback กลับมาเป็น ImageCapture.OnImageSavedCallback เพื่อให้นักพัฒนาสามารถรู้ได้ว่าถ่ายภาพเสร็จแล้ว

จะเห็นว่า onImageSaved(...) ที่ทำงานตอนถ่ายภาพสำเร็จจะมี ImageCapture.OutputFileResults ส่งมาให้ด้วย โดยค่าของ OutputFileResults จะมีค่าเป็น Null เสมอ เพราะใช้สำหรับกรณีที่บันทึกไฟล์ภาพลงใน Media Store ซึ่ง CameraView ไม่สามารถกำหนดแบบนั้นได้

และถ้ามีปัญหาใดๆที่ไม่สามารถทำให้ถ่ายภาพไม่ได้ก็จะมีการส่ง ImageCaptureException มาใน onError(...) โดยจะแนบ Error Code มาให้ด้วย เพื่อบอกว่าปัญหาเกิดจากอะไร

ImageCapture.ERROR_FILE_IO
ImageCapture.ERROR_CAPTURE_FAILED
ImageCapture.ERROR_CAMERA_CLOSED
ImageCapture.ERROR_INVALID_CAMERA
ImageCapture.ERROR_UNKNOWN

ถ่ายภาพอย่างเดียว ไม่ต้องบันทึกไฟล์

ในบางครั้งนักพัฒนาก็อาจจะต้องการถ่ายภาพเพื่อนำมาทำบางอย่างโดยที่ไม่ต้องการให้บันทึกไฟล์ลงในเครื่อง จึงต้องเปลี่ยนมาใช้คำสั่งแบบนี้แทน

val execute: Execute = ...
cameraView.takePicture(execute, object : ImageCapture.OnImageCapturedCallback() {
    override fun onCaptureSuccess(image: ImageProxy) {
        // Success
    }

    override fun onError(exception: ImageCaptureException) {
        // Error
    }
})

จะเห็นว่าคำสั่งดังกล่าวไม่ต้องกำหนดคลาส File เพราะไม่ต้องการบันทึกไฟล์ลงในเครื่อง และได้เปลี่ยนมาใช้ Callback เป็น ImageCapture.OnImageCapturedCallback แทน ซึ่งจะมี Override Method ข้างในแตกต่างกันตรงที่

override fun onCaptureSuccess(image: ImageProxy) {
    val format: Int = image.format
    val planes: Array<ImageProxy.PlaneProxy> = image.planes
    val byteBuffer: ByteBuffer = planes[0].buffer
    ...
}

ใน onCaptureSuccess(...) จะส่งข้อมูลออกมาเป็น ImageProxy แทน ซึ่งเป็นข้อมูลของภาพถ่ายเพื่อให้นักพัฒนานำข้อมูลไปใช้ได้ตามต้องการ

การถ่ายวีดีโอ (Video Capture)

การสั่งให้กล้องทำการถ่ายวีดีโอจะต้องกำหนด Capture Mode เป็น Video หรือ Mixed เท่านั้น และจะมีคำสั่งอยู่แบบเดียว

startRecording(file: File, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)

ซึ่งเป็นการถ่ายวีดีโอแล้วบันทึกเป็นไฟล์ลงในเครื่องทันที ซึ่งต่างจากการถ่ายภาพที่สามารถนำข้อมูลภาพมาใช้งานโดยตรงได้ โดยไม่จำเป็นต้องบันทึกลงในเครื่อง ทั้งนี้ก็เพราะว่าไม่ควรนำข้อมูลวีดีโอมาเก็บไว้ใน Memory เพื่อทำ Processing โดยตรง

ดังนั้นเวลาเรียกใช้งานจริง ก็จะได้ออกมาเป็น

val file: File = ...
val executor: Executor = ...
cameraView.startRecording(file, execute, object : VideoCapture.OnVideoSavedCallback {
    override fun onVideoSaved(file: File) {
        // Success
    }

    override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
        // Error
    }
})

โดย onVideoSaved(...) จะทำงานก็ต่อเมื่อบันทึกวีดีโอเสร็จ โดยจะมีคลาส File ส่งมาให้ด้วย ซึ่งเป็นตัวเดียวกันกับที่กำหนดในตอนแรกนั่นเอง ดังนั้นจึงไม่จำเป็นต้องเรียกใช้งานอะไร (นอกจากนี้ทีมพัฒนา CameraX จะลบคลาส File ใน Override Method ตัวนี้บนเวอร์ชันถัดไปด้วย)

ถ้ามีปัญหาใดๆก็ตามที่ทำให้ถ่ายวีดีโอหรือบันทึกไฟล์ไม่ได้ก็จะเรียก onError(...) แทน โดยมี Error Code และ Message ส่งมาให้ด้วย

VideoCapture.ERROR_RECORDING_IN_PROGRESS
VideoCapture.ERROR_ENCODER
VideoCapture.ERROR_MUXER
VideoCapture.ERROR_UNKNOWN

และการบันทึกวีดีโอนั้นไม่ได้เหมือนกับการถ่ายภาพที่สั่งปุปแล้วจะทำงานเสร็จในไม่กี่วินาที

cameraView.stopRecording()
cameraView.isRecording

ดังนั้นจึงต้องมีคำสั่งสำหรับหยุดบันทึกวีดีโอ และคำสั่งเช็คว่ากำลังบันทึกวีดีโออยู่หรือไม่ เพื่อให้นักพัฒนาเรียกใช้งานได้ตามความเหมาะสม

ทำไมต้องกำหนด Executor สำหรับการถ่ายภาพหรือวีดีโอ?

จะเห็นว่ามีคลาส Executor ที่นักพัฒนาจะต้องกำหนดในตอนเรียกคำสั่งสำหรับถ่ายภาพหรือวีดีโอด้วย ซึ่งเจ้าของบล็อกขอหยิบมาอธิบายในหัวข้อนี้แทน

ไม่ว่าจะเป็น Callback จากการถ่ายภาพหรือวีดีโอก็ตาม คำสั่งที่จะเรียกใช้งานในนั้นก็ขึ้นอยู่กับนักพัฒนาแต่ละคนด้วย บางคนอาจจะต้องการถ่ายภาพเสร็จแล้วแสดงผลบางอย่างบนหน้าจอทันที ในขณะที่บางคนอาจจะต้องการทำอะไรบางอย่างกับไฟล์ภาพก่อน

takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback)
takePicture(file: File, executor: Executor, callback: ImageCapture.OnImageSavedCallback)
startRecording(file: File, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)

จึงทำให้ CameraView ถูกออกแบบมาให้นักพัฒนาสามารถกำหนด Executor ที่จะทำงานใน Callback ได้ตามใจชอบว่าอยากให้คำสั่งที่ใส่ไว้ใน Callback นั้นทำงานบน Thread แบบไหน

ถ้าต้องการให้ทำงานใน Main Executor

ให้เรียก Main Executor ด้วยคำสั่ง

val context: Context = ...
val executor: Executor = ContextCompat.getMainExecutor(context)

เมื่อกำหนดเป็น Main Executor ก็จะทำให้คำสั่งใน Callback ก็จะทำงานอยู่บน Executor ที่เป็น Main Thread แล้ว

ถ้าต้องการให้ทำงานใน Executor อื่น

สามารถสร้างขึ้นมาจากคลาส Executors ได้ตามใจชอบเลย

val singleThreadExecutor: Executor = Executors.newSingleThreadExecutor()
val fixedThreadPoolExecutor: Executor = Executors.newFixedThreadPool(3)
val cachedThreadPoolExecutor: Executor = Executors.newCachedThreadPool()

อยากจะได้แค่ Thread เดียว, จะใช้เป็น Thread Pool หรือแบบอื่นๆ ก็สามารถสร้างขึ้นมาได้ตามใจชอบ โดยนักพัฒนาควรสร้างให้ความเหมาะสมของคำสั่งที่จะทำงาน

ของดี ใช้ง่าย ใช้เถอะ

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

ดังนั้นอยากใช้งานกล้องแบบง่ายๆก็ต้อง CameraView แต่ถ้าต้องทำอะไรมากกว่านั้นก็ค่อยไปเขียนคำสั่งใน CameraX เองนะ