前言
在開發Android應用的時候,如果需要調用攝像頭拍照或者錄像,除了通過Intent調用系統現有相機應用進行拍照錄像之外,還可以通過直接調用Camera硬件去去獲取攝像頭進行拍照錄像的操作。本篇博客將講解如何在Android應用中通過Camera拍照功能.
錄像功能因為需要與MediaRecorder配使用,反而是更偏向操作MediaRecorder,所以我把錄像功能放到了音視頻開發篇幅里,請參考以下博客了解錄像功能開發.
MediaRecorder視頻錄制入門:https://www.cnblogs.com/guanxinjing/p/10980906.html
MediaRecorder與Camera1配合使用:https://www.cnblogs.com/guanxinjing/p/10986766.html
拍照開發
流程
- 獲取權限
- 初始化曲面視圖View(SurfaceView或者TextureView)(用於顯示相機預覽圖像)
- 初始化打開相機,選擇前后攝像頭
- 配置相機參數
- 拍照
- 處理照片返回數據,旋轉照片與壓縮照片
獲取權限
<!-- 相機相關 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
拍照TextureView例子
其實目前最適合相機預覽圖像顯示使用的是TextureView,因為它是真正獨立刷新幀數的View,可以讓相機預覽減少卡頓問題
而使用TextureView需要開啟硬件加速功能.開啟硬件加速方法如下:
在AndroidManifest.xml 清單文件里,你需要實現相機功能的activity添加 android:hardwareAccelerated="true"
<activity android:name=".work.share.FaceCameraActivity" android:hardwareAccelerated="true"></activity>
以下是代碼部分:
public class FaceCameraActivity extends BaseActivity implements TextureView.SurfaceTextureListener,Camera.PictureCallback , View.OnClickListener{ private static final float PICTURE_SIZE_PROPORTION = 1.1f;//目標分辨率尺寸 private TextureView mTextureView; private Camera mCamera; private Button mBtnCamera; private boolean mMoveTakingPhotos = false; //用於防止連續點擊拍照多次引起報錯的問題 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initCamera(); initCameraParameters(); } @Override public int getLayout() { return R.layout.activity_face_camera; } @Override public void initView() { mTextureView = findViewById(R.id.texture_view); mBtnCamera = findViewById(R.id.btn_camera); mBtnCamera.setOnClickListener(this); mTextureView.setSurfaceTextureListener(this);//添加監聽,用於監聽TextureView的創建/變化/銷毀 } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_camera: if(!mMoveTakingPhotos){ mMoveTakingPhotos = true; mCamera.takePicture(null,null,this);//拍攝拍照 參數為快門圖片回調/原始圖片回調(未壓縮)/jpeg圖片回調 } break; default: break; } } /** * 初始化打開相機 */ private void initCamera(){ if (mCamera == null){ //大多數情況下:0是后置攝像頭 1是前置攝像頭 ,這里demo就不弄這么復雜,在下面的會提供選擇前后攝像頭的方法 mCamera = Camera.open(1); } } /** * 初始化相機參數 */ private void initCameraParameters(){ Camera.Parameters parameters = mCamera.getParameters(); // parameters.getPreviewSize();//當前預覽尺寸 // parameters.getPictureSize();//當前分辨率尺寸 // parameters.getJpegThumbnailSize(); //返回當前jpeg圖片中exif縮略圖的尺寸 // parameters.getSupportedPreviewSizes();//預覽尺寸List // parameters.getSupportedPictureSizes();//分辨率尺寸List // parameters.getSupportedJpegThumbnailSizes();//返回當前jpeg圖片中exif縮略圖的尺寸List DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); Camera.Size previewSize = getpreviewSize(parameters, displayMetrics.heightPixels, displayMetrics.widthPixels);//獲取最接近屏幕分辨率的的預覽尺寸 Camera.Size pictureSize = getPictureSize(parameters, PICTURE_SIZE_PROPORTION);//獲取對應比例的最大分辨率 parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//設置關閉閃光燈 parameters.setFocusMode(Camera.Parameters.FLASH_MODE_AUTO); //對焦設置為自動 parameters.setPictureFormat(PixelFormat.JPEG);//拍照格式 parameters.setPreviewSize(previewSize.width, previewSize.height);//設置預覽尺寸 parameters.setPictureSize(pictureSize.width, pictureSize.height);//分辨率尺寸 parameters.set("orientation", "portrait");//相片方向 parameters.set("rotation", 90); //相片鏡頭角度轉90度(默認攝像頭是橫拍) mCamera.setParameters(parameters);//添加參數 mCamera.setDisplayOrientation(90);//設置顯示方向 } /** * 計算獲得最接近比例的預覽尺寸 * @param parameters * @param height * @param width * @return */ private Camera.Size getpreviewSize(Camera.Parameters parameters, int height, int width){ List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes(); Camera.Size selectPreviewSize = null; //緩存當前最准確比例的Size float currentDifference = 0; //緩存當前最小的差值 /** * 下面這行代碼是求傳入高度和寬度的高寬比例,這里可以發現一個細節我下面的預覽尺寸求的的寬高比例. * 是的他們一個是高寬比一個是寬高比,說明為什么這樣,因為如果按照2個都是高寬比來獲得預覽尺寸你會發現,獲得的尺寸怎么都有可能會拉伸變形(除非狗屎運尺寸完美剛好) * 最好的辦法就是,不求最合適目標尺寸的長方形比例,而求一個最適合目標尺寸的正方形比例,這樣拉伸變形就不會出現了 */ float proportion = (float)height/(float)width; for (int i = 0; i < previewSizeList.size(); i++){ Camera.Size size = previewSizeList.get(i); float previewSizeProportion = ((float)size.width)/((float)size.height); //計算當前預覽尺寸的寬高比例 float tempDifference = Math.abs(previewSizeProportion - proportion); //相減求絕對值的差值 if(i == 0){ selectPreviewSize = size; currentDifference = tempDifference; continue; } if (tempDifference <= currentDifference){ //獲得最小差值 if (tempDifference == currentDifference){ //如果差值一樣 if ((selectPreviewSize.width + selectPreviewSize.height) < (size.width+size.height)){ //判斷那個尺寸大保留那個 selectPreviewSize = size; currentDifference = tempDifference; } }else { //如果差值更小更准確 selectPreviewSize = size; currentDifference = tempDifference; } } L.e("currentDifference="+currentDifference +"width="+selectPreviewSize.width+"height="+selectPreviewSize.height); } return selectPreviewSize; } /** * 計算獲得最接近比例的分辨率 * @param parameters * @param targetProportion * @return */ private Camera.Size getPictureSize(Camera.Parameters parameters, float targetProportion) { List<Camera.Size> pictureSizeList = parameters.getSupportedPictureSizes(); Camera.Size selectPreviewSize = null; float currentDifference = 0; for (int i = 0; i < pictureSizeList.size(); i++) { Camera.Size size = pictureSizeList.get(i); L.e("分辨率列表_" + i + "_width=" + size.width + "height=" + size.height); float pictureSizeProportion = ((float) size.width) / ((float) size.height); L.e("分辨率列表_" + i + "_比例=" + pictureSizeProportion); float tempDifference = Math.abs(pictureSizeProportion - targetProportion); if (i == 0) { selectPreviewSize = size; currentDifference = tempDifference; continue; } if (tempDifference <= currentDifference) { if (tempDifference == currentDifference) { if ((selectPreviewSize.width + selectPreviewSize.height) < (size.width + size.height)) { //判斷那個尺寸大保留那個 selectPreviewSize = size; currentDifference = tempDifference; } } else { //如果差值更小更准確 selectPreviewSize = size; currentDifference = tempDifference; } } } L.e("當前選擇分辨率width=" + selectPreviewSize.width + "height=" + selectPreviewSize.height); return selectPreviewSize; } /** * 照片拍照完成后的回調方法 * @param data * @param camera */ @Override public void onPictureTaken(final byte[] data, Camera camera) { //注意!此處返回是主線程,而處理圖片是耗時操作需要放到子線程里處理 handlerImageWaitDialog().show(); new Thread(new Runnable() { @Override public void run() { try { //這里有一個坑,如果你想要讀取照片的角度信息,那么就需要直接吧byte[] data的照片數據先保存成圖片文件在從文件讀成Bitmap //因為如果你先處理壓縮圖片或者裁剪圖片,只要是Bitmap.createBitmap處理過就都有可能丟失這些照片信息到時候你怎么獲取角度都是0 FilePathSession.deleteFaceImageFile(); FileOutputStream fileOutputStream = new FileOutputStream(FilePathSession.getFaceImagePath()); fileOutputStream.write(data,0,data.length); fileOutputStream.flush(); fileOutputStream.close(); int angle = readPictureDegree(FilePathSession.getFaceImagePath().toString()); //獲取角度 Bitmap bitmap = BitmapFactory.decodeFile(FilePathSession.getFaceImagePath().toString());//重新在文件里獲取圖片 Matrix matrix = new Matrix();//創建矩陣配置類,用與設置旋轉角度和旋轉位置 matrix.setRotate(angle, bitmap.getWidth(), bitmap.getHeight());//設置旋轉角度和旋轉位置 Bitmap handlerAngleBitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true); ImageHandle.bitmapImageConfig(handlerAngleBitmap)//這個是個人寫的壓縮圖片工具類 .setTargetKB(200) .setSize(1080f,1920f) .setHandleListener(new BitmapImageHandleListener() { @Override public boolean onReady(Bitmap inpBitmap) { return true; } @Override public void onSuccess(Bitmap outBitmap) { try { FileOutputStream fileOutputStream = new FileOutputStream(FilePathSession.getFaceImagePath()); outBitmap.compress(Bitmap.CompressFormat.JPEG,90,fileOutputStream); fileOutputStream.flush(); fileOutputStream.close(); outBitmap.recycle(); runOnUiThread(new Runnable() { @Override public void run() { handlerImageWaitDialog().dismiss(); Intent startFaceConfirm = new Intent(FaceCameraActivity.this, FaceConfirmActivity.class); startActivity(startFaceConfirm); FaceCameraActivity.this.finish(); } }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "圖像壓縮處理失敗", Toast.LENGTH_SHORT).show(); } }); } } @Override public void onFailure(String text) { runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "圖像壓縮處理失敗", Toast.LENGTH_SHORT).show(); } }); } @Override public void onError(Exception e) { runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "圖像壓縮處理失敗", Toast.LENGTH_SHORT).show(); } }); } }).build(); } catch (FileNotFoundException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "圖像壓縮處理失敗", Toast.LENGTH_SHORT).show(); } }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(new Runnable() { @Override public void run() { mMoveTakingPhotos = false; handlerImageWaitDialog().dismiss(); Toast.makeText(FaceCameraActivity.this, "圖像壓縮處理失敗", Toast.LENGTH_SHORT).show(); } }); } } }).start(); } /** * TextureView的創建完成后的可用狀態回調 */ @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { //可用 try { mCamera.setPreviewTexture(surface);//給相機添加預覽圖像的曲面View mCamera.startPreview();//啟動圖像預覽 } catch (IOException e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { //尺寸變化 } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { //銷毀 return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { //更新 } /** * 讀取照片旋轉角度 * * @param path 照片路徑 * @return 角度 */ public int readPictureDegree(String path) { int degree = 0; try { ExifInterface exifInterface = new ExifInterface(path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: L.e("觸發 90度"); degree = 90; degree = degree + 90;//這里我是直接處理要增加或者減少的角度,讓圖片豎起來 break; case ExifInterface.ORIENTATION_ROTATE_180: L.e("觸發 180度"); degree = 180; degree = degree + 0; break; case ExifInterface.ORIENTATION_ROTATE_270: L.e("觸發 270度"); degree = 270; degree = degree - 90; break; default: L.e("觸發 0度"); degree = 0; degree = degree + 180; break; } } catch (IOException e) { e.printStackTrace(); } return degree; } @Override protected void onResume() { super.onResume(); // try { // mCamera.reconnect(); //相機重連接 // mCamera.startPreview(); // } catch (IOException e) { // e.printStackTrace(); // } } @Override protected void onStop() { super.onStop(); // mCamera.stopPreview();暫停預覽 } @Override protected void onDestroy() { super.onDestroy(); if (mCamera != null){ mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } }
選擇前后攝像頭的代碼
/** * 選擇攝像頭 * @param isFacing true=前攝像頭 false=后攝像頭 * @return 攝像id */ private Integer selectCamera(boolean isFacing){ int cameraCount = Camera.getNumberOfCameras(); // CameraInfo.CAMERA_FACING_BACK 后攝像頭 // CameraInfo.CAMERA_FACING_FRONT 前攝像頭 int facing = isFacing ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK; Log.e(TAG, "selectCamera: cameraCount="+cameraCount); if (cameraCount == 0){ Log.e(TAG, "selectCamera: The device does not have a camera "); return null; } Camera.CameraInfo info = new Camera.CameraInfo(); for (int i=0; i < cameraCount; i++){ Camera.getCameraInfo(i,info); if (info.facing == facing){ return i; } } return null; }
如果你需要切換攝像頭
mCamera.stopPreview();//暫停預覽 mCamera.release();//釋放攝像頭 這個是關鍵 openCamera(selectCamera(mCurrentCameraFacing));//重新選擇攝像頭並且打開 configCameraParameters();//重新配置攝像頭參數 startPreview(mSurfaceTexture);//開啟預覽
拍照SurfaceView例子
package com.demo; import androidx.appcompat.app.AppCompatActivity; import android.graphics.PixelFormat; import android.hardware.Camera; import android.os.Bundle; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.Button; import com.example.user.demo.R; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class TakePictureActivity extends AppCompatActivity implements SurfaceHolder.Callback,Camera.PictureCallback { private static final String TAG = "TakePictureActivity"; private Button mBtnTake; private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceHolder; private Camera mCamera; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_take_picture); mBtnTake = (Button)findViewById(R.id.take); mSurfaceView = (SurfaceView)findViewById(R.id.surfaceView); init(); mBtnTake.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mCamera != null){ mCamera.takePicture(null,null,TakePictureActivity.this); } } }); } @Override protected void onDestroy() { super.onDestroy(); mSurfaceHolder.removeCallback(this); mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); } private void init(){ mSurfaceHolder = mSurfaceView.getHolder(); mCamera = Camera.open(); mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mSurfaceHolder.addCallback(this); Camera.Parameters parameters = mCamera.getParameters(); parameters.setFlashMode("off"); parameters.setPictureFormat(PixelFormat.JPEG); //設定相片格式為JPEG,默認為NV21 parameters.setPreviewSize(640, 480); parameters.set("orientation", "portrait");//相片方向 parameters.set("rotation", 90); //相片鏡頭角度轉90度(默認攝像頭是橫拍) mCamera.setParameters(parameters); mCamera.setDisplayOrientation(90); } @Override public void surfaceCreated(SurfaceHolder holder) { Log.e(TAG,"懸浮窗口生成"); try { mCamera.setPreviewDisplay(mSurfaceHolder); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.e(TAG,"懸浮窗口變化"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.e(TAG,"懸浮窗口銷毀"); } @Override public void onPictureTaken(final byte[] data, Camera camera) { Log.e(TAG,"拍照結果處理"); File file = getExternalFilesDir("takePiceture"); if (!file.exists()){ file.mkdirs(); } final File fileName = new File(file,System.currentTimeMillis()+".jpg"); new Thread(new Runnable() { @Override public void run() { try { FileOutputStream fileOutputStream = new FileOutputStream(fileName); fileOutputStream.write(data); fileOutputStream.close(); mCamera.startPreview(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }
Camera api 說明
Camera是Android攝像頭硬件的相機類,位於硬件包"android.hardware.Camera"下。它主要用於攝像頭捕獲圖片、啟動/停止預覽圖片、拍照、獲取視頻幀等,它是設備本地的服務,負責管理設備上的攝像頭硬件。
Camera既然用於管理設備上的攝像頭硬件,那么它也為開發人員提供了相應的方法,並且這些方法大部分都是native的,用C++在底層實現,下面簡單介紹一下Camera的一些方法:
| api | 說明 |
| open() | 打開Camera,返回一個Camera實例。 |
| open(int cameraId) | 根據cameraId打開一個Camera,返回一個Camera實例。 |
| release() | 釋放掉Camera的資源。 |
| getNumberOfCameras() | 獲取當前設備支持的Camera硬件個數。 |
| getParameters() | 獲取Camera的各項參數設置類。 |
| setParameters(Camera.Parameters params) | 通過params把Camera的各項參數寫入到Camera中。 |
| setDisplayOrientation(int degrees) | 攝像預覽的旋轉度。 |
| setPreviewDisplay(SurfaceHolder holder) | 設置Camera預覽的SurfaceHolder。 |
| starPreview() | 開始Camera的預覽。 |
| stopPreview() | 停止Camera的預覽 |
| setPreviewCallback() | 設置預覽回調 |
| reconnect() | 重新連接 |
| autoFocus(Camera.AutoFocusCallback cb) | 自動對焦 |
| cancelAutoFocus() | 取消啟動對焦 |
| setAutoFocusMoveCallback() | 自動對焦移動回調 |
| takePicture(Camera.ShutterCallback shutter,Camera.PictureCallback raw,Camera.PictureCallback jpeg) | 拍照。 |
| enableShutterSound() | 啟用快門聲音 |
| lock() | 鎖定Camera硬件,使其他應用無法訪問。 |
| unlock() | 解鎖Camera硬件,使其他應用可以訪問。 |
| startFaceDetection() | 啟動人臉識別 |
| stopFaceDetection() | 停止人臉識別 |
| setFaceDetectionListener() | 人臉識別監聽回調 |
| setPreviewCallback() | 設置預覽回調 |
| setPreviewCallbackWithBuffer() | 設置預覽緩沖回調 |
| setOneShotPreviewCallback() | 設置一個鏡頭預覽回調 |
| setErrorCallback() | 設置異常回調 |
| startSmoothZoom() | 啟動平滑縮放 |
| stopSmoothZoom() | 停止平滑縮放 |
| setZoomChangeListener() | 縮放監聽 |
| setPreviewTexture() | 設置預覽紋理 |
上面已經介紹了Camera的常用方法,下面根據這些方法詳細講解Android下使用Camera開發拍照應用最基本的過程:
- 使用open()方法獲取一個Camera對象,鑒於Android設備可能配置了多個攝像頭,open()方法可以通過攝像頭Id開啟指定的攝像頭。
- 為Camera對象設置預覽類,它是一個SurfaceHolder對象,通過setPreviewDisplay(SurfaceHolder)方法設置。
- 調用startPreview()方法開始Camera對象的預覽。
- 調用takePicture()方法進行拍照,其中可以通過Camera.PictureCallback()回調獲得拍攝的Image數據。
- 當拍攝完成后,需要調用stopPreview()方法停止預覽,並使用release()釋放Camera占用的資源。
