29 July 2017

[Android Code] รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 2]



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

บทความที่เกี่ยวข้อง

        • รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 1]
        • รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 2]

เหลืออะไรบ้าง?

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

การใช้งาน Camera API v1 (ภาคต่อ)

มาสั่งให้ถ่ายภาพกันเถอะ

        ในการสั่งให้ถ่ายภาพจะมีคำสั่งให้เลือกใช้งานอยู่ 2 แบบดังนี้

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 แล้ว

Camera camera ...
...
// ใช้ Lambda ของ Java 8 เพื่อย่อคำสั่งให้กระชับ
camera.takePicture(() -> {
    // Photo captured from the sensor
}, (data, camera) -> {
    // Raw image data (if available)
}, (data, camera) -> {
    // Post View image data (if available)
}, (data, camera) -> {
    // JPEG image data
});

        แต่ในความเป็นจริงนั้น ผู้ที่หลงเข้ามาอ่านก็ไม่ได้ต้องการ Callback ทุกแบบเสมอไป จึงสามารถกำหนด Callback ที่ไม่ต้องการให้เป็น Null ไปเลยก็ได้

Camera camera ...
...
// ใช้ Lambda ของ Java 8 เพื่อย่อคำสั่งให้กระชับ
camera.takePicture(() -> {
            // Photo captured from the sensor
        },
        null,
        null,
        (data, camera) -> {
            // JPEG image data
        });

         และเนื่องจาก Post View Callback กำหนดเป็น Null อยู่แล้ว จึงสามารถเรียกใช้คำสั่ง takePicture(...) แบบนี้ได้เลย

Camera camera ...
...
// ใช้ Lambda ของ Java 8 เพื่อย่อคำสั่งให้กระชับ
camera.takePicture(() -> {
            // Photo captured from the sensor
        },
        null,
        (data, camera) -> {
            // JPEG image data
        });

        ดังนั้นคำสั่งตอนเรียกใช้งานจริงจะมีลักษณะแบบนี้

Camera camera ...
...
// ใช้ Lambda ของ Java 8 เพื่อย่อคำสั่งให้กระชับ
private void takePicture() {
    camera.takePicture(this::playShutterSound,
            null,
            (data, camera) -> savePicture(data));
}

private void savePicture(byte[] data) {
    ...
}

private void playShutterSound() {
    ...
}

เล่นเสียงชัตเตอร์ในขณะที่กำลังถ่ายภาพ

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

private void playShutterSound() {
    MediaActionSound sound = new MediaActionSound();
    sound.play(MediaActionSound.SHUTTER_CLICK);
}

         โดยคลาส MediaActionSound ถูกเพิ่มเข้ามาใหม่ใน API 16

การบันทึกไฟล์ภาพลงเครื่อง

        เมื่อได้ไฟล์ภาพเป็น JPEG มาแล้ว สิ่งที่ต้องทำต่อก็คือการบันทึกภาพเก็บไว้ในเครื่อง ซึ่งตรงนี้ตัว Camera API ไม่ได้จัดการให้ ดังนั้นผู้ที่หลงเข้ามาอ่าจจะต้องเพิ่มคำสั่งเข้าไปเอง

        ดังนั้นอย่างแรกสุดคือต้องเพิ่ม Permission สำหรับบันทึกไฟล์ลงเครื่องเข้าไปด้วย

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

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

    ...

</manifest>

        และอย่าลืม Runtime Permission เด็ดขาดล่ะ แต่เนื่องจากโค้ดของ Runtime Permission นั้นค่อนข้างเยอะ ดังนั้นเจ้าของบล็อกจึงเปลี่ยนไปใช้ Library ที่ชื่อว่า Dexter เข้ามาช่วยแทน ดังนั้นโค้ดจะเปลี่ยนเป็นแบบนี้แทน

public class MainActivity extends AppCompatActivity {
    ...
    private void checkCameraPermission() {
        Dexter.withActivity(this)
                .withPermissions(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        if (report.areAllPermissionsGranted()) {
                            // Do something when all permissions granted
                        } else {
                            Toast.makeText(MainActivity.this, R.string.camera_and_write_external_storage_denied, Toast.LENGTH_SHORT).show();
                        }
                    }

                    @Override
                    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
                        token.continuePermissionRequest();
                    }
                })
                .check();
    }
    ...
}

        โค้ดกระชับขึ้นเยอะ

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

private void savePicture(byte[] data) {
    String fileName = getCurrentDate() + ".jpg";
    String filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
    File file = new File(filePath + "/" + fileName);
    try {
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(data);
        fos.flush();
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private String getCurrentDate() {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault());
    Date date = new 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(data) จะต้องมีการแก้ไขเล็กน้อยเพื่อให้ส่ง Path ของไฟล์ภาพที่บันทึกออกมาด้วย

public File savePicture(byte[] data) {
    String fileName = getCurrentDate() + ".jpg";
    String filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
    File file = new File(filePath + "/" + fileName);
    try {
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(data);
        fos.flush();
        fos.close();
        return file;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

        และสำหรับคำสั่งในการแก้ไขค่า Orientation ใน Exif ของไฟล์จะเป็นแบบนี้

public void setImageOrientation(File file, int orientation) {
    if (file != null) {
        try {
            ExifInterface exifInterface = new ExifInterface(file.getPath());
            String orientationValue = String.valueOf(getOrientationExifValue(orientation));
            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientationValue);
            exifInterface.saveAttributes();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

private int getOrientationExifValue(int orientation) {
    switch (orientation) {
        case 90:
            return ExifInterface.ORIENTATION_ROTATE_90;
        case 180:
            return ExifInterface.ORIENTATION_ROTATE_180;
        case 270:
            return ExifInterface.ORIENTATION_ROTATE_270;
        default:
            return ExifInterface.ORIENTATION_NORMAL;
    }
}

        เอ... แล้วจะรู้ได้ยังไงว่าต้องหมุนภาพไปทางไหน?

        อย่าลืมครับ อย่าลืมว่าเจ้าของบล็อกเคยสร้างคำสั่งนี้ไว้ในบทความตอนที่ 1

getCameraDisplayOrientation(Activity activity, int cameraId)

        ดังนั้นคำสั่งทั้งหมดจะออกมาในรูปแบบนี้

Camera camera ...
...
camera.takePicture(this::playShutterSound,
        null,
        (data, camera) -> {
            File file = savePicture(data);
            int orientation = getCameraDisplayOrientation(getActivity(), cameraId);
            setImageOrientation(file, orientation);
        });

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

         และเวลาเอาภาพไปแสดงผลที่ไหน ก็จะต้องอ่านค่า Exif แล้วหมุนภาพให้ถูกต้องอยู่แล้ว ซึ่งแอปฯทุกตัวมีการเขียนโค้ดในส่วนนี้ไว้เป็นพื้นฐานอยู่แล้ว

ไฟล์ภาพไม่แสดงใน Gallery แต่มีไฟล์อยู่ในเครื่อง

        ปัญหาสุดคลาสสิคสำหรับนักพัฒนาแอนดรอยด์ที่จะต้องบันทึกภาพลงในเครื่อง แต่พอไปเปิดดูใน Gallery ของเครื่องแล้วกลับพบว่าหาไฟล์ไม่เจอ แต่พอเปิดดูใน File Explorer ก็พบว่ามีไฟล์อยู่


         สำหรับปัญหานี้สามารถเรียนรู้เพิ่มเติมได้ที่ [Android Code] ทำไมภาพถึงไม่ยอมแสดงใน Gallery

         เพื่อแก้ปัญหาดังกล่าวจึงต้องเพิ่มโค้ดเข้าไปดังนี้

public void updateMediaScanner(Context context, File file) {
    Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    intent.setData(Uri.fromFile(file));
    context.sendBroadcast(intent);
}

        และเรียกใช้งานแบบนี้

Camera camera = ...
...
camera.takePicture(this::playShutterSound,
        null,
        (data, camera) -> {
            File file = savePicture(data);
            int orientation = getCameraDisplayOrientation(getActivity(), cameraId);
            setImageOrientation(file, orientation);
            updateMediaScanner(getContext(), file);
        });

การเปลี่ยนแปลงค่า Parameter ของกล้องระหว่างที่ทำงานอยู่

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

private Camera camera = ...
...
private void toggleNegativeColor(boolean isTurnOn) {
    Camera.Parameters parameters = camera.getParameters();
    List<String> supportedColorEffectList = parameters.getSupportedColorEffects();
    if (supportedColorEffectList != null && supportedColorEffectList.contains(Camera.Parameters.EFFECT_NEGATIVE)) {
        if (isTurnOn) {
            parameters.setColorEffect(Camera.Parameters.EFFECT_NEGATIVE);
        } else {
            parameters.setColorEffect(Camera.Parameters.EFFECT_NONE);
        }
    } else {
        Toast.makeText(this, R.string.negative_color_effect_unavailable, Toast.LENGTH_SHORT).show();
    }
    camera.setParameters(parameters);
}

สรุป

        ในที่สุดก็ครบเรียบร้อยแล้วจ้าาาาา กับพื้นฐาน (จริงๆนะ) การเรียกใช้งานกล้องด้วย Camera API v1 ซึ่งจะเห็นว่านอกจากการเรียกใช้งานกล้องแล้ว ยังมีอีกหลายๆอย่างที่ต้องจัดการเพิ่มด้วยเพื่อให้ทำงานได้สมบูรณ์

         และอย่าลืมว่าในปัจจุบันนี้ Camera API v1 ถูกประกาศ​ Deprecated ตั้งแต่ Android 5.0 Lollipop แล้ว ดังนั้นในตอนนี้ถึงแม้ว่าจะยังเรียกใช้งานได้อยู่ แต่ในอนาคตก็แนะนำให้เปลี่ยนไปใช้ Camera API v2 แทนเพื่อให้รองรับการทำงานใหม่ๆในอนาคต

        สำหรับโค้ดในบทความนี้สามารถเข้าไปดูได้ที่ Camera Sample [GitHub]




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium