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()
và 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.