Android – Camera và SurfaceView

5/5 - (1 vote)

Trong Android, các lớp View cơ bản như Button, TextView... được “vẽ” trên một luồng và hầu như chỉ thay đổi hình dáng, màu sắc của chúng khi có tương tác với người dùng, chẳng hạn như khi chúng ta click vào Button thì Button đó sáng lên. Trong trường hợp chúng ta cần hiển thị một thứ gì đó thay đổi liên tục như màn hình hiển thị camera, hay đồ họa game… thì chúng ta nên sử dụng lớp SurfaceView, lớp SurfaceView cho phép chúng ta can thiệp đến từng pixel trên màn hình.

Trong phần này chúng ta sẽ sử dụng SurfaceView để hiển thị camera lên màn hình.

AndroidManifest

Chúng ta tạo một project mới. Trong file AndroidManifest.xml chúng ta khai báo 2 quyền như sau:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.phocode" 
    android:versionCode="1" 
    android:versionName="1.0">
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
    <activity 
        android:name="MainActivity" 
        android:label="@string/app_name" 
        android:configChanges="orientation">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        </activity>
    </application>
</manifest>

Quyền android.permission.CAMERA là quyền được phép sử dụng camera, quyền android.permission.WRITE_EXTERNAL_STORAGE cho phép chúng ta ghi file lên thẻ nhớ ngoài. chúng ta xin quyền này vì ở đây mình còn “chụp hình” nữa 🙂

Layout

Trong file layout chúng ta thiết kế như sau:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" >
    <SurfaceView 
        android:id="@+id/cameraView" 
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" 
        android:layout_weight="1" 
        android:orientation="horizontal" />
    <LinearLayout 
        android:id="@+id/capture" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_gravity="center" 
        android:orientation="horizontal">
        <Button 
            android:id="@+id/button_ChangeCamera" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Switch" 
            android:onClick="switchCamera"/>
        <Button 
            android:id="@+id/button_capture" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Capture" 
            android:onClick="captureImage"/>
    </LinearLayout>
</LinearLayout>

Chúng ta thiết kế một SurfaceView, 1 LinearLayout khác để chứa 2 Button khác là Switch (đổi camera) và Capture (chụp hình).

MainActivity

Cuối cùng là file activity:

package com.phocode;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.ShutterCallback;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.OrientationEventListener;
import android.widget.TextView;
import android.widget.Toast;
import android.content.res.Configuration;

public class MainActivity extends Activity
    implements SurfaceHolder.Callback
{ 
    private Camera camera;
    private SurfaceView surfaceView;
    private SurfaceHolder surfaceHolder;
 
    private PictureCallback rawCallback;
    private ShutterCallback shutterCallback;
    private PictureCallback captureImageCallback;
 
    private boolean frontCam;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        surfaceView = (SurfaceView) findViewById(R.id.cameraView);
        surfaceHolder = surfaceView.getHolder();
 
        surfaceHolder.addCallback(this);
 
        captureImageCallback = new PictureCallback()
        {
            public void onPictureTaken(byte[] data, Camera camera)
            {
                FileOutputStream outStream = null;
                try
                {
                    String fileName = String.format("/sdcard/%d.jpg", System.currentTimeMillis());
                    outStream = new FileOutputStream(fileName);
                    outStream.write(data);
                    outStream.close();
                }
                catch(FileNotFoundException e)
                {
                    e.printStackTrace();
                }
                catch(IOException e)
                {
                    e.printStackTrace();
                }
                finally {}
                Toast.makeText(getApplicationContext(), "Picture saved", Toast.LENGTH_SHORT).show();
                refreshCamera();
            }
        };
   
        frontCam = false;
    }
 
    public void refreshCamera()
    {
        if(surfaceHolder.getSurface() == null) return;
 
        try
        {
            camera.stopPreview();
        }
        catch(Exception e) {}
 
        try
        {
            camera.setPreviewDisplay(surfaceHolder);
            camera.startPreview();
        }
        catch(Exception e) {} 
    }
 
    public void changeOrientation()
    {
        if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
            camera.setDisplayOrientation(0);
        else
            camera.setDisplayOrientation(90);
    }
  
    public int isFrontCameraExisted()
    {
        int cameraId = -1;
 
        int numberOfCameras = Camera.getNumberOfCameras();
        for(int i = 0 ; i < numberOfCameras ; i++)
        {
            CameraInfo info = new CameraInfo();
            Camera.getCameraInfo(i, info);
            if(info.facing == CameraInfo.CAMERA_FACING_FRONT)
            {
                cameraId = i; 
                break;
            }
        }
        return cameraId;
    }
 
    public int isBackCameraExisted()
    {
        int cameraId = -1;
 
        int numberOfCameras = Camera.getNumberOfCameras();
        for(int i = 0 ; i < numberOfCameras ; i++) 
        { 
            CameraInfo info = new CameraInfo(); 
            Camera.getCameraInfo(i, info); 
            if(info.facing == CameraInfo.CAMERA_FACING_BACK) 
            { 
                cameraId = i; 
                break; 
            } 
        } 
        return cameraId; 
    } 

    public void switchCamera(View view) 
    { 
        if(frontCam) 
        { 
            int cameraId = isBackCameraExisted(); 
            if(cameraId >= 0)
            {
                try
                {
                    camera.stopPreview();
                    camera.release();
 
                    camera = Camera.open(cameraId);
                    camera.setPreviewDisplay(surfaceHolder);
                    camera.startPreview(); 
 
                    frontCam = false;
 
                    changeOrientation();
                }
                catch(RuntimeException e) {}
                catch(Exception e) {}
 
                Camera.Parameters param;
                param = camera.getParameters();
 
                param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight());
                camera.setParameters(param); 
            }
        }
        else
        {
            int cameraId = isFrontCameraExisted();
            if(cameraId >= 0)
            {
                try
                {
                    camera.stopPreview();
                    camera.release();
 
                    camera = Camera.open(cameraId); 
                    camera.setPreviewDisplay(surfaceHolder);
                    camera.startPreview();
 
                    frontCam = true; 
 
                    changeOrientation();
                }
                catch(RuntimeException e) {}
                catch(Exception e) {}
 
                Camera.Parameters param;
                param = camera.getParameters();
 
                param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight());
                camera.setParameters(param);
            }
        }
    }
 
    public void captureImage(View view) throws IOException
    {
        camera.takePicture(null, null, captureImageCallback);
    }
 
    @Override
    public void surfaceCreated(SurfaceHolder holder)
    {
        try
        {
            camera = Camera.open(); 
            changeOrientation();
        }
        catch(RuntimeException e)
        {
            System.err.println(e);
        }
        Camera.Parameters param;
        param = camera.getParameters();
 
        param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight());
        camera.setParameters(param);
        try
        {
            camera.setPreviewDisplay(surfaceHolder);
            camera.startPreview();
        }
        catch(Exception e)
        {
            System.err.println(e);
            return;
        } 
    } 
 
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h)
    {
        refreshCamera();
    }
 
    @Override
    public void surfaceDestroyed(SurfaceHolder holder)
    {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
 
    @Override
    public void onConfigurationChanged(Configuration config)
    {
        super.onConfigurationChanged(config); 
        changeOrientation(); 
    }
}

Ngoài lớp SurfaceView để hiển thị lên màn hình, Android còn cung cấp lớp SurfaceHolder cho phép chúng ta thay đổi kích thước và định dạng của SurfaceView, truy xuất đến từng pixel, quản lý những thay đổi diễn ra trên SurfaceView...

public class MainActivity extends Activity
    implements SurfaceHolder.Callback
{
...
}

Lớp SurfaceHolder cung cấp giao diện Callback, giao diện này có 3 phương thức trừu tượng cần được override là surfaceCreated(), surfaceChanged()surfaceDestroyed(), 3 phương thức này được gọi khi có sự thay đổi diễn ra trên đối tượng SurfaceView.

surfaceView = (SurfaceView) findViewById(R.id.cameraView);
surfaceHolder = surfaceView.getHolder();
 
surfaceHolder.addCallback(this);

Chúng ta implement giao diện SurfaceHolder.Callback, sau đó trong phương thức onCreate(), chúng ta lấy tham chiếu đến đối tượng SurfaceView, mỗi đối tượng SurfaceView sẽ có một đối tượng SurfaceHolder của riêng nó, chúng ta có thể lấy bằng phương thức getHolder(). Cuối cùng chúng ta gắn listener của giao diện Callback vào đối tượng SurfaceHolder bằng phương thức addCallback().

@Override
public void surfaceCreated(SurfaceHolder holder)
{
    try
    {
        camera = Camera.open(); 
        changeOrientation();
    }
    catch(RuntimeException e)
    {
        System.err.println(e);
    }
    Camera.Parameters param;
    param = camera.getParameters();
 
    param.setPreviewSize(surfaceView.getWidth(), surfaceView.getHeight());
    camera.setParameters(param);
    try
    {
        camera.setPreviewDisplay(surfaceHolder);
        camera.startPreview();
    }
    catch(Exception e)
    {
        System.err.println(e);
        return;
    } 
} 

Phương thức surfaceCreated() sẽ được gọi khi ứng dụng bắt đầu chạy, trong phương thức này chúng ta khởi tạo đối tượng android.hardware.Camera bằng phương thức Camera.open(). Ở đây chúng ta phải bắt lỗi RunTimeException vì có những thiết bị không có camera (chẳng hạn như máy ảo). Phương thức changeOrientation() sẽ thay đổi hướng của camera, chúng ta sẽ tìm hiểu ngay bên dưới.

Lớp Camera.Parameters cho phép chúng ta thiết lập một số cấu hình trên đối tượng Camera, ở đây chúng ta chỉ đơn giản là resize lại kích thước ảnh mà camera thu được thành kích thước của SurfaceView, chúng ta resize bằng phương thức setPreviewSize(). Để thiết lập các thông số này lên đối tượng Camera thì chúng ta gọi phương thức setParameters. 

Phương thức setPreviewDisplay() sẽ chỉ định cho camera biết ảnh thu được hiện lên đối tượng SurfaceView nào, phương thức startPreview() sẽ thông báo cho camera bắt đầu thu hình.

public void changeOrientation()
{
    if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
        camera.setDisplayOrientation(0);
    else
        camera.setDisplayOrientation(90);
}

Trong phương thức changeOrientation(), chúng ta thay đổi hướng của camera, lý do là vì người dùng có thể xoay ngang (LANDSCAPE) hoặc xoay dọc (PORTRAIT) thiết bị của họ nhưng camera thì không tự động xoay được nên ở đây chúng ta tự xoay bằng tay bằng cách gọi phương thức setDisplayOrientation().

public void refreshCamera()
{
    if(surfaceHolder.getSurface() == null) return;
 
    try
    {
        camera.stopPreview();
    }
    catch(Exception e) {}
 
    try
    {
        camera.setPreviewDisplay(surfaceHolder);
        camera.startPreview();
    }
    catch(Exception e) {} 
}

Chúng ta định nghĩa phương thức refreshCamera() để tái tạo trạng thái của Camera, bởi vì ở đây chúng ta có nút chụp hình, việc chụp hình rồi lưu file vào máy sẽ làm camera bị “đơ” nên chúng ta cần khởi động lại camera.

@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
    camera.stopPreview();
    camera.release();
    camera = null;
}

Trong phương thức surfaceDestroyed(), chúng ta dừng việc thu hình bằng phương thức stopPreview() và giải phóng camera bằng phương thức release(). Việc gọi các phương thức này là rất quan trọng, bởi vì khi chúng ta sử dụng camera thì camera đó sẽ bị khóa và các ứng dụng khác sẽ không thể sử dụng được camera đó, nếu chúng ta tắt ứng dụng mà quên không giải phóng camera thì các ứng dụng khác sẽ không thể sử dụng được.

public void onCreate(Bundle savedInstanceState)
{
    ...
    captureImageCallback = new PictureCallback()
    {
        public void onPictureTaken(byte[] data, Camera camera)
        {
            FileOutputStream outStream = null;
            try
            {
                outStream = new FileOutputStream(String.format("/sdcard/%d.jpg", System.currentTimeMillis()));
                outStream.write(data);
                outStream.close();
            }
            catch(FileNotFoundException e)
            {
                e.printStackTrace();
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }
            finally {}
            Toast.makeText(getApplicationContext(), "Picture saved", Toast.LENGTH_SHORT).show();
            refreshCamera();
        }
    };
    ...
}
...
public void captureImage(View view) throws IOException
{
    camera.takePicture(null, null, captureImageCallback);
}

Phương thức Camera.takePicture() sẽ thực hiện việc chụp hình, tham số thứ 3 của phương thức này là một đối tượng PictureCallback, đối tượng này có phương thức onPictureTaken() sẽ được gọi mỗi khi chụp, tham số của phương thức onPictureTaken() là dữ liệu ảnh chụp được lưu trong một mảng byte và đối tượng camera đã thực hiện chụp hình. Tại đây chúng ta thực hiện lưu ảnh vào file trên thẻ nhớ ngoài.

public void switchCamera(View view) 
{ 
    if(frontCam)
    {
        int cameraId = isBackCameraExisted();
        ...
    }
    else
    {
        int cameraId = isFrontcameraExisted();    
        ...
    }
}

Chúng ta định nghĩa phương thức switchCamera() để chuyển đổi qua lại giữa các camera có trong máy (như camera trước và camera sau), mỗi camera sẽ được định danh bằng một Id, ở đây chúng ta lấy Id đó rồi truyền vào trong phương thức Camera.open(), nếu chúng ta không truyền vào Id nào thì phương thức này sẽ tự mở một camera sau mà nó tìm thấy.

public int isFrontCameraExisted()
{
    int cameraId = -1;
 
    int numberOfCameras = Camera.getNumberOfCameras();
    for(int i = 0 ; i < numberOfCameras ; i++)
    {
        CameraInfo info = new CameraInfo();
        Camera.getCameraInfo(i, info);
        if(info.facing == CameraInfo.CAMERA_FACING_FRONT)
        {
            cameraId = i; 
            break;
        }
    }
    return cameraId;
}

Phương thức isFrontCameraExisted() cho biết máy có tồn tại camera trước hay không. Phương thức Camera.getNumberOfCameras() sẽ trả về số lượng camera có trong máy, chúng ta duyệt qua từng camera, với mỗi đối tượng Camera chúng ta lấy đối tượng lớp CameraInfo, lớp này lưu những thông tin về từng Camera, trong đó có thuộc tính facing cho biết Camera đó hướng ra trước hay ra sau, chúng ta so sánh với hằng số CAMERA_FACING_FRONT để biết. Nếu có tồn tại Camera trước thì chúng ta trả về Id đó, nếu máy không tồn tại camera trước thì phương thức isFrontCameraExisted() sẽ trả về -1. Tương tự chúng ta định nghĩa phương thức isBackCameraExisted() để kiểm tra xem máy có tồn tại camera sau không.

@Override
public void onConfigurationChanged(Configuration config)
{
    super.onConfigurationChanged(config);
    changeOrientation();
}

Ngoài ra ở đây chúng ta override phương thức onConfigurationChanged(), phương thức này được gọi khi có một số thông tin về ứng dụng bị thay đổi trong quá trình chạy, chẳng hạn như hướng màn hình, màn hình bật lên bàn phím ảo, hoặc thay đổi ngôn ngữ… ở đây chúng ta chỉ quan tâm đến sự thay đổi hướng màn hình, nếu người dùng xoay ngang-dọc máy thì chúng ta thay đổi hướng tương ứng của camera.

Screenshot_2016-05-31-13-40-53

5 1 vote
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments