前言
由于最近一個項目需要自定義相機(jī)這塊,踩了很多坑凿可,在這里做個記錄孩等,以防忘記。
Android Camera 相關(guān)API可以說是Android 生態(tài)碎片化最嚴(yán)重的一塊
目前有兩套Camera Api 以android 5.0為分界線缰猴,5.0以下的是Camera ,5.0以上的是Camera2疤剑,然而Camera2 各個產(chǎn)商支持的各不相同滑绒,這就導(dǎo)致我們在相機(jī)開發(fā)中要花很大的精力去處理兼容性問題。
相機(jī)開發(fā)的流程
自定義相機(jī)開發(fā)流程大概可以分為5步
- 1隘膘、檢測并訪問相機(jī)資源蹬挤,檢查手機(jī)是否存在相機(jī)資源,如果存在則請求訪問相機(jī)資源棘幸。
- 2焰扳、創(chuàng)建預(yù)覽界面,將預(yù)覽畫面與設(shè)計好的用戶界面控件融合在一起误续,實時顯示相機(jī)的預(yù)覽圖像吨悍。
- 3、設(shè)置拍照監(jiān)聽蹋嵌,給用戶界面控件綁定監(jiān)聽器育瓜,使其能響應(yīng)用戶操作, 開始拍照過程。
- 4栽烂、拍照并保存文件躏仇,將拍攝獲得的圖像轉(zhuǎn)換成位圖文件,最終輸出保存成各種常用格式的圖片腺办。
- 5焰手、釋放相機(jī)資源,相機(jī)是一個共享資源怀喉,當(dāng)相機(jī)使用完畢后书妻,必須正確地將其釋放,以免其它程序訪問使用時發(fā)生沖突躬拢。
Camera相關(guān)API了解
Camera Api中主要涉及以下幾個關(guān)鍵類
- Camera:操作和管理相機(jī)資源躲履,支持相機(jī)資源切換见间,設(shè)置預(yù)覽和拍攝尺寸,設(shè)置光圈工猜、曝光等相關(guān)參數(shù)米诉。
- SurfaceView:用于繪制相機(jī)預(yù)覽圖像,提供實時預(yù)覽的圖像篷帅。
- SurfaceHolder:用于控制Surface的一個抽象接口荒辕,它可以控制Surface的尺寸、格式與像素等犹褒,并可以監(jiān)視Surface的變化。
- SurfaceHolder.Callback:用于監(jiān)聽Surface狀態(tài)變化的接口弛针。
1叠骑、Camera
方法 | 說明 |
---|---|
open(int cameraId) | 獲取Camera實例 cameraId 的值有兩個,一個是CameraInfo.CAMERA_FACING_BACK,CameraInfo.CAMERA_FACING_FRONT 前置攝像頭和后置攝像頭 |
setPreviewDisplay | 綁定繪制預(yù)覽圖像的surface削茁。 |
setPrameters | 設(shè)置相機(jī)參數(shù)宙枷,包括前后攝像頭,閃光燈模式茧跋、聚焦模式慰丛、預(yù)覽和拍照尺寸等 |
startPreview() | 開始預(yù)覽,將camera底層硬件傳來的預(yù)覽幀數(shù)據(jù)顯示在綁定的surface上 |
stopPreview() | 停止預(yù)覽瘾杭,關(guān)閉camra底層的幀數(shù)據(jù)傳遞以及surface上的繪制诅病。 |
release() | 釋放Camera實例 |
takePicture(ShutterCallback shutter, PictureCallback raw,PictureCallback jpeg) | 這個是實現(xiàn)相機(jī)拍照的主要方法,包含了三個回調(diào)參數(shù)粥烁。shutter是快門按下時的回調(diào)贤笆,raw是獲取拍照原始數(shù)據(jù)的回調(diào),jpeg是獲取經(jīng)過壓縮成jpg格式的圖像數(shù)據(jù)的回調(diào)讨阻。 |
更多API 可以查看這篇博客
2芥永、SurfaceHolder.Callback
接口 | 說明 |
---|---|
surfaceCreated(SurfaceHolder holder) | 在surface第一次創(chuàng)建的時候調(diào)用。 |
surfaceChanged(SurfaceHolder holder, int format, int width, int height) | 在surface的format或size等發(fā)生變化時調(diào)用钝吮。 |
surfaceDestroyed(SurfaceHolder holder) | 在surface銷毀的時候被調(diào)用埋涧。 |
Camera2相關(guān)API了解
1、Camera2
- CameraManager:攝像頭管理器奇瘦,用于打開和關(guān)閉系統(tǒng)攝像頭
- CameraCharacteristics:描述攝像頭的各種特性棘催,我們可以通過CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法來獲取。
- CameraDevice:描述系統(tǒng)攝像頭耳标,類似于早期的Camera巧鸭。
- CameraCaptureSession:Session類,當(dāng)需要拍照麻捻、預(yù)覽等功能時纲仍,需要先創(chuàng)建該類的實例呀袱,然后通過該實例里的方法進(jìn)行控制(例如:拍照 capture())。
- CaptureRequest:描述了一次操作請求郑叠,拍照夜赵、預(yù)覽等操作都需要先傳入
- CaptureRequest參數(shù),具體的參數(shù)控制也是通過CameraRequest的成員變量來設(shè)置乡革。
- CaptureResult:描述拍照完成后的結(jié)果
關(guān)于SurfaceView/TextureView
SurfaceView是一個有自己Surface的View寇僧。界面渲染可以放在單獨線程而不是主線程中。它更像是一個Window沸版,自身不能做變形和動畫嘁傀。
TextureView同樣也有自己的Surface。但是它只能在擁有硬件加速層層的Window中繪制视粮,它更像是一個普通View细办,可以做變形和動畫。
更多關(guān)于SurfaceView與TextureView區(qū)別的內(nèi)容可以參考這篇文章Android 5.0(Lollipop)中的SurfaceTexture蕾殴,TextureView, SurfaceView和GLSurfaceView.
封裝
API大概熟悉了笑撞,那么重點來了。那該如何封裝呢钓觉?其實我們封裝相機(jī)無非就是需要以下功能
- 打開相機(jī)
- 開啟預(yù)覽
- 拍照
- 開關(guān)閃關(guān)燈
- 聚焦
- 等等
- 關(guān)閉預(yù)覽
- 關(guān)閉相機(jī)
那么如何封裝呢茴肥,官方的開源庫cameraview給出了方案。
查看源碼荡灾,大概的就是這幾個類瓤狐。
類不多,大致使用的設(shè)計模式是抽象工廠模式批幌。這里也只是簡單的拍照功能芬首。
如果你不是重度的去定制相機(jī)的話,大概也只會用到預(yù)覽界面逼裆,保存圖片,以及閃光燈郁稍,聚焦這幾個,這幾個中最容易出現(xiàn)適配問題的就是預(yù)覽界面失真以及保存圖片的方向問題胜宇。保存圖片方向的問題其實也可以在最后輸出結(jié)果時進(jìn)行轉(zhuǎn)向耀怜,這樣的話又要多一步操作,能夠一步到位的事情桐愉,堅決不多一步财破。
那么接下來根據(jù)分析一下預(yù)覽界面失真問題。
public class CameraView extends FrameLayout {
public CameraView(Context context) {
this(context, null);
}
public CameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@SuppressWarnings("WrongConstant")
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
....
依據(jù)不同版本實例化不同的camera
// Internal setup
final PreviewImpl preview = createPreviewImpl(context);
mCallbacks = new CallbackBridge();
if (Build.VERSION.SDK_INT < 21) {
mImpl = new Camera1(mCallbacks, preview);
} else if (Build.VERSION.SDK_INT < 23) {
mImpl = new Camera2(mCallbacks, preview, context);
} else {
mImpl = new Camera2Api23(mCallbacks, preview, context);
}
.....
}
//獲取相應(yīng)的預(yù)覽界面
@NonNull
private PreviewImpl createPreviewImpl(Context context) {
PreviewImpl preview;
if (Build.VERSION.SDK_INT >= 23) {
preview = new SurfaceViewPreview(context, this);
} else {
preview = new TextureViewPreview(context, this);
}
return preview;
}
//根據(jù)設(shè)計稿設(shè)計的預(yù)覽界面尺寸去獲取相應(yīng)的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (isInEditMode()) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// Handle android:adjustViewBounds
//需要調(diào)整預(yù)覽界面从诲。這個設(shè)計和UI設(shè)計的預(yù)覽會有點不同左痢,可能過高或者過矮。如果底部按鈕會相應(yīng)的適配可采用此方式
if (mAdjustViewBounds) {
if (!isCameraOpened()) {
mCallbacks.reserveRequestLayoutOnOpen();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
final AspectRatio ratio = getAspectRatio();
assert ratio != null;
int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
super.onMeasure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
} else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
final AspectRatio ratio = getAspectRatio();
assert ratio != null;
int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
heightMeasureSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// Measure the TextureView
int width = getMeasuredWidth();
int height = getMeasuredHeight();
//獲取當(dāng)前設(shè)置的比例,當(dāng)前設(shè)置的比例也是通過預(yù)覽尺寸計算出來的
AspectRatio ratio = getAspectRatio();
if (mDisplayOrientationDetector.getLastKnownDisplayOrientation() % 180 == 0) {
ratio = ratio.inverse();
}
assert ratio != null;
//根據(jù)當(dāng)前的比例計算預(yù)覽View相應(yīng)的寬高俊性。同比例放大或縮小略步。保證preview不會出現(xiàn)圖形壓扁或者拉伸的情況
if (height < width * ratio.getY() / ratio.getX()) {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),
MeasureSpec.EXACTLY));
} else {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
}
}
class Camera1 extends CameraViewImpl {
.....
void adjustCameraParameters() {
SortedSet<Size> sizes = mPreviewSizes.sizes(mAspectRatio);
if (sizes == null) { // Not supported
mAspectRatio = chooseAspectRatio();
sizes = mPreviewSizes.sizes(mAspectRatio);
}
Size size = chooseOptimalSize(sizes);
// Always re-apply camera parameters
// Largest picture size in this ratio
final Size pictureSize = mPictureSizes.sizes(mAspectRatio).last();
if (mShowingPreview) {
mCamera.stopPreview();
}
mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());
mCameraParameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
mCameraParameters.setRotation(calcCameraRotation(mDisplayOrientation));
setAutoFocusInternal(mAutoFocus);
setFlashInternal(mFlash);
mCamera.setParameters(mCameraParameters);
if (mShowingPreview) {
mCamera.startPreview();
}
}
//獲取最佳預(yù)覽尺寸。
@SuppressWarnings("SuspiciousNameCombination")
private Size chooseOptimalSize(SortedSet<Size> sizes) {
//如果預(yù)覽界面的尺寸是0定页,0 那么就隨便取一個尺寸趟薄。
if (!mPreview.isReady()) { // Not yet laid out
return sizes.first(); // Return the smallest size
}
//等到View的繪制完成(onMeasure())后拿到預(yù)覽界面的view寬高去獲取相近的預(yù)覽寬高。
int desiredWidth;
int desiredHeight;
final int surfaceWidth = mPreview.getWidth();
final int surfaceHeight = mPreview.getHeight();
if (isLandscape(mDisplayOrientation)) {
desiredWidth = surfaceHeight;
desiredHeight = surfaceWidth;
} else {
desiredWidth = surfaceWidth;
desiredHeight = surfaceHeight;
}
Size result = null;
for (Size size : sizes) { // Iterate from small to large
if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
return size;
}
result = size;
}
return result;
}
.....
}
上面大概分析的是根據(jù)當(dāng)前設(shè)置的比例去計算camera預(yù)覽界面寬高典徊,然后根據(jù)預(yù)覽界面的寬高去獲取相近的預(yù)覽尺寸杭煎,這樣可以保證預(yù)覽時顯示的圖像不會出現(xiàn)失真。
網(wǎng)上比較多的方案是以下這個方法卒落,這個方式其實是這個根據(jù)這個項目open camera進(jìn)行修改的羡铲。是根據(jù)surfaceview的寬高比去獲取相camera對應(yīng)相近的預(yù)覽尺寸,但是這個有一個缺點儡毕,就是如果 SurfaceView的寬高比和camera對應(yīng)預(yù)覽尺寸的寬高比不一致也切,有一點點的誤差,就會出現(xiàn)一點點失真妥曲,而官方的開源庫cameraview 寬高比是根據(jù)預(yù)覽尺寸計算出來的,因此官方開源那種方式百分百不會出現(xiàn)失真钦购,當(dāng)然如果這個SurfaceView的寬高比和相機(jī)提供的預(yù)覽尺寸中的寬高比一致的話檐盟,那么也不會失真。因此下面這個方式寬高要設(shè)置好押桃。
/**
* 獲取最佳預(yù)覽大小
*
* @param sizes 所有支持的預(yù)覽大小
* @param w SurfaceView寬
* @param h SurfaceView高
*/
private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h;
if (sizes == null)
return null;
Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = h;
// Try to find an size match aspect ratio and size
for (Camera.Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
照片保存方向的問題相對簡單一點葵萎,只需要給camera設(shè)置
setDisplayOrientation()就可以了。以下兩種方式均可以唱凯。
這個是官方的開源庫cameraview 提供的方式
private int calcDisplayOrientation(int screenOrientationDegrees) {
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (360 - (mCameraInfo.orientation + screenOrientationDegrees) % 360) % 360;
} else { // back-facing
return (mCameraInfo.orientation - screenOrientationDegrees + 360) % 360;
}
}
這個是根據(jù)open camera進(jìn)行相應(yīng)修改后的方式羡忘。
/**
* 照片方向
*/
public static void onOrientationChanged(Activity activity, Camera.Parameters parameters, int cameraId) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int camera_orientation = info.orientation;
int result;
int device_orientation = getDeviceDefaultOrientation(activity);
if (device_orientation == Configuration.ORIENTATION_PORTRAIT) {
// should be equivalent to onOrientationChanged(0)
result = camera_orientation;
} else {
// should be equivalent to onOrientationChanged(90)
if ((info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)) {
result = (camera_orientation + 270) % 360;
} else {
result = (camera_orientation + 90) % 360;
}
}
parameters.setRotation(result);
}
關(guān)于Camera2 這個由于項目時間問題,目前沒有采用Camera2 相應(yīng)的API磕昼,官方開源庫這塊也是有點小問題的卷雕,在修改對應(yīng)的預(yù)覽比例也會出現(xiàn)失真,除了4:3的情況票从,因此本文沒有去分析camera2相應(yīng)的源碼漫雕。如果想要使用Camera2新特性的功能,那么建議可以去研究一下Jetpack 新出的CameraX
具體內(nèi)容請查看官方文檔 和 代碼示例峰鄙。