29 July 2017

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



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

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

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

Camera API v1 was deprecated

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

        แต่ทว่าในปัจจุบันนี้ (ที่เจ้าของบล็อกเขียนบทความนี้) ยังมีผู้ใช้อุปกรณ์แอนดรอยด์เป็นเวอร์ชันต่ำกว่า Android 5.0 Lollipop อยู่มากมาย จึงทำให้ไม่สามารถย้ายไปใช้ Camera API v2 ได้อย่างเต็มที่ จึงเป็นที่มาว่าทำไม Camera API v1 จึงยังจำเป็นอยู่ ซึ่งขึ้นอยู่กับว่าจะเลือกเขียนแบบไหน ระหว่าง

        • เรียกใช้ Camera API v1 เพื่อรองรับทั้งเวอร์ชันเก่าและใหม่
        • เรียกใช้ Camera API v1 สำหรับเวอร์ชันที่ต่ำกว่า API 21 และเรียกใช้ Camera API v2 สำหรับ API 21 ขึ้นไป

        แต่เมื่อถึงเวลาที่นักพัฒนาสามารถกำหนด Minimum SDK เป็น API 21 ได้ ก็แนะนำให้เลิกใช้ Camera API v1 แล้วเปลี่ยนไปใช้ Camera API v2 แทนนะครับ

เริ่มต้นเรียกใช้งาน Camera API v1

        ก่อนอื่นต้องเข้าใจก่อนว่า การเรียกใช้งาน Camera API v1 นั้นจะมีองค์ประกอบอยู่หลายๆส่วนด้วยกัน โดยจะแบ่งเป็นสองส่วนดังรูปข้างล่างนี้


Camera 

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

Preview

        เป็นส่วนที่ใช้ในการ Preview ภาพเมื่อเรียกใช้งานกล้อง ซึ่งเดิมทีนั้น Camera ไม่ได้จัดการเรื่องนี้เอง แต่จะมีคำสั่งสำหรับกำหนดว่าจะให้แสดง Preview จากหน้ากล้องที่ใด ซึ่งในบทความนี้จะใช้ TextureView ในการแสดง Preview ซึ่งผู้ที่หลงเข้ามาอ่านสามารถใช้ SurfaceView ก็ได้เช่นกัน แต่ว่า TextureView มีความสะดวกและยืดหยุ่นในการเรียกใช้งานมากกว่า (แต่ก็ต้องเป็น API 16 ขึ้นไปเช่นกัน)

        ซึ่งโค้ดของ Camera และ Preview จะทำงานอยู่ร่วมกันเสมอ เพราะต้องมีการจัดการกับทั้งสองส่วนเพื่อให้ทำงานตาม Lifecycle ของ Activity/Fragment ได้อย่างถูกต้อง

รูปแบบการเรียกใช้งาน Camera และ Preview ในแบบฉบับของเจ้าของบล็อก

        ปัญหาหลักๆของนักพัฒนาส่วนใหญ่ที่ต้องเรียกใช้งาน Camera API v1 คือ จัดการกับคำสั่งของ Camera และ Preview ไม่ถูกต้อง เพราะจะต้องทำให้ Camera และ Preview ทำงานตาม Lifecycle ของ Activity/Fragment ด้วย โดยจะต้องรองรับการใช้งานในรูปแบบต่างๆดังนี้

        • เปิดใช้งานกล้อง แล้วปิดใช้งานกล้องด้วยการทำลาย Activity หรือ Fragment ทิ้ง
        • เปิดใช้งานกล้อง แล้วหมุนหน้าจอไปมา
        • เปิดใช้งานกล้อง แล้วเปิด Activity หรือ Fragment ตัวอื่นขึ้นมาแล้วกลับไปเปิดหน้าเดิมอีกครั้ง
        • เปิดใช้งานกล้อง แล้วสลับไปใช้งานกล้องตัวอื่น แล้วกลับมาเปิดแอปฯต่อจากเดิมอีกครั้ง
        • เปิดใช้งานกล้องบนแอปฯที่แสดงผลแบบ MultiWindow อยู่ แล้วย่อ/ขยายหน้าต่างไปมา

        ซึ่งทั้ง 5 ข้อนี้เป็นรูปแบบการทำงานของแอปฯโดยพื้นฐานที่สามารถเกิดขึ้นได้ ดังนั้นเวลาเรียกใช้งาน Camera API ก็จะต้องทดสอบด้วยการทำงานทั้ง 5 แบบนี้เป็นอย่างน้อย เพื่อให้มั่นใจว่าผู้ใช้จะไม่เจอแอปฯเด้งระหว่างใช้งาน
       

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

        จากภาพดังกล่าวสามารถเขียนเป็นโค้ดแบบคร่าวๆได้ประมาณนี้

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // Setup Camera Preview and Listener
}

@Override
protected void onStart() {
    // Setup Camera();
    // Start Camera Preview
}

@Override
protected void onStop() {
    // Stop Camera and Camera Preview
}

@Override
protected void onDestroy() {
    // Clear Listener
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    // Setup Camera
    // Start Camera Preview
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    // Stop Camera and Camera Preview
}

        สำหรับ onSurfaceTextureAvailable กับ onSurfaceTextureDestroyed เป็น Event Listener ของ TextureView ซึ่งจะพูดถึงในภายหลัง

เตรียม TextureView ให้พร้อม


        TextureView เป็น View ที่มีอยู่ใน API 16 ขึ้นไป สามารถเรียกใช้งานใน Layout XML ได้ตามปกติเลย

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/textureViewCamera"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

        แล้วเตรียมไว้ใน Activity ให้พร้อมซะ

import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.TextureView;

public class CameraActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
    private TextureView textureViewCamera;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        // ใช้ Support library v26 ไม่ต้อง Cast class ให้ TextureView แล้ว
        textureViewCamera = findViewById(R.id.textureViewCamera);
        textureViewCamera.setSurfaceTextureListener(this);
    }

    ...

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {

    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

    }
}

        จะเห็นว่า TextureView จะมี Event Listener ที่ชื่อ SurfaceTextureListener ด้วย ซึ่งในนี้จะต้องมีการใส่คำสั่งของ Camera เข้าไป ซึ่งจะพูดถึงในภายหลัง

เรียกใช้งาน Camera API v1 

กำหนด Uses Permission และ Uses Feature

        ในการเรียกใช้งาน Camera API นั้นจะต้องมีการประกาศ Permission ใน Android Manifest ด้วยทุกครั้ง

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

         และถ้าต้องการให้ติดตั้งได้เฉพาะบนเครื่องที่มีกล้องเท่านั้น (บน Google Play) ให้กำหนดเพิ่มเข้าไปดังนี้

<manifest ...>
    ...
    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />

    <uses-feature
        android:name="android.hardware.camera.autofocus"
        android:required="false" />
    ...
</manifest>

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

        ซึ่งในการเรียกใช้งานกล้องโดยพื้นฐานนั้น เครื่องจะรองรับ Autofocus หรือไม่นั้นไม่ได้ส่งผลอะไรกับการทำงานของแอปฯซักเท่าไร

อย่าลืม Runtime Permission สำหรับ Android 6.0 Marshmallow ขึ้นไปด้วย

        ถ้าเป็นไปได้ แนะนำให้เรียกคำสั่งของ Runtime Permission ใน Activity ก่อนที่จะเป็น Activity ที่เรียกใช้งานกล้อง เพราะคำสั่ง Runtime Permission อาจจะทำให้ลำดับการทำงานของคำสั่งใน Camera API v1 ชวนสับสนและผิดได้ง่าย

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;

public class SplashScreenActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        checkCameraPermission();
    }

    private void checkCameraPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Camera permission granted
            } else {
                // Camera permission denied
            }
        }
    }

    ...
}


        หรือจะใช้ Library มาช่วยจัดการเพื่อให้โค้ดกระชับมากกว่านี้ก็ได้เช่นกัน

การเช็คผ่านโค้ดว่าอุปกรณ์นั้นๆมีกล้องให้ใช้งานหรือไม่

        ในกรณีที่อยากจะเช็คผ่านโค้ดว่าเครื่องนั้นๆมีกล้องให้ใช้งานหรือไม่ สามารถใช้คำสั่งดังนี้ได้เลย

public boolean isCameraSupport(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
}

เปิดใช้งานกล้อง

        มีคำสั่งอยู่ 2 แบบ แบบแรกคือเรียกใช้งาน Default Camera ของตัวเครื่อง กับระบุเองว่าจะเอากล้องตัวไหน

import android.hardware.Camera;

...

Camera camera = Camera.open();

// หรือกำหนดกล้องที่ต้องการด้วย
// Camera.CameraInfo.CAMERA_FACING_BACK
// Camera.CameraInfo.CAMERA_FACING_FRONT
Camera camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK)

        จากการทดสอบบนเครื่อง Asus Nexus 7 1st Gen (2012) ที่มีแต่กล้องหน้า เวลากำหนดค่าเป็น CAMERA_FACING_BACK จะเป็นการเรียกใช้งานกล้องหน้าแทน และกำหนดเป็น CAMERA_FACING_FRONT ไม่ได้

        และสามารถเช็คได้ว่าเครื่องนั้นๆมีกล้องกี่ตัวด้วยคำสั่งดังนี้

int cameraCount = Camera.getNumberOfCameras()

        แต่คำสั่ง Camera.open() และ Camera.open(cameraId) มีโอกาสเกิด RuntimeException ได้  ดังนั้นเพื่อให้ยืดหยุ่นต่อการเรียกใช้งาน เจ้าของบล็อกจึงทำเป็นคำสั่งแบบนี้

public Camera openDefaultCamera() {
    Camera camera = null;
    try {
        camera = Camera.open();
    } catch (RuntimeException e) {
        Log.e(TAG, "Error open camera: " + e.getMessage());
    }
    return camera;
}

public Camera openCamera(int cameraId) {
    Camera camera = null;
    try {
        camera = Camera.open(cameraId);
    } catch (RuntimeException e) {
        Log.e(TAG, "Error open camera: " + e.getMessage());
    }
    return camera;
}

        ถ้าเปิดใช้งานกล้องไม่ได้ก็จะได้ค่าเป็น Null แทน

กำหนดค่าต่างๆให้กับกล้อง

        Camera Instance ที่ได้จากคำสั่ง Camera.open() หรือ Camera.open(cameraId) สามารถเช็คได้ว่ากล้องดังกล่าวรองรับการตั้งค่าอะไรบ้าง เพื่อใช้ในการกำหนดค่าเริ่มต้นให้กับกล้อง ด้วยคำสั่ง

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();

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


        ในการกำหนดค่าต่างๆในคลาส Camera.Parameters ก็จะใช้คำสั่ง Set ทั้งหมด


        เมื่อกำหนดค่าใน Camera.Parameters เสร็จเรียบร้อยแล้ว จะต้องกำหนดกลับเข้าไปใน Camera ด้วยทุกครั้ง

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
parameters.setJpegQuality(100);
parameters.setJpegThumbnailQuality(50);
parameters.setVideoStabilization(true);
camera.setParameters(parameters);

การกำหนดขนาดของภาพถ่าย

        เนื่องจากกล้องของอุปกรณ์แอนดรอยด์แต่ละเครื่องนั้นมีขนาดแตกต่างกันไป ซึ่งสามารถเช็คได้จาก Parameters ของกล้องนั้นๆ

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportPictureSizeList = parameters.getSupportedPictureSizes();

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

        แต่ในตัวอย่างนี้เจ้าของบล็อกจะใช้วิธีดึงค่าขนาดของภาพภ่ายที่มากที่สุดมากำหนดให้กับกล้อง จึงต้องเขียน Method เพิ่มขึ้นมาดังนี้

public Camera.Size getBestPictureSize(@NonNull List<Camera.Size> pictureSizeList) {
    Camera.Size bestPictureSize = null;
    for (Camera.Size pictureSize : pictureSizeList) {
        if (bestPictureSize == null ||
                (pictureSize.height >= bestPictureSize.height &&
                        pictureSize.width >= bestPictureSize.width)) {
            bestPictureSize = pictureSize;
        }
    }
    return bestPictureSize;
}

        สาเหตุที่ต้องเขียนคำสั่งแบบนี้ก็เพราะว่าขนาดภาพถ่ายที่รองรับของแต่ละเครื่องอาจจะไม่เหมือนกัน บางเครื่องเก็บค่าขนาดภาพถ่ายสูงสุดไว้ที่ Array ตัวแรกสุด บางเครื่องก็เก็บไว้ที่ Array ตัวท้ายสุด

        แล้วกำหนดขนาดของภาพถ่ายให้กับกล้องดังนี้

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportPictureSizeList = parameters.getSupportedPictureSizes();
Camera.Size bestPictureSize = getBestPictureSize(supportPictureSizeList);
parameters.setPictureSize(bestPictureSize.width, bestPictureSize.height);

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

การกำหนด Preview Size ของกล้อง

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

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> pictureSizeList = parameters.getSupportedPictureSizes();

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

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes();

        โดยสเปคของกล้องนั้นไม่ได้สัมพันธ์กับขนาดหน้าจอของเครื่องโดยตรง นั่นหมายความว่าจะมีโอกาสที่ตัวกล้องสามารถ Preview ที่ความละเอียดมากกว่าหน้าจอได้เช่นกัน

1920x1080
1440x1080
1088x1088
1280x720    << เหมาะสมที่สุดสำหรับหน้าจอ 1280x800
1056x704
1024x768
960x720
800x450
720x720
720x480
640x480
352x288
320x240
256x144
176x144

         จากตัวอย่างข้างบนนี้ ถ้ากำหนดให้ Preview Size มีขนาดสูงสุดก็จะทำให้สิ้นเปลืองเกินจำเป็น เพราะสุดท้ายก็แสดงผลแค่บน 1280x800 อยู่ดี

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

public static Camera.Size getBestPreviewSize(@NonNull List<Camera.Size> previewSizeList, int previewWidth, int previewHeight) {
    Camera.Size bestPreviewSize = null;
    for (Camera.Size previewSize : previewSizeList) {
        if (bestPreviewSize != null) {
            int diffBestPreviewWidth = Math.abs(bestPreviewSize.width - previewWidth);
            int diffPreviewWidth = Math.abs(previewSize.width - previewWidth);
            int diffBestPreviewHeight = Math.abs(bestPreviewSize.height - previewHeight);
            int diffPreviewHeight = Math.abs(previewSize.height - previewHeight);
            if (diffPreviewWidth + diffPreviewHeight < diffBestPreviewWidth + diffBestPreviewHeight) {
                bestPreviewSize = previewSize;
            }
        } else {
            bestPreviewSize = previewSize;
        }
    }
    return bestPreviewSize;
}

        เพียงเท่านี้ก็จะได้ขนาดของ Preview ที่เหมาะสมที่สุดแล้ว

int width = ...
int height = ...

Camera camera = openCamera(cameraId);
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes();
Camera.Size bestPreviewSize = getBestPreviewSize(previewSizeList, width, height);
parameters.setPreviewSize(bestPreviewSize.width, bestPreviewSize.height);

การกำหนด Orientation ของภาพจากกล้อง

        เนื่องจากทิศทางของภาพจากกล้องนั้นไม่ได้สัมพันธ์กับทิศทางของหน้าจอ หรือพูดง่ายๆก็คือภาพบนหน้าจออาจจะกลับหัวกลับหางได้นั่นเอง


        ดังนั้นผู้ที่หลงเข้ามาอ่านจะต้องใช้คำสั่งเพื่อดึงค่า Orientation ของกล้องมาคำนวณเพื่อให้ภาพที่แสดงบนหน้าจอนั้นมีทิศทางที่ถูกต้อง

int cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;

Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int orientation = cameraInfo.orientation;

         ซึ่งคำสั่งตรงนี้ไม่ต้องจำอะไรมาก เพราะเป็นเรื่องของการคำนวณ โดยมีรูปแบบคำสั่งดังนี้

public static int getCameraDisplayOrientation(Activity activity, int cameraId) {
    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, cameraInfo);
    int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    int degree = 0;
    switch (rotation) {
        case Surface.ROTATION_0:
            degree = 0;
            break;
        case Surface.ROTATION_90:
            degree = 90;
            break;
        case Surface.ROTATION_180:
            degree = 180;
            break;
        case Surface.ROTATION_270:
            degree = 270;
            break;
    }
    int orientation = 0;
    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        orientation = (cameraInfo.orientation + degree) % 360;
        orientation = (360 - orientation) % 360;
    } else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
        orientation = (cameraInfo.orientation - degree + 360) % 360;
    }
    return orientation;
}

         โดยจะเอาค่า Orientation ของกล้องมาคำนวณกับค่า Orientation ของหน้าจอเพื่อให้รู้ว่าภาพที่ Preview อยู่บนหน้าจอนั้นจะต้องหมุนไปกี่องศาถึงจะได้ภาพที่ไม่กลับหัวกลับหาง

         การนำค่าทิศทางที่ได้ไปกำหนด ใน Camera API จะใช้คำสั่ง setDisplayOrientation(orientation)

int cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;

Camera camera = openCamera(cameraId);
...
camera.setDisplayOrientation(getCameraDisplayOrientation(getActivity(), cameraId));


กำหนดให้แสดงภาพ Preview แบบ Center Crop

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


        ดังนั้นจึงต้องมีการเขียนเพิ่มเล็กน้อยเพื่อปรับให้ภาพเป็นแบบ Center Crop (สามารถเขียนเป็น Fit Center ได้เช่นกัน) โดยที่ TextureView จะมีคำสั่ง setTransform(matrix) เพื่อจัดการเรื่องนี้ให้แล้ว (SurfaceView ไม่มีคำสั่งนี้)

        ก่อนอื่นต้องสร้าง Method เพื่อคำนวณขนาดภาพ Preview ให้เป็น Center Crop ก่อน

public static Matrix getCropCenterScaleMatrix(float viewWidth, float viewHeight, float previewWidth, float previewHeight) {
    float scaleX = 1.0f;
    float scaleY = 1.0f;
    if (previewWidth > viewWidth && previewHeight > viewHeight) {
        scaleX = previewWidth / viewWidth;
        scaleY = previewHeight / viewHeight;
    } else if (previewWidth < viewWidth && previewHeight < viewHeight) {
        scaleY = viewWidth / previewWidth;
        scaleX = viewHeight / previewHeight;
    } else if (viewWidth > previewWidth) {
        scaleY = (viewWidth / previewWidth) / (viewHeight / previewHeight);
    } else if (viewHeight > previewHeight) {
        scaleX = (viewHeight / previewHeight) / (viewWidth / previewWidth);
    }
    return createScaleMatrix(scaleX, scaleY, viewWidth, viewHeight);
}

        เวลากำหนดให้ภาพ Preview ใน TextureView เป็น Center Crop ก็จะใช้คำสั่งแบบนี้

int viewWidth = ...
int viewHeight = ...
int previewWidth = ...
int previewHeight = ...
textureViewCamera.setTransform(getCropCenterMatrix(viewWidth, viewHeight, previewWidth, previewHeight));

รวบคำสั่งกำหนดค่าเริ่มต้นของกล้อง

        เนื่องจากไม่ได้ต้อการตั้งค่าอะไรมากนัก ดังนั้นจึงกำหนดแค่อันที่จำเป็นๆเท่านั้น จึงรวมเป็น Method แบบนี้

private Camera camera;
...
private void setupCamera(int width, int height) {
    camera = openCamera(cameraId);
    Camera.Parameters parameters = camera.getParameters();
    Camera.Size bestPreviewSize = getBestPreviewSize(parameters.getSupportedPreviewSizes(), width, height);
    parameters.setPreviewSize(bestPreviewSize.width, bestPreviewSize.height);
    camera.setParameters(parameters);
    camera.setDisplayOrientation(getCameraDisplayOrientation(getActivity(), cameraId));
    textureViewCamera.setTransform(getCropCenterMatrix(width, height, bestPreviewSize.width, bestPreviewSize.height));
}
     

กำหนด TextureView เพื่อใช้ Preview ภาพจากกล้อง

        ในการกำหนด TextureView ให้กับคลาส Camera จะใช้คำสั่ง setPreviewTexture(surfaceTexture)

Camera camera = ...
SurfaceTexture surfaceTexture = ...
camera.setPreviewTexture(surfaceTexture);

         เมื่อกำหนด SurfaceTexture เสร็จแล้วก็สามารถแสดงภาพ Preview จากกล้องได้ด้วยคำสั่ง startPreview()

Camera camera = ...
SurfaceTexture surfaceTexture = ...
camera.setPreviewTexture(surfaceTexture);
camera.startPreview();

รวบคำสั่งกำหนดค่าให้ TextureView แสดงภาพ Preview จากกล้อง

         เมื่อกำหนด SurfaceTexture ให้กับคลาส Camera เรียบร้อยแล้ว ก็จะให้เริ่มทำการ Preview ทันที ดังนั้นจึงขอรวบคำสั่งเป็น Method แบบนี้

private Camera camera;
...
private void startCameraPreview(SurfaceTexture surfaceTexture) {
    try {
        camera.setPreviewTexture(surfaceTexture);
        camera.startPreview();
    } catch (IOException e) {
        Log.e(TAG, "Error start camera preview: " + e.getMessage());
    }
}

        เนื่องจากคำสั่ง setPreviewTexture(surfaceTexture) มีโอกาสเกิด IOException ได้ ดังนั้นจึงต้องครอบ Try-catch ไว้ด้วย

คำสั่งเพื่อเริ่มต้นใช้งาน Camera และ Preview พร้อมแล้ว

        สำหรับคำสั่ง setupCamera(width, height) และ startCameraPreview(surfaceTexture) จะเอาไปเรียกใช้งานด้วยกัน 2 ที่ ตามรูปแบบที่เจ้าของบล็อกกำหนดไว้


        นั่นก็คือ onStart() ของ Activity/Fragment และ onSurfaceTextureAvailable(...) ของ TextureView

public class CameraApiV1Activity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
    ...

    @Override
    protected void onStart() {
        ...
        if (textureViewCamera.isAvailable()) {
            setupCamera(textureViewCamera.getWidth(), textureViewCamera.getHeight());
            startCameraPreview(textureViewCamera.getSurfaceTexture());
        }
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        setupCamera(width, height);
        startCameraPreview(surfaceTexture);
    }
    ...
}

        ทำไมต้องเรียกคำสั่งซ้ำ 2 ที่? และทำไมใน onStart ถึงต้องมีการเช็คด้วยคำสั่ง isAvailable()?

        ขอทิ้งคำถามน่าฉุกคิดแบบนี้ทิ้งไว้ก่อนนะครับ แล้วเดี๋ยวจะอธิบายในภายหลัง

เมื่อไม่ได้ใช้งาน Camera และ Preview ก็ต้องจัดการให้เรียบร้อยซะ

        ทีนี้มาถึงขั้นตอนที่การทำงานของ Camera กับ Preview เสร็จสิ้น ก็ต้องเคลียร์ทิ้งให้เรียบร้อยซะ


        โดยจะต้องหยุดการ Preview ด้วยคำสั่ง stopPreview() ก่อน แล้วยกเลิกเรียกใช้งานกล้องด้วยคำสั่ง release()

Camera camera = ...
...
camera.stopPreview();
camera.release();

        เจ้าของบล็อกจึงขอรวบเป็น Method แบบนี้

private void stopCamera() {
    try {
        camera.stopPreview();
        camera.release();
    } catch (Exception e) {
        Log.e(TAG, "Error stop camera preview: " + e.getMessage());
    }
}

        โดยคำสั่งดังกล่าวจะถูกเรียกใน onStop() ของ Activity และ onSurfaceTextureDestroyed(...) ของ TextureView

public class CameraApiV1Activity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
    ...
    @Override
    protected void onStop() {
        ...
        stopCamera();
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        stopCamera();
        return true;
    }
}


ทำไมถึงต้องเรียกคำสั่งซ้ำ 2 ที่?

        กลับมายังคำถามที่คาไว้ก่อนหน้านี้ จะเห็นว่ามีการเรียกคำสั่งซ้ำ 2 ที่

        เนื่องจากการทำงานของ TextureView เมื่อถูกแสดงขึ้นมา อาจจะยังไม่ทำงานในทันที จึงต้องมี onSurfaceTextureAvailable(...) เพิ่มเข้ามาเพื่อบอกให้รู้ว่า TextureView พร้อมทำงานแล้ว และเมื่อ TextureView ถูกทำลายก็จะเข้าไปที่คำสั่ง onSurfaceTextureDestroyed(...)

         ซึ่งทั้ง 2 Event นี้จะทำงานก็ต่อเมื่อ Activity/Fragment ถูกสร้างขึ้น (Create) และทำลายลง (Destroy) เท่านั้น ในกรณีที่ผู้ใช้กดย่อแอปฯหรือสลับไปใช้งานแอปฯอื่นๆจะไม่มีผลอะไรใดๆ

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

        ดังนั้นเจ้าของบล็อกจึงต้องเพิ่มคำสั่งไว้ใน onStart() และ onStop() เพื่อแก้ปัญหานี้ แต่ก็ต้องระวังไม่ให้คำสั่งใน onStart() ถูกเรียกซ้ำกับ onSurfaceTextureAvailable(...) จึงทำให้เจ้าของบล็อกต้องเพิ่มคำสั่งนี้เข้าไป

@Override
protected void onStart() {
    ...
    if (textureViewCamera.isAvailable()) {
        ...
    }
}

        ซึ่งคำสั่งนี้จะช่วยให้คำสั่งกำหนดค่าให้กับ Camera กับ Preview ไม่ทำงานซ้ำซ้อน โดยที่

        • เมื่อ Activity/Fragment ถูกสร้างขึ้นมาครั้งแรก จะเรียกใช้คำสั่งจากใน onSurfaceTextureAvailable(...) โดย SurfaceTexture และขนาดของพื้นที่ Preview จะถูกส่งมาจากคำสั่งดังกล่าว
        • เมื่อ Activity/Fragment ถูกซ่อนแล้วกลับขึ้นมาทำงานใหม่ จะเรียกใช้คำสั่งจากใน onStart() โดย SurfaceTexture จะดึงมาจาก TextureView และขนาดของพื้นที่ Preview ก็จะดึงมาจาก TextureView เช่นกัน

        ส่วนกรณีของ onStop() กับ onSurfaceTextureDestroyed(...) จะใช้วิธีต่างกันออกไป นั่นก็คือ...

เคลียร์ Event Listener ของ TextureView ทิ้งด้วย


        เพื่อไม่ให้คำสั่งใน onStop() กับ onSurfaceTextureDestroyed(...) ทำงานซ้ำซ้อนกัน ให้เคลียร์ Event Listener ของ TextureView ทิ้งใน onDestroy() ซะ

public class CameraApiV1Activity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
    ...
    @Override
    protected void onDestroy() {
        ...
        textureViewCamera.setSurfaceTextureListener(null);
    }
}


        เรียบร้อย!!

สรุป

        ในการเรียกใช้งาน Camera นั้นไม่ใช่แค่เรียกคำสั่งเพียงไม่กี่บรรทัด แต่จะต้องมีการกำหนดว่าจะใช้งานกล้องตัวไหน, กำหนด Camera Parameters, สร้าง View สำหรับ Preview ภาพจากกล้อง และอื่นๆอีกมากมาย

        โดยในบทความนี้เลือกใช้งาน TextureView เพื่อทำเป็นพื้นที่สำหรับ Preview ภาพจากกล้อง แทนที่จะเป็น SurfaceView แบบที่หลายๆบทความทำกัน ทั้งนี้เพราะว่า SurfaceView นั้นค่อนข้างเก่าและไม่รองรับคำสั่งบางอย่าง (ในขณะที่ TextureView รองรับ) อย่างการกำหนดให้ภาพ Preview เป็นแบบ Center Crop ซึ่งเดิมทีไม่สามารถทำได้ง่ายๆใน SurfaceView

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

        สำหรับโค้ดในบทความนี้สามารถเข้าไปดูได้ที่ Camera Sample [GitHub] ซึ่งในนี้จะรวมโค้ดในบทความตอนที่ 2 ด้วยนะ

        และนี่คือขั้นตอนการเรียกใช้งานกล้องและแสดงภาพ Preview บนหน้าจอเท่านั้นเองนะ!! ยังไม่ได้เพิ่มคำสั่งให้ถ่ายภาพได้เลย!! ดังนั้นจงตามไปอ่านกันต่อที่ [Android Code] รู้จักและเรียกใช้งาน Camera API v1 บนแอนดรอยด์แบบง่ายๆ [ตอนที่ 2]




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

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