一砚亭、前言
一般APP開發(fā)或多或少會涉及到相機相關(guān)功能,對應(yīng)一般的功能,調(diào)用系統(tǒng)的拍攝功能能滿足要求捅膘,但是如果需要自定義UI添祸,或希望在本APP內(nèi)完成,這就需要了解Camera的使用了篓跛。
本篇將先介紹Camera相關(guān)的知識點膝捞,然后結(jié)合單例例子總結(jié)如何自定義Camera坦刀,最后梳理Camera開發(fā)需要注意的問題愧沟。
通過本篇可以了解一下知識點:
1、什么是SurfaceView鲤遥,有什么作用沐寺?何為雙緩沖機制?
2盖奈、Camera與Camera2對比混坞,如何選擇?
3钢坦、相機涉及的方向的概念究孕,如何旋轉(zhuǎn)到正確的方向?
4爹凹、Camera常用的方法及屬性有哪些厨诸?
5、Camera的調(diào)用流程禾酱?
6微酬、需要哪些權(quán)限,怎樣動態(tài)申請颤陶?
7颗管、如何設(shè)置參數(shù),適配預(yù)覽區(qū)域大凶易摺垦江?
8、什么時機打開和關(guān)閉攝像頭搅方?
9比吭、如何切換前后攝像頭?
二腰懂、理解SurfaceView
1梗逮、為什么需要SurfaceView
安卓系統(tǒng)設(shè)定的刷新頻率是60FPS(Frame Per Second),即每隔16.6ms底層會發(fā)出VSYNC信號重繪界面绣溜。如果繪制過于復(fù)雜慷彤,無法保證60FPS,則會出現(xiàn)卡頓現(xiàn)象。View,ViewGroup底哗,Animator的代碼執(zhí)行全部是在主線程中完成的岁诉,如果執(zhí)行復(fù)雜邏輯,輕則容易出現(xiàn)卡頓跋选,嚴(yán)重則可能導(dǎo)致ANR涕癣。為了解決這個問題,引入了SurfaceView前标,主要用于游戲坠韩、視頻等視覺效果復(fù)雜、刷新頻率高的場景炼列。
SurfaceView的改進(jìn)在于引入了雙緩沖機制只搁,及多線程繪制。
2俭尖、雙緩沖機制
如果不用畫布氢惋,直接在窗口上繪制叫無緩沖繪圖;
如果只有一個畫布稽犁,先將所有內(nèi)容繪制到畫布上焰望,后一次性繪制到窗口叫單緩沖繪圖,畫布是一層緩沖區(qū)已亥;
如果用2個畫布熊赖,先在一個緩沖畫布上繪制所有圖像,繪制好后將內(nèi)容拷貝到正式繪制的畫布上陷猫,這是雙緩沖機制秫舌,拷貝比直接繪制效率要高。
在SurfaceView中绣檬,一般會開啟一個新線程足陨,然后在新線程的中通過SurfaceHolder的lockCanvas方法獲取到Canvas進(jìn)行繪制操作,繪制完以后再通過SurfaceHolder的unlockCanvasAndPost方法釋放canvas并提交更改娇未,下次刷新顯示新內(nèi)容墨缘。
3、SurfaceView零抬、SurfaceHolder镊讼、Surface三者關(guān)系
典型的MVC關(guān)系:
Surface:Model層,持有緩沖畫布Canvas和繪圖內(nèi)容相關(guān)的各種信息平夜;
SurfaceView:View層蝶棋,與用戶交互,負(fù)責(zé)將Surface的內(nèi)容展示給用戶忽妒;
SurfaceHolder:Controller層玩裙,通過SurfaceHolder控制Surface中的數(shù)據(jù)兼贸。
三、Camera開發(fā)相關(guān)知識
1吃溅、Camera與Camera2
Google從android 5.0開始推出的一套新相機接口Camera2溶诞,并摒棄了舊的接口Camera,從Camera到Camera2整套相機框架都發(fā)生了變化决侈,所以接口有很大的不同螺垢,Camera2解決了Camera寥寥無幾的接口和靈活性低的問題,給應(yīng)用層提供了更多控制權(quán)限赖歌,以構(gòu)建更高質(zhì)量的相機應(yīng)用枉圃。Camera2有很多Camera不支持的特性,如更加先進(jìn)的框架俏站,可以控制每一預(yù)覽幀的參數(shù)讯蒲,高速連拍,調(diào)整focus distance肄扎,控制曝光時間等。
不過很多應(yīng)用要支持5.0以下設(shè)備赁酝,所以很多時候依然使用Camera開發(fā)相機應(yīng)用犯祠。
本篇主要介紹Camera的應(yīng)用。
Camera2的應(yīng)用可以參考Camera2 系列教程《Android Camera2 教程 · 第一章 · 概覽》
2酌呆、相機方向
相機方向比較難理解衡载,容易搞混,所以這里專門介紹隙袁。
首先理解幾個方向的概念痰娱。
自然方向
人自然站立的方向。
設(shè)備方向
設(shè)備方向是指設(shè)備方向與自然方向的順時針夾角菩收,例如手機豎著拿正對屏幕是手機的自然方向梨睁,即設(shè)備方向為0°,如果手機橫著拿正對屏幕且頂部在右邊娜饵,則設(shè)備方向為90°坡贺,以此類推,而平板橫著拿正對屏幕是平板的自然方向箱舞,即0°遍坟。
獲取方向可以通過
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
也可以通過OrientationEventListener 監(jiān)聽旋轉(zhuǎn)。
攝像頭方向
手機Camera的圖像數(shù)據(jù)都是來自于攝像頭硬件的圖像傳感器(Image Sensor)晴股,攝像頭的方向取決于圖像傳感器的安裝方向愿伴。安裝之后,有一個默認(rèn)的取景方向电湘,且不會被改變隔节。但為什么手機旋轉(zhuǎn)預(yù)覽畫面也能跟著旋轉(zhuǎn)到正確的畫面(自然方向)万搔?是因為Android系統(tǒng)底層根據(jù)當(dāng)前手機屏幕的方向?qū)D像Sensor采集到的數(shù)據(jù)進(jìn)行了旋轉(zhuǎn)處理,然后后才送給顯示系統(tǒng)官帘。
手機后置攝像頭一般是橫屏安裝的瞬雹,
CameraInfo.orientation表示相機圖像的方向。它的值是相機圖像順時針旋轉(zhuǎn)到設(shè)備自然方向一致時的角度刽虹。
注意:不同機型的攝像頭方向可能不一致酗捌,并不是所有都是 90°,也有小部分是 0° 的涌哲,所以我們要通過 Camera.CameraInfo.orientation 去判斷方向胖缤,而不是假設(shè)所有設(shè)備的攝像頭傳感器方向都是 90°。
預(yù)覽方向
系統(tǒng)提供了接口設(shè)置預(yù)覽方向阀圾,setDisplayOrientation哪廓,默認(rèn)情況是0°,即預(yù)覽方向與攝像頭方向一致初烘,對于橫屏應(yīng)用涡真,不需要設(shè)置預(yù)覽方向。而對于豎屏應(yīng)用肾筐,則需要通過該接口將Camera的預(yù)覽方向旋轉(zhuǎn)90哆料,與手機屏幕方向一致,這樣才會得到正確的預(yù)覽畫面吗铐。
拍攝方向
相機采集圖像后需要進(jìn)行順時針旋轉(zhuǎn)的角度东亦,即相機屬性的orientation的值。當(dāng)點擊拍照時唬渗,得到的照片方向不一定與預(yù)覽方向一致典阵,因為通過setDisplayOrientation僅僅修改了預(yù)覽圖像的方向,不會影響到實際拍攝圖像的方向镊逝,需要修改拍攝圖像方向可以通過camera.setRotation實現(xiàn)壮啊。
3、適配預(yù)覽區(qū)域大小
一般手機會提供多個預(yù)覽和拍照尺寸蹋半,通過接口getSupportedPreviewSizes和getSupportedPictureSizes可以獲得這些尺寸列表他巨。如果previewSize比例與預(yù)覽區(qū)(SurfaceView)比例不一致,則看到的預(yù)覽圖像會變形拉伸减江。如何適配不同預(yù)覽區(qū)大小解決拉伸問題染突,一般有2種方案:
一是根據(jù)previewSize比例修改SurfaceView的比例,調(diào)整預(yù)覽區(qū)比例(只調(diào)整寬或高)為預(yù)覽尺寸比例辈灼,從而使圖像不發(fā)生變形份企。
二是根據(jù)SurfaceView大小固定,然后根據(jù)其比例選擇最佳的(比例最接近的)預(yù)覽尺寸巡莹。
4司志、YUV/NV21
通過相機預(yù)覽拿到的圖像幀默認(rèn)是NV21格式的byte數(shù)組甜紫,Google支持的Camera Preview Callback的YUV常用格式有兩種:NV21 / YV12,至于YUV的理解可以參考YUV骂远。而NV21格式數(shù)據(jù)需要經(jīng)過特定轉(zhuǎn)化才能轉(zhuǎn)化為BitMap:
public Bitmap nv21ToBitmap(byte[] data){
try{
YuvImage image = new YuvImage(data, ImageFormat.NV21, w, h, null);
ByteArrayOutputStream os = new ByteArrayOutputStream(mData.length);
if(!image.compressToJpeg(new Rect(0, 0, w, h), 100, os)){
return null;
}
byte[] tmp = os.toByteArray();
os.close();
return BitmapFactory.decodeByteArray(tmp, 0,tmp.length);
}catch(Exception e){
}
return null;
}
四囚霸、 Camera方法及內(nèi)部類介紹
1、常用方法介紹
getNumberOfCameras():獲取攝像頭個數(shù)
getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo):獲取相機信息
open(int cameraId):打開相機
setPreviewDisplay(SurfaceHolder holder):設(shè)置預(yù)覽方向
startPreview():開始預(yù)覽
stopPreview():停止預(yù)覽
setPreviewCallback(Camera.PreviewCallback cb):設(shè)置預(yù)覽回調(diào)激才,callback中可以得到二進(jìn)制預(yù)覽幀
setPreviewCallbackWithBuffer(Camera.PreviewCallback cb):setPreviewCallback每產(chǎn)生一幀都開辟一個新的buffer拓型,會導(dǎo)致頻繁GC,影響效率瘸恼,如果對效率要求比較高劣挫,則用setPreviewCallbackWithBuffer,指定緩沖區(qū)东帅,通過內(nèi)存復(fù)用提高效率
autoFocus(Camera.AutoFocusCallback cb):自動聚焦
takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):拍攝照片
setParameters(Camera.Parameters params):設(shè)置相機參數(shù)
Camera.Parameters getParameters():獲取相機參數(shù)
2压固、內(nèi)部類
CameraInfo
facing:攝像頭的方向,包括前置和后置方向靠闭,可選值有 Camera.CameraInfo.CAMERA_FACING_BACK 和Camera.CameraInfo.CAMERA_FACING_FRONT帐我。
orientation:攝像頭畫面經(jīng)過多少度旋轉(zhuǎn)可變?yōu)樽匀环较颉?/p>
Parameters
通過LinkedHashMap<String, String>存儲相機的參數(shù)。
get(String key):從map中讀取指定key的參數(shù)
setPreviewSize(int width, int height):設(shè)置預(yù)覽尺寸
Camera.Size getPreviewSize():獲取預(yù)覽尺寸
List<Size> getSupportedPreviewSizes:獲取相機支持的預(yù)覽尺寸阎毅,不同機型支持的參數(shù)不同
List<Size> getSupportedVideoSizes():獲取錄制視頻的尺寸焚刚,不同機型支持的參數(shù)不同
setPictureSize(int width, int height):設(shè)置拍攝圖像尺寸,是拍攝后圖像扇调,不是預(yù)覽
List<Size> getSupportedPictureSizes():獲取相機支持的拍攝尺寸,不同機型支持的參數(shù)不同
setRotation(int rotation):設(shè)置拍攝返回圖像的方向抢肛,不是預(yù)覽方向
六狼钮、相機調(diào)用流程
因為相機設(shè)計的接口比較多,為了職責(zé)分明捡絮,將相機相關(guān)的邏輯封裝到CameraManager類熬芜,相機參數(shù)設(shè)置封裝到CameraConfigurationManager類,這里直接參考Zxing開源代碼中的Camera部分福稳,并稍作修改涎拉。
1、Acitivty部分
注冊權(quán)限
因為拍攝照片要保存到本地的圆,所以除了相機權(quán)限還需要存儲權(quán)限鼓拧。
<!-- 操作本地文件 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 攝像頭使用 -->
<uses-permission android:name="android.permission.CAMERA"/>
動態(tài)申請權(quán)限
對于target sdk 為23(安卓6.0)以上的應(yīng)用,運行中6.0以上系統(tǒng)越妈,需要動態(tài)申請權(quán)限季俩。在打開相機前先判斷系統(tǒng)版本,如果大于6.0則判斷是否已經(jīng)申請權(quán)限梅掠,沒有則申請酌住。申請權(quán)限如果客戶拒絕了則退出拍攝頁面(也可以在跳著拍攝頁面前申請店归,不過需要在所有打開拍攝頁面的地方都申請),如果客戶選擇了不再提示酪我,則彈出提示框消痛,引導(dǎo)客戶到系統(tǒng)設(shè)置中開啟權(quán)限,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] deniList = checkPermissionsGranted(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA});
if (deniList != null && deniList.length > 0) { //未授權(quán)
requestPermissions(deniList, REQ_PERMISSION);
} else {
openCamera();
}
} else {
openCamera();
}
@TargetApi(23)
public String[] checkPermissionsGranted(String[] permissions) {
List<String> deniList = new ArrayList<>();
// 遍歷每一個申請的權(quán)限都哭,把沒有通過的權(quán)限放在集合中
for (String permission : permissions) {
if (checkSelfPermission(permission) !=
PackageManager.PERMISSION_GRANTED) {
deniList.add(permission);
}
}
return deniList.toArray(new String[deniList.size()]);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
boolean camera = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA);
if (!camera) { // 判斷是否勾選了不再提醒秩伞,如果有勾選,提權(quán)限用途质涛,點擊確定跳轉(zhuǎn)到App設(shè)置頁面
AlertDialog.Builder builder = new AlertDialog.Builder(this);
mDialog = builder.setMessage("請在設(shè)置-應(yīng)用-xxx中稠歉,設(shè)置運行使用攝像頭權(quán)限").setPositiveButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).setNegativeButton("去設(shè)置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
goIntentSetting();
}
}).create();
mDialog.setCancelable(false);
mDialog.show();
} else { //拒絕了權(quán)限
finish();
}
}
}
}
/**
* 應(yīng)用設(shè)置頁面
*/
private void goIntentSetting() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
try {
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
布局定義SurfaceView并設(shè)置Callback
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
mSurfaceView = findViewById(R.id.surfaceView);
mSurfaceView.getHolder().addCallback(this);
...
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
打開相機并預(yù)覽
在surfaceCreated回調(diào)中,執(zhí)行判斷權(quán)限汇陆,確保有權(quán)限后再打開相機怒炸,打開相機加try-catch以防遇到異常程序crash,遇到異常則彈出提示毡代。
private void openCamera() {
try {
//設(shè)置前置或后置攝像頭
mCameraManager.setManualCameraId(mIsFront ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK);
//打開攝像頭
mCameraManager.openDriver(mSurfaceView.getHolder());
//開始預(yù)覽
mCameraManager.startPreview();
} catch (Exception ioe) {
//捕獲異常,提示并退出
AlertDialog.Builder builder = new AlertDialog.Builder(this);
mDialog = builder.setMessage("打開攝像頭失敗阅羹,請退出重試").setNegativeButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).create();
mDialog.setCancelable(false);
mDialog.show();
}
}
拍照
mShoot = findViewById(R.id.shoot);//拍攝按鈕,點擊拍攝
mShoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mCameraManager.getCamera().takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] bytes, Camera camera) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Camera.CameraInfo info = mCameraManager.getCameraInfo();
bitmap = BitmapUtil.rotateAndMirrorBitmap(bitmap, info.orientation, info.facing ==
Camera.CameraInfo.CAMERA_FACING_FRONT);
mImgResult.setImageBitmap(bitmap);//展示照片
mImgResult.setVisibility(View.VISIBLE);
mLayoutOpe.setVisibility(View.VISIBLE);
mShoot.setVisibility(View.GONE);
mImgChange.setVisibility(View.GONE);
mSurfaceView.setVisibility(View.GONE);
mCameraManager.stopPreview();//停止預(yù)覽
}
});
}
});
切換前后攝像頭
每次切換攝像頭都需要先將當(dāng)前攝像頭關(guān)閉教寂,然后設(shè)置并開啟新的攝像頭捏鱼。
//點擊切換按鈕
mImgChange.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mCameraManager.closeDriver();//關(guān)閉當(dāng)前攝像頭
mIsFront = !mIsFront;
openCamera();//開啟新攝像頭
}
});
關(guān)閉攝像頭
每次打開攝像頭后都需要主動關(guān)閉攝像頭,不然攝像頭沒有解鎖酪耕,下次不能正常打開导梆。這里在surfaceDestroyed回調(diào)中關(guān)閉,也可以在activity的onpause方法執(zhí)行迂烁,當(dāng)打開新的頁面看尼、退出頁面或者藏后臺都會調(diào)用關(guān)閉。
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mCameraManager.closeDriver();
}
2盟步、CameraManager類
打開攝像頭
public synchronized void openDriver(SurfaceHolder holder)
throws IOException {
Camera theCamera = camera;
if (theCamera == null) {
//打開攝像頭
if (requestedCameraId >= 0) {
theCamera = OpenCameraInterface.open(requestedCameraId);
} else {
theCamera = OpenCameraInterface.open();
}
if (theCamera == null) {
throw new IOException();
}
camera = theCamera;
}
//設(shè)置SurfaceHolder
theCamera.setPreviewDisplay(holder);
if (!initialized) {
initialized = true;
//初始化相機參數(shù)藏斩,根據(jù)surfaceview大小判斷最近預(yù)覽尺寸(只是判斷纱兑,沒有實際設(shè)置)
configManager.initFromCameraParameters(theCamera, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
setManualFramingRect(requestedFramingRectWidth,
requestedFramingRectHeight);
requestedFramingRectWidth = 0;
requestedFramingRectHeight = 0;
}
}
Camera.Parameters parameters = theCamera.getParameters();
String parametersFlattened = parameters == null ? null : parameters
.flatten(); // Save these, temporarily
try {
//設(shè)置相機參數(shù),預(yù)覽方向
configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
} catch (RuntimeException re) {
// Driver failed
Log.w(TAG,
"Camera rejected parameters. Setting only minimal safe-mode parameters");
Log.i(TAG, "Resetting to saved camera params: "
+ parametersFlattened);
// Reset:
if (parametersFlattened != null) {
parameters = theCamera.getParameters();
parameters.unflatten(parametersFlattened);
try {
theCamera.setParameters(parameters);
configManager.setDesiredCameraParameters(theCamera, requestedCameraId);
} catch (RuntimeException re2) {
// Well, darn. Give up
Log.w(TAG,
"Camera rejected even safe-mode parameters! No configuration");
}
}
}
}
開始嘉裤、停止預(yù)覽
/**
* Asks the camera hardware to begin drawing preview frames to the screen.
*/
public synchronized void startPreview() {
Camera theCamera = camera;
if (theCamera != null && !previewing) {
theCamera.startPreview();
previewing = true;
autoFocusManager = new AutoFocusManager(camera);
}
}
/**
* Tells the camera to stop drawing preview frames.
*/
public synchronized void stopPreview() {
if (autoFocusManager != null) {
autoFocusManager.stop();
autoFocusManager = null;
}
if (camera != null && previewing) {
camera.stopPreview();
previewCallback.setHandler(null, 0);
previewing = false;
}
}
3狮含、CameraConfigurationManager類
判斷相機最佳預(yù)覽尺寸
從支持的尺寸列表中選出與預(yù)覽界面大小比例最接近的
public void initFromCameraParameters(Camera camera, int width, int height) {
Camera.Parameters parameters = camera.getParameters();
WindowManager manager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
screenResolution = new Point(display.getWidth(), display.getHeight());
Log.d(TAG, "Screen resolution: " + screenResolution);
Point screenResolutionForCamera = new Point();
if (width == 0 || height == 0) {
screenResolutionForCamera.x = screenResolution.x;
screenResolutionForCamera.y = screenResolution.y;
} else {
screenResolutionForCamera.x = width;
screenResolutionForCamera.y = height;
}
if (screenResolution.x < screenResolution.y) {
screenResolutionForCamera.x = screenResolution.y;
screenResolutionForCamera.y = screenResolution.x;
}
previewResolution = getPreviewResolution(parameters, screenResolutionForCamera);
pictureResolution = getPictureResolution(parameters, screenResolutionForCamera);
}
獲取最近預(yù)覽尺寸:
private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {
previewSize = previewSize.trim();
int dimPosition = previewSize.indexOf('x');
if (dimPosition < 0) {
Log.w(TAG, "Bad preview-size: " + previewSize);
continue;
}
int newX;
int newY;
try {
newX = Integer.parseInt(previewSize.substring(0, dimPosition));
newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
} catch (NumberFormatException nfe) {
Log.w(TAG, "Bad preview-size: " + previewSize);
continue;
}
int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
if (newDiff == 0) {
bestX = newX;
bestY = newY;
break;
} else if (newDiff < diff) {
bestX = newX;
bestY = newY;
diff = newDiff;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
獲取最佳拍攝尺寸:
private Point getPictureResolution(Camera.Parameters parameters, Point screenSize) {
String pictureSizeValueString = parameters.get("picture-size-values");
// saw this on Xperia
if (pictureSizeValueString == null) {
pictureSizeValueString = parameters.get("picture-size-value");
}
Point pictureSize = null;
if (pictureSizeValueString != null) {
pictureSize = findBestSizeValue(pictureSizeValueString, screenSize);
}
if (pictureSize == null) {
// Ensure that the camera resolution is a multiple of 8, as the screen may not be.
pictureSize = new Point(
(screenSize.x >> 3) << 3,
(screenSize.y >> 3) << 3);
}
return pictureSize;
}
判斷最佳尺寸:比例最接近的
private static Point findBestSizeValue(CharSequence sizeValueString, Point screenResolution) {
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
for (String previewSize : COMMA_PATTERN.split(sizeValueString)) {
previewSize = previewSize.trim();
int dimPosition = previewSize.indexOf('x');
if (dimPosition < 0) {
Log.w(TAG, "Bad preview-size: " + previewSize);
continue;
}
int newX;
int newY;
try {
newX = Integer.parseInt(previewSize.substring(0, dimPosition));
newY = Integer.parseInt(previewSize.substring(dimPosition + 1));
} catch (NumberFormatException nfe) {
Log.w(TAG, "Bad preview-size: " + previewSize);
continue;
}
int newDiff = Math.abs(newX - screenResolution.x) + Math.abs(newY - screenResolution.y);
if (newDiff == 0) {
bestX = newX;
bestY = newY;
break;
} else if (newDiff < diff) {
bestX = newX;
bestY = newY;
diff = newDiff;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
設(shè)置相機參數(shù)
設(shè)置預(yù)覽尺寸蜂科、拍攝尺寸科贬、焦距蒙秒、預(yù)覽方向
public void setDesiredCameraParameters(Camera camera, int cameraId) {
Camera.Parameters parameters = camera.getParameters();
Log.d(TAG, "Setting preview size: " + previewResolution);
parameters.setPreviewSize(previewResolution.x, previewResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
setZoom(parameters);
setCameraDisplayOrientation(camera, cameraId);
camera.setParameters(parameters);
}
設(shè)置預(yù)覽方向
private void setCameraDisplayOrientation(Camera camera, int cameraId) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else {
// back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
旋轉(zhuǎn)照片
對于拍攝照片苔埋,一般可以對拍攝后照片根據(jù)相機角度(orientation)進(jìn)行旋轉(zhuǎn)到自然方向贱迟,特別地旬陡,對于前置攝像頭的照片拓颓,需要做鏡像對換。代碼如下:
public static Bitmap rotateAndMirrorBitmap(Bitmap bm, int degree, boolean needMirror) {
Bitmap newBm = null;
Matrix matrix = new Matrix();
matrix.postRotate(degree);
if (needMirror) {
matrix.postScale(-1, 1); // 鏡像水平翻轉(zhuǎn)
}
try {
// 將原始圖片按照旋轉(zhuǎn)矩陣進(jìn)行旋轉(zhuǎn)描孟,并得到新的圖片
newBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
bm.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
}
if (newBm == null) {
newBm = bm;
}
if (bm != newBm) {
bm.recycle();
}
return newBm;
}
七驶睦、測試效果
下面通過實驗測試各種設(shè)置的圖像效果砰左,測試機:華為P10。
1场航、預(yù)覽方向
如果不設(shè)置預(yù)覽方向:
public void setDesiredCameraParameters(Camera camera, int cameraId) {
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewSize(previewResolution.x, previewResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
setZoom(parameters);
// setCameraDisplayOrientation(camera, cameraId);
camera.setParameters(parameters);
}
如果Activity可隨手機方向旋轉(zhuǎn)的情況:
如果Activity不可隨手機方向旋轉(zhuǎn)的情況:
可以看到缠导,如果是橫屏應(yīng)用(Acitivity方向是橫向),預(yù)覽方向是自然方向溉痢,不需要旋轉(zhuǎn)僻造,如果是豎屏應(yīng)用(Acitivity豎直方向),預(yù)覽圖像需要旋轉(zhuǎn)90°孩饼。
前置攝像頭同樣效果髓削。
設(shè)置預(yù)覽方向后
無論Acitivity是否可隨手機方向旋轉(zhuǎn),預(yù)覽圖像都是自然方向镀娶。前置攝像頭也是自然方向立膛,但圖形是鏡像圖,左右對換的梯码,跟照鏡子一樣宝泵。
2、預(yù)覽尺寸
如果不設(shè)置預(yù)覽尺寸:
public void setDesiredCameraParameters(Camera camera, int cameraId) {
Camera.Parameters parameters = camera.getParameters();
// parameters.setPreviewSize(previewResolution.x, previewResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
setZoom(parameters);
setCameraDisplayOrientation(camera, cameraId);
camera.setParameters(parameters);
}
如果預(yù)覽界面大小是全屏轩娶,預(yù)覽尺寸沒有變形儿奶,說明相機默認(rèn)預(yù)覽尺寸比例與手機寬高比例一致的。
修改一下預(yù)覽界面大小鳄抒,測試一下預(yù)覽圖像效果闯捎。
設(shè)置最佳預(yù)覽尺寸后
可以發(fā)現(xiàn),如果不設(shè)置最佳預(yù)覽尺寸许溅,預(yù)覽圖像可能會嚴(yán)重變形隙券,測試中橫屏特別明顯,變形程度與預(yù)覽寬高比例與SurfaceView寬高比例差異大小有關(guān)闹司。
拍照方向
不設(shè)置拍照方向
如果不設(shè)置拍照方向,對于后置攝像頭沐飘,無論界面是否可以旋轉(zhuǎn)的效果:
可以看到游桩,對于橫屏拍攝,拍出的照片都是自然方向的耐朴,而對于豎屏拍攝借卧,拍出來的照片需要旋轉(zhuǎn)后90°才是自然方向。(一般手機是旋轉(zhuǎn)90°筛峭,但也有少數(shù)是270°铐刘,如Nexus 5X)。
再看下前置攝像頭的效果:
界面不隨手機旋轉(zhuǎn)的效果同樣影晓。
可以看到镰吵,橫屏拍攝是自然方向檩禾,豎屏拍照,照片需要旋轉(zhuǎn)270°才是自然方向(一般手機是旋轉(zhuǎn)270°疤祭,但也有少數(shù)是90°)
對拍攝照片旋轉(zhuǎn)后
無論前后攝像頭盼产,無論橫屏還是豎屏,拍照后需要根據(jù)相機方向調(diào)整照片到自然方向勺馆,另外戏售,前置攝像頭調(diào)整到自然方向發(fā)現(xiàn)跟預(yù)覽圖像剛好左右相反的,正常應(yīng)該跟預(yù)覽草穆,或者照鏡子看到的一致灌灾,所以還需要做鏡像對換。鏡像對換前(左)和鏡像對換后(右)的效果如下:
八悲柱、其他問題
1锋喜、預(yù)覽模糊/拍照照片模糊
設(shè)置的previewSize和pictureSize太小,或者沒有對焦诗祸。
2跑芳、預(yù)覽/拍攝照片已經(jīng)對焦,但光線很暗
可能跟預(yù)覽分辨率有關(guān)直颅,調(diào)整試下博个。
如測試過程發(fā)現(xiàn),Honor 8C(EMUI 8.2.0功偿,android8.1.0) 這款手機盆佣,預(yù)覽分別是1280*720的時候,預(yù)覽會很暗械荷。
九共耍、總結(jié)
1、SurfaceView相比一般的View改進(jìn)在于引入了雙緩沖機制吨瞎,多線程繪制痹兜,主要用于視頻、游戲等視覺復(fù)雜颤诀、刷新頻率高的場景字旭;
2、雙緩沖機制簡單來說就是使用多個畫布崖叫,在新線程繪制到緩沖畫布上遗淳,然后直接拷貝到正式繪制畫布上,實現(xiàn)高效繪制顯示心傀;
3屈暗、Surface、SurfaceView、SurfaceHolder三者是典型的MVC關(guān)系养叛;
4种呐、Camera2支持更多的相機特性,但不支持5.0以下手機一铅,為了兼容低版本陕贮,很多應(yīng)用依然使用Camera1;
5潘飘、攝像頭一般是橫屏安裝的肮之,決定了取景方向,且不會隨設(shè)備改變方向卜录。CameraInfo.orientation表示相機圖像的方向戈擒,表示相機圖像順時針旋轉(zhuǎn)到設(shè)備自然方向一致時的角度,一般是90°艰毒;
6筐高、camera.setDisplayOrientation可以設(shè)置預(yù)覽方向,但不會影響拍攝照片的方向丑瞧,橫屏應(yīng)用不需要設(shè)置柑土,豎屏需要;
7绊汹、camera.setRotation可以設(shè)置拍攝照片的方向稽屏,另一種方法是根據(jù)CameraInfo.orientation對拍攝后的照片進(jìn)行旋轉(zhuǎn)到自然方向;
8西乖、為了適配不同預(yù)覽區(qū)域大小狐榔、解決預(yù)覽拉伸問題,一種方案是從相機支持的預(yù)覽尺寸中選出寬高比例最接近預(yù)覽區(qū)域比例的尺寸获雕;另外一種是相機預(yù)覽尺寸不變薄腻,調(diào)整預(yù)覽區(qū)域?qū)捇蚋叩囊贿叀?br>
9、預(yù)覽返回的圖像是NV21格式的届案,需要轉(zhuǎn)換才能變?yōu)锽itMap庵楷。
文中demo:STakePicture
參考
Android自定義View之雙緩沖機制和SurfaceView
Android: Camera相機開發(fā)詳解(上) —— 知識儲備