Android實戰開發:自定義照相機
參考資料:
感謝以上大神們的的無私分享!
之前在公司寫了一個自定義CameraView,年代久遠,回頭看程式碼時居然有點看不懂了。。。
真是好記性不如爛筆頭啊~
趁著年底不忙有時間,再次重寫下Camera,話不多說,開始擼程式碼。
1.許可權
首先需要在AndroidManifest檔案中配置許可權:
<!-- 許可權 -->
<!-- 攝像頭許可權 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 閃光燈許可權 -->
<uses-permission android:name="android.permission.FLASHLIGHT" />
<!-- 在SDCard中建立與刪除檔案許可權 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!-- 寫入SD卡許可權 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 功能 -->
<!-- 攝像頭功能 -->
<uses-feature android:name="android.hardware.camera" />
<!-- 攝像頭自動對焦功能 -->
<uses-feature android:name="android.hardware.camera.autofocus" />
2.預覽影象
2.1 SurfaceView和SurfaceHolder
2.1.1 SurfaceView
因為我們需要預覽照相機中的影象,而這個影象又是動態變化的,所以必須用到這個SurfaceView。
SurfaceView繼承自View,能夠在非UI執行緒中在螢幕上繪圖,所以我們可以在預覽影象的同時進行一些別的操作。
我們把它寫在佈局檔案中,使用findViewById獲得它的例項即可。
mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
2.1.2 SurfaceHolder
SurfaceHolder相當於是SurfaceView的控制器,用來操縱surface。處理它的Canvas上畫的效果和動畫,控制表面,大小,畫素等。
mSurfaceHolder = mSurfaceView.getHolder();//通過getHolder方法獲取例項
實現SurfaceView需要實現SurfaceHolder.Callback介面,可以自定義一個類繼承SurfaceView並實現這個介面,也可以讓Activity直接實現這個介面,我們這裡使用第二種。
實現SurfaceHolder.Callback介面需要實現三個方法:
public class CameraActivity extends AppCompatActivity implements SurfaceHolder.Callback {
...
mSurfaceHolder.addCallback(this);
...
@Override
public void surfaceCreated(SurfaceHolder holder) {
//建立時觸發,surfaceView生命週期的開始,在這裡開啟相機
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//surface的大小發生改變時觸發,在這裡預覽影象
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//銷燬時觸發,surfaceView生命週期的結束,在這裡關閉相機
}
}
有了預覽影象的容器,下面就真正開始使用Camera了。
2.2 Camera
2.2.1 import注意事項
匯入包的時候注意是android.hardware.Camera,而不是android.graphics.Camera,不要搞錯了;
hardware中的Camera是控制裝置攝像頭的,graphics中的Camera是對影象進行處理的。
PS:在android5.0以上,android.hardware.Camera已過時,推薦使用的是android.hardware.Camera2類;但由於Camera2類不向下相容,而且目前安卓手機5.0以上的不多,所以我們還是使用過時的android.hardware.Camera。
2.2.2 生成預覽影象
在重寫的surfaceCreated方法中初始化camera,並開啟預覽
@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();//使用靜態方法open初始化camera物件,預設開啟的是後置攝像頭
try {
mCamera.setPreviewDisplay(mSurfaceHolder);//設定在surfaceView上顯示預覽
mCamera.startPreview();//開始預覽
} catch (IOException e) {
//在異常處理裡釋放camera並置為null
mCamera.release();
mCamera = null;
e.printStackTrace();
}
}
可以直接把預覽寫在surfaceCreated裡,也可以寫在surfaceChanged裡
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//也可以把預覽寫在這裡
}
在surfaceView被銷燬時,停止預覽並釋放camera物件並置為null
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//停止預覽並釋放camera物件並置為null
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
這時候執行程式,點選同意呼叫攝像頭,我們會發現有影象了,
可是,為什麼是歪的?
。。。whta the hell。。。
2.2.3 設定預覽方向
之所以是歪的,是因為攝像頭預設捕獲的畫面byte[]是根據橫向來的,而我們的應用是豎向的,
解決辦法是呼叫setDisplayOrientation來設定PreviewDisplay的方向,效果就是將捕獲的畫面旋轉多少度顯示。
詳情點這裡:http://blog.sina.com.cn/s/blog_777c69930100y7nv.html
所以我們只要在預覽前呼叫下setDisplayOrientation這個方法就好了
@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
//設定預覽偏移90度,一般的裝置都是90,但某些裝置會偏移180
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
mCamera.release();
mCamera = null;
e.printStackTrace();
}
}
2.2.4 Camera.Parameters 相機引數類
Camera.Parameters是相機引數類,在這裡可以給camera物件設定解析度,圖片方向,閃光燈模式等等一些引數,用以實現更豐富的功能。
使用方式如下:
@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.setDisplayOrientation(90);
/**Camera.Parameters**/
Camera.Parameters parameters = mCamera.getParameters();//得到一個已有的(預設的)引數
parameters.setPreviewSize(1920, 1080);//設定解析度,後面有詳細說明
parameters.setRotation(90);//設定照相生成的圖片的方向,後面有詳細說明
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//設定閃光燈模式為關
mCamera.setParameters(parameters);//將引數賦給camera
mCamera.startPreview();
} catch (IOException e) {
mCamera.release();
mCamera = null;
e.printStackTrace();
}
}
注意:這裡設定解析度是不能隨便設定的,因為每個裝置所支援的解析度不一樣,如果設定了裝置不支援的解析度程式就會崩潰,所以上面我把解析度寫死是非常不可取的;
可以通過getSupportedPreviewSizes()獲得裝置支援的解析度list。
List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();//獲得裝置所支援的解析度列表
for (int i = 0; i < sizeList.size(); i++) {
//這裡的TAG是一個常量字串,用來標識log
Log.i(TAG, "width:" + sizeList.get(i).width + ",height:" + sizeList.get(i).height);
}
執行這行程式碼,我們可以看到如下log
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:176,height:144
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:320,height:240
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:352,height:288
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:480,height:320
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:480,height:368
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:640,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:720,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:800,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:800,height:600
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:864,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:960,height:540
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1280,height:720
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1088,height:1088
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1440,height:1080
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1920,height:1080
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1920,height:1088
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1680,height:1248
在這裡面我們可以獲得所有的支援的解析度;
但是注意,這裡面的width都比height大,這是因為系統預設的影象捕捉是橫屏的,而非我們所熟悉的豎屏。
2.2.5 獲得最佳解析度
我們發現,sizeList的解析度大小雖然有一定的規律,但是並不是最後一個就是螢幕的解析度;
比如我測試的手機螢幕解析度為1920x1080,但是sizeList的最後一個是1680x1248,
所以我們還要對這些資料進行一下篩選,找到最適合螢幕的解析度。
程式碼如下:
/**
* 獲得最佳解析度
* 注意:因為相機預設是橫屏的,所以傳參的時候要注意,width和height都是橫屏下的
*
* @param parameters 相機引數物件
* @param width 期望寬度
* @param height 期望高度
* @return
*/
private int[] getBestResolution(Camera.Parameters parameters, int width, int height) {
int[] bestResolution = new int[2];//int陣列,用來儲存最佳寬度和最佳高度
int bestResolutionWidth = -1;//最佳寬度
int bestResolutionHeight = -1;//最佳高度
List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();//獲得裝置所支援的解析度列表
int difference = 99999;//最小差值,初始化市需要設定成一個很大的數
//遍歷sizeList,找出與期望解析度差值最小的解析度
for (int i = 0; i < sizeList.size(); i++) {
int differenceWidth = Math.abs(width - sizeList.get(i).width);//求出寬的差值
int differenceHeight = Math.abs(height - sizeList.get(i).height);//求出高的差值
//如果它們兩的和,小於最小差值
if ((differenceWidth + differenceHeight) < difference) {
difference = (differenceWidth + differenceHeight);//更新最小差值
bestResolutionWidth = sizeList.get(i).width;//賦值給最佳寬度
bestResolutionHeight = sizeList.get(i).height;//賦值給最佳高度
}
}
//最後將最佳寬度和最佳高度新增到陣列中
bestResolution[0] = bestResolutionWidth;
bestResolution[1] = bestResolutionHeight;
return bestResolution;//返回最佳解析度陣列
}
注意:這裡期望寬度和期望高度是為了擴充套件功能,
一般來說照相機都全屏,直接傳入手機解析度就可以了
但是如果不全屏呢?
所以就可以在這裡傳入希望的高度和寬度,保證預覽影象不變形。
修改surfaceCreate方法中的程式碼:
@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.setDisplayOrientation(90);
Camera.Parameters parameters = mCamera.getParameters();
/**獲得螢幕解析度**/
Display display = this.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenWidth = size.x;
int screenHeight = size.y;
/**獲得最佳解析度,注意此時要傳的width和height是指橫屏時的,所以要顛倒一下**/
int[] bestResolution = Utils.getBestResolution(parameters, screenHeight, screenWidth);//Utils是一個工具類,我習慣把操作的方法放在一個工具類中,作為靜態方法使用
parameters.setPreviewSize(bestResolution[0], bestResolution[1]);
parameters.setRotation(90);
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
mCamera.setParameters(parameters);
mCamera.startPreview();
} catch (IOException e) {
mCamera.release();
mCamera = null;
e.printStackTrace();
}
}
好了,現在應該就可以看到不變形的影象了~
如果還變形,請隱藏通知欄,ActionBar和底部的虛擬導航鍵欄,等等等等等。。。
所以這個時候期望寬度和期望高度就有用了,我們可以計算出除去這些系統欄的高度,然後傳入我們算好的值,就可以自動匹配出最佳解析度~
2.2.6 設定自動對焦
現在預覽出來的影象終於是正常大小了,可還是模模糊糊的,這咋拍?這坑定不能拍啊
所以我們需要實現相機的自動對焦功能,在這裡Android已經給我們封裝的很好了,我們只需要簡單的呼叫一下方法就行。
好多手機裡自帶的那種停下來就自動進行對焦的方式我還不太會,就先寫了個觸控自動對焦
自動對焦需要配置相關許可權,和實現Camera.AutoFocusCallback介面
程式碼如下:
//重寫onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
mCamera.autoFocus(this);//讓Activity實現介面
return true;
}
//實現介面中的方法,自動對焦完成時的回撥
@Override
public void onAutoFocus(boolean success, Camera camera) {
//在這裡可以判斷對焦是否成功,進行一些操作
}
自動對焦已經完成,呼叫Google的東西真是很簡單,我們可以在這裡加個提示框啊什麼的,後面有時間再說。
2.2.7 照相
終於要照相了,T^T
照相呼叫camera.takePicture方法,
程式碼如下:
“`
private Camera.PictureCallback pictureCallback = new Camera.PictureCallback() {//照相動作回撥用的pictureCallback
//在這裡可以獲得拍照後的圖片資料
@Override
public void onPictureTaken(byte[] data, Camera camera) {
//byte[]陣列data就是圖片資料,可以在這裡對圖片進行處理
mCamera.startPreview();//恢復預覽
}
};
//點選事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.imgbtnTakePhoto:
mCamera.takePicture(null, null, pictureCallback);//拍照會停止預覽
break;
default:
break;
}
}
就先寫到這裡吧,有精力再補充。。