CameraX ตอนที่ 2 — ใช้งานแบบง่าย ๆ ด้วย PreviewView และ CameraController

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

บทความทั้งหมดในซีรีย์นี้

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

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

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

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

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

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

ถ้าทั้งหมดนี้สามารถตอบโจทย์ความต้องการของผู้ที่หลงเข้ามาอ่านได้ ก็ไม่จำเป็นต้องเขียนเองทั้งหมดหรอก สามารถใช้ PreviewView และ CameraController ได้เลย

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

Dependency ที่ต้องใช้

ในการใช้ PreviewView และ CameraController จะต้องเพิ่ม Dependency ของ CameraX เพียงแค่ 1 ตัว

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

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

และเหตุผลที่ต้องใส่ 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

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

<androidx.camera.view.PreviewView ... />

โดย PreviewView จะไม่มี Custom Attribute เป็นของตัวเอง เพราะจะต้องควบคุมการทำงานของกล้องผ่าน CameraController

สร้าง CameraController ให้ PreviewView

นักพัฒนาจะต้องสร้างคลาส CameraController เพื่อกำหนดให้กับ PreviewView ทุกครั้ง ถึงจะควบคุมการทำงานของกล้องได้

val context: Context = /* ... */
val previewView: PreviewView = /* ... */
val cameraController = LifecycleCameraController(context)
/* ... */
previewView.controller = controller

ในการสร้าง CameraController จะต้องสร้างเป็น LifecycleCameraController ที่จะช่วยควบคุมการทำงานของกล้องให้สอดคล้องกับ Lifecycle ของ Activity หรือ Fragment โดยอัตโนมัติ (ใช่ครับ ผู้ที่หลงเข้ามาอ่านไม่ต้องเขียนเองเลย)

และในระหว่างนี้ก็สามารถกำหนดค่าต่าง ๆ ให้กับ CameraController ได้เลย

กำหนด Use Case ที่ต้องการใช้งาน

จากที่บอกไปในตอนแรกว่า CameraX ออกแบบการทำงานในลักษณะของ Use Case Interfacing เพื่อให้การทำงานแต่ละแบบเป็นอิสระจากกัน โดย CameraX จะมี Use Case พื้นฐานเพื่อใช้งานกับ CameraController ดังนี้

  • IMAGE_CAPTURE สำหรับการถ่ายรูป
  • IMAEG_ANALYSIS สำหรับการนำภาพที่แสดงใน View Finder มาประมวลผลต่อ
  • VIDEO_CAPTURE สำหรับการถ่ายวีดีโอ

โดยนักพัฒนาสามารถกำหนดได้ว่าจะให้ CameraController เรียกใช้งาน Use Case ใดบ้างด้วยคำสั่งแบบนี้

val cameraController: LifecycleCameraController = /* ... */
cameraController.setEnabledUseCases(
    CameraController.IMAGE_CAPTURE or 
    CameraController.VIDEO_CAPTURE
)

จะเห็นว่าการกำหนด Use Case จะเป็นลักษณะของ Bitmask อยากได้ Use Case ไหนก็ให้เอามารวมกันด้วยคำสั่ง or ได้เลย

แนะนำให้กำหนด Use Case ก่อนที่จะใช้คำสั่ง bindToLifecycle(...)

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

CameraX จะกำหนดให้ใช้งานกล้องหลังเป็นค่าเริ่มต้น แต่ถ้าต้องการกำหนดเป็นกล้องหน้า ก็ให้เช็คก่อนว่าอุปกรณ์แอนดรอยด์เครื่องนั้นมีกล้องหน้าหรือไม่ แล้วค่อยกำหนดค่าผ่าน CameraSelector ของ CameraController

val cameraController: LifecycleCameraController = /* ... */
/* ... */
if(controller.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) {
    controller.cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
}

กำหนดค่าอื่น ๆ

ยังมีค่าอื่น ๆ อีกมากมายที่นักพัฒนาสามารถกำหนดใน CameraController ได้ ไม่ว่าจะเป็น

  • เปิด/ปิดการซูมกล้องด้วยการ Pinch to Zoom บน PreviewView
  • เปิด/ปิดการปรับระยะโฟกัสด้วยการแตะภาพจาก View Finder บน PreviewView
  • กำหนด Capture Mode ว่าจะเน้น Maximum Quality หรือ Minimize Latency
  • กำหนดความกว้าง/สูงที่ต้องการสำหรับภาพจาก Image Capture หรือ Video Capture
  • ฯลฯ
val controller: LifecycleCameraController = /* ... */
/* ... */
controller.isPinchToZoomEnabled = true
controller.isTapToFocusEnabled = true
controller.imageCaptureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
controller.imageCaptureTargetSize = CameraController.OutputSize(Size(1024, 1024))
controller.videoCaptureTargetSize = CameraController.OutputSize(Size(1920, 1080))
controller.setImageAnalysisAnalyzer(Executors.newFixedThreadPool(4), { imageProxy ->
    /* ... */
})

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

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

// SimpleCameraActivity.kt
class SimpleCameraActivity : AppCompatActivity() {
    /* ... */

    private fun bindCamera() {
        /* ... */
        controller.bindToLifecycle(this)
    }
}

ให้เรียกคำสั่งดังกล่าวตอนที่ต้องการเปิดใช้งานกล้อง และกรณีที่ Activity หรือ Fragment นั้น ๆ ถูกปิดลง ก็ไม่ต้องทำอะไรเพิ่ม ปล่อยให้เป็นหน้าที่ของ LifecycleCameraController ได้เลย

แต่ถ้าต้องการหยุดใช้งานกล้องโดยไม่ต้องรอให้ Activity หรือ Fragment หยุดทำงาน ก็สามารถใช้คำสั่ง unbind() ได้เลย

สลับกล้องหน้าและกล้องหลัง

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

private fun toggleCamera() {
        when {
            controller.cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA &&
                    controller.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ->
                controller.cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
            controller.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA &&
                    controller.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ->
                controller.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
        }
    }

การซูม

สำหรับการกำหนดค่าระยะซูมให้กับกล้องที่เป็น Default Camera นั่นหมายความว่าอุปกรณ์แอนดรอยด์ที่มีกล้องหลัง 3 ตัว ก็จะกำหนดระยะซูมโดยอ้างอิงจากตัวที่เป็น Default Camera เท่านั้น

โดยนักพัฒนาสามารถกำหนดระยะซูมได้ 2 แบบด้วยกันคือ Linear Zoom และ Zoom Ratio

Linear Zoom

เป็นการกำหนดระยะในการซูมด้วยค่าระหว่าง 0 - 1 โดยที่

  • 0 คือระยะที่ใกล้ที่สุด โดยส่วนใหญ่จะมีระยะอยู่ที่ 1x (ไม่รวมระยะซูมที่ใกล้กว่า 1x ของกล้องที่เป็นเลนส์ Ultra Wide)
  • 1 คือระยะที่ไกลที่สุด ซึ่งขึ้นอยู่กับว่ากล้องตัวนั้นซูมได้สูงสุดเท่าไร (ไม่รวมระยะซูมของกล้องที่เป็นเลนส์ Telephoto)
val cameraController: LifecycleCameraController = /* ... */
/* ... */
cameraController.setLinearZoom(0.4f)

ข้อดีของการใช้ Linear Zoom คือนักพัฒนาไม่ต้องรู้ว่าอุปกรณ์แอนดรอยด์แต่ละเครื่องมีระยะซูมเท่าไร สามารถกำหนดเป็นค่า Float ที่อยู่ระหว่าง 0 - 1 ได้เลย แต่จะไม่รู้ว่าระยะที่กำหนดไปนั้นเป็นระยะซูมที่เท่าไร

Zoom Ratio

เป็นการกำหนดระยะในการซูมด้วยค่าระยะซูมโดยตรง เช่น ต้องการให้ซูมที่ 1x ต้องกำหนดค่าเป็น 1f และ 4f สำหรับระยะซูมที่ 4x

val cameraController: LifecycleCameraController = /* ... */
/* ... */
cameraController.setZoomRatio(2f)

ข้อดีของการใช้ Zoom Ratio คือนักพัฒนาสามารถกำหนดระยะซูมที่ต้องการได้โดยตรง แต่ต้องเช็คระยะซูมใกล้สุดและสูงสุดจากกล้องในอุปกรณ์แอนดรอยด์แต่ละเครื่องเอง เพราะถ้ากำหนดค่าเกินที่ระยะซูมของกล้องจะได้เป็น IllegalArgumentException แทน

จะรู้ระยะซูมของกล้องได้ยังไง?

นักพัฒนาสามารถเช็คข้อมูลเกี่ยวกับระยะซูมของกล้องได้จาก Zoom State ของ CameraController ที่เป็น LiveData ซึ่งจะมีบอกให้ทั้ง Linear Zoom กับ Zoom Ratio รวมไปถึง Max/Min ของ Zoom Ratio สำหรับกล้องตัวนั้น ๆ ด้วย

val cameraController: LifecycleCameraController = /* ... */
/* ... */
controller.zoomState.observe(this) { zoomState ->
   val linearZoom = zoomState.linearZoom
   val zoomRatio = zoomState.zoomRatio
   val maxZoomRatio = zoomState.maxZoomRatio
   val minZoomRatio = zoomState.minZoomRatio
   /* ... */       
}

โดย Zoom State จะส่งค่ามาให้ตั้งแต่เปิดใช้งานกล้อง และจะอัปเดตค่าใหม่ให้ทุกครั้งที่ระยะซูมมีการเปลี่ยนแปลง

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

การสั่งให้กล้องถ่ายภาพได้ จะต้องกำหนด Use Case ของ IMAGE_CAPTURE ด้วย โดยจะมีคำสั่งให้เลือก 2 แบบด้วยกัน

takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback)
takePicture(outputFileResults: ImageCapture.OutputFileResults, executor: Executor, callback: ImageCapture.OnImageSavedCallback)

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

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

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

โดยคลาสดังกล่าวจะเปิดให้นักพัฒนาสามารถกำหนด Directory ที่ต้องการได้อย่างอิสระ ไม่ว่าจะกำหนดด้วยคลาส File, OutputStream หรือ Uri + ContentResolver ก็ได้

ImageCapture.OutputFileOptions.Builder(file: File)
ImageCapture.OutputFileOptions.Builder(outputStream: OutputStream)
ImageCapture.OutputFileOptions.Builder(contentResolver: ContentResolver, uri: Uri, contentValues: ContentValues)

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

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

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

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

และจะเห็นว่า onImageSaved(...) ที่ทำงานตอนถ่ายภาพสำเร็จจะมี ImageCapture.OutputFileResults ส่งมาให้ด้วย โดยค่าของ OutputFileResults จะไม่เป็น null ก็ต่อเมื่อตอนที่สร้าง ImageCapture.OutputFileOptions ด้วย Uri + ContentResolver เท่านั้น ถ้าสร้างด้วย File หรือ OutputStream จะได้ค่าเป็น null เสมอ

และถ้ามีปัญหาใดๆที่ไม่สามารถทำให้ถ่ายภาพไม่ได้ก็จะมีการส่ง 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
    }
})

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

override fun onCaptureSuccess(image: ImageProxy) {
    val width: Int = image.width
    val height: Int = image.height
    val image: Image = image.image
    val imageInfo: ImageInfo = image.imageInfo
    val format: Int = image.format
    val planes: Array<ImageProxy.PlaneProxy> = image.planes
    val byteBuffer: ByteBuffer = planes[0].buffer
    val cropRect: Rect = image.cropRect
    /* ... */
}

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

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

การสั่งให้กล้องถ่ายภาพได้ จะต้องกำหนด Use Case ของ VIDEO_CAPTURE ด้วย โดยจะมีคำสั่งให้เลือก 3 แบบด้วยกัน

startRecording(file: File, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)
startRecording(fileDescriptor: ParcelFileDescriptor, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)
startRecording(outputFileOptions: OutputFileOptions, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)

จะเห็นว่าคำสั่งทั้งหมดของ startRecording(...) จะเป็นการถ่ายวีดีโอแล้วบันทึกเป็นไฟล์ลงในเครื่องทันที ซึ่งต่างจากการถ่ายภาพที่สามารถนำข้อมูลภาพมาใช้งานโดยตรงได้ โดยไม่จำเป็นต้องบันทึกลงในเครื่อง ทั้งนี้ก็เพราะว่าเราไม่ควรนำข้อมูลวีดีโอมาเก็บไว้ใน 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(...) จะทำงานก็ต่อเมื่อบันทึกวีดีโอเสร็จ โดยจะมีคลาส OutputFileResults ส่งมาให้ด้วย ซึ่งจะมีค่าเป็น null เสมอ เพราะนักพัฒนาไม่สามารถกำหนดค่าเป็น Uri สำหรับการไฟล์วีดีโอได้

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

OnVideoSavedCallback.ERROR_RECORDING_IN_PROGRESS
OnVideoSavedCallback.ERROR_ENCODER
OnVideoSavedCallback.ERROR_MUXER
OnVideoSavedCallback.ERROR_INVALID_CAMERA
OnVideoSavedCallback.ERROR_FILE_IO
OnVideoSavedCallback.ERROR_UNKNOWN

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

cameraView.stopRecording()
cameraView.isRecording

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

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

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

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

takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback)
takePicture(outputFileResults: ImageCapture.OutputFileResults, executor: Executor, callback: ImageCapture.OnImageSavedCallback)

startRecording(file: File, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)
startRecording(fileDescriptor: ParcelFileDescriptor, executor: Executor, callback: VideoCapture.OnVideoSavedCallback)
startRecording(outputFileOptions: OutputFileOptions, 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 หรือแบบอื่นๆ ก็สามารถสร้างขึ้นมาได้ตามใจชอบ โดยนักพัฒนาควรสร้างให้ความเหมาะสมของคำสั่งที่จะทำงาน

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

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

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