จากตอนที่แล้วที่ได้พูดถึงขั้นตอนการเรียกใช้งานเบื้องต้นเพื่อให้กล้องทำงานและแสดงภาพจากกล้องลงบนแอปฯได้ ในคราวนี้ก็จะเป็นส่วนอื่นๆที่เหลืออยู่เพื่อให้เรียกใช้งานกล้องได้อย่างสมบูรณ์
บทความที่เกี่ยวข้อง
- รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 1]
- รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 2]
เหลืออะไรบ้าง?
เมื่อเรียกใช้งานกล้องได้และแสดงภาพ Preview ได้แล้ว ที่เหลือก็จะเป็นการสั่งงานให้กล้องถ่ายภาพ, การสั่งให้กล้องโฟกัสภาพ และการเปลี่ยนค่า Parameters ในระหว่างที่กล้องกำลังทำงานอยู่ ซึ่งบอกเลยว่าขั้นตอนน้อยกว่าบทความก่อนหน้านี้มากๆ
การใช้งาน Camera API v1 (ภาคต่อ)
มาสั่งให้ถ่ายภาพกันเถอะ
ในการสั่งให้ถ่ายภาพจะมีคำสั่งให้เลือกใช้งานอยู่ 2 แบบดังนี้
val camera: Camera? = /* ... */
/* ... */
camera.takePicture(shutterCallback, rawPictureCallback, jpegPictureCallback)
// หรือ
camera.takePicture(shutterCallback, rawPictureCallback, postViewPictureCallback, jpegPictureCallback)
โดยจะเห็นว่าในคำสั่ง takePicture(...)
นั้นจะต้องกำหนด Callback เข้าไปด้วย ซึ่งจะมี Callback หลายแบบมาก
- Shutter Callback : เมื่อกล้องเริ่มถ่ายภาพ เหมาะสำหรับเล่นเสียงชัตเตอร์
- Raw Picture Callback : ข้อมูลภาพที่เป็นแบบ RAW จะถูกส่งเข้ามาที่ Callback ตัวนี้ โดยจะเป็น Null ถ้าตัวเครื่องไม่รองรับหรือ Buffer เกิด Overflow
- Post View Picture Callback : ข้อมูลภาพที่มีการปรับขนาดไว้ให้แล้ว ซึ่งสามารถทำได้เฉพาะอุปกรณ์แอนดรอยด์บางรุ่นเท่านั้น
- JPEG Picture Callback : ข้อมูลภาพที่บีบอัดเป็น JPEG แล้ว
val camera: Camera? = /* ... */
/* ... */
camera?.takePicture({
// Photo captured from the sensor
}, { data: ByteArray, camera: Camera ->
// Raw image data (if available)
}, { data: ByteArray, camera: Camera ->
// Post View image data (if available)
}, { data: ByteArray, camera: Camera ->
// JPEG image data
})
แต่ในความเป็นจริงนั้น ผู้ที่หลงเข้ามาอ่านก็ไม่ได้ต้องการ Callback ทุกแบบเสมอไป จึงสามารถกำหนด Callback ที่ไม่ต้องการให้เป็น Null ไปเลยก็ได้
val camera: Camera? = /* ... */
/* ... */
camera?.takePicture({
// Photo captured from the sensor
}, null, null, { data: ByteArray, camera: Camera ->
// JPEG image data
})
หรือจะตัด Post View Callback ออกไปเลยก็ได้
val camera: Camera? /* ... */
/* ... */
camera?.takePicture({
// Photo captured from the sensor
}, null, { data: ByteArray, camera: Camera ->
// JPEG image data
})
ดังนั้นคำสั่งตอนเรียกใช้งานจริงจะมีลักษณะแบบนี้
val camera: Camera? = /* ... */
/* ... */
private fun takePicture() {
camera?.takePicture({
playShutterSound()
}, null, { data: ByteArray, camera: Camera ->
savePicture(getContext(), data)
})
}
private fun playShutterSound() {
/* ... */
}
private fun savePicture(context: Context, data: ByteArray) {
/* ... */
}
เล่นเสียงชัตเตอร์ในขณะที่กำลังถ่ายภาพ
เพื่อให้ผู้ใช้รู้ว่ากดถ่ายภาพแล้ว จึงควรใส่คำสั่งเพื่อให้เล่นเสียงชัตเตอร์ด้วย ซึ่งผู้ที่หลงเข้ามาอ่านสามารถกำหนดได้ตามใจชอบว่าจะเอาเสียงจากที่ไหนมาใช้ แต่ในตัวอย่างนี้จะเรียกเสียงชัตเตอร์ของที่มีอยู่แล้วในเครื่องแทน โดยใช้คำสั่งดังนี้
private fun playShutterSound() {
val sound = MediaActionSound()
sound.play(MediaActionSound.SHUTTER_CLICK)
}
โดยคลาส MediaActionSound ถูกเพิ่มเข้ามาใหม่ใน API 16
การสั่งให้โฟกัสภาพ
เมื่อใดก็ตามที่อยากจะให้โฟกัสภาพใหม่ จะมีคำสั่งง่ายๆแบบนี้
camera?.autoFocus { success: Boolean, camera: Camera ->
// Play focus sound
}
เมื่อกล้องทำการโฟกัสภาพเสร็จแล้วก็จะส่ง Callback กลับมาด้วย เพื่อให้ผู้ที่หลงเข้ามาอ่านสามารถใส่คำสั่งอื่นๆอย่างเช่นการเล่นเสียงเมื่อโฟกัสภาพเสร็จแล้ว
และถ้าอยากให้กล้องทำการโฟกัสภาพแบบต่อเนื่องโดยไม่ต้องสั่งงานเองทุกครั้ง ก็สามารถกำหนด Focus Mode ของกล้องให้เป็นแบบ Continuous Video ได้เลย
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
แต่อย่าลืมว่าไม่ใช่ทุกรุ่นที่สามารถทำแบบนี้ได้ ดังนั้นจะต้องเช็คก่อนทุกครั้งว่าเครื่องนั้นๆรองรับ Continuous Focus หรือไม่ แล้วค่อยกำหนดค่าให้กับกล้อง
fun setupCameraParameter() {
val parameters: Camera.Parameters = camera.parameters
/* ... */
if (isContinuousFocusModeSupported(parameters.supportedFocusModes)) {
parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
}
}
fun isContinuousFocusModeSupported(supportedFocusModes: List<String>?): Boolean {
return supportedFocusModes?.find { mode -> mode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, ignoreCase = true) } != null
}
การบันทึกไฟล์ภาพลงเครื่อง
เมื่อได้ไฟล์ภาพเป็น JPEG มาแล้ว สิ่งที่ต้องทำต่อก็คือการบันทึกภาพเก็บไว้ในเครื่อง ซึ่งตรงนี้ตัว Camera API ไม่ได้จัดการให้ ดังนั้นผู้ที่หลงเข้ามาอ่าจจะต้องเพิ่มคำสั่งเข้าไปเอง
ดังนั้นอย่างแรกสุดคือต้องเพิ่ม Permission สำหรับบันทึกไฟล์ลงเครื่องเข้าไปด้วย
<!-- AndroidManifest.xml -->
<manifest>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- ... -->
</manifest>
และอย่าลืม Runtime Permission เด็ดขาดล่ะ แต่เนื่องจากโค้ดของ Runtime Permission นั้นค่อนข้างเยอะ ดังนั้นเจ้าของบล็อกจึงเปลี่ยนไปใช้ Library ที่ชื่อว่า Dexter เข้ามาช่วยแทน ดังนั้นโค้ดจะเปลี่ยนเป็นแบบนี้แทน
class SplashScreenActivity : AppCompatActivity() {
/* ... */
private fun checkCameraAndWriteExternalPermission() {
Dexter.withActivity(getActivity())
.withPermissions(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
if (!report.areAllPermissionsGranted()) {
Toast.makeText(getContext(), R.string.camera_and_write_external_storage_denied, Toast.LENGTH_SHORT).show()
}
}
override fun onPermissionRationaleShouldBeShown(permissions: List<PermissionRequest>, token: PermissionToken) {
token.continuePermissionRequest()
}
})
.check()
}
/* ... */
}
โค้ดกระชับขึ้นเยอะ
เนื่องจากข้อมูลที่ได้จากการถ่ายภาพจะอยู่ในรูปของ Byte Array ดังนั้นการจะบันทึกเป็นไฟล์ลงในเครื่องจะใช้คำสั่งแบบนี้
fun savePicture(context: Context, data: ByteArray) {
val fileName = "${getCurrentDate()}.jpg"
val filePath = context.getExternalFilesDir(null)?.absolutePath
val file = File(filePath, fileName)
try {
if (file.exists()) {
file.delete()
}
file.createNewFile()
val fos = FileOutputStream(file)
fos.write(data)
fos.flush()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
fun getCurrentDate(): String {
val simpleDateFormat = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault())
val date = Date()
return simpleDateFormat.format(date)
}
ในตัวอย่างนี้กำหนด Path ของไฟล์ไว้ที่ External Storage ของแอปนั้นๆ ส่วนชื่อไฟล์ก็ตั้งเอาจากวันที่และเวลา ณ ตอนนั้น
แก้ปัญหาภาพกลับด้าน
ถ้ายังจำกันได้ ตอนภาพเอาจากกล้องมา Preview บนหน้าจอจะมีปัญหาภาพกลับด้าน ดังนั้นก็อย่าแปลกใจอะไรถ้าภาพถ่ายที่บันทึกลงเครื่องก็ดันกลับด้านเหมือนกัน
แล้วจะแก้ปัญหานี้ยังไงดีล่ะ?
แก้ปัญหาด้วยการทำ Byte Array ให้เป็น Bitmap แล้วหมุนภาพก่อนจะบันทึก
เจ้าของบล็อกพบว่ามีผู้ที่หลงเข้ามาอ่านบางคนใช้วิธีแบบนี้ โดยแปลง Byte Array ให้กลายเป็น Bitmap ซะ แล้วใช้ Matrix เข้ามาช่วยเพื่อหมุนทิศทางของภาพให้ถูกต้อง แล้วจึงเอา Bitmap ที่ได้ไปบันทึกลงเครื่อง
แต่วิธีนี้ไม่โอเคซักเท่าไร เพราะถึงแม้เราจะแก้ปัญหาภาพหมุนกลับด้านได้ แต่การแปลงเป็น Bitmap ถือว่าเป็นวิธีที่ค่อนข้างแพงมากๆ ใช้ Memory เยอะ และคำสั่งนี้ใช้เวลาทำงานนานมากจนผู้ใช้รู้สึกว่าแอปฯค้างไปชั่วขณะ (ขึ้นอยู่กับสเปคเครื่องและขนาดของภาพถ่าย)
ดังนั้นเจ้าของบล็อกจึงไม่แนะนำวิธีนี้ซักเท่าไร
แก้ปัญหาด้วยการกำหนดค่า Orientation ลงใน Exif ของไฟล์ภาพ
วิธีนี้จะไม่ต้องแปลงข้อมูล Byte Array เลย จึงทำให้คำสั่งทำงานได้ไวมาก โดยจะบันทึกไฟล์ลงในเครื่องเหมือนเดิมนั่นแหละ แล้วค่อยแก้ไขค่า Exif ของไฟล์นั้นๆทีหลังเพื่อกำหนด Orientation ให้ถูกต้อง
ดังนั้นคำสั่ง savePicture(context, data)
จะต้องมีการแก้ไขเล็กน้อยเพื่อให้ส่ง Path ของไฟล์ภาพที่บันทึกออกมาด้วย
fun savePicture(context: Context, data: ByteArray): File? {
val fileName = "${getCurrentDate()}.jpg"
val filePath = context.getExternalFilesDir(null)?.absolutePath
val file = File(filePath, fileName)
try {
if (file.exists()) {
file.delete()
}
file.createNewFile()
val fos = FileOutputStream(file)
fos.write(data)
fos.flush()
fos.close()
return file
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
และสำหรับคำสั่งในการแก้ไขค่า Orientation ใน Exif ของไฟล์จะเป็นแบบนี้
fun setImageOrientation(file: File?, orientation: Int) {
file?.let {
try {
val exifInterface = ExifInterface(file.path)
val orientationValue = getOrientationExifValue(orientation).toString()
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientationValue)
exifInterface.saveAttributes()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
fun getOrientationExifValue(orientation: Int): Int {
return when (orientation) {
90 -> ExifInterface.ORIENTATION_ROTATE_90
180 -> ExifInterface.ORIENTATION_ROTATE_180
270 -> ExifInterface.ORIENTATION_ROTATE_270
else -> ExifInterface.ORIENTATION_NORMAL
}
}
เอ… แล้วจะรู้ได้ยังไงว่าต้องหมุนภาพไปทางไหน?
อย่าลืมครับ อย่าลืมว่าเจ้าของบล็อกเคยสร้างคำสั่งนี้ไว้ในบทความตอนที่ 1
getCameraDisplayOrientation(activity: Activity, cameraId: Int)
ดังนั้นคำสั่งทั้งหมดจะออกมาในรูปแบบนี้
private val cameraId: Int = /* ... */
/* ... */
private fun savePicture(context: Context, data: ByteArray) {
savePicture(getContext(), data)?.let { file: File ->
val orientation = getCameraDisplayOrientation(getActivity(), cameraId)
setImageOrientation(file, orientation)
/* ... */
} ?: run {
/* ... */
}
/* ... */
}
จะเห็นว่าวิธีนี้จะทำงานได้ไวมาก เพราะไม่ต้องทำอะไรที่ฟุ่มเฟือยอย่างการแปลงภาพเป็น Bitmap โดยวิธีนี้จะไม่ได้หมุนไฟล์ภาพให้ถูกต้องตั้งแต่แรก แต่จะกำหนดค่าไว้ใน Exif เพื่อบอกให้รู้ว่าภาพนี้ต้องหมุนไปทางไหนถึงจะแสดงผลได้อย่างถูกต้อง
เวลาเอาภาพไปแสดงผลที่ไหน ก็จะต้องอ่านค่า Exif แล้วหมุนภาพให้ถูกต้องอยู่แล้ว ซึ่งแอปฯทุกตัวมีการเขียนโค้ดในส่วนนี้ไว้เป็นพื้นฐานอยู่แล้ว
ไฟล์ภาพไม่แสดงใน Gallery แต่มีไฟล์อยู่ในเครื่อง
ปัญหาสุดคลาสสิคสำหรับนักพัฒนาแอนดรอยด์ที่จะต้องบันทึกภาพลงในเครื่อง แต่พอไปเปิดดูใน Gallery ของเครื่องแล้วกลับพบว่าหาไฟล์ไม่เจอ แต่พอเปิดดูใน File Explorer ก็พบว่ามีไฟล์อยู่
สำหรับปัญหานี้สามารถเรียนรู้เพิ่มเติมได้ที่ ทำไมภาพถึงไม่ยอมแสดงใน Gallery
เพื่อแก้ปัญหาดังกล่าวจึงต้องเพิ่มโค้ดเข้าไปดังนี้
fun updateMediaScanner(context: Context, file: File?) {
if (file != null) {
return
}
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
intent.data = Uri.fromFile(file)
context.sendBroadcast(intent)
}
และเรียกใช้งานแบบนี้
private val cameraId: Int = /* ... */
/* ... */
private fun savePicture(context: Context, data: ByteArray) {
savePicture(getContext(), data)?.let { file: File ->
/* ... */
updateMediaScanner(getContext(), file)
} ?: run {
/* ... */
}
/* ... */
}
การเปลี่ยนแปลงค่า Parameter ของกล้องระหว่างที่ทำงานอยู่
ในกรณีที่เปิดใช้งานกล้องอยู่และอยากตั้งค่า Parameter ต่างๆของกล้อง ก็สามารถกำหนดแบบนี้ได้เลย
private var camera: Camera? = /* ... */
/* ... */
private fun toggleNegativeColor(isTurnOn: Boolean) {
camera?.let { camera: Camera ->
val parameters = camera.parameters
parameters.supportedColorEffects?.let { colorEffectList: List<String> ->
if (colorEffectList.contains(Camera.Parameters.EFFECT_NEGATIVE)) {
if (isTurnOn) {
parameters.colorEffect = Camera.Parameters.EFFECT_NEGATIVE
} else {
parameters.colorEffect = Camera.Parameters.EFFECT_NONE
}
} else {
Toast.makeText(getContext(), R.string.negative_color_effect_unavailable, Toast.LENGTH_SHORT).show()
}
}
camera.parameters = parameters
}
}
สรุป
ในที่สุดก็ครบเรียบร้อยแล้วจ้าาาาา กับพื้นฐาน (จริงๆนะ) การเรียกใช้งานกล้องด้วย Camera API v1 ซึ่งจะเห็นว่านอกจากการเรียกใช้งานกล้องแล้ว ยังมีอีกหลายๆอย่างที่ต้องจัดการเพิ่มด้วยเพื่อให้ทำงานได้สมบูรณ์
และอย่าลืมว่าในปัจจุบันนี้ Camera API v1 ถูกประกาศ Deprecated ตั้งแต่ Android 5.0 Lollipop แล้ว ดังนั้นในตอนนี้ถึงแม้ว่าจะยังเรียกใช้งานได้อยู่ แต่ในอนาคตก็แนะนำให้เปลี่ยนไปใช้ Camera API v2 แทนเพื่อให้รองรับการทำงานใหม่ๆในอนาคต
สำหรับโค้ดในบทความนี้สามารถเข้าไปดูได้ที่ Camera Sample [GitHub]