Android系統提供了兩種使用手機相機資源實現拍攝功能的方法, : - 一種是直接經過Intent調用系統相機組件.這種方法快速方便,適用於直接得到照片的場景,如上傳相冊,微博、朋友圈發照片等。 : - 一種是使用相機API來定製自定義相機.這種方法適用於須要定製相機界面或者開發特殊相機功能的場景,如須要對照片作裁剪、濾鏡處理,添加貼紙,表情,地點標籤等。這篇文章主要是從如何使用相機API來定製自定義相機這個方向展開的。java
若是你的App的需求只是調用攝像頭拍照並拿到照片,老司機的建議是別本身實現拍照模塊,這裏面坑多水深。你徹底可使用Intent來調用系統相機或第三方具有拍照功能的App來拍照並獲取返回照片數據。 建立一個Intent,指定兩個拍攝類型之一android
MediaStore.ACTION_IMAGE_CAPTURE
拍攝照片;MediaStore.ACTION_VIDEO_CAPTURE
拍攝視頻;Intent intent = new Intent(MediaStore.ACTION_IMAGE/VIDEO_CAPTURE);
複製代碼
通用流程startActivityForResult()
和onActivityResult()
就不表述了。說說拍攝照片的Intent參數吧。canvas
首先是設置拍攝後返回數據的地址:數組
intent.putExtra(MediaStore.EXTRA_OUTPUT, your-store-uri);
複製代碼
MediaStore.EXTRA_OUTPUT
參數用於指定拍攝完成後的照片/視頻的儲存路徑。你可使用Android默認的儲存照片目錄來保存:異步
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURE)
複製代碼
public class MainActivity extends Activity {
private ImageView show_iv;
private String mFilePath = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
show_iv = (ImageView) findViewById(R.id.show_iv);
}
public void onClick(View view) {
Intent intent = new Intent();
// 1. 直接調用系統相機 沒有返回值
// intent.setAction("android.media.action.STILL_IMAGE_CAMERA");
// startActivity(intent);
// 2 調用系統相機 有返回值 可是返回值是 縮略圖
// intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
// startActivityForResult(intent, 100);
// 3 .返回原圖
mFilePath =
Environment.getExternalStorageDirectory().getAbsolutePath() +
"/picture.png";
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri = Uri.fromFile(new File(mFilePath));
// 指定路徑
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, 300);
// 4. 打開系統相冊
// intent.setAction(Intent.ACTION_PICK);
// intent.setType("image/*");
// startActivityForResult(intent, 500);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
// 返回縮略圖
if (requestCode == 100) {
if (data != null) {
Bitmap bitmap = (Bitmap) data.getExtras().get("data");
if (bitmap != null) {
show_iv.setImageBitmap(bitmap);
}
}
}
// 原圖
if (requestCode == 300) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(mFilePath);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
show_iv.setImageBitmap(rotateBitmap(bitmap));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 相冊
if (requestCode == 500) {
Uri uri = data.getData();
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
show_iv.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
// 三星機型拍照的時候照片會旋轉90度 因此須要轉回來
public Bitmap rotateBitmap(Bitmap bitmap) {
Matrix matrix = new Matrix();
matrix.postRotate(90);
Bitmap map = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
return map;
}
}
複製代碼
相機的權限問題和Android7.0以上的拍照返回圖片文件Uri問題,參考Android踩坑日記(一):android7.0動態相機權限ide
1.2.1 相機API關鍵類解析函數
Camera: 最主要的類,用於管理和操做camera資源。它提供了完整的相機底層接口,支持相機資源切換,設置預覽、拍攝尺寸,設定光圈,曝光,聚焦等相關參數,獲取預覽,拍攝幀數據等功能,主要一下方法:工具
/ **
*建立一個新的Camera對象來訪問特定的硬件攝像機。若是
*相同的相機被其餘應用程序打開,這會拋出一個
* RuntimeException。
*
* <p>完成使用相機後,您必須致電{@link #release()},
*不然它將保持鎖定狀態並對其餘應用程序不可用。
*
* <p>您的應用程序一次只能有一個Camera對象處於活動狀態
*用於特定的硬件攝像機。
*
* <p>來自其餘方法的回調被傳遞給調用open()的線程。若是這個線程沒有事件循 * 環loop,那麼
*回調傳遞到主應用程序事件循環loop。若是有
*沒有主應用程序事件循環,回調不會傳遞。
*
* <p class =「caution」> <b>注意:</ b>
* 在某些設備上,此方法可能會發生
* 須要很長時間才能完成。最好從一個工做線程
*(可能使用{@link android.os.AsyncTask})調用這個方法,來避免
*阻塞主應用程序UI線程。
*
* @參數cameraId硬件攝像頭訪問,介於0和
* {@link #getNumberOfCameras()} - 1。
* @返回一個新的Camera對象,鏈接,鎖定並準備使用。
*若是打開相機失敗,則引起RuntimeException(例如,若是
*相機正在被其餘進程或設備策略管理器使用
*禁用相機)
public static Camera open(int cameraId) {
return new Camera(cameraId);
}
/**
*建立一個新的攝像頭對象,以訪問攝像頭上的第一個後置攝像頭
*設備。 若是設備沒有後置攝像頭,則返回
* 空值。
* @see #open(int)
*/
public static Camera open() {
int numberOfCameras = getNumberOfCameras();
CameraInfo cameraInfo = new CameraInfo();
for (int i = 0; i < numberOfCameras; i++) {
getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
return new Camera(i);
}
}
return null;
}
複製代碼
/ **
*設置{@link Surface}用於實時預覽。
*預覽時須要surface or surface texture,而且
*預覽對拍照是必要的。相同的surface能夠毫無影響的從新設置
*設置預覽surface將取消設置任何經過{@link#setPreviewTexture}設置預覽surface texture
*
* <p>此時,當這個方法被調用的時候,{@link SurfaceHolder}必須已經包含一個surface
*若是您使用{@link android.view.SurfaceView},
*您須要註冊{@link SurfaceHolder.Callback}
*經過調用{@link SurfaceHolder#addCallback(SurfaceHolder.Callback)}並等待
* {@link SurfaceHolder.Callback#surfaceCreated(SurfaceHolder)}而且在調用
*調用setPreviewDisplay()或開始預覽。
*
* <p>必須在{@link #startPreview()}以前調用此方法。
*有一個異常Exception的狀況是,在調用startPreview()以前,若是預覽surafce未設置(或設置爲空)
*,則能夠調用此方法一次 ,使用非空參數設置預覽surface(這容許相機設置和表面建立並行發生,節省時間。)
* 預覽運行時,預覽surface可能不會改變。
*
* @param 持有者包含放置預覽的Surface,
*或null刪除預覽表面
*若是方法失敗,則引起IOException(例如,若是表面
*不可用或不適用)。
* /
public final void setPreviewDisplay(SurfaceHolder holder) throws IOException {
if (holder != null) {
setPreviewSurface(holder.getSurface());
} else {
setPreviewSurface((Surface)null);
}
}
複製代碼
/** *更改此相機服務的設置。 * * @參數params用於此Camera服務的參數 *若是任何參數無效或不受支持,則引起RuntimeException。 * @see #getParameters() */
public void setParameters(Parameters params) {
// If using preview allocations, don't allow preview size changes
if (mUsingPreviewAllocation) {
Size newPreviewSize = params.getPreviewSize();
Size currentPreviewSize = getParameters().getPreviewSize();
if (newPreviewSize.width != currentPreviewSize.width ||
newPreviewSize.height != currentPreviewSize.height) {
throw new IllegalStateException("Cannot change preview size" +
" while a preview allocation is configured.");
}
}
native_setParameters(params.flatten());
}
複製代碼
/** *開始捕捉和繪製預覽幀到屏幕上。 *預覽不會實際開始,直到提供surface *使用{@link #setPreviewDisplay(SurfaceHolder)}或 * {@link #setPreviewTexture(SurfaceTexture)}。 * * <p>若是經過{@link #setPreviewCallback(Camera.PreviewCallback)}, * {@link #setOneShotPreviewCallback(Camera.PreviewCallback)}或 * {@link #setPreviewCallbackWithBuffer(Camera.PreviewCallback)}設置了回調callback *,那麼回調的{@ Camera.PreviewCallback#onPreviewFrame(byte [],Camera)} *在預覽數據可用時將被調用。即預覽時onPreviewFrame時一直被回調的 */
public native final void startPreview();
複製代碼
/ **
*觸發異步圖像捕捉。相機服務將啓動
*隨着圖像捕捉的進行,對應用程序進行一系列回調。
*拍攝圖像後發生快門回調。這可使用
*觸發聲音讓用戶知道圖像已被捕捉。該
*當原始圖像數據可用時發生原始回調(注意:數據
*若是沒有可用的原始圖像回調緩衝區,則它將爲空
*原始圖像回調緩衝區不足以容納原始圖像)。
* postview回調時發生縮放,徹底處理的postview
*圖像可用(注意:並不是全部硬件都支持此功能)。 jpeg
*當壓縮圖像可用時發生回調。若是
*應用程序不須要特定的回調,能夠傳遞null
*而不是回調方法。
*
* <p>此方法僅在預覽處於活動狀態時有效(以後
* {@link #startPreview()})。預覽將在圖像後中止
*採起;調用者必須再次調用{@link #startPreview()}
*從新開始預覽或拍攝更多照片。這不該該被稱爲之間
* {@link android.media.MediaRecorder#start()}和
* {@link android.media.MediaRecorder#stop()}。
*
* <p>調用此方法後,您不得調用{@link #startPreview()}
*或拍攝另外一張照片,直到JPEG回調已返回。
*
* @參數快門圖像捕捉時刻的回調,或爲空
* @參數爲原始(未壓縮)圖像數據的回調,或爲空
* postview圖像數據的@param postview回調,可能爲空
* @param jpeg JPEG圖像數據的回調,或者爲null
* /
public final void takePicture(ShutterCallback shutter, PictureCallback raw,
PictureCallback postview, PictureCallback jpeg) {
mShutterCallback = shutter;
mRawImageCallback = raw;
mPostviewCallback = postview;
mJpegCallback = jpeg;
// If callback is not set, do not send me callbacks.
int msgType = 0;
if (mShutterCallback != null) {
msgType |= CAMERA_MSG_SHUTTER;
}
if (mRawImageCallback != null) {
msgType |= CAMERA_MSG_RAW_IMAGE;
}
if (mPostviewCallback != null) {
msgType |= CAMERA_MSG_POSTVIEW_FRAME;
}
if (mJpegCallback != null) {
msgType |= CAMERA_MSG_COMPRESSED_IMAGE;
}
native_takePicture(msgType);
mFaceDetectionRunning = false;
}
複製代碼
/** * 關閉camra底層的幀數據傳遞以及surface上的繪製, and * 重置相機,爲了之後調用 {@link #startPreview()} */
public final void stopPreview() {
_stopPreview();
mFaceDetectionRunning = false;
mShutterCallback = null;
mRawImageCallback = null;
mPostviewCallback = null;
mJpegCallback = null;
synchronized (mAutoFocusCallbackLock) {
mAutoFocusCallback = null;
}
mAutoFocusMoveCallback = null;
}
private native final void _stopPreview();
複製代碼
/** * Disconnects and releases the Camera object resources. * * <p>You must call this as soon as you're done with the Camera object.</p> */
public final void release() {
native_release();
mFaceDetectionRunning = false;
}
private native final void native_release();
複製代碼
SurfaceView:這個以前寫過文章oop
SurfaceView: : 用於繪製相機預覽圖像的類,提供給用戶實時的預覽圖像。普通的view以及派生類都是共享同一個surface的,全部的繪製都必須在UI線程中進行。 而surfaceview是一種比較特殊的view,它並不與其餘普通view共享surface,而是在內部持有了一個獨立的surface,surfaceview負責管理這個surface的格式、尺寸以及顯示位置。因爲UI線程還要同時處理其餘交互邏輯,所以對view的更新速度和幀率沒法保證,而surfaceview因爲持有一個獨立的surface,於是能夠在獨立的線程中進行繪製,所以能夠提供更高的幀率。自定義相機的預覽圖像因爲對更新速度和幀率要求比較高,因此比較適合用surfaceview來顯示。post
SurfaceHolder:surfaceholder是控制surface的一個抽象接口,它可以控制surface的尺寸和格式,修改surface的像素,監視surface的變化等等,surfaceholder的典型應用就是用於surfaceview中。surfaceview經過getHolder()方法得到surfaceholder 實例,經過後者管理監聽surface 的狀態。
SurfaceHolder.Callback 接口:負責監聽surface狀態變化的接口,有三個方法:
surfaceCreated(SurfaceHolder holder)
:在surface建立後當即被調用。在開發自定義相機時,能夠經過重載這個函數調用camera.open()、camera.setPreviewDisplay(),來實現獲取相機資源、鏈接camera和surface等操做。surfaceChanged(SurfaceHolder holder, int format, int width, int height)
:在surface發生format或size變化時調用。在開發自定義相機時,能夠經過重載這個函數調用camera.startPreview來開啓相機預覽,使得camera預覽幀數據能夠傳遞給surface,從而實時顯示相機預覽圖像。surfaceDestroyed(SurfaceHolder holder)
:在surface銷燬以前被調用。在開發自定義相機時,能夠經過重載這個函數調用camera.stopPreview(),camera.release()來實現中止相機預覽及釋放相機資源等操做。1.2.2 相機開發流程
開發第一步是在 Android Manifest.xml 文件中聲明使用相機的權限:
<uses-permission android:name="android.permission.CAMERA" />
複製代碼
有些同窗在開發時忘了聲明權限,運行時應用可能會崩潰掉。另外也要增長如下兩個特性聲明:
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
複製代碼
required
屬性是說明這個特性是否必須知足。比方說示例的設置就是要求必須擁有相機設備但能夠沒有自動對焦功能。
這兩個聲明是可選的,它們用於應用商店(GooglePlay)過濾不支持相機和不支持自動對焦的設備。
另外在保存照片時須要寫入儲存器的權限,也須要加上讀寫儲存器的權限聲明:
<uses-permission android:name="android.permission.WEITE_EXTERNAL_STORAGE" />
複製代碼
在市面上銷售的手機/平板等消費產品基本標配兩個攝像頭。如華爲P9,更有前置雙攝像頭。講真,我很好奇開發雙攝像頭的App是怎樣的體驗。在打開相機設備前,先獲取當前設備有多少個相機設備。若是你的開發需求裏包含切換先後攝像頭功能,能夠獲取攝像頭數量來判斷是否存在後置攝像頭。
int cameras = Camera.getNumberOfCameras();
複製代碼
這個接口返回值爲攝像頭的數量:非負整數。對應地,攝像頭序號爲: cameras - 1。例如在擁有先後攝像頭的手機設備上,其返回結果是2,則第一個攝像頭的cameraId是0,一般對應手機背後那個大攝像頭;第二個攝像頭的cameraId是1,一般對應着手機的前置自拍攝像頭;
相機是一個硬件設備資源,在使用設備資源前須要將它打開,能夠經過接口**Camera.open(cameraId)**來打開。參考如下代碼:
public static Camera openCamera(int cameraId) {
try{
return Camera.open(cameraId);
}catch(Exception e) {
return null;
}
}
複製代碼
注意
打開相機設備可能會失敗,你必定要檢查打開操做是否成功。打開失敗的可能緣由有兩種: : 一是安裝App的設備上根本沒有攝像頭,例如某些平板或特殊Android設備; 二是cameraId對應的攝像頭正被使用,可能某個App正在後臺使用它錄製視頻。
在打開相機設備後,你將得到一個Camera對象,並獨佔相機設備資源。 經過**Camera.getParameters()**接口能夠獲取當前相機設備的默認配置參數。下面列舉一些我能理解的參數:
閃光燈配置參數,能夠經過Parameters.getFlashMode()
接口獲取當前相機的閃光燈配置參數:
對焦模式配置參數,能夠經過Parameters.getFocusMode()
接口獲:
場景模式配置參數,能夠經過Parameters.getSceneMode()
接口獲取:
相機預覽圖須要設置正確的預覽方向才能正常地顯示預覽畫面,不然預覽畫面會被擠壓得很慘。 在一般狀況下,若是咱們須要知道設備的屏幕方向,能夠經過Resources.Configuration.orientation
來獲取。Android屏幕方向有「豎屏」和「橫屏」兩種,對應的值分別是ORIENTATION_PORTRAIT和ORIENTATION_LANDSCAPE
。但相機設備的方向卻有些特別,設置預覽方向的接口Camera.setDisplayOrientaion(int)
的參數是以角度爲單位的,並且只能是0,90,180,270
其中之一,``默認爲0,是指手機的左側爲攝像頭頂部畫面。記得只能是[0、90、180、270]其中之一,輸入其它角度數值會報錯`。
若是你想讓相機跟隨設備的方向,預覽界面頂部一直保持正上方,如下代碼供參考:
public static void followScreenOrientation(Context context, Camera camera){
final int orientation = context.getResources().getConfiguration().orientation;
if(orientation == Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(180);
}else if(orientation == Configuration.ORIENTATION_PORTRAIT) {
camera.setDisplayOrientation(90);
}
}
複製代碼
咱們通常使用SurfaceView做爲相機預覽View,你也可使用Texture。在SurfaceView中獲取得SurfaceHolder,並經過setPreviewDisplay()接口設置預覽。在設置預覽View後,必定要記得如下兩點:
Camera接受一個SurfaceHolder接口,這個接口能夠經過SurfaceHolder.Callback得到。咱們能夠經過繼承SurfaceView來實現相機預覽效果。在NextQRCode項目中,實現了LiveCameraView類,它內部已實現了相機預覽所須要的處理過程,很簡潔的類,如下是它的所有源碼:
ublic class LiveCameraView extends SurfaceView implements SurfaceHolder.Callback {
private final static String TAG = LiveCameraView.class.getSimpleName();
private Camera mCamera;
private SurfaceHolder mSurfaceHolder;
public LiveCameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSurfaceHolder = this.getHolder();
mSurfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "Start preview display[SURFACE-CREATED]");
startPreviewDisplay(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mSurfaceHolder.getSurface() == null){
return;
}
Cameras.followScreenOrientation(getContext(), mCamera);
Log.d(TAG, "Restart preview display[SURFACE-CHANGED]");
stopPreviewDisplay();
startPreviewDisplay(mSurfaceHolder);
}
public void setCamera(Camera camera) {
mCamera = camera;
final Camera.Parameters params = mCamera.getParameters();
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
params.setSceneMode(Camera.Parameters.SCENE_MODE_BARCODE);
}
private void startPreviewDisplay(SurfaceHolder holder){
checkCamera();
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.e(TAG, "Error while START preview for camera", e);
}
}
private void stopPreviewDisplay(){
checkCamera();
try {
mCamera.stopPreview();
} catch (Exception e){
Log.e(TAG, "Error while STOP preview for camera", e);
}
}
private void checkCamera(){
if(mCamera == null) {
throw new IllegalStateException("Camera must be set when start/stop preview, call <setCamera(Camera)> to set");
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Stop preview display[SURFACE-DESTROYED]");
stopPreviewDisplay();
}
}
複製代碼
從上面代碼能夠看出LiveCameraView的核心代碼是SurfaceHolder.Callback
的回調:在建立/銷燬時啓動/中止預覽動做。在LiveCameraView類中,咱們利用了View的生命週期回調來實現自動管理預覽生命週期控制:
預覽View須要注意預覽輸出畫面的尺寸。相機輸出畫面只支持部分尺寸。關於尺寸部分,後面再更新
在啓用預覽View後,就能夠經過Camera.takePicture()
方法拍攝一張照片,返回的照片數據經過Callback接口獲取
。takePicture()
接口能夠獲取三個類型的照片:
ShutterCallback
接口,在拍攝瞬間瞬間被回調,一般用於播放「咔嚓」這樣的音效;PictureCallback
接口,返回未經壓縮的RAW類型照片;PictureCallback
接口,返回通過壓縮的JPEG類型照片;咱們使用第三個參數,JPEG類型的照片的圖片精度便可知足識別二維碼的需求。在NextQRCode項目中,ZXing識別二維碼的數據格式爲Bitmap,經過BitmapFactory能夠很方便方便地將byte數組轉換成Bitmap。
public abstract class BitmapCallback implements Camera.PictureCallback {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
onPictureTaken(BitmapFactory.decodeByteArray(data, 0, data.length));
}
public abstract void onPictureTaken(Bitmap bitmap);
}
複製代碼
在打開一個相機設備後,意味着你的App就獨佔了這個設備,其它App將沒法使用它。所以在你不須要相機設備時,記得調用**release()方法釋放設備,再使用時能夠從新打開,這並不須要多大的成本。能夠選擇在stopPreview()**後即釋放相機設備。
附加工具性代碼實現
public static boolean hasCameraDevice(Context ctx) {
return ctx.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_CAMERA);
}
複製代碼
public static boolean isAutoFocusSupported(Camera.Parameters params) {
List<String> modes = params.getSupportedFocusModes();
return modes.contains(Camera.Parameters.FOCUS_MODE_AUTO);
}
複製代碼