android.hardware.camera2詳解(實時更新,未完待續...)
看到有些讀者對部落格提出的疑問,在此真誠說一句實在抱歉,本人是一名在校非科班出身大學生,之前 由於是期末季,所以非常忙,沒有空更新,一月二十幾號才放假,非常抱歉,評論所訴內容應該都已解決,只是我沒有更新,近期將會更新!再次致歉!
前言:原有的android.hardware.camera已經被google廢除,改用功能更強大的camera2,camera2的複雜度遠遠超過camera,所以寫這篇部落格之前也是經歷了各種心態爆炸。非常費解,谷歌官網竟然沒有給出一個好的demo,導致我只能去網上各處搜部落格,其間大多數部落格真的讓我惱火,不是寫的有問題,就是介紹不全,還有就是完全不解釋,我很討厭這種不負責的部落格,對我來說,簡直就是扯淡!(實在忍不住,所以爆了點粗口,望諒解)。出於以上原因,我會盡我最大努力,寫出一篇邏輯清晰,易於理解,程式碼完整的部落格。
一,讓我們先來了解一下其大致原理和使用流程
先看兩張圖:
可以看到圖中有個pipeline,這是camera2引入的概念,在我看來,這個單詞給定的很恰當。通過在Android Device(程式碼中的一個物件CameraDevice)和Camera Device(照相的硬體裝置)搭建一個通道,來進行兩者資訊的交流,Android Device向Camera Device發出請求——CaptureRequest,這個CaptureRequest可以設定各種我們想要設定的相機屬性——自動聚焦,閃光燈等等,而Camera Device拍攝對應影像,收集影象資訊,通過CameraMatadata傳遞給Android Device,從而使Android程式端可以獲取到照片資料,進行對應的處理。
另外,我們還注意到的上面有個箭頭指向Surface,Android端將Camera Device傳遞過來的影象資訊投射到Surface上,實現預覽的功能。
這張圖展示各個類之間的關係,讓我們來分別介紹一下:
CameraManager:管理Camera的類,通過這個類我們可以開啟Camera——對應方法為CameraManager.openCamera;獲取各種Camera引數——對應方法為CameraManager.getCameraCharacteristics。這裡我們先大概瞭解就行,稍後都會介紹。
其中開啟一個Camera時,會有對應的回撥介面——CameraDevice.StateCallback,處理相機開啟失敗,成功後者錯誤的情況。CameraCharacteristics:含有Camera的各種引數,如閃光燈,自動對焦,自動曝光等等。
CameraDevice:java程式碼中代表Camera的物件,可以關閉相機,向相機硬體端發出請求等等。
CameraCaptureSession:session直譯為”會議“,這是一個很形象的詞,其實就是上文提到的pipeline,程式中通過創造一個CameraCaptureSession,在安卓端和相機硬體端建立管道,從而可以獲取拍攝的圖片資訊。
在創造一個會議時,會回撥兩個介面——
StateCallback:處理session建立成功和失敗的情況,通常在這裡會進行預覽的一些初始化設定。
CaptureCallback:捕獲影象成功、失敗、進行時等情況的處理。CameraRequest:安卓端相機引數的設定請求,會在建立session時被當作引數。
CameraMetadata:控制相機和帶有相機引數的基礎類,它的子類是:
CameraCharacteristics,CaptureRequest,CaptureResult。CaptureResult:從影象感測器捕獲單個影象的結果的子集。包含捕獲硬體(感測器,鏡頭,快閃記憶體),處理流水線,控制演算法和輸出緩衝區的最終配置的一個子集。概念有點難懂,可以去官網仔細瞭解下。
這張圖很重要,大家要仔細看看,稍後的程式碼的邏輯結構就是依照這個而來的,不一定要全部看懂,可以結合下面的程式碼流程去理解。
有了這些基礎知識儲備,讓我們來打造一個最簡單的android.hardware.camera2 demo吧!
二,最簡單的camera2 demo
(一)新增許可權,以及部分依賴
在app/gradle中新增依賴:
compile 'de.hdodenhof:circleimageview:2.1.0'
這是一個具有將任何形狀圖片變為圓形功能的開源View,圖片中左下角的圓形圖片就是呼叫了這個控制元件。
在AndroidManifest.xml新增相關許可權:
<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" />
這裡的讀取和讀入功能以後擴充的程式碼要用到。
(二)為你的相機建立個性佈局
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<SurfaceView
android:id="@+id/mFirstSurfaceView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#0E0E0E">
<!--提供預覽功能,以及從相簿選取圖片進行識別功能-->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/img_show"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:src="@mipmap/ic_launcher"
android:layout_marginStart="10dp"/>
<Button
android:id="@+id/take_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_alignParentBottom="true"
android:text="拍照"/>
<!--點選觀看識別結果-->
<Button
android:id="@+id/recognition_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:text="結果"
android:layout_marginEnd="10dp"/>
</RelativeLayout>
</LinearLayout>
其中SurfaceView是用來預覽拍攝圖片的。CircleImageView是我們第一步提到的開源庫。
(三)在MainActivity中實現相關功能
老規矩,先貼整體程式碼:
MainActivity.class
package com.example.wordrecognition;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Camera;
import android.graphics.ImageFormat;
import android.hardware.camera2.*;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import android.Manifest.permission.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.jar.Manifest;
public class MainActivity extends AppCompatActivity {
private CameraManager mCameraManager;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceViewHolder;
private Handler mHandler;
private String mCameraId;
private ImageReader mImageReader;
private CameraDevice mCameraDevice;
private CaptureRequest.Builder mPreviewBuilder;
private CameraCaptureSession mSession;
private ImageView img_show;
private Button take_picture_bt;
private Handler mainHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null){
actionBar.hide();
}
img_show= (ImageView) findViewById(R.id.img_show);
take_picture_bt=(Button)findViewById(R.id.take_picture);
initSurfaceView();//初始化SurfaceView
/**
* 拍照按鈕監聽
*/
take_picture_bt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
takePicture();
}
});
}
public void initSurfaceView(){
mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView);
mSurfaceViewHolder = mSurfaceView.getHolder();//通過SurfaceViewHolder可以對SurfaceView進行管理
mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
initCameraAndPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//釋放camera
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
});
}
@TargetApi(19)
public void initCameraAndPreview() {
HandlerThread handlerThread = new HandlerThread("My First Camera2");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mainHandler = new Handler(getMainLooper());//用來處理ui執行緒的handler,即ui執行緒
try {
mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT;
mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這裡必須傳入mainHandler,因為涉及到了Ui操作
mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;//按理說這裡應該有一個申請許可權的過程,但為了使程式儘可能最簡化,所以先不新增
}
mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
} catch (CameraAccessException e) {
Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
}
}
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//進行相片儲存
mCameraDevice.close();
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//將image物件轉化為byte,再轉化為bitmap
final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap != null) {
img_show.setImageBitmap(bitmap);
}
}
};
private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
try {
takePreview();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(CameraDevice camera, int error) {
Toast.makeText(MainActivity.this, "開啟攝像頭失敗", Toast.LENGTH_SHORT).show();
}
};
public void takePreview() throws CameraAccessException {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
}
private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session;
//配置完畢開始預覽
try {
/**
* 設定你需要配置的引數
*/
//自動對焦
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//開啟閃光燈
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//無限次的重複獲取影象
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show();
}
};
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
mSession = session;
}
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
mSession = session;
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
}
};
public void takePicture() {
try {
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設定拍照請求的request
captureRequestBuilder.addTarget(mImageReader.getSurface());
// 自動對焦
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自動曝光
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
int rotation = getWindowManager().getDefaultDisplay().getRotation();
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉
CaptureRequest mCaptureRequest = captureRequestBuilder.build();
mSession.capture(mCaptureRequest, null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//獲取圖片應該旋轉的角度,使圖片豎直
public int getOrientation(int rotation) {
switch (rotation) {
case Surface.ROTATION_0:
return 90;
case Surface.ROTATION_90:
return 0;
case Surface.ROTATION_180:
return 270;
case Surface.ROTATION_270:
return 180;
default:
return 0;
}
}
//獲取圖片應該旋轉的角度,使圖片豎直
private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN)
return 0;
int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
// Round device orientation to a multiple of 90
deviceOrientation = (deviceOrientation + 45) / 90 * 90;
// LENS_FACING相對於裝置螢幕的方向,LENS_FACING_FRONT相機裝置面向與裝置螢幕相同的方向
boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;
if (facingFront) deviceOrientation = -deviceOrientation;
// Calculate desired JPEG orientation relative to camera orientation to make
// the image upright relative to the device orientation
int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;
return jpegOrientation;
}
}
程式碼有點長,下面我們來分解它的邏輯
1,隱藏ActionBar等以及基本部件初始化操作
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null){
actionBar.hide();
}
img_show= (ImageView) findViewById(R.id.img_show);
take_picture_bt=(Button)findViewById(R.id.take_picture);
2,初始化surfaceView,即照相機預覽介面
mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView);
mSurfaceViewHolder = mSurfaceView.getHolder();//通過SurfaceViewHolder可以對SurfaceView進行管理
mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() {
//SurfaceView被成功建立
@Override
public void surfaceCreated(SurfaceHolder holder) {
initCameraAndPreview();
}
//SurfaceView被銷燬
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//釋放camera
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
//SurfaceView內容發生改變
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
});
首先我們獲取到了SurfaceView例項,然後通過SurfaceView獲取SurfaceViewHolder,SurfaceViewHolder是用來管理SurfaceView的類,別的類通過SurfaceViewHolder可以對SurfaceView進行編輯。
然後我們通過SurfaceViewHolder為SurfaceView添加回調介面,三個回撥介面方法的功能註釋已經解釋了。在第二個方法中,釋放了Camera資源。
而在第一個方法中,SurfaceView被成功建立後,我們緊接著呼叫initCameraAndPreView,初始化Camera和將相機拍攝介面投射到SurfaceView上。
3,初始化相機和實現相機預覽功能
@TargetApi(19)
public void initCameraAndPreview() {
HandlerThread handlerThread = new HandlerThread("My First Camera2");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mainHandler = new Handler(getMainLooper());//用來處理ui執行緒的handler,即ui執行緒
try {
mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT;
mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7); mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這裡必須傳入mainHandler,因為涉及到了Ui操作
mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;//按理說這裡應該有一個申請許可權的過程,但為了使程式儘可能最簡化,所以先不新增
}
mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
} catch (CameraAccessException e) {
Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
}
}
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//進行相片儲存
mCameraDevice.close();
//mSurfaceView.setVisibility(View.GONE);//存疑
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//將image物件轉化為byte,再轉化為bitmap
final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap != null) {
img_show.setImageBitmap(bitmap);
}
}
};
在這步中首先我們初始化了兩個執行緒:
HandlerThread handlerThread = new HandlerThread("My First Camera2");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mainHandler = new Handler(getMainLooper());//用來處理ui執行緒的handler,即ui執行緒
關於HandlerThread不熟悉的讀者可以看下鴻洋大神的部落格——HandlerThread.
mHandler用來處理普通執行緒,mainHandler用來處理主執行緒——ui執行緒。
然後我們設定了CameraId:
前置攝像頭:LENS_FACING_BACK,名字不要弄反了。
後置攝像頭:LENS_FACING_FRONT。
設定ImageReader,用來讀取拍攝影象的類,ImageReader.newInstance方法的原型:
ImageReader.newInstance(int width,int height,int format,int maxImages);
其它引數好理解,maxImages代表使用者想讀取的最大Image物件數量。
然後我們為ImageReader設定了監聽介面:
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//這裡必須傳入mainHandler,因為涉及到了Ui操作
mOnImageAvailiableListener物件如下:
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//進行相片儲存和展示
mCameraDevice.close();
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//將image物件轉化為byte,再轉化為bitmap
final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap != null) {
img_show.setImageBitmap(bitmap);
}
}
};
這個介面在圖片拍攝完成後就會回撥,這個方法中我們關閉了相機,並進行了圖片的讀取,其中buffer.remaining返回buffer的元素數量,image.getPlanes()代表獲取圖片畫素資訊組成的陣列。
同時還要注意到,我們設定監聽介面時還傳入了mainHandler,這代表我們可以在回撥方法中進行ui操作。
緊接著獲取CameraManager,並打開了Camera:
mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;//按理說這裡應該有一個申請許可權的過程,但為了使程式儘可能最簡化,所以先不新增
}
mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
} catch (CameraAccessException e) {
Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
}
注意開啟Camera之前一定要動態申請許可權,這裡暫時還沒寫,執行時我是直接在手機開啟相關許可權的。否則無法發運行!!!
在開啟Camera時傳入了CameraDevice.StateCallback,用來反饋相機工作狀態:
private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
try {
takePreview();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(CameraDevice camera, int error) {
Toast.makeText(MainActivity.this, "開啟攝像頭失敗", Toast.LENGTH_SHORT).show();
}
};
相信聰明的讀者從字面意思就可以理解程式碼的意義了,就不細說了。
4,顯示預覽介面,進行正式預覽
在成功開啟相機後,回撥onOpened方法,呼叫takePreview()方法,進行正式的預覽,經過這步,我們就可以實時看到拍攝的畫面。
public void takePreview() throws CameraAccessException {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
}
private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session;
//配置完畢開始預覽
try {
/**
* 設定你需要配置的引數
*/
//自動對焦
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//開啟閃光燈
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//無限次的重複獲取影象
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show();
}
};
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
mSession = session;
}
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
mSession = session;
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
}
};
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder是前文講過的CaptureRequest的Builder(Builder設計模式),用來對CaptureRequest進行編輯。這裡有個引數——CameraDevice.TEMPLATE_PREVIEW:看名字可以猜到,這是表明這個request是針對於相機預覽介面的。
mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
將request設定的引數資料應用於SurfaceView對應的surface,request設定的引數形成的影象資料都會儲存在SurfaceView的surface中。
來簡單介紹下Surface:
應用產生的影象資料儲存的地方,SurfaceView和ImageReader都有一個對應的surface。
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
這個在前文也提到過,在安卓端和相機硬體端建立通道,進行資訊的交換。
我們來看看這個方法的原型:
createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)
只有第一個引數有點難理解,它是在宣告相機拍攝影象資料的儲存點——Surface物件。
再回到我們程式中,第一個引數是:
Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()
即是SurfaceView和ImageReader的Surface組成的一個數組,表明影象資料將會輸出到這兩個地方。
再看mSessionPreviewStateCallback:
private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session;
//配置完畢開始預覽
try {
/**
* 設定你需要配置的引數
*/
//自動對焦
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//開啟閃光燈
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//無限次的重複獲取影象
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(MainActivity.this, "配置失敗", Toast.LENGTH_SHORT).show();
}
};
這是建立session的一個回撥,前文也提到過。
方法中分別對session建立成功和失敗的情況進行了處理,成功後,設定了一系列引數,最後呼叫:
//無限次的重複獲取影象
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
這個很好理解,預覽的過程其實就是不斷重複請求影象資料的過程,所以叫”repeating”。
其原型為:
setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)
注意第二個引數,雖然我們目前的程式碼設定為null沒有用到,但是還是給出了這個回撥介面的程式碼:
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
//拍攝完全完成並且成功,拍攝影象資料可用時回撥
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
mSession = session;
}
//拍攝進行中,但拍攝影象資料部分可用時回撥
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
mSession = session;
}
};
//拍攝失敗時回撥
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
}
我們可以在這個回撥做圖片的儲存工作,但當然還是出於最簡化原則,暫時不寫。
5,一番辛苦,我們終於可以拍照片了!!!
public void takePicture() {
try {
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設定拍照請求的request
captureRequestBuilder.addTarget(mImageReader.getSurface());
// 自動對焦
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自動曝光
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
int rotation = getWindowManager().getDefaultDisplay().getRotation();
CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉
//captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))兩種方法都可以。
CaptureRequest mCaptureRequest = captureRequestBuilder.build();
mSession.capture(mCaptureRequest, null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用來設定拍照請求的request
這個之前講過,只是引數改變了——CameraDevice.TEMPLATE_STILL_CAPTURE,這次是針對拍攝圖片的request。
接下來挑關鍵的講:
int rotation = getWindowManager().getDefaultDisplay().getRotation();
獲取螢幕的方向,你旋轉螢幕這個值會發生相應變化,因為對這個理解也不深,不透徹,不敢亂講,大家可以google一下,很多這個的講解,如果我以後理解深了,就會寫進部落格。
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使圖片做順時針旋轉
//captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))兩種方法都可以。
這個是設定圖片的方向使圖片豎直放置,注意第二個引數,代表應該圖片應該旋轉的角度,因為這個比較複雜(真的複雜,,,),大家想了解可以看看這篇部落格——Android相機開發那些坑。
程式碼中我提供了兩種第二個引數:
網友提出的:
//獲取圖片應該旋轉的角度,使圖片豎直
public int getOrientation(int rotation) {
switch (rotation) {
case Surface.ROTATION_0:
return 90;
case Surface.ROTATION_90:
return 0;
case Surface.ROTATION_180:
return 270;
case Surface.ROTATION_270:
return 180;
default:
return 0;
}
}
Google官網上的:
//獲取圖片應該旋轉的角度,使圖片豎直
private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN)
return 0;
int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
// Round device orientation to a multiple of 90
deviceOrientation = (deviceOrientation + 45) / 90 * 90;
// LENS_FACING相對於裝置螢幕的方向,LENS_FACING_FRONT相機裝置面向與裝置螢幕相同的方向
boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;
if (facingFront) deviceOrientation = -deviceOrientation;
// Calculate desired JPEG orientation relative to camera orientation to make
// the image upright relative to the device orientation
int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;
return jpegOrientation;
}
目前發現兩種都可以,所以兩種都放上來,但個人也不是全部理解,所以很抱歉,暫時還不能給大家做解釋,不過相信聰明的你們可以通過閱讀上面的部落格理解它!
mSession.capture(mCaptureRequest, null, mHandler);
這是最關鍵的一步,通過這步,拍照動作才真正完成。
其原型是:
capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)
相對簡單,就不解釋了。
經過上述有點複雜的步驟,相信大家都可以做出一個最簡單的相機應用!
三,進階應用之圖片的儲存
我將它整合在一個方法中,方便需要時插入到程式碼中:
//將照片儲存在相機照片儲存位置,這裡採用bitmap方式儲存
public String savePicture(byte[] imgBytes) {
pictureId++;
String imgPath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/WordRecognition_picture" + pictureId + ".jpg";
Bitmap bitmap = BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);//影象資料被轉化為bitmap
File outputImage = new File(imgPath);
FileOutputStream outputStream = null;
try {
if (outputImage.exists()) {
outputImage.delete();//存在就刪除
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream = new FileOutputStream(outputImage);
bitmap.compress(Bitmap.CompressFormat.JPEG,